Files
maestro/docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.html
T
wangjia 1fcd811c12 docs: maestro 后端重构 Phase1 设计文档(schedule_status × work_type)
8 章设计文档 HTML(架构决策/状态设计/DB Schema/记忆注入/Agent 规格/
运维护栏/安全沙箱/前端 API 契约),全图深色内联 SVG,附 docs/index.html 索引。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 13:52:53 +08:00

2639 lines
220 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maestro 重构全景 · 现状→目标→计划</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<style>
:root{--bg:#0d0d0d;--bg2:#141414;--bg3:#1a1a1a;--border:#252525;--border2:#333;--text:#e8e8e8;--text2:#a0a0a0;--text3:#555;--cyan:#00d4d4;--cyan2:#009999;--green:#4ec94e;--green2:#2d6e2d;--amber:#f0a500;--amber2:#9c6a00;--violet:#9b7bff;--violet2:#6644cc;--red:#ff5555;--red2:#aa2222;--blue:#5599ff;--blue2:#2255cc;--code-bg:#0a0a0a;--shadow:0 4px 32px rgba(0,0,0,.7)}
.light{--bg:#f5f4ef;--bg2:#eae9e4;--bg3:#dddcd7;--border:#ccc;--border2:#bbb;--text:#111;--text2:#555;--text3:#999;--cyan:#006666;--cyan2:#004444;--green:#1a5e1a;--green2:#0d380d;--amber:#7a5200;--amber2:#5a3c00;--violet:#4422aa;--violet2:#2211aa;--red:#991111;--red2:#770000;--blue:#1133aa;--blue2:#001188;--code-bg:#e8e7e2;--shadow:0 4px 16px rgba(0,0,0,.12)}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'IBM Plex Mono',monospace;background:var(--bg);color:var(--text);font-size:13px;line-height:1.7}
/* topbar */
.topbar{position:sticky;top:0;z-index:100;background:var(--bg2);border-bottom:1px solid var(--border);height:48px;display:flex;align-items:center;padding:0 24px;gap:16px}
.topbar-brand{font-size:13px;font-weight:600;color:var(--cyan);letter-spacing:.06em}
.topbar-meta{font-size:11px;color:var(--text3);margin-left:auto}
.theme-btn{background:none;border:1px solid var(--border2);color:var(--text2);font-family:inherit;font-size:11px;padding:3px 10px;border-radius:4px;cursor:pointer;transition:all .15s}
.theme-btn:hover{border-color:var(--cyan);color:var(--cyan)}
/* sidebar layout */
.main-layout{display:flex;height:calc(100vh - 48px);overflow:hidden}
.sidebar{width:176px;background:var(--bg2);border-right:1px solid var(--border);flex-shrink:0;position:sticky;top:48px;height:calc(100vh - 48px);overflow-y:auto;display:flex;flex-direction:column}
.content{flex:1;overflow-y:auto}
/* tabs (now vertical) */
.section-tabs{display:flex;flex-direction:column;padding:8px 0;flex:1}
.tab{padding:11px 18px;font-size:12px;color:var(--text2);cursor:pointer;border-left:2px solid transparent;transition:all .15s;white-space:nowrap;text-align:left;line-height:1.5}
.tab:hover{color:var(--text);background:var(--bg3)}
.tab.active{color:var(--cyan);border-left-color:var(--cyan);background:rgba(0,212,212,.06)}
/* sections */
.section{display:none;max-width:1280px;margin:0 auto;padding:32px 40px}
.section.visible{display:block}
/* card */
.card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:20px;margin-bottom:24px}
.card-title{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--text3);margin-bottom:12px}
/* headings */
.sec-heading{font-size:22px;font-weight:600;color:var(--text);margin-bottom:6px}
.sec-desc{font-size:13px;color:var(--text2);margin-bottom:28px;line-height:1.8}
/* arch diagram */
.arch-diagram{background:var(--code-bg);border:1px solid var(--border);border-radius:8px;padding:20px;font-size:11.5px;line-height:1.9;overflow-x:auto;white-space:pre;color:var(--text2);margin-bottom:20px}
.arch-diagram .lbl{color:var(--cyan);font-weight:600}
.arch-diagram .nd{color:var(--amber)}
.arch-diagram .grp{color:var(--text3)}
.arch-diagram .new{color:var(--green);font-weight:600}
.arch-diagram .warn{color:var(--red)}
.arch-diagram .note{color:var(--text3);font-style:italic}
/* grids */
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
.grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:20px}
.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
@media(max-width:900px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}}
/* box */
.box{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:12px 14px}
.box h4{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text3);margin-bottom:8px}
.box.cyan{border-left:3px solid var(--cyan)}
.box.green{border-left:3px solid var(--green)}
.box.amber{border-left:3px solid var(--amber)}
.box.violet{border-left:3px solid var(--violet)}
.box.red{border-left:3px solid var(--red)}
.box.blue{border-left:3px solid var(--blue)}
/* rows */
.row{display:flex;gap:8px;margin-bottom:4px;font-size:12px;color:var(--text2);align-items:flex-start}
.row code{color:var(--cyan);background:var(--code-bg);padding:1px 5px;border-radius:3px;font-size:11px;flex-shrink:0}
/* table */
.tbl{width:100%;border-collapse:collapse;font-size:12px;margin-bottom:16px}
.tbl th{text-align:left;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text3);border-bottom:1px solid var(--border)}
.tbl td{padding:7px 10px;border-bottom:1px solid var(--border);color:var(--text2);vertical-align:top}
.tbl td:first-child{color:var(--text);font-weight:500}
.tbl tr:last-child td{border-bottom:none}
/* badge */
.badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:4px;font-size:10px;font-weight:600;border:1px solid;margin-right:4px}
.badge.cyan{color:var(--cyan);border-color:var(--cyan2);background:rgba(0,212,212,.07)}
.badge.green{color:var(--green);border-color:var(--green2);background:rgba(78,201,78,.07)}
.badge.amber{color:var(--amber);border-color:var(--amber2);background:rgba(240,165,0,.08)}
.badge.violet{color:var(--violet);border-color:var(--violet2);background:rgba(155,123,255,.08)}
.badge.red{color:var(--red);border-color:var(--red2);background:rgba(255,85,85,.07)}
/* change tag */
.chg{display:inline-block;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;margin-left:4px}
.chg.add{background:rgba(78,201,78,.15);color:var(--green)}
.chg.mod{background:rgba(240,165,0,.13);color:var(--amber)}
.chg.del{background:rgba(255,85,85,.13);color:var(--red)}
/* diff panels */
.diff-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
.diff-panel{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:14px;overflow-x:auto}
.diff-panel h4{font-size:10px;text-transform:uppercase;letter-spacing:.1em;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid var(--border)}
.diff-panel.before h4{color:var(--red)}
.diff-panel.after h4{color:var(--green)}
.diff-panel pre{font-size:11px;line-height:1.7;color:var(--text2)}
.diff-panel pre .add{color:var(--green)}
.diff-panel pre .del{color:var(--red);opacity:.6;text-decoration:line-through}
.diff-panel pre .key{color:var(--violet)}
.diff-panel pre .cmt{color:var(--text3);font-style:italic}
/* pipeline */
.pipe-stage{display:flex;align-items:center;flex-wrap:wrap;gap:4px;margin-bottom:8px}
.pipe-box{padding:5px 10px;border-radius:4px;font-size:11px;font-weight:600;border:1px solid;white-space:nowrap}
.pipe-box.exec{color:var(--cyan);border-color:var(--cyan2);background:rgba(0,212,212,.07)}
.pipe-box.gate{color:var(--amber);border-color:var(--amber2);background:rgba(240,165,0,.1)}
.pipe-box.gate-new{color:var(--green);border-color:var(--green2);background:rgba(78,201,78,.1)}
.pipe-box.review{color:var(--violet);border-color:var(--violet2);background:rgba(155,123,255,.1)}
.pipe-box.human{color:var(--blue);border-color:var(--blue2);background:rgba(85,153,255,.1)}
.pipe-arr{color:var(--text3);margin:0 2px;font-size:14px}
/* progress bar */
.prog-bar-wrap{background:var(--bg2);border-bottom:1px solid var(--border);padding:8px 24px;display:flex;align-items:center;gap:12px}
.prog-label{font-size:11px;color:var(--text2);min-width:90px}
.prog-track{flex:1;height:5px;background:var(--border);border-radius:3px;overflow:hidden}
.prog-fill{height:100%;background:linear-gradient(90deg,var(--green),var(--cyan));border-radius:3px;transition:width .4s}
.prog-pct{font-size:11px;color:var(--green);min-width:36px;text-align:right}
/* task cards */
.task-card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:24px;overflow:hidden}
.task-hdr{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--border);cursor:pointer;user-select:none}
.task-num{width:26px;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;flex-shrink:0}
.t1{background:rgba(0,212,212,.12);color:var(--cyan)}
.t2{background:rgba(155,123,255,.12);color:var(--violet)}
.t3{background:rgba(240,165,0,.12);color:var(--amber)}
.t4{background:rgba(78,201,78,.12);color:var(--green)}
.t5{background:rgba(255,85,85,.12);color:var(--red)}
.t6{background:rgba(85,153,255,.12);color:var(--blue)}
.task-info{flex:1}
.task-title2{font-size:13px;font-weight:600;color:var(--text)}
.task-sub{font-size:11px;color:var(--text3);margin-top:2px}
.task-prog{font-size:11px;color:var(--text3)}
.task-prog .dn{color:var(--green)}
.caret{color:var(--text3);font-size:15px;transition:transform .2s}
.task-card.collapsed .caret{transform:rotate(-90deg)}
.task-body{padding:18px}
.task-card.collapsed .task-body{display:none}
/* steps */
.steps{list-style:none}
.step{display:grid;grid-template-columns:22px 1fr;gap:10px;margin-bottom:18px;align-items:flex-start}
.chkbox{width:18px;height:18px;border:1px solid var(--border2);border-radius:3px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:4px;transition:all .15s;background:var(--bg)}
.chkbox:hover{border-color:var(--green)}
.chkbox.checked{background:var(--green2);border-color:var(--green)}
.chkbox.checked::after{content:'✓';font-size:11px;color:var(--green)}
.step-ttl{font-size:12px;font-weight:600;margin-bottom:6px;display:flex;align-items:center;gap:6px}
.kind{font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600}
.kind.test{background:rgba(155,123,255,.15);color:var(--violet)}
.kind.run{background:rgba(0,212,212,.12);color:var(--cyan)}
.kind.impl{background:rgba(240,165,0,.12);color:var(--amber)}
.kind.commit{background:rgba(78,201,78,.12);color:var(--green)}
.step-desc{font-size:12px;color:var(--text2);margin-bottom:6px;line-height:1.7}
.step-desc code,.step-con code{color:var(--cyan);background:var(--code-bg);padding:1px 4px;border-radius:2px;font-size:11px}
pre{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:12px 14px;overflow-x:auto;margin:6px 0;font-size:11px;line-height:1.6}
pre .kw{color:var(--violet)}
pre .str{color:var(--green)}
pre .cm{color:var(--text3);font-style:italic}
pre .fn{color:var(--cyan)}
pre .nu{color:var(--amber)}
pre .tp{color:var(--amber)}
.note{background:var(--bg3);border-left:3px solid var(--amber);border-radius:4px;padding:10px 14px;margin:8px 0;font-size:12px;color:var(--text2);line-height:1.7}
.note code{color:var(--cyan);background:var(--code-bg);padding:1px 4px;border-radius:2px;font-size:11px}
hr{border:none;border-top:1px solid var(--border);margin:24px 0}
/* design sub-nav */
.design-sticky{position:sticky;top:0;z-index:50;background:var(--bg);margin:-32px -40px 24px;padding:16px 40px 12px;border-bottom:1px solid var(--border)}
.design-sticky .sec-heading{margin-bottom:4px}
.design-sticky .sec-desc{margin-bottom:14px}
.design-nav{display:flex;gap:4px;flex-wrap:wrap;margin:0}
.design-nav a{font-size:11px;padding:5px 12px;border-radius:4px;color:var(--text2);text-decoration:none;border:1px solid var(--border);transition:all .15s;white-space:nowrap}
.design-nav a:hover{color:var(--cyan);border-color:var(--cyan)}
.design-nav a.act{color:var(--cyan);border-color:var(--cyan);background:rgba(0,212,212,.07)}
.chapter-anchor{display:block;height:0;margin-top:-2px;padding-top:2px;scroll-margin-top:150px}
.chapter-hdr{font-size:14px;font-weight:600;color:var(--text);margin:32px 0 16px;padding-bottom:8px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}
.chapter-hdr span{font-size:10px;color:var(--cyan);border:1px solid var(--cyan2);border-radius:4px;padding:2px 8px;font-weight:600}
.chapter-hdr .phase{margin-left:auto;font-size:10px;font-weight:600;border-radius:10px;padding:2px 10px;border:1px solid}
.chapter-hdr .phase.p1{color:var(--green);border-color:var(--green2);background:rgba(78,201,78,.1)}
.chapter-hdr .phase.p2{color:var(--violet);border-color:var(--violet2);background:rgba(155,123,255,.1)}
.chapter-hdr .phase.mix{color:var(--amber);border-color:var(--amber2);background:rgba(240,165,0,.1)}
</style>
</head>
<body>
<div class="topbar">
<span class="topbar-brand">MAESTRO · 重构全景</span>
<span class="topbar-meta">2026-06-22 · 现状 → 目标 → 计划</span>
<button class="theme-btn" id="themeBtn">切换主题</button>
</div>
<div class="main-layout">
<div class="sidebar">
<div class="section-tabs">
<div class="tab active" onclick="showSec('s-current',this)">① 现状架构</div>
<div class="tab" onclick="showSec('s-target',this)">② 重构目标</div>
<div class="tab" onclick="showSec('s-design',this)">③ 设计方案</div>
<div class="tab" onclick="showSec('s-impl',this)">④ 具体实现</div>
</div>
</div><!-- /.sidebar -->
<div class="content">
<div class="prog-bar-wrap" id="prog-wrap" style="display:none">
<span class="prog-label">实施进度</span>
<div class="prog-track"><div class="prog-fill" id="prog-fill" style="width:0%"></div></div>
<span class="prog-pct" id="prog-pct">0%</span>
</div>
<!-- ═══════════════════════════════ ① 现状架构 ═══════════════════════════════ -->
<div class="section visible" id="s-current">
<div class="sec-heading">现状架构</div>
<div class="sec-desc">Maestro 当前系统全景——进程模型、前后端、数据流、调度算法、执行管道与模型档位。</div>
<div class="card">
<div class="card-title">系统总览</div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 716" style="width:100%;display:block;margin:16px 0;background:#0a0f1a;border-radius:6px;">
<text x="24" y="26" font-family="monospace" font-size="15" font-weight="bold" fill="#e8e8e8">MAESTRO 系统架构(现状)</text>
<rect x="930" y="12" width="238" height="22" fill="#11161f" stroke="#252b36" rx="4"/>
<text x="940" y="27" font-family="monospace" font-size="9.5" fill="#e07070">⚠ 现状问题点 / 重构待改</text>
<rect x="12" y="40" width="1156" height="92" fill="#0b1626" stroke="#1e4a7a" stroke-width="1" rx="4"/>
<rect x="12" y="40" width="1156" height="22" fill="#1e3a5f" rx="4"/>
<rect x="12" y="54" width="1156" height="8" fill="#1e3a5f"/>
<text x="22" y="55" font-family="monospace" font-size="11" font-weight="bold" fill="#60a5fa">用户端</text>
<rect x="28" y="70" width="200" height="50" fill="#13243d" stroke="#1e4a7a" stroke-width="1" rx="4"/>
<text x="128" y="92.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#93c5fd">Browser — Vanilla JS SPA</text>
<text x="128" y="105" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#7d93b0">web/index.html + app.js</text>
<rect x="244" y="70" width="180" height="50" fill="#13243d" stroke="#1e4a7a" stroke-width="1" rx="4"/>
<text x="334" y="92.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#93c5fd">多项目看板</text>
<text x="334" y="105" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#7d93b0">任务列表实时刷新</text>
<rect x="440" y="70" width="200" height="50" fill="#13243d" stroke="#1e4a7a" stroke-width="1" rx="4"/>
<text x="540" y="92.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#93c5fd">任务详情</text>
<text x="540" y="105" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#7d93b0">plan/spec/events/runs</text>
<rect x="656" y="70" width="160" height="50" fill="#13243d" stroke="#1e4a7a" stroke-width="1" rx="4"/>
<text x="736" y="92.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#93c5fd">审批</text>
<text x="736" y="105" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#7d93b0">approve / reject</text>
<rect x="832" y="70" width="200" height="50" fill="#2a1414" stroke="#b85450" stroke-width="1" rx="4"/>
<text x="932" y="92.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#e07070">⚠ 无流式输出</text>
<text x="932" y="105" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#e07070">只能看 transcript</text>
<rect x="12" y="144" width="1156" height="222" fill="#1c1708" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<rect x="12" y="144" width="1156" height="22" fill="#4a3a14" rx="4"/>
<rect x="12" y="158" width="1156" height="8" fill="#4a3a14"/>
<text x="22" y="159" font-family="monospace" font-size="11" font-weight="bold" fill="#fbbf24">Daemon 进程(唯一 DB 写者)</text>
<rect x="28" y="174" width="210" height="52" fill="#2c2410" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<text x="133" y="191.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fcd34d">Fastify API Server :4517</text>
<text x="133" y="203.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9b878">REST 40+ · WS /ws</text>
<text x="133" y="216.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a8a5a">src/api/server.ts</text>
<rect x="254" y="174" width="230" height="52" fill="#2c2410" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<text x="369" y="191.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fcd34d">Orchestrator Loop · 每 15s</text>
<text x="369" y="203.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9b878">ingest → reap → claim</text>
<text x="369" y="216.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a8a5a">src/daemon/orchestrator.ts</text>
<rect x="28" y="238" width="456" height="52" fill="#2a1414" stroke="#b85450" stroke-width="1" rx="4"/>
<text x="256" y="261.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#e07070">⚠ rankByScore:简单优先级加总,无 CPM / 无防饥饿 / 忽略文件耦合</text>
<text x="256" y="274" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#e07070">⚠ claimOne 无 CAS → 并发可双重领取同一任务</text>
<rect x="508" y="174" width="200" height="52" fill="#2c2410" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<text x="608" y="191.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fcd34d">SQLite DB</text>
<text x="608" y="203.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9b878">~/.maestro/maestro.db</text>
<text x="608" y="216.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a8a5a">WAL + 外键</text>
<rect x="728" y="174" width="304" height="52" fill="#2c2410" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<text x="880" y="191.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fcd34d">tables5 张)</text>
<text x="880" y="203.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9b878">projects · tasks · runs · events · approvals</text>
<text x="880" y="216.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a8a5a">task 用单一扁平 status 列建模</text>
<rect x="508" y="238" width="524" height="52" fill="#2a1414" stroke="#b85450" stroke-width="1" rx="4"/>
<text x="770" y="261.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#e07070">⚠ 缺列:claimed_at · task_type · scope · owned_files · version</text>
<text x="770" y="274" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#e07070">⚠ 状态用扁平 FSM(16 个混合 status),无 schedule × work_type 正交建模</text>
<rect x="12" y="378" width="1156" height="232" fill="#0a1f10" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<rect x="12" y="378" width="1156" height="22" fill="#14401f" rx="4"/>
<rect x="12" y="392" width="1156" height="8" fill="#14401f"/>
<text x="22" y="393" font-family="monospace" font-size="11" font-weight="bold" fill="#4ade80">Worker 进程(每任务独立 · 不接触 DB)</text>
<text x="24" y="414" font-family="monospace" font-size="10" font-weight="bold" fill="#4ade80">executor pipeline(复审内联、串行)</text>
<rect x="24" y="420" width="92" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="70" y="440.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">syncMain</text>
<text x="125" y="441" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="134" y="420" width="118" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="193" y="440.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">createWorktree</text>
<text x="261" y="441" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="270" y="420" width="100" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="320" y="440.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">runTask (CC)</text>
<text x="379" y="441" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="388" y="420" width="90" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="433" y="440.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">verify 可选</text>
<text x="487" y="441" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="496" y="420" width="108" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="550" y="440.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">worktreeDiff</text>
<text x="613" y="441" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="622" y="420" width="120" height="34" fill="#2a1414" stroke="#b85450" stroke-width="1" rx="4"/>
<text x="682" y="440.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#e07070">⚠ reviewCode 串行</text>
<text x="751" y="441" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="760" y="420" width="120" height="34" fill="#2a1414" stroke="#b85450" stroke-width="1" rx="4"/>
<text x="820" y="440.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#e07070">⚠ reviewSec 串行</text>
<text x="889" y="441" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="898" y="420" width="108" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="952" y="440.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">exec_review</text>
<text x="24" y="480" font-family="monospace" font-size="10" font-weight="bold" fill="#4ade80">planner pipeline(只读)</text>
<rect x="24" y="486" width="160" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="104" y="506.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">buildPlannerPrompt</text>
<text x="193" y="507" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="202" y="486" width="160" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="282" y="506.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">runPlanner (CC 只读)</text>
<text x="371" y="507" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="380" y="486" width="210" height="34" fill="#2a1414" stroke="#b85450" stroke-width="1" rx="4"/>
<text x="485" y="506.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#e07070">⚠ decompose JSON 缺 ownedFiles</text>
<text x="24" y="546" font-family="monospace" font-size="10" font-weight="bold" fill="#4ade80">conflict pipeline(无专用 agent</text>
<rect x="24" y="552" width="120" height="30" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="84" y="570.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">git merge</text>
<text x="153" y="571" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="162" y="552" width="120" height="30" fill="#2a1414" stroke="#b85450" stroke-width="1" rx="4"/>
<text x="222" y="570.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#e07070">⚠ CC 自己猜着解</text>
<text x="291" y="571" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="300" y="552" width="100" height="30" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="350" y="570.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">双复审</text>
<rect x="12" y="622" width="1156" height="82" fill="#160c28" stroke="#5a3a8a" stroke-width="1" rx="4"/>
<rect x="12" y="622" width="1156" height="22" fill="#2d1a4f" rx="4"/>
<rect x="12" y="636" width="1156" height="8" fill="#2d1a4f"/>
<text x="22" y="637" font-family="monospace" font-size="11" font-weight="bold" fill="#a78bfa">Claude Code AgentHeadless</text>
<rect x="28" y="648" width="240" height="46" fill="#241640" stroke="#5a3a8a" stroke-width="1" rx="4"/>
<text x="148" y="668.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#c4b5fd">cc.ts · @anthropic-ai/claude-code SDK</text>
<text x="148" y="681" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a86c4">for-await 流式(仅落 transcript</text>
<rect x="284" y="648" width="330" height="46" fill="#241640" stroke="#5a3a8a" stroke-width="1" rx="4"/>
<text x="449" y="668.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#c4b5fd">executor 白名单</text>
<text x="449" y="681" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a86c4">Read/Edit/Write/Glob/Grep · Bash(git,test) · WebSearch</text>
<rect x="630" y="648" width="290" height="46" fill="#241640" stroke="#5a3a8a" stroke-width="1" rx="4"/>
<text x="775" y="668.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#c4b5fd">planner 白名单</text>
<text x="775" y="681" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a86c4">Read/Glob/Grep · Bash(git log/diff/show)</text>
<rect x="936" y="648" width="96" height="46" fill="none" stroke="#444" stroke-width="1" stroke-dasharray="4 3" rx="4"/>
<text x="984" y="668.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">src/mcp/</text>
<text x="984" y="681" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">MCP</text>
<rect x="1044" y="648" width="96" height="46" fill="none" stroke="#444" stroke-width="1" stroke-dasharray="4 3" rx="4"/>
<text x="1092" y="668.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">src/sync/</text>
<text x="1092" y="681" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">todo 同步</text>
</svg>
</div>
<div class="grid-2">
<div class="card">
<div class="card-title">前端(现状)</div>
<table class="tbl">
<tr><th>维度</th><th>现状</th></tr>
<tr><td>技术栈</td><td>Vanilla JS + CSS,无构建工具</td></tr>
<tr><td>入口</td><td>web/index.html 静态文件,Fastify 直接 serve</td></tr>
<tr><td>状态管理</td><td>全局 JS 变量 + 手动 DOM 操作</td></tr>
<tr><td>实时通信</td><td>WebSocket 收事件后手动更新 DOM</td></tr>
<tr><td>流式输出</td><td>无(只看完成后 transcript</td></tr>
<tr><td>代码分割</td><td>单文件 app.js (~1500 行)</td></tr>
<tr><td>类型安全</td><td>无(纯 JS</td></tr>
</table>
</div>
<div class="card">
<div class="card-title">进程模型(现状)</div>
<table class="tbl">
<tr><th>角色</th><th>职责</th></tr>
<tr><td>Daemon</td><td>唯一 DB 写者;API+WS 服务;Orchestrator 调度循环;ingest worker 产出</td></tr>
<tr><td>Worker</td><td>每任务独立 OS 进程;不碰 DB;纯文件通信</td></tr>
<tr><td>CC Agent</td><td>在 worker 内 SDK headless 运行;工具白名单隔离</td></tr>
<tr><td>通信</td><td>file + SIGTERM(可存活 daemon crash</td></tr>
</table>
</div>
</div>
<div class="card">
<div class="card-title">REST API 主要路由(重构后 · 完整契约见 ③设计方案 ⑧ 前端 API 契约)</div>
<div class="grid-3">
<div class="box cyan"><h4>Projects</h4>
<div class="row"><code>GET /api/projects</code></div>
<div class="row"><code>POST /api/projects</code></div>
<div class="row"><code>GET /api/projects/:id</code></div>
<div class="row"><code>PATCH /api/projects/:id</code></div>
<div class="row"><code>DELETE /api/projects/:id</code></div>
<div class="row"><code>POST /api/projects/:id/sync</code></div>
<div class="row"><code>POST /api/projects/reorder</code></div>
</div>
<div class="box amber"><h4>Tasks</h4>
<div class="row"><code>GET /api/projects/:id/tasks</code></div>
<div class="row"><code>POST /api/projects/:id/tasks</code></div>
<div class="row"><code>GET /api/tasks/:id</code></div>
<div class="row"><code>POST /api/tasks/:id/plan</code></div>
<div class="row"><code>POST /api/tasks/:id/spec</code></div>
<div class="row"><code>POST /api/tasks/:id/decide</code></div>
<div class="row"><code>POST /api/tasks/:id/requeue</code></div>
<div class="row"><code>POST /api/tasks/:id/cancel</code></div>
<div class="row"><code>DELETE /api/tasks/:id</code></div>
<div class="row"><code>POST /api/tasks/:id/attachments</code> <span class="chg add"></span></div>
<div class="row"><code>POST /api/tasks/:id/takeover</code> <span class="chg add"></span></div>
</div>
<div class="box violet"><h4>Runs / Events</h4>
<div class="row"><code>GET /api/tasks/:id/runs</code></div>
<div class="row"><code>GET /api/runs/:id/transcript</code></div>
<div class="row"><code>GET /api/tasks/:id/events</code></div>
<div class="row"><code>GET /api/projects/:id/events</code></div>
<div class="row"><code>GET /api/agents</code></div>
<div class="row"><code>GET /api/usage</code></div>
<div class="row"><code>GET /api/metrics</code></div>
<div class="row"><code>GET /api/tasks/:id/stream</code> <span class="chg add"></span></div>
<div class="row"><code>GET /api/health</code> <span class="chg add"></span></div>
<div class="row"><code>WS /ws</code></div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">数据库 Schema(现状)</div>
<div class="arch-diagram"><span class="lbl">projects</span> id · name · repo_path · autonomy · concurrency · max_retries · timeout_ms
verify_cmd · model · checks · auto_approve_plan · auto_approve_exec
<span class="lbl">tasks</span> id · project_id · parent_id · depth · title · body · complexity · status
priority · deps(JSON) · plan · spec · operations · result(JSON)
retry_baseline · next_eligible_at · last_run_error · created_at · updated_at
<span class="warn">⚠ 缺少:schedule_status · work_type · work_plan · work_history · claimed_at · scope · expected_output · version</span>
<span class="lbl">runs</span> id · task_id · kind(planner/executor/conflict) · status · worktree · branch
transcript_ref · session_id · worker_pid · last_seq · started_at · ended_at
<span class="warn">⚠ 缺少:trace_id</span>
<span class="lbl">events</span> id · project_id · task_id · type · payload(JSON) · at
<span class="lbl">approvals</span> id · task_id · gate(plan/spec/exec) · action · actor · reason · at</div>
</div>
<div class="card">
<div class="card-title">任务状态机(生命周期)</div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 600" style="width:100%;display:block;margin:16px 0;">
<defs>
<marker id="fg" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#82b366"/></marker>
<marker id="fo" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#d6783b"/></marker>
<marker id="fr" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#b85450"/></marker>
<marker id="fv" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#9673a6"/></marker>
<marker id="fb" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#5b8fd6"/></marker>
<marker id="fa" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#d6b656"/></marker>
<marker id="fk" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#777777"/></marker>
</defs>
<text x="24" y="24" font-family="monospace" font-size="14" font-weight="bold" fill="#e8e8e8">Maestro 任务状态机(现状 · 扁平 FSM)</text>
<!-- edges -->
<path d="M 97 284 Q 190 195 282 107" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#fo)"/>
<path d="M 107 286 Q 190 245 270 205" stroke="#5b8fd6" stroke-width="1.5" fill="none" marker-end="url(#fb)"/>
<path d="M 122 300 Q 190 300 254 300" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#fg)"/>
<path d="M 82 318 Q 95 420 108 522" stroke="#777777" stroke-width="1.5" fill="none" marker-end="url(#fk)"/>
<path d="M 360 90 Q 410 90 456 90" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#fo)"/>
<path d="M 584 90 Q 630 90 676 90" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#fo)"/>
<path d="M 484 75 Q 410 44 335 75" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 696 77 Q 520 26 343 77" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#fo)"/>
<path d="M 781 104 Q 949 160 1101 284" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#fo)"/>
<path d="M 356 190 Q 410 190 456 190" stroke="#5b8fd6" stroke-width="1.5" fill="none" marker-end="url(#fb)"/>
<path d="M 489 206 Q 410 245 328 286" stroke="#5b8fd6" stroke-width="1.5" fill="none" marker-end="url(#fb)"/>
<path d="M 481 176 Q 410 150 337 177" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 346 300 Q 406 300 462 300" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#fg)"/>
<path d="M 475 288 Q 406 266 336 289" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#fg)"/>
<path d="M 562 300 Q 619 300 667 300" stroke="#9673a6" stroke-width="1.5" fill="none" marker-end="url(#fv)"/>
<path d="M 783 300 Q 830 300 871 300" stroke="#9673a6" stroke-width="1.5" fill="none" marker-end="url(#fv)"/>
<path d="M 999 300 Q 1028 300 1078 300" stroke="#9673a6" stroke-width="1.5" fill="none" marker-end="url(#fv)"/>
<path d="M 308 318 Q 326 359 308 400" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 292 400 Q 274 359 292 318" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#fg)"/>
<path d="M 732 318 Q 747 359 732 400" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 906 316 Q 830 359 752 403" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 902 285 Q 618 150 329 286" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 690 406 Q 609 376 533 316" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#fg)"/>
<path d="M 696 432 Q 619 471 540 510" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 300 108 Q 298 360 491 508" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 303 208 Q 330 405 488 509" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#fr)"/>
<path d="M 505 506 Q 450 371 329 314" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#fo)"/>
<path d="M 516 506 Q 586 219 333 105" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#fo)"/>
<path d="M 512 506 Q 507 293 330 205" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#fo)"/>
<path d="M 277 316 Q 205 365 133 414" stroke="#d6b656" stroke-width="1.5" fill="none" stroke-dasharray="4,3" marker-end="url(#fa)"/>
<path d="M 133 414 Q 205 365 277 316" stroke="#d6b656" stroke-width="1.5" fill="none" stroke-dasharray="4,3" marker-end="url(#fa)"/>
<!-- labels -->
<text x="175" y="185" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">Hard</text>
<text x="180" y="238" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#5b8fd6">Medium</text>
<text x="190" y="293" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#82b366">Easy</text>
<text x="64" y="430" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#777777">cancel</text>
<text x="410" y="82" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">拆解完成</text>
<text x="630" y="82" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">accept</text>
<text x="410" y="128" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">reject</text>
<text x="520" y="30" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">重拆</text>
<text x="1000" y="150" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">子任务全 done</text>
<text x="410" y="182" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#5b8fd6">方案完成</text>
<text x="418" y="248" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#5b8fd6">accept</text>
<text x="410" y="225" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">reject</text>
<text x="406" y="293" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#82b366">claim</text>
<text x="406" y="330" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#82b366">退回</text>
<text x="619" y="293" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#9673a6">worker 启动</text>
<text x="830" y="293" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#9673a6">执行完成</text>
<text x="1028" y="293" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#9673a6">accept(合并)</text>
<text x="230" y="360" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">依赖未满足</text>
<text x="372" y="360" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#82b366">依赖满足</text>
<text x="660" y="360" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">执行失败</text>
<text x="850" y="360" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">失败</text>
<text x="620" y="212" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">reject(返工)</text>
<text x="600" y="350" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#82b366">重试</text>
<text x="650" y="480" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">超阈值</text>
<text x="150" y="430" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">失败超限</text>
<text x="250" y="470" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">失败超限</text>
<text x="360" y="470" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">重排队</text>
<text x="600" y="430" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">重新分析</text>
<text x="430" y="360" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">重写方案</text>
<text x="175" y="360" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6b656">暂停</text>
<text x="225" y="380" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6b656">恢复</text>
<!-- nodes -->
<ellipse cx="80" cy="300" rx="42" ry="18" fill="#2a2a2a" stroke="#888888"/>
<text x="80" y="304" text-anchor="middle" font-family="monospace" font-size="11" fill="#aaaaaa">init</text>
<ellipse cx="300" cy="90" rx="60" ry="18" fill="#2e2014" stroke="#d6783b"/>
<text x="300" y="94" text-anchor="middle" font-family="monospace" font-size="11" fill="#f0a060">analyzing</text>
<ellipse cx="520" cy="90" rx="64" ry="18" fill="#2e2014" stroke="#d6783b"/>
<text x="520" y="94" text-anchor="middle" font-family="monospace" font-size="11" fill="#f0a060">plan_review</text>
<ellipse cx="740" cy="90" rx="64" ry="18" fill="#2e2014" stroke="#d6783b"/>
<text x="740" y="94" text-anchor="middle" font-family="monospace" font-size="11" fill="#f0a060">decomposed</text>
<ellipse cx="300" cy="190" rx="56" ry="18" fill="#14233a" stroke="#5b8fd6"/>
<text x="300" y="194" text-anchor="middle" font-family="monospace" font-size="11" fill="#7ab0ff">speccing</text>
<ellipse cx="520" cy="190" rx="64" ry="18" fill="#14233a" stroke="#5b8fd6"/>
<text x="520" y="194" text-anchor="middle" font-family="monospace" font-size="11" fill="#7ab0ff">spec_review</text>
<ellipse cx="300" cy="300" rx="46" ry="18" fill="#1a2e1a" stroke="#82b366"/>
<text x="300" y="304" text-anchor="middle" font-family="monospace" font-size="11" fill="#82b366">ready</text>
<ellipse cx="512" cy="300" rx="50" ry="18" fill="#1a2e1a" stroke="#82b366"/>
<text x="512" y="304" text-anchor="middle" font-family="monospace" font-size="11" fill="#82b366">queued</text>
<ellipse cx="725" cy="300" rx="58" ry="18" fill="#221a2e" stroke="#9673a6"/>
<text x="725" y="304" text-anchor="middle" font-family="monospace" font-size="11" fill="#c4a8d4">executing</text>
<ellipse cx="935" cy="300" rx="64" ry="18" fill="#221a2e" stroke="#9673a6"/>
<text x="935" y="304" text-anchor="middle" font-family="monospace" font-size="11" fill="#c4a8d4">exec_review</text>
<ellipse cx="1120" cy="300" rx="42" ry="18" fill="#1a472a" stroke="#2d7a40"/>
<text x="1120" y="304" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#ffffff">done</text>
<ellipse cx="300" cy="418" rx="52" ry="18" fill="#2e1a1a" stroke="#b85450"/>
<text x="300" y="422" text-anchor="middle" font-family="monospace" font-size="11" fill="#e07070">blocked</text>
<ellipse cx="725" cy="418" rx="48" ry="18" fill="#2e1a1a" stroke="#b85450"/>
<text x="725" y="422" text-anchor="middle" font-family="monospace" font-size="11" fill="#e07070">failed</text>
<ellipse cx="512" cy="524" rx="46" ry="18" fill="#2e2014" stroke="#d6783b"/>
<text x="512" y="528" text-anchor="middle" font-family="monospace" font-size="11" fill="#f97316">stuck</text>
<ellipse cx="110" cy="430" rx="50" ry="18" fill="#2e2a14" stroke="#d6b656"/>
<text x="110" y="434" text-anchor="middle" font-family="monospace" font-size="11" fill="#d6b656">paused</text>
<ellipse cx="110" cy="540" rx="60" ry="18" fill="#2e2e2e" stroke="#555555"/>
<text x="110" y="544" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#888888">cancelled</text>
<rect x="895" y="454" width="220" height="102" fill="#11161f" stroke="#252b36" rx="4"/>
<line x1="905" y1="467" x2="927" y2="467" stroke="#d6783b" stroke-width="2"/><text x="935" y="470" font-family="sans-serif" font-size="9" fill="#9aa4b2">Hard 拆解</text>
<line x1="905" y1="482" x2="927" y2="482" stroke="#5b8fd6" stroke-width="2"/><text x="935" y="485" font-family="sans-serif" font-size="9" fill="#9aa4b2">Medium 方案</text>
<line x1="905" y1="497" x2="927" y2="497" stroke="#82b366" stroke-width="2"/><text x="935" y="500" font-family="sans-serif" font-size="9" fill="#9aa4b2">执行调度</text>
<line x1="905" y1="512" x2="927" y2="512" stroke="#9673a6" stroke-width="2"/><text x="935" y="515" font-family="sans-serif" font-size="9" fill="#9aa4b2">Agent 运行</text>
<line x1="905" y1="527" x2="927" y2="527" stroke="#b85450" stroke-width="2"/><text x="935" y="530" font-family="sans-serif" font-size="9" fill="#9aa4b2">异常/失败</text>
<line x1="905" y1="542" x2="927" y2="542" stroke="#d6b656" stroke-width="2"/><text x="935" y="545" font-family="sans-serif" font-size="9" fill="#9aa4b2">暂停(虚线)</text>
<text x="24" y="585" font-family="sans-serif" font-size="9" fill="#64748b">注:paused 与所有活跃态双向互通;cancelled 任意态可达(略去以保主干清晰)。本图为重构前扁平 FSM,对照「重构目标」的 schedule × work_type 正交模型。</text>
</svg>
</div>
<div class="card">
<div class="card-title">调度算法(现状)</div>
<div class="grid-2">
<div class="box amber"><h4>评分公式</h4>
<div style="font-size:12px;color:var(--text2);line-height:2">
<code>score(t) = baseScore(priority) P0=3,P1=2,P2=1</code><br>
<code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+ Σ baseScore(d) d=已完成依赖</code><br>
<code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+ Σ baseScore(b) b=被本任务阻塞</code>
</div>
<div style="font-size:11px;color:var(--red);margin-top:8px">⚠ 无防饥饿&nbsp;·&nbsp;忽略文件耦合&nbsp;·&nbsp;无 CAS</div>
</div>
<div class="box"><h4>调度流程</h4>
<div class="row"><code>ingest</code> 读 outbox → 写 DB</div>
<div class="row"><code>reap</code> 心跳超时 → failAttempt</div>
<div class="row"><code>claim</code> rankByScore → startRun</div>
<div style="font-size:11px;color:var(--red);margin-top:6px">⚠ 并发 tick 可双重领取</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">执行管道(现状)</div>
<div class="pipe-stage">
<div class="pipe-box exec">syncMain</div><div class="pipe-arr"></div>
<div class="pipe-box exec">createWorktree</div><div class="pipe-arr"></div>
<div class="pipe-box exec">runTask (CC)</div><div class="pipe-arr"></div>
<div class="pipe-box gate">verify 可选</div><div class="pipe-arr"></div>
<div class="pipe-box exec">worktreeDiff</div>
</div>
<div class="pipe-stage">
<div style="width:8px"></div>
<div class="pipe-box review" style="margin-left:0">reviewCode <span style="color:var(--red);font-size:10px">串行</span></div><div class="pipe-arr"></div>
<div class="pipe-box review">reviewSecurity <span style="color:var(--red);font-size:10px">串行</span></div><div class="pipe-arr"></div>
<div class="pipe-box human">exec_review</div>
</div>
<div style="font-size:11px;color:var(--red);margin-top:6px">⚠ 串行复审(耗时翻倍)&nbsp;·&nbsp; verdict=reject 只是建议不挡人工审核&nbsp;·&nbsp; 无分项检查闸&nbsp;·&nbsp; 无 diff 体量限制</div>
</div>
<div class="card">
<div class="card-title">模型档位(现状)</div>
<table class="tbl">
<tr><th>角色</th><th>Easy</th><th>Medium</th><th>Hard</th></tr>
<tr><td>executor</td><td>claude-sonnet-4-6</td><td>claude-opus-4-8</td><td>claude-opus-4-8</td></tr>
<tr><td>planner</td><td>claude-sonnet-4-6</td><td>claude-opus-4-8</td><td><span style="color:var(--red)">claude-opus-4-8 ⚠</span></td></tr>
<tr><td>reviewer</td><td>claude-sonnet-4-6</td><td>claude-opus-4-8</td><td><span style="color:var(--red)">claude-opus-4-8 ⚠</span></td></tr>
<tr><td>conflict</td><td colspan="3"><span style="color:var(--red)">claude-opus-4-8 ⚠(不够强)</span></td></tr>
<tr><td>回退链</td><td colspan="3"><code>['claude-opus-4-8', 'claude-sonnet-4-6']</code>(缺 fable-5</td></tr>
</table>
</div>
</div><!-- /s-current -->
<!-- ═══════════════════════════════ ② 重构后架构 ═══════════════════════════════ -->
<div class="section" id="s-target">
<div class="sec-heading">重构后架构</div>
<div class="sec-desc">Phase 1(本次实施)完成后的目标态。<span class="chg add">绿=新增</span> <span class="chg mod">琥珀=变更</span> <span class="chg del">红=移除</span></div>
<div class="card">
<div class="card-title">系统总览(重构后)</div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 952" style="width:100%;display:block;margin:16px 0;background:#0a0f1a;border-radius:6px;">
<text x="24" y="26" font-family="monospace" font-size="15" font-weight="bold" fill="#e8e8e8">MAESTRO 系统架构(重构后目标态)</text>
<rect x="858" y="12" width="310" height="22" fill="#11161f" stroke="#252b36" rx="4"/>
<text x="868" y="27" font-family="monospace" font-size="9.5" fill="#fdba74">★ Phase 1 新增/变更</text>
<text x="1010" y="27" font-family="monospace" font-size="9.5" fill="#64748b">★★ Phase 2 规划</text>
<rect x="12" y="40" width="1156" height="92" fill="#0b1626" stroke="#1e4a7a" stroke-width="1" rx="4"/>
<rect x="12" y="40" width="1156" height="22" fill="#1e3a5f" rx="4"/>
<rect x="12" y="54" width="1156" height="8" fill="#1e3a5f"/>
<text x="22" y="55" font-family="monospace" font-size="11" font-weight="bold" fill="#60a5fa">用户端</text>
<rect x="28" y="70" width="250" height="50" fill="#13243d" stroke="#1e4a7a" stroke-width="1" rx="4"/>
<text x="153" y="86.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#93c5fd">Browser — Vanilla JS SPA</text>
<text x="153" y="98.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#7d93b0">web/index.html + app.js</text>
<text x="153" y="111.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#6a7d8a">Phase 1 不变)</text>
<rect x="300" y="70" width="250" height="50" fill="none" stroke="#444" stroke-width="1" stroke-dasharray="4 3" rx="4"/>
<text x="425" y="86.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#64748b">★★ React 18 + Vite</text>
<text x="425" y="98.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">Zustand · IBM Plex Mono</text>
<text x="425" y="111.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">[Phase 2]</text>
<rect x="572" y="70" width="250" height="50" fill="none" stroke="#444" stroke-width="1" stroke-dasharray="4 3" rx="4"/>
<text x="697" y="86.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#64748b">★★ SSE 流式 token 输出</text>
<text x="697" y="98.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">GET /api/tasks/:id/stream</text>
<text x="697" y="111.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">[Phase 2]</text>
<rect x="12" y="144" width="1156" height="250" fill="#1c1708" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<rect x="12" y="144" width="1156" height="22" fill="#4a3a14" rx="4"/>
<rect x="12" y="158" width="1156" height="8" fill="#4a3a14"/>
<text x="22" y="159" font-family="monospace" font-size="11" font-weight="bold" fill="#fbbf24">Daemon 进程(唯一 DB 写者)</text>
<rect x="28" y="174" width="250" height="52" fill="#2c2410" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<text x="153" y="191.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fcd34d">Fastify API Server :4517</text>
<text x="153" y="203.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9b878">REST 40+ 路由 · WS /ws</text>
<text x="153" y="216.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a8a5a">src/api/server.ts(沿用)</text>
<rect x="28" y="234" width="250" height="40" fill="#2c2410" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<text x="153" y="251.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fcd34d">Orchestrator Loop · 每 15s</text>
<text x="153" y="264" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9b878">ingest → reap → claim</text>
<rect x="28" y="282" width="250" height="98" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="153" y="303.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fdba74">★ 调度重构(scoring.ts</text>
<text x="153" y="316" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">rankU = CPM 最长链</text>
<text x="153" y="328.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">+ agingBonus(防饥饿)</text>
<text x="153" y="341" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74"> couplingPenalty(文件耦合)</text>
<text x="153" y="353.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">★ casClaimTaskCAS 防双 claim</text>
<text x="153" y="366" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">★ 按 work_type dispatch agent</text>
<rect x="300" y="174" width="200" height="52" fill="#2c2410" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<text x="400" y="191.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fcd34d">SQLite DB</text>
<text x="400" y="203.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9b878">~/.maestro/maestro.db</text>
<text x="400" y="216.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a8a5a">WAL + 外键</text>
<rect x="520" y="174" width="640" height="52" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="840" y="197.5" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fdba74">★ tasksschedule_status × work_type(取代扁平 status</text>
<text x="840" y="210" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">+ work_plan / work_history 队列 · owned_files · scope · expected_output · claimed_at · version</text>
<rect x="300" y="234" width="860" height="42" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="730" y="258.75" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fdba74">★ runs.kind 扩 plan|code|review|pr|conflict|research · + trace_id ★ projects + checks / diff_max_files / agent_rules</text>
<text x="300" y="300" font-family="monospace" font-size="9" fill="#9a8a5a">⛔ 已弃用:task_type / sub_status / state_snapshot / parent_version_id(被 schedule_status + work_plan 队列取代)</text>
<rect x="300" y="312" width="860" height="68" fill="#2c2410" stroke="#7a5e1e" stroke-width="1" rx="4"/>
<text x="730" y="337.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fcd34d">work_plan 队列(线性、运行时决定执行路径)</text>
<text x="730" y="349.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9b878">[ {type, spec, owned_files, expected_output}, … ] ← Orchestrator pop 队头 → (ready, 下一 work_type)</text>
<text x="730" y="362.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#9a8a5a">waiting(plan|review) 为人工闸,不占并发名额</text>
<rect x="12" y="406" width="1156" height="372" fill="#0a1f10" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<rect x="12" y="406" width="1156" height="22" fill="#14401f" rx="4"/>
<rect x="12" y="420" width="1156" height="8" fill="#14401f"/>
<text x="22" y="421" font-family="monospace" font-size="11" font-weight="bold" fill="#4ade80">Worker 进程(每任务独立 · 不接触 DB) — 按 work_type 拆独立 run</text>
<text x="24" y="442" font-family="monospace" font-size="10" font-weight="bold" fill="#4ade80">plan runPlanner Agent</text>
<rect x="24" y="448" width="150" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="99" y="462.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">runPlanner</text>
<text x="99" y="475" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">maxTurns 90/60/40</text>
<text x="183" y="469" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="192" y="448" width="210" height="34" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="297" y="462.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">★ decompose JSON</text>
<text x="297" y="475" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">+ ownedFiles / expectedOutput</text>
<text x="411" y="469" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="420" y="448" width="170" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="505" y="468.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">ingest → 子任务 work_plan</text>
<text x="24" y="514" font-family="monospace" font-size="10" font-weight="bold" fill="#4ade80">① code runExecutor Agent</text>
<rect x="24" y="520" width="92" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="70" y="540.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">syncMain</text>
<text x="125" y="541" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="134" y="520" width="118" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="193" y="540.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">createWorktree</text>
<text x="261" y="541" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="270" y="520" width="100" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="320" y="540.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">runTask (CC)</text>
<text x="379" y="541" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="388" y="520" width="80" height="34" fill="#2a2206" stroke="#f0a500" stroke-width="1" rx="4"/>
<text x="428" y="540.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fbbf24">verify ✓</text>
<text x="477" y="541" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="486" y="520" width="108" height="34" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="540" y="540.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">★ checks 分项闸</text>
<text x="603" y="541" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="612" y="520" width="108" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="666" y="540.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">worktreeDiff</text>
<text x="729" y="541" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="738" y="520" width="100" height="34" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="788" y="540.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">★ diff 体量闸</text>
<text x="847" y="541" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="856" y="520" width="78" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="895" y="540.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">commit</text>
<text x="24" y="586" font-family="monospace" font-size="10" font-weight="bold" fill="#4ade80">② review runReviewer Agent · 独立、可选)</text>
<rect x="24" y="592" width="240" height="34" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="144" y="612.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">★ Promise.all 双复审(code + security</text>
<text x="273" y="613" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="282" y="592" width="210" height="34" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="387" y="612.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">★ verdict=reject → 硬拦截(回 code</text>
<text x="501" y="613" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="510" y="592" width="170" height="34" fill="#0f2038" stroke="#5599ff" stroke-width="1" rx="4"/>
<text x="595" y="612.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#93c5fd">waiting(review) 人审</text>
<text x="24" y="658" font-family="monospace" font-size="10" font-weight="bold" fill="#4ade80">③ pr runPR Agent · review 通过后串联)</text>
<rect x="24" y="664" width="110" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="79" y="684.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">创建 PR</text>
<text x="143" y="685" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="152" y="664" width="120" height="34" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="212" y="684.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">--no-ff 合并</text>
<text x="281" y="685" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="290" y="664" width="170" height="34" fill="#2a2206" stroke="#f0a500" stroke-width="1" rx="4"/>
<text x="375" y="684.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fbbf24">冲突 → conflict run</text>
<text x="469" y="685" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="478" y="664" width="90" height="34" fill="#0f2038" stroke="#5599ff" stroke-width="1" rx="4"/>
<text x="523" y="684.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#93c5fd">done ✓</text>
<text x="24" y="730" font-family="monospace" font-size="10" font-weight="bold" fill="#4ade80">conflict runConflict Agent · 抢占调度 · opus-4-8</text>
<rect x="24" y="736" width="160" height="30" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="104" y="754.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">git merge 制造冲突</text>
<text x="193" y="755" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="202" y="736" width="110" height="30" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="257" y="754.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">CC 解冲突</text>
<text x="321" y="755" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="330" y="736" width="100" height="30" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="380" y="754.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">★ 双复审</text>
<text x="439" y="755" text-anchor="middle" font-family="monospace" font-size="13" fill="#64748b"></text>
<rect x="448" y="736" width="90" height="30" fill="#10301a" stroke="#1e7a3e" stroke-width="1" rx="4"/>
<text x="493" y="754.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">commit</text>
<rect x="12" y="790" width="1156" height="150" fill="#160c28" stroke="#5a3a8a" stroke-width="1" rx="4"/>
<rect x="12" y="790" width="1156" height="22" fill="#2d1a4f" rx="4"/>
<rect x="12" y="804" width="1156" height="8" fill="#2d1a4f"/>
<text x="22" y="805" font-family="monospace" font-size="11" font-weight="bold" fill="#a78bfa">Claude Code AgentHeadless</text>
<rect x="28" y="820" width="210" height="98" fill="#241640" stroke="#5a3a8a" stroke-width="1" rx="4"/>
<text x="133" y="854" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#c4b5fd">cc.ts</text>
<text x="133" y="866.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">@anthropic-ai/</text>
<text x="133" y="879" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">claude-code SDK</text>
<text x="133" y="891.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#cbd5e1">for-await 流式</text>
<rect x="258" y="820" width="340" height="98" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="428" y="847.75" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fdba74">★ 模型档位(统一最强档)</text>
<text x="428" y="860.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">executor: sonnet / opus / opus</text>
<text x="428" y="872.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">planner-hard · reviewer · conflict → opus-4-8</text>
<text x="428" y="885.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">回退链 [opus-4-8, sonnet-4-6]</text>
<text x="428" y="897.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">fable-5 暂不可用,opus 充当最强档)</text>
<rect x="618" y="820" width="300" height="98" fill="#2a1c0a" stroke="#fb923c" stroke-width="1" rx="4"/>
<text x="768" y="854" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#fdba74">★ Agent 记忆注入 L1L4</text>
<text x="768" y="866.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">L4 全局规范 · L2 项目 agent_rules</text>
<text x="768" y="879" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">L3 拆解背景(父/兄弟)· L1 上次失败</text>
<text x="768" y="891.5" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#fdba74">daemon claim 时组装进 job.json</text>
<rect x="938" y="820" width="210" height="98" fill="none" stroke="#444" stroke-width="1" stroke-dasharray="4 3" rx="4"/>
<text x="1043" y="860.25" text-anchor="middle" font-family="monospace" font-size="9.5" font-weight="bold" fill="#64748b">★★ onToken callback</text>
<text x="1043" y="872.75" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">→ EventEmitter → SSE</text>
<text x="1043" y="885.25" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#64748b">[Phase 2]</text>
</svg>
</div>
<div class="card">
<div class="card-title">Phase 1 变更清单</div>
<table class="tbl">
<tr><th>文件</th><th>变更内容</th><th>类型</th></tr>
<tr><td>schema.sql + db.ts</td><td>tasks 状态列重构:<code>status→schedule_status</code> + <code>work_type/work_plan/work_history/scope/owned_files/expected_output/claimed_at/version</code>runs.kind 扩 <code>review|pr|conflict</code> + trace_idprojects 加 diff_max_files/checks/agent_rulesensureColumn 向后兼容旧库</td><td><span class="chg mod">变更</span></td></tr>
<tr><td>types.ts</td><td>新增 <code>ScheduleStatus</code> / <code>WorkType</code> / <code>WorkStep</code> 类型;Task 接口以 schedule_status × work_type + work_plan 队列取代旧 status 单态</td><td><span class="chg add">新增</span></td></tr>
<tr><td>mappers.ts</td><td>TaskRow/ProjectRow 追加字段;rowToTask/rowToProject 映射</td><td><span class="chg mod">变更</span></td></tr>
<tr><td>store.ts</td><td>casClaimTask(CAS)transition→ready 清 claimed_atrequeueTask 清零;assertNoActiveRun 执行期锁</td><td><span class="chg mod">变更</span></td></tr>
<tr><td>orchestrator.ts</td><td>claimOne 接 casClaimTaskrankByScore 传 inFlight+nowMs;按 <code>work_type</code> dispatch agentcode/plan/review/pr/conflict</td><td><span class="chg mod">变更</span></td></tr>
<tr><td>scoring.ts</td><td>rankU(CPM) + agingBonus(防饥饿) + couplingPenalty(文件耦合) + 新签名 rankByScore</td><td><span class="chg mod">变更</span></td></tr>
<tr><td>models.ts</td><td>planner-hard/reviewer/conflict → opus-4-8(最强档);回退链 [opus-4-8, sonnet-4-6]<span style="color:var(--text3)">(fable-5 暂不可用,恢复后改档位表即可)</span></td><td><span class="chg mod">变更</span></td></tr>
<tr><td>runner.ts + protocol.ts</td><td>decompose JSON 扩展 ownedFiles/expectedOutputprompt 更新</td><td><span class="chg mod">变更</span></td></tr>
<tr><td>ingest.ts</td><td>decompose-result 映射新字段到子任务</td><td><span class="chg mod">变更</span></td></tr>
<tr><td>pipeline.ts</td><td>code runchecks 分项闸(verify 后逐项执行)+ diff 体量闸(超 diffMaxFiles 硬失败);<strong>复审从 code pipeline 拆出</strong>为独立 reviewer run;新增 pr / conflict run 分支</td><td><span class="chg add">新增</span></td></tr>
</table>
</div>
<div class="diff-grid">
<div class="diff-panel before">
<h4>调度算法(重构前)</h4>
<pre>score(t) = <span class="key">baseScore</span>(priority)
+ Σ baseScore(d) <span class="cmt">// 已完成依赖</span>
+ Σ baseScore(b) <span class="cmt">// 被阻塞任务</span>
<span class="del">无防饥饿(低优先永远靠后)</span>
<span class="del">忽略文件耦合</span>
<span class="del">无 CAS 防双重 claim</span></pre>
</div>
<div class="diff-panel after">
<h4>调度算法(重构后)</h4>
<pre>score(t) = <span class="add">rankU(t)</span> <span class="cmt">// CPM 关键路径(最长链)</span>
+ <span class="add">agingBonus(t)</span> <span class="cmt">// 防饥饿,48h 线性到 base</span>
- <span class="add">couplingPenalty(t)</span> <span class="cmt">// 未合并 worktree 文件重叠 -0.5/个</span>
<span class="add">rankU(t) = baseScore(t.priority)</span>
<span class="add"> + max(rankU(d) | d 直接依赖我) // 取最长,非求和</span>
<span class="cmt">// 叶子无 dependent → 仅 baseScore;最长链封顶,不膨胀</span>
<span class="add">agingBonus = base × min(1, waitHours/48) // 48h 达 base</span>
<span class="add">casClaimTask: WHERE claimed_at IS NULL</span></pre>
</div>
</div>
<div class="card">
<div class="card-title">执行管道(重构后)— 按 work_type 拆独立 run</div>
<div style="font-size:11px;color:var(--text2);margin:0 0 10px">每个 work_type 是一次独立 Worker runrun 间由 <code>work_plan</code> 队列串联——复审<strong>不再内联</strong>进 executor,而是独立 reviewer run;合并独立成 pr run。</div>
<div style="font-size:11px;color:var(--violet);font-weight:600;margin:2px 0 3px">① code run</div>
<div class="pipe-stage">
<div class="pipe-box exec">syncMain</div><div class="pipe-arr"></div>
<div class="pipe-box exec">createWorktree</div><div class="pipe-arr"></div>
<div class="pipe-box exec">runTask (CC)</div><div class="pipe-arr"></div>
<div class="pipe-box gate">verify ✓</div><div class="pipe-arr"></div>
<div class="pipe-box gate-new">checks分项闸 ★</div><div class="pipe-arr"></div>
<div class="pipe-box exec">worktreeDiff</div><div class="pipe-arr"></div>
<div class="pipe-box gate-new">diff体量闸 ★</div><div class="pipe-arr"></div>
<div class="pipe-box exec">commit</div>
</div>
<div style="font-size:11px;color:var(--cyan);font-weight:600;margin:10px 0 3px">② review run(独立 reviewer run,可选)</div>
<div class="pipe-stage">
<div class="pipe-box review">Promise.all 双复审(code + security)★</div><div class="pipe-arr"></div>
<div class="pipe-box gate">verdict=reject → 硬拦截(回 code)★</div><div class="pipe-arr"></div>
<div class="pipe-box human">waiting(review) 人审</div>
</div>
<div style="font-size:11px;color:var(--green);font-weight:600;margin:10px 0 3px">③ pr runreview 通过后串联)</div>
<div class="pipe-stage">
<div class="pipe-box exec">创建 PR</div><div class="pipe-arr"></div>
<div class="pipe-box exec">--no-ff 合并</div><div class="pipe-arr"></div>
<div class="pipe-box gate">冲突 → conflict run</div><div class="pipe-arr"></div>
<div class="pipe-box human">done ✓</div>
</div>
<div style="font-size:11px;color:var(--text3);margin-top:8px">★=新增硬闸 · 复审为<strong>独立 reviewer run</strong>(非 code 内联),run 内 code+security 仍 Promise.all 并行 · <code>work_plan</code> 不含 review 时整段可跳过、code 完成直接进 pr</div>
</div>
<div class="card">
<div class="card-title">模型档位(重构后)</div>
<table class="tbl">
<tr><th>角色</th><th>Easy</th><th>Medium</th><th>Hard</th><th>备注</th></tr>
<tr><td>executor</td><td>claude-sonnet-4-6</td><td>claude-opus-4-8</td><td>claude-opus-4-8</td><td>不变</td></tr>
<tr><td>planner</td><td>claude-sonnet-4-6</td><td>claude-opus-4-8</td><td><span class="chg add">claude-opus-4-8</span></td><td>Hard 升最强档</td></tr>
<tr><td>reviewer</td><td><span class="chg add">claude-opus-4-8</span></td><td><span class="chg add">claude-opus-4-8</span></td><td><span class="chg add">claude-opus-4-8</span></td><td>三档统一最强,不被 project.model 降档</td></tr>
<tr><td>conflict</td><td colspan="3"><span class="chg add">claude-opus-4-8(固定,不降档)</span></td><td></td></tr>
<tr><td>回退链</td><td colspan="4"><span class="chg add">['claude-opus-4-8', 'claude-sonnet-4-6']</span></td></tr>
</table>
<div class="note" style="margin-top:10px;font-size:11px">⚠ 设计目标是「最强档」;<code>claude-fable-5</code> 暂不可用,当前一律以 <code>claude-opus-4-8</code> 充当最强档(commit a5a3c96 已全量替换)。fable-5 恢复后只需改 <code>models.ts</code> 档位表,结构不变。</div>
</div>
<div class="card">
<div class="card-title">Phase 2 预告(本次不含)</div>
<div class="grid-4">
<div class="box"><h4>SSE 流式</h4>
<div class="row">cc.ts onToken callback</div>
<div class="row">→ daemon EventEmitter</div>
<div class="row">→ GET /api/tasks/:id/stream</div>
<div class="row" style="color:var(--text3);font-size:11px">前端实时看 token</div>
</div>
<div class="box"><h4>Hook 合约</h4>
<div class="row">.maestro/hooks.ts</div>
<div class="row">MaestroHooks 接口</div>
<div class="row">5s 超时取消</div>
</div>
<div class="box"><h4>Trace ID</h4>
<div class="row">JobSpec.traceId</div>
<div class="row">outbox 每条携带</div>
<div class="row">跨进程可观测</div>
</div>
<div class="box"><h4>React 前端</h4>
<div class="row">React 18 + Vite</div>
<div class="row">Zustand 状态管理</div>
<div class="row">IBM Plex Mono 设计</div>
<div class="row">SSE token 显示</div>
</div>
</div>
</div>
</div><!-- /s-target -->
<!-- ═══════════════════════════════ ③ 实施计划 ═══════════════════════════════ -->
<!-- /s-plan -->
<script>
// theme
if(localStorage.getItem('maestro-theme')==='light')document.documentElement.classList.add('light');
document.getElementById('themeBtn').onclick=function(){
document.documentElement.classList.toggle('light');
localStorage.setItem('maestro-theme',document.documentElement.classList.contains('light')?'light':'dark');
};
// tabs
function showSec(id,tab){
document.querySelectorAll('.section').forEach(s=>s.classList.remove('visible'));
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
document.getElementById(id).classList.add('visible');
tab.classList.add('active');
document.getElementById('prog-wrap').style.display=(id==='s-design'||id==='s-impl')?'flex':'none';
setTimeout(function(){window.dispatchEvent(new Event('resize'));},80);
}
// task collapse
function toggleTask(id){document.getElementById(id).classList.toggle('collapsed');}
// steps
const STEPS={tc1:10,tc2:10,tc3:7,tc4:6,tc5:8,tc6:10};
const SK='maestro-p1-ck';
let state={};
try{state=JSON.parse(localStorage.getItem(SK)||'{}');}catch{}
function saveState(){localStorage.setItem(SK,JSON.stringify(state));}
function ck(el,tid){
el.classList.toggle('checked');
const idx=[...el.closest('ul').querySelectorAll('.chkbox')].indexOf(el);
if(!state[tid])state[tid]={};
state[tid][idx]=el.classList.contains('checked');
saveState();updateProg();
}
function updateProg(){
let total=0,done=0;
Object.entries(STEPS).forEach(([tid,max])=>{
total+=max;
const d=Object.values(state[tid]||{}).filter(Boolean).length;
done+=d;
const el=document.getElementById('dn-'+tid);
if(el)el.textContent=d;
});
const pct=total?Math.round(done/total*100):0;
document.getElementById('prog-fill').style.width=pct+'%';
document.getElementById('prog-pct').textContent=pct+'%';
}
Object.entries(STEPS).forEach(([tid])=>{
const ul=document.getElementById('sl-'+tid);
if(!ul)return;
ul.querySelectorAll('.chkbox').forEach((el,i)=>{
if((state[tid]||{})[i])el.classList.add('checked');
});
});
updateProg();
</script>
<script>
// design sub-nav active state
(function(){
const links=document.querySelectorAll('.design-nav a[data-ch]');
if(!links.length)return;
const obs=new IntersectionObserver(entries=>{
entries.forEach(e=>{
if(e.isIntersecting){
const id=e.target.id;
links.forEach(l=>l.classList.toggle('act',l.dataset.ch===id));
}
});
},{rootMargin:'-10% 0px -80% 0px'});
['d-overview','d-statechart','d-db','d-memory','d-agentspec','d-ops','d-security','d-api'].forEach(id=>{
const el=document.getElementById(id);
if(el)obs.observe(el);
});
})();
</script>
<!-- ═══════════════════════════════ ⑦ schedule × work_type 状态机 ═══════════════════════════════ -->
<!-- end #s-statechart -->
<!-- ═══════════════════ ④ 完整设计方案(合并 实施计划 + DB ER + Agent 记忆 + Statechart)═══════════════════ -->
<div class="section" id="s-design">
<div class="design-sticky">
<div class="sec-heading">完整设计方案</div>
<div class="sec-desc">Maestro 大重构的完整技术蓝图:schedule × work_type 状态机架构 · 数据库 Schema 变更 · Agent 记忆注入 · Phase 1 实施路线。</div>
<!-- 内部导航 -->
<nav class="design-nav">
<a href="#d-overview" data-ch="d-overview">① 架构决策总览</a>
<a href="#d-statechart" data-ch="d-statechart">② 状态设计</a>
<a href="#d-db" data-ch="d-db">③ 数据库 Schema</a>
<a href="#d-memory" data-ch="d-memory">④ Agent 记忆注入</a>
<a href="#d-agentspec" data-ch="d-agentspec">⑤ Agent 规格</a>
<a href="#d-ops" data-ch="d-ops">⑥ 运维与护栏</a>
<a href="#d-security" data-ch="d-security">⑦ 安全与沙箱</a>
<a href="#d-api" data-ch="d-api">⑧ 前端 API 契约</a>
</nav>
</div><!-- /.design-sticky -->
<!-- ══════════ ① 架构决策总览 ══════════ -->
<a class="chapter-anchor" id="d-overview"></a>
<div class="chapter-hdr"><span></span> 架构决策总览</div>
<div class="note" style="margin:0 0 16px">🗂 <strong>分期标注</strong>:各章头部 <span class="phase p1" style="margin:0">Phase 1</span> = 本期核心重构(对应 ④具体实现 T1–T6);<span class="phase p2" style="margin:0">Phase 2</span> = 能力扩展(成本预算 / 安全沙箱 / 交互接管 / SSE 等);<span class="phase mix" style="margin:0">混合</span> = 章内分属两期,以卡片内标注为准。标注为<strong>建议范围</strong>,最终以实施计划为准。</div>
<div class="card">
<div class="card-title">六大核心决策</div>
<table class="tbl">
<tr><th>#</th><th>决策</th><th>选择</th><th>关键理由</th></tr>
<tr><td><span class="badge cyan">D1</span></td><td>状态机架构</td><td><strong>schedule_status × work_type 双轴</strong>,替代平铺 FSM</td><td>schedule_status 对齐 OS 进程模型(OS 五态),描述资源调度;work_type 描述当前执行何种工作(plan/code/review/pr…);执行路径由混合模式动态决定,无需静态枚举所有路径</td></tr>
<tr><td><span class="badge amber">D2</span></td><td>任务类型扩展</td><td><strong>work_type 注册表</strong>(新类型 = 加 agent + dispatcher 分支)</td><td>核心代码零改动;schedule_status 通用状态(ready/running/waiting…)完全复用,无需改状态机</td></tr>
<tr><td><span class="badge violet">D3</span></td><td>DB 状态存储</td><td><strong>schedule_status + work_type + work_plan 队列(JSON</strong></td><td>schedule_status / work_type 双列供 SQL 索引快速过滤与调度;work_plan 队列自带恢复位置,<strong>不引入 XState snapshot</strong></td></tr>
<tr><td><span class="badge green">D4</span></td><td>Agent 记忆</td><td><strong>四层注入</strong>(L1 失败原因 / L2 项目规范 / L3 跨任务上下文 / L4 全局规范)</td><td>worker 不碰 DB,所有记忆由 daemon claimOne 组装进 job.jsonROI 由高到低</td></tr>
<tr><td><span class="badge red">D5</span></td><td>流程硬闸</td><td>复审 <strong>reject 变硬闸</strong> + <strong>分项 checks</strong> + <strong>diff 体量闸</strong></td><td>消除"复审仅建议"漏洞;静态检查前置防止无效执行进审批</td></tr>
<tr><td><span class="badge cyan">D6</span></td><td>并发调度</td><td><strong>CAS claimed_at</strong> + <strong>couplingPenalty(文件重叠降分)</strong></td><td>防双重 claim 竞态;相同文件的任务并发执行概率降低,冲突减少</td></tr>
</table>
</div>
<div class="card">
<div class="card-title">重构前后对比快览</div>
<div class="diff-grid">
<div class="diff-panel before">
<h4>现有设计(平铺 FSM</h4>
<pre>• 16 状态平铺,全局 TRANSITIONS 表
• CANCEL/FAIL 每个子状态重复声明
• code + security 复审串行
• 暂停恢复需 paused_return_to 字段
• 新任务类型 = 修改核心转移表
• claimed_at 无 CAS,存在双重领取竞态
• reviewer reject 不挡流,仅建议
• agent prompt 无失败记忆 / 无规范注入</pre>
</div>
<div class="diff-panel after">
<h4>重构后(schedule × work_type + 四层记忆)</h4>
<pre class="add">• schedule_status × work_type 双轴正交状态
• work_plan 队列动态决定执行路径(混合模式)
• waiting 人工闸不占并发名额,running 才占
• (paused, work_type) 自带恢复位置,无需额外字段
• 新 work_type = 加 agent + dispatcher 分支,零状态机改动
• CAS claimed_at + couplingPenalty 调度
• reviewer reject = 硬闸,回退重执行
• L1-L4 记忆注入 + 项目/全局规范</pre>
</div>
</div>
</div>
<div class="card">
<div class="card-title">进程模型铁律(不改)</div>
<div class="grid-2">
<div class="box cyan">
<h4>Daemon — 唯一 DB 写者</h4>
<div class="row">REST + WebSocket API:4517</div>
<div class="row">Orchestrator 调度循环(ingest → reap → claim</div>
<div class="row">写 job.json 时组装全部记忆(L1-L4</div>
<div class="row">读 outbox.ndjson 入 DB,状态机 send(event)</div>
</div>
<div class="box green">
<h4>Worker — 不碰 DB</h4>
<div class="row">读 runs/&lt;runId&gt;/job.json</div>
<div class="row">执行 agent pipeline,追加 outbox.ndjson</div>
<div class="row">任何 DB 数据 → daemon 组装进 job.json 下发</div>
<div class="row">状态机 actor 在 daemon 侧管理</div>
</div>
</div>
</div>
<!-- ══════════ ② Statechart 设计 ══════════ -->
<a class="chapter-anchor" id="d-statechart"></a>
<div class="chapter-hdr"><span></span> 状态设计(schedule × work_type<span class="phase p1">Phase 1</span></div>
<div class="card">
<div class="card-title">任务提交与入口(Ingestion</div>
<p class="lead" style="font-size:12px;margin:0 0 12px;color:var(--text2)">提交方式<strong>不变</strong>——主入口仍是<strong>网页表单</strong>。提交后任务落 <code>(init, 无 work_type)</code>,再由系统赋 work_type / 产 work_plan 进入调度。</p>
<h4 style="margin:14px 0 8px">三条入口</h4>
<table class="tbl">
<tr><th style="width:160px">入口</th><th>路径</th><th>用途</th></tr>
<tr><td>网页表单</td><td>「+ 新建任务」→ <code>POST /api/projects/:id/tasks</code></td><td>人手动提交(<strong>主入口</strong></td></tr>
<tr><td>MCP 工具</td><td><code>create_task</code> / <code>decompose_task</code></td><td>Claude Code 会话 / agent 编程建任务</td></tr>
<tr><td>todo.json 同步</td><td><code>src/sync/</code> 单向同步</td><td>legacy <code>/todo</code> 数据自动导入</td></tr>
<tr><td style="color:var(--text3)">GitHub issue(未来)</td><td style="color:var(--text3)">webhook 入站 → 建任务</td><td style="color:var(--text3)">TODO #1 双向同步,本期不含</td></tr>
</table>
<h4 style="margin:18px 0 8px">提交字段(极简 + 可选增强)</h4>
<table class="tbl">
<tr><th style="width:230px">字段</th><th style="width:110px">必填?</th><th>说明</th></tr>
<tr><td><code>title</code></td><td>✅ 必填</td><td>唯一硬要求</td></tr>
<tr><td><code>complexity</code></td><td>默认 <code>auto</code></td><td><code>auto</code> = LLM 异步分类回填(hard/medium/easy);也可手填</td></tr>
<tr><td><code>priority</code> / <code>deps</code></td><td>可选</td><td>缺省 P2 / 无依赖</td></tr>
<tr><td><code>owned_files</code> / <code>scope</code> / <code>expected_output</code></td><td>可选</td><td>新模型增强:喂调度 couplingPenalty + 给 planner 更准起点;<strong>缺省由 planner 补全</strong></td></tr>
</table>
<div class="note" style="margin-top:8px;font-size:11px">设计取向:<strong>提交极简(只逼 title),质量靠 planner 兜</strong>;可选字段让愿意填的人给更强的调度/拆解信号,但从不强制。</div>
<h4 style="margin:18px 0 8px">提交后流转</h4>
<div class="pipe-stage">
<div class="pipe-box human">提交</div><div class="pipe-arr"></div>
<div class="pipe-box exec">(init, 无 work_type)</div><div class="pipe-arr"></div>
<div class="pipe-box review">plan runplanner 产 work_plan</div><div class="pipe-arr"></div>
<div class="pipe-box gate">(ready, 首 work_type)</div><div class="pipe-arr"></div>
<div class="pipe-box exec">调度</div>
</div>
<div class="note" style="margin-top:6px">Hard/Medium 走 <code>plan</code>planner 拆解 / 写 spec 产 work_plan 队列);<strong>Easy 跳过 plan</strong>,直接 <code>work_plan=[code, review, pr]</code> → ready。<code>autonomy</code>manual / auto-easy / auto-approved)决定提交后是<strong>自动起跑</strong>还是<strong>等人放行</strong></div>
<h4 style="margin:18px 0 8px">附件支持(图片 / 文件)<span class="phase p2" style="margin-left:8px">Phase 2</span></h4>
<table class="tbl">
<tr><th style="width:140px"></th><th>设计</th></tr>
<tr><td>表单</td><td>「+ 新建任务」支持<strong>拖拽 / 粘贴截图 + 上传文件</strong>(bug 截图、设计稿、规格文档、数据样本)</td></tr>
<tr><td>API / 存储</td><td><code>POST /api/tasks/:id/attachments</code>multipart)→ 存 <code>tasks/&lt;taskId&gt;/attachments/</code><code>tasks.attachments</code> JSON 存 <code>[{name, type, path}]</code></td></tr>
<tr><td>喂给 agent</td><td>附件路径随 job.json 下发 → worker 把<strong>图片直接喂进 agent 多模态输入</strong>、文件作为 Read 目标;附件是任务上下文的一等部分(并入 ⑤ AgentContext</td></tr>
</table>
<h4 style="margin:18px 0 8px">AI 提任务(agent-initiated<span class="phase p2" style="margin-left:8px">Phase 2</span></h4>
<p style="font-size:12px;margin:0 0 8px;color:var(--text2)">已部分存在(MCP <code>create_task</code> + planner decompose 产子任务)。扩展为一等能力:</p>
<table class="tbl">
<tr><th style="width:140px">来源</th><th>设计</th></tr>
<tr><td>执行中衍生</td><td>agent 执行时发现额外工作 → 经 MCP 提<strong>关联任务</strong>parent / sibling),不打断当前 run</td></tr>
<tr><td>巡检 agent</td><td>可注册的 groomer agentwork_type,呼应 ⑤ 动态扩展)周期扫 repo / backlog / issue → 提议任务</td></tr>
<tr><td>来源标记</td><td><code>tasks.source</code>humanaisyncgithub+ <code>created_by</code>(提交它的 agent run id),区分人提 vs AI 提</td></tr>
</table>
<div class="note" style="margin-top:8px">🛡 <strong>护栏</strong>AI 提的任务默认进<strong>「待确认新任务」人工闸</strong>(不自动起跑),人审后才入调度——防 AI 自我增殖任务失控烧钱(呼应 ⑥ 预算护栏)。是否需人审由项目 <code>autonomy</code> + AI-task 开关决定;纯子任务拆解(planner decompose)已有 plan_review 闸覆盖。</div>
</div>
<!-- ── schedule_status 状态说明 ── -->
<div class="card">
<div class="card-title">schedule_status(调度状态,对齐 OS 进程模型)</div>
<table class="tbl">
<tr><th>状态</th><th>OS 类比</th><th>含义</th><th>可用 work_type</th></tr>
<tr><td><span class="badge cyan">init</span></td><td>New</td><td>任务刚创建,尚未确定执行路径,无 work_type</td><td></td></tr>
<tr><td><span class="badge green">ready</span></td><td>Ready</td><td>有 work_type,等待 Orchestrator 调度</td><td>任意</td></tr>
<tr><td><span class="badge violet">running</span></td><td>Running</td><td>Worker 正在执行当前 work_type 对应的 agent</td><td>任意</td></tr>
<tr><td><span class="badge amber">waiting</span></td><td>Blocked (I/O)</td><td>等待<strong>人工输入</strong>(审批/拒绝),无 agent 在运行</td><td>保持完成步骤的 work_type(如 plan / review</td></tr>
<tr><td><span class="badge red">blocked</span></td><td>Blocked (deps)</td><td>等待<strong>任务依赖</strong>完成,deps 满足后自动回 ready</td><td>保持上次 work_type</td></tr>
<tr><td><span class="badge amber">paused</span></td><td>Stopped</td><td>人工暂停,恢复时回到 ready(保持 work_type</td><td>保持上次 work_type</td></tr>
<tr><td><span class="badge red">failed</span></td><td>Zombie</td><td>当前步骤失败,退避后自动重试(回 ready);超重试上限 → stuck</td><td>同失败时 work_type</td></tr>
<tr><td><span class="badge" style="color:#f97316;border-color:#f97316">stuck</span></td><td>Stopped (wait human)</td><td>任务卡住需<strong>人工介入</strong>:①重试耗尽 ②agent 主动上报歧义/不可解 ③审批超时兜底。自动化停止,等人决策</td><td>保持卡住时 work_type</td></tr>
<tr><td style="color:var(--text3)"><span class="badge" style="color:var(--text3);border-color:var(--text3)">done</span></td><td>Terminated</td><td>work_plan 队列全部完成,终态</td><td></td></tr>
<tr><td style="color:var(--text3)"><span class="badge" style="color:var(--text3);border-color:var(--text3)">cancelled</span></td><td>Killed</td><td>人工取消,终态</td><td></td></tr>
</table>
<div class="note"><strong>stuck</strong> 是一等 schedule_status 值,直接存 DB 列——可用 <code>WHERE schedule_status = 'stuck'</code> 直接查,无需 retry_count 联合判断。人工处理完成后可转 ready / done / cancelled。</div>
</div>
<!-- ── work_type 目录 ── -->
<div class="card">
<div class="card-title">work_type(工作类型,决定用哪个 Agent)</div>
<div class="grid-2">
<div>
<h4 style="color:var(--cyan);margin:0 0 10px">Agent 类型(schedule = ready → running</h4>
<table class="tbl">
<tr><th>work_type</th><th>执行者</th><th>主要工作</th></tr>
<tr><td><code>plan</code></td><td>Planner Agent</td><td>分析任务,产出初始 work_plan(步骤队列 + 每步 spec</td></tr>
<tr><td><code>code</code></td><td>Executor Agent</td><td>在 worktree 里实现代码变更,commit</td></tr>
<tr><td><code>review</code></td><td>Reviewer Agent</td><td>代码审查 + 安全审计(run 内 Promise.all 并行),输出 verdict。<strong>独立 run</strong>、不并入 code<strong>可选</strong>work_plan 可省略)</td></tr>
<tr><td><code>pr</code></td><td>PR Agent</td><td>review 通过后创建 PR + 完成合并(冲突 → conflict)</td></tr>
<tr><td><code>conflict</code></td><td>Conflict Agent</td><td>git merge 制造冲突 → CC 解 → 复审</td></tr>
<tr><td><code>research</code></td><td>Research Agent</td><td>技术调研,产出报告(无代码修改)</td></tr>
</table>
</div>
<div>
<h4 style="color:var(--amber);margin:0 0 10px">人工闸(schedule = waiting,无 Agent 运行)</h4>
<div class="box amber" style="margin-bottom:12px">
<p style="font-size:12px;color:var(--text2);margin:0"><strong>waiting 的 work_type 直接沿用上一步完成时的值</strong>,不引入新枚举——<code>waiting(plan)</code> 等人审 plan 输出,<code>waiting(review)</code> 等人审代码复审结果。</p>
</div>
<table class="tbl">
<tr><th>当前 work_type</th><th>等待内容</th><th>通过 → 下一步</th><th>拒绝 → 下一步</th></tr>
<tr><td><code>plan</code></td><td>人审 Planner 产出的 work_plan</td><td>pop 队头,进 ready(下一 work_type)</td><td>重回 ready(plan),带拒绝原因重规划</td></tr>
<tr><td><code>review</code></td><td>人审执行结果 + 复审 verdict</td><td>pop → pr 步(提 PR + 合并)→ done</td><td>ready(code),带拒绝原因重做</td></tr>
</table>
<div class="box amber" style="margin-top:12px">
<h4>为什么 waiting ≠ running</h4>
<p style="font-size:12px;color:var(--text2);margin:0">人工闸没有 worker 进程在运行,Orchestrator 不应把并发名额算在它上面;running 才占名额,waiting 不占。</p>
</div>
</div>
</div>
</div>
<!-- ── 混合模式:下一步如何决定 ── -->
<div class="card">
<div class="card-title">混合模式:下一步如何决定(Hybrid Next-Step</div>
<div class="grid-2">
<div>
<h4 style="color:var(--cyan);margin:0 0 10px">Plan 阶段:产出初始 work_plan 队列</h4>
<pre style="font-size:11px;background:var(--code-bg);padding:12px;border-radius:6px;line-height:1.7">{
"type": "plan-complete",
"work_plan": [
{ "type": "code",
"spec": "实现 auth 模块",
"owned_files": ["src/auth/*.ts"],
"expected_output": "typecheck 通过" },
{ "type": "review" },
{ "type": "pr",
"spec": "创建 PR 到 main" }
]
}
// → waiting(plan) 等人审计划
// 人 approve → pop 队头 → ready(code)</pre>
</div>
<div>
<h4 style="color:var(--green);margin:0 0 10px">执行阶段:每步可修改剩余队列</h4>
<pre style="font-size:11px;background:var(--code-bg);padding:12px;border-radius:6px;line-height:1.7">{
"type": "step-complete",
"result": "发现 auth 复杂,拆成2步",
"update_plan": [ // ← 可选,修改剩余队列
{ "type": "code",
"spec": "先实现 session 层" },
{ "type": "code", // ← 动态插入新步骤
"spec": "再实现 token 层" },
{ "type": "review" }
]
}
// 无 update_plan → pop 队头继续
// update_plan 非空 → 替换剩余队列</pre>
</div>
</div>
<h4 style="color:var(--cyan);margin:16px 0 8px">队列为空时</h4>
<div class="grid-2">
<div class="box green">
<p style="font-size:12px;color:var(--text2);margin:0">step-complete 后 <code>work_plan</code> 队列为空 → <code>schedule_status = done</code>,任务完成。</p>
</div>
<div class="box amber">
<p style="font-size:12px;color:var(--text2);margin:0">Easy 任务无需 plan 步骤:init 时 Orchestrator 直接写入默认队列 <code>[code, review]</code>,跳过 plan。</p>
</div>
</div>
</div>
<!-- ── 完整状态转移规则 ── -->
<div class="card">
<div class="card-title">完整转移规则</div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1160 520" style="width:100%;display:block;margin:16px 0;">
<defs>
<marker id="ag" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#82b366"/></marker>
<marker id="ao" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#d6783b"/></marker>
<marker id="ar" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#b85450"/></marker>
<marker id="ap" markerWidth="7" markerHeight="6" refX="6" refY="3" orient="auto"><polygon points="0 0,7 3,0 6" fill="#9673a6"/></marker>
</defs>
<!-- ── 主干(绿,自动) ── -->
<path d="M 146 210 L 248 210" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#ag)" />
<path d="M 352 210 L 464 210" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#ag)" />
<path d="M 576 210 L 691 210" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#ag)" />
<path d="M 799 210 L 917 210" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#ag)" />
<!-- pr merge 顶弧(绿)running→done -->
<path d="M 548 198 Q 745 100 940 198" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#ag)" />
<!-- retry 回流(绿)failed→ready -->
<path d="M 500 394 Q 400 308 332 228" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#ag)" />
<!-- deps✓(绿)blocked→ready -->
<path d="M 312 392 L 312 230" stroke="#82b366" stroke-width="1.5" fill="none" marker-end="url(#ag)" />
<!-- ── 人工(橙) ── -->
<!-- approve/reject 顶弧 waiting→ready(高过 paused -->
<path d="M 745 192 Q 430 -120 300 192" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#ao)" />
<!-- agent 上报 running→stuck -->
<path d="M 542 228 Q 650 320 722 393" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#ao)" />
<!-- 人工: 重试 stuck→ready(低位绕行,避开中部交叉) -->
<path d="M 712 430 C 540 500 300 500 200 445 C 175 380 205 250 272 228" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#ao)" />
<!-- 人工: 完成 stuck→done -->
<path d="M 782 396 Q 862 326 942 228" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#ao)" />
<!-- 人工: 取消 stuck→cancelled -->
<path d="M 795 410 L 903 410" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#ao)" />
<!-- 暂停 running→paused -->
<path d="M 502 193 Q 458 148 446 104" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#ao)" />
<!-- 恢复 paused→ready -->
<path d="M 396 92 C 300 108 286 165 288 191" stroke="#d6783b" stroke-width="1.5" fill="none" marker-end="url(#ao)" />
<!-- ── 异常(红) ── -->
<path d="M 520 228 L 520 392" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#ar)" />
<path d="M 572 410 L 695 410" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#ar)" />
<path d="M 288 228 L 288 392" stroke="#b85450" stroke-width="1.5" fill="none" marker-end="url(#ar)" />
<!-- ── 自动串联(紫虚线)running 自环 ── -->
<path d="M 498 194 C 498 158 542 158 542 194" stroke="#9673a6" stroke-width="1.5" fill="none" stroke-dasharray="4,3" marker-end="url(#ap)" />
<!-- ── 节点 ── -->
<ellipse cx="100" cy="210" rx="46" ry="18" fill="#2a2a2a" stroke="#888888"/>
<text x="100" y="214" text-anchor="middle" font-family="monospace" font-size="11.5" fill="#aaaaaa">init</text>
<ellipse cx="300" cy="210" rx="52" ry="18" fill="#1a2e1a" stroke="#82b366"/>
<text x="300" y="214" text-anchor="middle" font-family="monospace" font-size="11.5" fill="#82b366">ready</text>
<ellipse cx="520" cy="210" rx="56" ry="18" fill="#221a2e" stroke="#9673a6"/>
<text x="520" y="214" text-anchor="middle" font-family="monospace" font-size="11.5" fill="#c4a8d4">running</text>
<ellipse cx="745" cy="210" rx="54" ry="18" fill="#2e2a14" stroke="#d6b656"/>
<text x="745" y="214" text-anchor="middle" font-family="monospace" font-size="11.5" fill="#d6b656">waiting</text>
<ellipse cx="965" cy="210" rx="48" ry="18" fill="#1a472a" stroke="#2d7a40"/>
<text x="965" y="214" text-anchor="middle" font-family="monospace" font-size="11.5" font-weight="bold" fill="#ffffff">done</text>
<ellipse cx="440" cy="85" rx="54" ry="18" fill="#2e2a14" stroke="#d6b656"/>
<text x="440" y="89" text-anchor="middle" font-family="monospace" font-size="11.5" fill="#d6b656">paused</text>
<ellipse cx="300" cy="410" rx="56" ry="18" fill="#2e1a1a" stroke="#b85450"/>
<text x="300" y="414" text-anchor="middle" font-family="monospace" font-size="11.5" fill="#e07070">blocked</text>
<ellipse cx="520" cy="410" rx="52" ry="18" fill="#2e1a1a" stroke="#b85450"/>
<text x="520" y="414" text-anchor="middle" font-family="monospace" font-size="11.5" fill="#e07070">failed</text>
<ellipse cx="745" cy="410" rx="50" ry="18" fill="#2e2014" stroke="#d6783b"/>
<text x="745" y="414" text-anchor="middle" font-family="monospace" font-size="11.5" fill="#f97316">stuck</text>
<ellipse cx="965" cy="410" rx="62" ry="18" fill="#2e2e2e" stroke="#555555"/>
<text x="965" y="414" text-anchor="middle" font-family="monospace" font-size="11.5" font-weight="bold" fill="#888888">cancelled</text>
<!-- ── 边标签 ── -->
<text x="197" y="202" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#666">写入队列</text>
<text x="408" y="202" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#666">claim</text>
<text x="633" y="202" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#666">plan/review done</text>
<text x="858" y="202" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#666">review approve→pr</text>
<text x="430" y="16" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">approve / reject</text>
<text x="745" y="90" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#666">pr merge</text>
<text x="520" y="150" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#9673a6">code→review (auto)</text>
<text x="538" y="318" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">error</text>
<text x="392" y="338" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#666">retry</text>
<text x="633" y="402" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">retry 耗尽</text>
<text x="648" y="300" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">agent 上报</text>
<text x="430" y="478" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">人工: 重试</text>
<text x="878" y="318" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">人工: 完成</text>
<text x="849" y="402" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">人工: 取消</text>
<text x="266" y="318" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#b85450">deps⬇</text>
<text x="334" y="318" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#666">deps✓</text>
<text x="430" y="140" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">暂停</text>
<text x="296" y="128" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#d6783b">恢复</text>
<!-- ── 图例 ── -->
<line x1="1058" y1="150" x2="1090" y2="150" stroke="#82b366" stroke-width="1.5" marker-end="url(#ag)"/>
<text x="1095" y="154" font-family="sans-serif" font-size="9" fill="#82b366">自动</text>
<line x1="1058" y1="172" x2="1090" y2="172" stroke="#d6783b" stroke-width="1.5" marker-end="url(#ao)"/>
<text x="1095" y="176" font-family="sans-serif" font-size="9" fill="#d6783b">人工</text>
<line x1="1058" y1="194" x2="1090" y2="194" stroke="#b85450" stroke-width="1.5" marker-end="url(#ar)"/>
<text x="1095" y="198" font-family="sans-serif" font-size="9" fill="#b85450">异常</text>
<line x1="1058" y1="216" x2="1090" y2="216" stroke="#9673a6" stroke-dasharray="4,3" stroke-width="1.5" marker-end="url(#ap)"/>
<text x="1095" y="220" font-family="sans-serif" font-size="9" fill="#9673a6">自动串联</text>
</svg>
</div>
<!-- ── 执行路径示例 ── -->
<div class="card">
<div class="card-title">执行路径示例</div>
<div class="grid-2">
<div>
<h4 style="color:var(--amber);margin:0 0 8px">Hard 任务(需规划)</h4>
<div class="arch-diagram" style="font-size:10.5px;line-height:1.9;margin:0">
(init, -)
↓ Orchestrator 写队列 [plan, code, review, pr]
(ready, plan)
↓ worker 启动
(running, plan)
↓ plan 产出 work_plan(可拆多 code 步 / 决定免 review
(waiting, plan)
↓ 人 approve
(ready, code) → (running, code)
↓ code agent 完成(commitrun 结束)
(running, review) <span style="color:var(--text3)">← 自动串联(独立 reviewer run,非 code 内联)</span>
↓ review 完成(run 内并行双复审 verdict
(waiting, review)
↓ 人 approve
(running, pr) <span style="color:var(--text3)">← approve 后串联</span>
↓ pr agent 提 PR + 合并(冲突→conflict run
done ✓
</div>
</div>
<div>
<h4 style="color:var(--green);margin:0 0 8px">Easy 任务(跳过规划)</h4>
<div class="arch-diagram" style="font-size:10.5px;line-height:1.9;margin:0">
(init, -)
↓ Orchestrator 写队列 [code, review, pr]
(ready, code)
↓ worker 启动
(running, code)
↓ code agent 完成
(running, review) <span style="color:var(--text3)">← 自动串联(独立 reviewer run</span>
↓ review agent 完成
(waiting, review)
↓ 人 approve
(running, pr) → 提 PR + 合并 → done ✓
</div>
<div class="box amber" style="margin-top:10px"><p style="font-size:11px;color:var(--text2);margin:0"><strong>review 可选</strong>work_plan 不含 review 时,code 完成 → 直接 (running, pr);纯只读任务可仅 <code>[research]</code>,无 code/review/pr。</p></div>
<h4 style="color:var(--cyan);margin:16px 0 8px">动态拆步(运行中修改计划)</h4>
<div class="arch-diagram" style="font-size:10.5px;line-height:1.9;margin:0">
(running, code)
↓ agent 发现比预期复杂
↓ step-complete + update_plan:
[code(token层), review]
(ready, code) <span style="color:var(--text3)">← 新步骤(session 层)</span>
(running, code) <span style="color:var(--text3)">← 再次 codetoken 层)</span>
(running, review) → (waiting, review) → done ✓
</div>
</div>
</div>
</div>
<!-- ── DB 设计 ── -->
<div class="card">
<div class="card-title">DB 字段(tasks 表关键变更)</div>
<div class="grid-2">
<div>
<table class="tbl">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td><code>schedule_status</code></td><td>TEXT</td><td>取代旧 <code>status</code>init|ready|running|waiting|blocked|paused|failed|<strong>stuck</strong>|done|cancelled</td></tr>
<tr><td><code>work_type</code></td><td>TEXT</td><td>当前工作类型:plan|code|review|pr|conflict|research|null</td></tr>
<tr><td><code>work_plan</code></td><td>TEXT JSON</td><td>剩余步骤队列 <code>[{type, spec, owned_files, expected_output}]</code></td></tr>
<tr><td><code>work_history</code></td><td>TEXT JSON</td><td>已完成步骤记录(审计+重试上下文)</td></tr>
<tr><td><code>claimed_at</code></td><td>TEXT</td><td>CAS 锁,NULL=可领取,非NULL=已锁</td></tr>
</table>
</div>
<div>
<h4 style="color:var(--cyan);margin:0 0 10px">Orchestrator 关键查询</h4>
<pre style="font-size:11px;background:var(--code-bg);padding:12px;border-radius:6px;line-height:1.7">-- 可调度任务(schedule 维度)
SELECT * FROM tasks
WHERE schedule_status = 'ready'
AND claimed_at IS NULL
AND (next_eligible_at IS NULL
OR next_eligible_at &lt;= datetime('now'))
ORDER BY score DESC;
-- 派发 agentwork_type 维度)
switch (task.work_type) {
case 'plan': return runPlannerAgent(task);
case 'code': return runExecutorAgent(task);
case 'review': return runReviewerAgent(task);
case 'pr': return runPRAgent(task);
case 'conflict':return runConflictAgent(task);
}</pre>
</div>
</div>
<div class="note" style="margin-top:12px"><strong>waiting(人工闸)不占并发名额</strong>Orchestrator 的 <code>inflight</code> 计数只统计 <code>schedule_status=running</code> 的任务;<code>waiting</code> 的任务不计入,也不占 <code>project.concurrency</code></div>
</div>
<!-- ── 与旧平铺 FSM 对比 ── -->
<div class="card">
<div class="card-title">与旧设计对比</div>
<table class="tbl">
<tr><th>维度</th><th>旧设计(平铺 FSM</th><th>新设计(schedule × work_type</th></tr>
<tr><td>状态数量</td><td>16 个混合状态(analyzing/speccing/ready/executing/exec_review…)</td><td>10 个 schedule_status × N 个 work_type,正交组合</td></tr>
<tr><td>执行路径</td><td>静态,TRANSITIONS 表写死</td><td>动态,work_plan 队列运行时决定</td></tr>
<tr><td>新增工作类型</td><td>改 TRANSITIONS 表 + 加 status 枚举</td><td>加 work_type 枚举 + 加 agent dispatcher 分支</td></tr>
<tr><td>人工审批闸</td><td>plan_review/spec_review/exec_review 是独立 status(额外枚举)</td><td>waiting(plan) / waiting(review),直接沿用卡住时 work_type,无新枚举</td></tr>
<tr><td>stuck</td><td>逻辑态:failed + retry_count ≥ max_retries 联合推导,无独立 DB 值</td><td><strong>一等状态</strong>:独立 DB 值,直接查询;人工处理后出 ready / done / cancelled</td></tr>
<tr><td>并发占用</td><td>所有非终态都计入 inflight</td><td>只有 running 计入 inflightwaiting / stuck 不占名额</td></tr>
<tr><td>暂停恢复</td><td>需 paused_return_to 字段记录位置</td><td>(paused, work_type) 自带位置,恢复即 (ready, same work_type)</td></tr>
<tr><td>可观测性</td><td>一个 status 字段,需要枚举含义</td><td>两个字段,(running, code) 直接表达"正在写代码"</td></tr>
<tr><td>实现复杂度</td><td>中(平铺 transition 守卫)</td><td>低(两字段 + 队列 pop/push</td></tr>
</table>
</div>
<!-- ── 并发调度 ── -->
<div class="card">
<div class="card-title">并发调度(调度单位 · CAS 防双领 · couplingPenalty 错峰)</div>
<table class="tbl">
<tr><th>维度</th><th>决策</th></tr>
<tr><td>最小调度单位</td><td><strong>task</strong>(一次一个 work_step);并发发生在不同 task 之间,不在 task 内部。work_plan 队列<strong>严格线性</strong></td></tr>
<tr><td>阶段内并行</td><td>判据 = 是否需要<strong>独立 worktree / 写权限</strong>:只读·汇总型 → agent 内 <code>Promise.all</code>(占 1 槽);需独立写 → 拆子任务交调度器(占 N 槽)</td></tr>
<tr><td>并行建模</td><td>并行 = <strong>拆子任务</strong><code>parent decomposed → 子全 done → done</code> 做 join;不引入队列内并行分支节点(保队列模型简洁)</td></tr>
<tr><td>防双重 claim</td><td>CAS <code>claimed_at</code>(见下);即「执行期锁」的 DB 落地</td></tr>
<tr><td>文件错峰</td><td>couplingPenalty(见下):与<strong>所有持未合并 worktree 的任务</strong>running + waiting(review) + 带改动 failed)的 owned_files 重叠扣分,软错峰非硬禁;agingBonus 兜底防饥饿</td></tr>
<tr><td>并发预算</td><td>槽只认 <code>running</code>;agent 内并行不额外占槽,调度器并行占 N 槽,<code>project.concurrency</code> 封顶</td></tr>
</table>
<div class="grid-2" style="margin-top:16px">
<div>
<h4 style="color:var(--cyan);margin:0 0 8px">CAS 防双重 claim</h4>
<pre>-- 抢占:仅当未被锁定才成功
UPDATE tasks SET <span class="str">claimed_at</span> = :now
WHERE id = :id AND <span class="str">claimed_at IS NULL</span>;
-- changes==1 → 抢到;==0 → 已被占,放弃</pre>
<div class="row" style="color:var(--text3);margin-top:6px">单条 SQL 行级原子,无论多少路径同时领同一任务,<strong>最多一个成功</strong>。run 结束(finishRun / failAttempt / reap)置 NULL 释放。比"用 run.status 反推"更直接、可索引。</div>
</div>
<div>
<h4 style="color:var(--amber);margin:0 0 8px">couplingPenalty 文件错峰</h4>
<pre>score(t) = rankU(t) + agingBonus(t)
<span class="add"> 0.5 × | t.owned_files ∩
(未合并 worktree 任务的 owned_files 并集) |</span></pre>
<div class="row" style="color:var(--text3);margin-top:6px">重叠越多分越低 → 越靠后领 → 大概率等冲突任务先跑完。按<strong>任务级 owned_files</strong> 并集算,覆盖<strong>所有分支上有未合并提交的任务</strong>running + waiting(review) 待合并 + 带改动 failed),<strong>不只 running</strong>——否则 code 完进 waiting 等人审时分支已有提交,只看 running 会漏算、照样撞冲突。软错峰:实在没别的可干仍会跑,agingBonus 防永久饥饿。</div>
</div>
</div>
<h4 style="margin:18px 0 8px">阶段内并行:放调度器还是放 agent?</h4>
<table class="tbl">
<tr><th>并行的性质</th><th>放哪</th><th>占槽</th><th>例子</th></tr>
<tr><td>共享同一 worktree、只读 / 汇总(不互相写)</td><td><strong>agent 自己并行</strong>worker 内 Promise.all</td><td>1 槽</td><td>双复审 code + security(都只读同一 diff</td></tr>
<tr><td>各自要写、可隔离(不同文件 / 模块)</td><td><strong>拆子任务交调度器</strong>(各自 worktree</td><td>N 槽</td><td>feature 拆「auth 层」+「UI 层」</td></tr>
</table>
<div class="note" style="margin-top:14px">🔗 <strong>闭环</strong>planner 拆并行子任务时须给<strong>互斥的 owned_files</strong>,它们才真并行;给重叠了 couplingPenalty 自动把它们串起来——拆解质量与调度错峰相互校正。<br><strong>反模式</strong>:同一 worktree 内多个写手并发(git index / 文件竞态)。要并行写就必须隔离 worktree,而隔离 worktree 即「子任务」——于是天然回到调度器并行。</div>
</div>
<div class="card">
<div class="card-title">交互平面 · stuck 任务人工接管(interactive run<span class="phase p2" style="margin-left:8px">Phase 2</span></div>
<p class="lead" style="font-size:12px;margin:0 0 12px;color:var(--text2)">自治流水线之外的<strong>第二执行平面</strong>:任务卡在 <span class="badge" style="color:#f97316;border-color:#f97316">stuck</span> 时,人可「接管」,像终端一样与带完整上下文的 agent 长对话改完,再交回自治管道。<strong>不改造自治 Worker</strong>,作为并列能力接入。</p>
<h4 style="margin:14px 0 8px">两个执行平面</h4>
<table class="tbl">
<tr><th>维度</th><th>自治平面(现有)</th><th>交互平面(新增)</th></tr>
<tr><td>触发</td><td>orchestrator 按 score 调度</td><td>人显式接管(on-demand<code>POST /api/tasks/:id/takeover</code></td></tr>
<tr><td>传输</td><td><strong>纯文件</strong>job.json / outbox / heartbeat</td><td><strong>实时双向</strong><code>runs/&lt;runId&gt;/console.sock</code>unix socket)↔ daemon ↔ 客户端 WS</td></tr>
<tr><td>节奏</td><td>跑完即退、可 reap</td><td>人驱动、按对话推进</td></tr>
<tr><td>重启可恢复</td><td>✅ 文件协议保证</td><td>❌ 牺牲,改由 <strong>session resume</strong> 找回</td></tr>
<tr><td>run kind</td><td>plan / code / review / pr / conflict</td><td><span class="chg add">interactive</span></td></tr>
</table>
<div class="note" style="margin-top:8px">两平面<strong>共享</strong> worktree / transcript+session / DB 状态(经 daemon/ task 模型,只在<strong>传输 + 生命周期触发</strong>上不同——自治内核保持纯净,交互作为边界清晰的独立能力。</div>
<h4 style="margin:18px 0 8px">接管会话流程</h4>
<div class="pipe-stage">
<div class="pipe-box human">stuck</div><div class="pipe-arr"></div>
<div class="pipe-box exec">人点接管 /takeover</div><div class="pipe-arr"></div>
<div class="pipe-box exec">spawn interactive run(复用 worktree + resume session</div><div class="pipe-arr"></div>
<div class="pipe-box review">人 ↔ agent 长对话</div><div class="pipe-arr"></div>
<div class="pipe-box human">人选出口</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div class="box cyan"><h4>接管期间</h4>
<div class="row"><code>schedule_status=running</code> + <code>run.kind=interactive</code></div>
<div class="row">work_type 保持卡住时的值(不污染枚举)</div>
<div class="row">reaper 对 interactive <strong>关执行超时</strong>heartbeat 仍发</div>
<div class="row"><strong>idle-TTL 30min</strong> 无活动 → checkpoint session id → 退出 → 回 stuck;重连即 resume(挂起/恢复无缝)</div>
</div>
<div class="box green"><h4>结束出口(人选)</h4>
<div class="row">修好 → 交回:commit WIP,回 <code>ready</code> 进下一 work_typereview/pr</div>
<div class="row">修好 → 直接完成:走 pr / 合并 → done</div>
<div class="row">放弃 → 回 stuck / cancelled</div>
<div class="row">session id 落 <code>work_history</code> → 后续自治 run 可 resume,接管对话喂 L1L4 记忆</div>
</div>
</div>
<h4 style="margin:18px 0 8px">关键决策(已定)</h4>
<table class="tbl">
<tr><th>决策点</th><th>取定</th></tr>
<tr><td>schedule_status</td><td><strong>复用 <code>running</code></strong> + <code>run.kind=interactive</code>,不加状态枚举;reaper 单点特判</td></tr>
<tr><td>工具权限</td><td><strong>有界可配</strong>:项目级自定义白名单,但只能在<strong>允许超集</strong>内勾选——<code>effective = 用户选 ∩ 允许超集</code><code>git push</code> / 部署等红线<strong>永不入超集</strong></td></tr>
<tr><td>idle-TTL</td><td><strong>30 分钟</strong>挂起 + resume 重连</td></tr>
<tr><td>并发名额</td><td><strong>占 1 个</strong> <code>project.concurrency</code>(在用 worktree + agent),可单独配小池避免抢占自治吞吐</td></tr>
</table>
<div class="note" style="margin-top:12px">📦 <strong>Schema 影响极小</strong><code>runs.kind</code> 扩枚举加 <code>interactive</code>TEXT 兼容、非 work_type);session id 复用 <code>work_history</code><code>console.sock</code><code>runs/&lt;runId&gt;/</code> 下的文件——<strong>无需新增列</strong>。新增的是 daemon 的 takeover API + WS↔socket 桥接 + reaper 对 interactive 的特判 + 项目级 <code>interactive_tools</code> 配置(受允许超集约束)。</div>
</div>
<a class="chapter-anchor" id="d-db"></a>
<div class="chapter-hdr"><span></span> 数据库 Schema 变更<span class="phase p1">Phase 1</span></div>
<div class="card" style="border-left:3px solid var(--amber)">
<div class="card-title">★ 本方案新增 / 变更字段汇总(schedule × work_type 模型)</div>
<p class="lead" style="font-size:12px;margin:0 0 12px;color:var(--text2)">核心是 <code>tasks</code> 表的状态建模:用 <strong>schedule_status + work_type + work_plan 队列</strong> 取代旧 <code>status</code> 单字段;状态机以「队列驱动」实现(非 XState 快照),故 <code>state_snapshot / sub_status</code> 不引入。</p>
<table class="tbl">
<tr><th></th><th>字段</th><th>类型</th><th>用途</th><th>性质</th></tr>
<tr><td><code>tasks</code></td><td><code>schedule_status</code></td><td>TEXT</td><td>调度状态机:init|ready|running|waiting|blocked|paused|failed|<strong>stuck</strong>|done|cancelled<strong>取代旧 status</strong></td><td><span class="chg mod">取代 status</span></td></tr>
<tr><td><code>tasks</code></td><td><code>work_type</code></td><td>TEXT</td><td>当前工作类型,决定 dispatch 哪个 agentplan|code|review|pr|conflict|research|null</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>work_plan</code></td><td>TEXT JSON</td><td>剩余步骤队列 <code>[{type, spec, owned_files, expected_output}]</code>,运行时动态可改</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>work_history</code></td><td>TEXT JSON</td><td>已完成步骤记录(审计 + 重试上下文)</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>claimed_at</code></td><td>TEXT</td><td>CAS 锁,NULL = 可领取、非 NULL = 已锁定(防双重领取)</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>version</code></td><td>INTEGER</td><td>乐观锁版本号,执行期任务修改检测</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>scope</code></td><td>TEXT</td><td>file / module / service / cross-service 改动范围,喂调度 couplingPenalty</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>owned_files</code></td><td>TEXT JSON</td><td>任务声明的主要修改文件路径,喂 couplingPenalty 文件重叠 + diff 越界闸;planner decompose 给子任务赋值</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>expected_output</code></td><td>TEXT</td><td>任务级一句话可验证完成标准(per-step 的在 work_plan 里)</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>attachments</code></td><td>TEXT JSON</td><td>提交附件 <code>[{name, type, path}]</code>(图片/文件);存 tasks/&lt;id&gt;/attachments/,喂 agent 多模态/Read</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>source</code></td><td>TEXT</td><td>任务来源 humanaisyncgithubAI 提的默认进待确认闸</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>tasks</code></td><td><code>created_by</code></td><td>TEXT</td><td>创建者(人 / 提交它的 agent run id),配合 source 审计</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>projects</code></td><td><code>agent_rules</code></td><td>TEXT</td><td>项目级 agent 专用规范(markdown),L2 记忆注入</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>projects</code></td><td><code>diff_max_files</code></td><td>INTEGER</td><td>diff 体量闸阈值(默认 100 文件)</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>projects</code></td><td><code>checks</code></td><td>TEXT JSON</td><td>分项检查命令 <code>{lint, typecheck, build}</code></td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>projects</code></td><td><code>budget_usd</code></td><td>REAL</td><td>项目级预算上限(当期);当期 spend 超额 → 自动 paused 停领(见 ⑥ 护栏)</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>projects</code></td><td><code>budget_period</code></td><td>TEXT</td><td>预算周期 <code>day|month</code>(配合 budget_usd</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>runs</code></td><td><code>kind</code></td><td>TEXT</td><td>对齐 work_typeplan|code|review|pr|<strong>conflict</strong>|research,外加 <strong>interactive</strong>(人工接管 run,非 work_type、不进调度队列)</td><td><span class="chg mod">扩枚举</span></td></tr>
<tr><td><code>runs</code></td><td><code>trace_id</code></td><td>TEXT</td><td>跨 run 追踪 ID,关联同任务多次运行</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>runs</code></td><td><code>usage</code></td><td>TEXT JSON</td><td>每次模型调用的用量 <code>[{model, input_tokens, output_tokens, cache_read, cache_write}]</code>,喂成本计算</td><td><span class="chg add">新增</span></td></tr>
<tr><td><code>runs</code></td><td><code>cost_usd</code></td><td>REAL</td><td>本 run 折算成本(按 模型×token 价目表);汇总到 task / project</td><td><span class="chg add">新增</span></td></tr>
</table>
<div class="note" style="margin-top:12px"><strong>已弃用(早期 XState 草案)</strong><code>state_snapshot</code> / <code>sub_status</code>(被 schedule_status + work_plan 队列取代)、<code>task_type</code>(任务性质由 work_plan 步骤组成表达,并入 work_type)、<code>parent_version_id</code>(无对应场景)。<br><span style="color:var(--text3)">注:<code>owned_files</code> 同时存在「任务级」(声明级,喂调度/diff 闸)与「work_plan 每步级」(执行级,当前步声明),两者并存不冲突。</span></div>
</div>
<div class="card">
<div class="card-title">Entity Relationship 图(数据库表结构与关系)</div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 950 874" style="width:100%;display:block;margin:16px 0;background:#0a0f1a;border-radius:6px;">
<defs>
<marker id="er-one" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto"><line x1="8" y1="1" x2="8" y2="9" stroke="#64748b" stroke-width="1.3"/></marker>
<marker id="er-many" markerWidth="14" markerHeight="10" refX="1" refY="5" orient="auto"><line x1="1" y1="5" x2="9" y2="1" stroke="#64748b" stroke-width="1.3"/><line x1="1" y1="5" x2="9" y2="5" stroke="#64748b" stroke-width="1.3"/><line x1="1" y1="5" x2="9" y2="9" stroke="#64748b" stroke-width="1.3"/><line x1="9" y1="1" x2="9" y2="9" stroke="#64748b" stroke-width="1.3"/></marker>
<marker id="er-many-r" markerWidth="14" markerHeight="10" refX="13" refY="5" orient="auto"><line x1="13" y1="5" x2="5" y2="1" stroke="#64748b" stroke-width="1.3"/><line x1="13" y1="5" x2="5" y2="5" stroke="#64748b" stroke-width="1.3"/><line x1="13" y1="5" x2="5" y2="9" stroke="#64748b" stroke-width="1.3"/><line x1="5" y1="1" x2="5" y2="9" stroke="#64748b" stroke-width="1.3"/></marker>
</defs>
<!-- projects x=10 y=10 w=285 h=318 -->
<rect x="10" y="10" width="285" height="24" fill="#1e3a5f" rx="3"/>
<text x="152.5" y="26" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#60a5fa">projects</text>
<rect x="10" y="34" width="285" height="294" fill="#0d1f35"/>
<text x="18" y="47" font-family="monospace" font-size="9" fill="#60a5fa">🔑 id TEXT PK</text>
<text x="18" y="60" font-family="monospace" font-size="9" fill="#94a3b8"> name TEXT</text>
<text x="18" y="73" font-family="monospace" font-size="9" fill="#94a3b8"> repo_path TEXT UNIQUE</text>
<text x="18" y="86" font-family="monospace" font-size="9" fill="#94a3b8"> default_branch TEXT</text>
<text x="18" y="99" font-family="monospace" font-size="9" fill="#94a3b8"> verify_cmd TEXT</text>
<text x="18" y="112" font-family="monospace" font-size="9" fill="#94a3b8"> autonomy TEXT</text>
<text x="18" y="125" font-family="monospace" font-size="9" fill="#94a3b8"> model TEXT</text>
<text x="18" y="138" font-family="monospace" font-size="9" fill="#94a3b8"> concurrency INTEGER</text>
<text x="18" y="151" font-family="monospace" font-size="9" fill="#94a3b8"> max_retries INTEGER</text>
<text x="18" y="164" font-family="monospace" font-size="9" fill="#94a3b8"> timeout_ms INTEGER</text>
<text x="18" y="177" font-family="monospace" font-size="9" fill="#fb923c">★ checks TEXT (JSON)</text>
<text x="18" y="190" font-family="monospace" font-size="9" fill="#fb923c">★ diff_max_files INTEGER</text>
<text x="18" y="203" font-family="monospace" font-size="9" fill="#fb923c">★ budget_usd REAL</text>
<text x="18" y="216" font-family="monospace" font-size="9" fill="#fb923c">★ budget_period TEXT</text>
<text x="18" y="229" font-family="monospace" font-size="9" fill="#94a3b8"> auto_approve_plan INTEGER</text>
<text x="18" y="242" font-family="monospace" font-size="9" fill="#94a3b8"> auto_approve_exec INTEGER</text>
<text x="18" y="255" font-family="monospace" font-size="9" fill="#fb923c">★ agent_rules TEXT</text>
<text x="18" y="268" font-family="monospace" font-size="9" fill="#94a3b8"> status TEXT</text>
<text x="18" y="281" font-family="monospace" font-size="9" fill="#94a3b8"> sort_order INTEGER</text>
<text x="18" y="294" font-family="monospace" font-size="9" fill="#94a3b8"> logo TEXT</text>
<text x="18" y="307" font-family="monospace" font-size="9" fill="#94a3b8"> last_sync_at TEXT</text>
<text x="18" y="320" font-family="monospace" font-size="9" fill="#94a3b8"> created_at TEXT</text>
<rect x="10" y="10" width="285" height="318" fill="none" stroke="#1e4a7a" stroke-width="1"/>
<!-- tasks x=340 y=10 w=300 h=370 -->
<rect x="340" y="10" width="300" height="24" fill="#2d1a4f" rx="3"/>
<text x="490" y="26" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#a78bfa">tasks</text>
<rect x="340" y="34" width="300" height="346" fill="#1a0f2e"/>
<text x="348" y="47" font-family="monospace" font-size="9" fill="#a78bfa">🔑 id TEXT PK</text>
<text x="348" y="60" font-family="monospace" font-size="9" fill="#4ade80">🔗 project_id → projects.id</text>
<text x="348" y="73" font-family="monospace" font-size="9" fill="#4ade80">🔗 parent_id → tasks.id (可空)</text>
<text x="348" y="86" font-family="monospace" font-size="9" fill="#94a3b8"> depth INTEGER</text>
<text x="348" y="99" font-family="monospace" font-size="9" fill="#94a3b8"> title TEXT</text>
<text x="348" y="112" font-family="monospace" font-size="9" fill="#94a3b8"> complexity TEXT</text>
<text x="348" y="125" font-family="monospace" font-size="9" fill="#fb923c">★ schedule_status TEXT</text>
<text x="348" y="138" font-family="monospace" font-size="9" fill="#fb923c">★ work_type TEXT</text>
<text x="348" y="151" font-family="monospace" font-size="9" fill="#fb923c">★ work_plan TEXT (JSON)</text>
<text x="348" y="164" font-family="monospace" font-size="9" fill="#fb923c">★ work_history TEXT (JSON)</text>
<text x="348" y="177" font-family="monospace" font-size="9" fill="#94a3b8"> priority INTEGER</text>
<text x="348" y="190" font-family="monospace" font-size="9" fill="#94a3b8"> deps TEXT (JSON)</text>
<text x="348" y="203" font-family="monospace" font-size="9" fill="#fb923c">★ scope TEXT</text>
<text x="348" y="216" font-family="monospace" font-size="9" fill="#fb923c">★ owned_files TEXT (JSON)</text>
<text x="348" y="229" font-family="monospace" font-size="9" fill="#fb923c">★ expected_output TEXT</text>
<text x="348" y="242" font-family="monospace" font-size="9" fill="#fb923c">★ claimed_at TEXT</text>
<text x="348" y="255" font-family="monospace" font-size="9" fill="#fb923c">★ version INTEGER</text>
<text x="348" y="268" font-family="monospace" font-size="9" fill="#fb923c">★ attachments TEXT (JSON)</text>
<text x="348" y="281" font-family="monospace" font-size="9" fill="#fb923c">★ source TEXT</text>
<text x="348" y="294" font-family="monospace" font-size="9" fill="#fb923c">★ created_by TEXT</text>
<text x="348" y="307" font-family="monospace" font-size="9" fill="#94a3b8"> result TEXT (JSON)</text>
<text x="348" y="320" font-family="monospace" font-size="9" fill="#94a3b8"> assignee TEXT</text>
<text x="348" y="333" font-family="monospace" font-size="9" fill="#94a3b8"> retry_baseline INTEGER</text>
<text x="348" y="346" font-family="monospace" font-size="9" fill="#94a3b8"> next_eligible_at TEXT</text>
<text x="348" y="359" font-family="monospace" font-size="9" fill="#94a3b8"> source_ref TEXT</text>
<text x="348" y="372" font-family="monospace" font-size="9" fill="#94a3b8"> created_at / updated_at TEXT</text>
<rect x="340" y="10" width="300" height="370" fill="none" stroke="#4c2d8f" stroke-width="1"/>
<!-- approvals x=685 y=10 w=265 h=123 -->
<rect x="685" y="10" width="265" height="24" fill="#3d2800" rx="3"/>
<text x="817.5" y="26" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#f59e0b">approvals</text>
<rect x="685" y="34" width="265" height="99" fill="#231800"/>
<text x="693" y="47" font-family="monospace" font-size="9" fill="#f59e0b">🔑 id TEXT PK</text>
<text x="693" y="60" font-family="monospace" font-size="9" fill="#4ade80">🔗 task_id → tasks.id</text>
<text x="693" y="73" font-family="monospace" font-size="9" fill="#94a3b8"> gate TEXT (plan|review)</text>
<text x="693" y="86" font-family="monospace" font-size="9" fill="#94a3b8"> action TEXT (accept|reject)</text>
<text x="693" y="99" font-family="monospace" font-size="9" fill="#94a3b8"> actor TEXT</text>
<text x="693" y="112" font-family="monospace" font-size="9" fill="#94a3b8"> reason TEXT</text>
<text x="693" y="125" font-family="monospace" font-size="9" fill="#94a3b8"> at TEXT</text>
<rect x="685" y="10" width="265" height="123" fill="none" stroke="#6b4400" stroke-width="1"/>
<!-- runs x=685 y=155 w=265 h=227 -->
<rect x="685" y="155" width="265" height="24" fill="#0d3320" rx="3"/>
<text x="817.5" y="171" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#4ade80">runs</text>
<rect x="685" y="179" width="265" height="203" fill="#061a10"/>
<text x="693" y="192" font-family="monospace" font-size="9" fill="#4ade80">🔑 id TEXT PK</text>
<text x="693" y="205" font-family="monospace" font-size="9" fill="#4ade80">🔗 task_id → tasks.id</text>
<text x="693" y="218" font-family="monospace" font-size="9" fill="#94a3b8"> kind TEXT (plan|code|review|pr|conflict|research)</text>
<text x="693" y="231" font-family="monospace" font-size="9" fill="#fb923c">★ trace_id TEXT</text>
<text x="693" y="244" font-family="monospace" font-size="9" fill="#fb923c">★ usage TEXT (JSON)</text>
<text x="693" y="257" font-family="monospace" font-size="9" fill="#fb923c">★ cost_usd REAL</text>
<text x="693" y="270" font-family="monospace" font-size="9" fill="#94a3b8"> worktree TEXT</text>
<text x="693" y="283" font-family="monospace" font-size="9" fill="#94a3b8"> branch TEXT</text>
<text x="693" y="296" font-family="monospace" font-size="9" fill="#94a3b8"> status TEXT</text>
<text x="693" y="309" font-family="monospace" font-size="9" fill="#94a3b8"> started_at / ended_at TEXT</text>
<text x="693" y="322" font-family="monospace" font-size="9" fill="#94a3b8"> transcript_ref TEXT</text>
<text x="693" y="335" font-family="monospace" font-size="9" fill="#94a3b8"> claude_session_id TEXT</text>
<text x="693" y="348" font-family="monospace" font-size="9" fill="#94a3b8"> error TEXT</text>
<text x="693" y="361" font-family="monospace" font-size="9" fill="#94a3b8"> worker_pid INTEGER</text>
<text x="693" y="374" font-family="monospace" font-size="9" fill="#94a3b8"> last_seq INTEGER</text>
<rect x="685" y="155" width="265" height="227" fill="none" stroke="#0f5c33" stroke-width="1"/>
<!-- events x=10 y=350 w=285 h=110 -->
<rect x="10" y="350" width="285" height="24" fill="#3d1515" rx="3"/>
<text x="152.5" y="366" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#f87171">events</text>
<rect x="10" y="374" width="285" height="86" fill="#210c0c"/>
<text x="18" y="387" font-family="monospace" font-size="9" fill="#f87171">🔑 id TEXT PK</text>
<text x="18" y="400" font-family="monospace" font-size="9" fill="#64748b">🔗 project_id (无 FK 约束)</text>
<text x="18" y="413" font-family="monospace" font-size="9" fill="#64748b">🔗 task_id (可空, 无 FK 约束)</text>
<text x="18" y="426" font-family="monospace" font-size="9" fill="#94a3b8"> type TEXT</text>
<text x="18" y="439" font-family="monospace" font-size="9" fill="#94a3b8"> payload TEXT (JSON)</text>
<text x="18" y="452" font-family="monospace" font-size="9" fill="#94a3b8"> at TEXT</text>
<rect x="10" y="350" width="285" height="110" fill="none" stroke="#7f1d1d" stroke-width="1"/>
<!-- ─── 关系连线(crow's foot ─── -->
<path d="M 295 169 L 317.5 169 L 317.5 57 L 340 57" stroke="#64748b" stroke-width="1.5" fill="none" marker-start="url(#er-one)" marker-end="url(#er-many-r)"/>
<text x="317.5" y="165" text-anchor="middle" font-family="sans-serif" font-size="8" fill="#64748b">1:N</text>
<path d="M 640 97 L 662.5 97 L 662.5 57 L 685 57" stroke="#64748b" stroke-width="1.5" fill="none" marker-start="url(#er-one)" marker-end="url(#er-many-r)"/>
<text x="662.5" y="53" text-anchor="middle" font-family="sans-serif" font-size="8" fill="#64748b">1:N</text>
<path d="M 640 142 L 662.5 142 L 662.5 202 L 685 202" stroke="#64748b" stroke-width="1.5" fill="none" marker-start="url(#er-one)" marker-end="url(#er-many-r)"/>
<text x="662.5" y="138" text-anchor="middle" font-family="sans-serif" font-size="8" fill="#64748b">1:N</text>
<path d="M 152.5 328 L 152.5 350" stroke="#64748b" stroke-width="1.5" fill="none" marker-start="url(#er-one)" marker-end="url(#er-many)"/>
<text x="168.5" y="342" font-family="sans-serif" font-size="8" fill="#64748b">1:N</text>
<path d="M 490 380 L 490 405 L 295 405" stroke="#64748b" stroke-width="1.5" fill="none" stroke-dasharray="4,3" marker-end="url(#er-many)"/>
<text x="392.5" y="401" text-anchor="middle" font-family="sans-serif" font-size="8" fill="#64748b">0..1:N (可空)</text>
<path d="M 340 70 L 318 70 L 318 40 L 340 40" stroke="#64748b" stroke-width="1.5" fill="none" stroke-dasharray="3,2" marker-end="url(#er-many-r)"/>
<text x="310" y="65" text-anchor="middle" font-family="sans-serif" font-size="8" fill="#64748b">parent</text>
<!-- ─── 图例 ─── -->
<rect x="685" y="400" width="265" height="96" fill="#080d18" rx="3" stroke="#2a3448"/>
<rect x="685" y="400" width="265" height="18" fill="#131c30" rx="3"/>
<text x="817.5" y="413" text-anchor="middle" font-family="sans-serif" font-size="9" font-weight="bold" fill="#94a3b8">图 例</text>
<text x="695" y="434" font-family="sans-serif" font-size="9" fill="#fb923c">★ = 本方案新增列</text>
<text x="695" y="448" font-family="sans-serif" font-size="9" fill="#4ade80">🔗 = 外键引用 🔑 = 主键</text>
<line x1="695" y1="460" x2="729" y2="460" stroke="#64748b" stroke-width="1.5"/>
<text x="735" y="463" font-family="sans-serif" font-size="9" fill="#64748b">有 FK 约束</text>
<line x1="695" y1="476" x2="729" y2="476" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4,3"/>
<text x="735" y="479" font-family="sans-serif" font-size="9" fill="#64748b">弱引用(无 FK</text>
<!-- ─── 关键字段说明表 ─── -->
<rect x="10" y="518" width="940" height="344" fill="#080d18" rx="3" stroke="#2a3448"/>
<rect x="10" y="518" width="940" height="20" fill="#131c30" rx="3"/>
<text x="480" y="532" text-anchor="middle" font-family="sans-serif" font-size="9.5" font-weight="bold" fill="#94a3b8">关键字段说明</text>
<rect x="10" y="538" width="940" height="16" fill="#0f1a2e"/>
<text x="20" y="550" font-family="monospace" font-size="8.5" font-weight="bold" fill="#64748b">字段</text>
<text x="175" y="550" font-family="monospace" font-size="8.5" font-weight="bold" fill="#64748b">所属表</text>
<text x="280" y="550" font-family="monospace" font-size="8.5" font-weight="bold" fill="#64748b">含义</text>
<line x1="168" y1="538" x2="168" y2="862" stroke="#2a3448" stroke-width="0.5"/>
<line x1="273" y1="538" x2="273" y2="862" stroke="#2a3448" stroke-width="0.5"/>
<text x="20" y="566" font-family="monospace" font-size="8.5" fill="#a78bfa">schedule_status</text>
<text x="175" y="566" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="566" font-family="monospace" font-size="8.5" fill="#94a3b8">调度状态机,取代旧 statusinit|ready|running|waiting|blocked|paused|failed|stuck|done|cancelled</text>
<line x1="10" y1="569" x2="950" y2="569" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="581" font-family="monospace" font-size="8.5" fill="#a78bfa">work_type</text>
<text x="175" y="581" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="581" font-family="monospace" font-size="8.5" fill="#94a3b8">当前工作类型,决定 dispatch 哪个 agentplan|code|review|pr|conflict|research|null</text>
<line x1="10" y1="584" x2="950" y2="584" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="596" font-family="monospace" font-size="8.5" fill="#a78bfa">work_plan</text>
<text x="175" y="596" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="596" font-family="monospace" font-size="8.5" fill="#94a3b8">剩余步骤队列 [{type, spec, owned_files, expected_output}],运行时动态可改</text>
<line x1="10" y1="599" x2="950" y2="599" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="611" font-family="monospace" font-size="8.5" fill="#a78bfa">work_history</text>
<text x="175" y="611" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="611" font-family="monospace" font-size="8.5" fill="#94a3b8">已完成步骤记录,审计 + 重试上下文</text>
<line x1="10" y1="614" x2="950" y2="614" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="626" font-family="monospace" font-size="8.5" fill="#a78bfa">claimed_at</text>
<text x="175" y="626" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="626" font-family="monospace" font-size="8.5" fill="#94a3b8">CAS 锁,NULL=可领取、非 NULL=已锁定,防 Orchestrator 双重领取</text>
<line x1="10" y1="629" x2="950" y2="629" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="641" font-family="monospace" font-size="8.5" fill="#a78bfa">version</text>
<text x="175" y="641" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="641" font-family="monospace" font-size="8.5" fill="#94a3b8">乐观锁版本号,执行期任务修改检测(D6)</text>
<line x1="10" y1="644" x2="950" y2="644" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="656" font-family="monospace" font-size="8.5" fill="#a78bfa">scope</text>
<text x="175" y="656" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="656" font-family="monospace" font-size="8.5" fill="#94a3b8">改动范围 file/module/service/cross-service,喂调度 couplingPenalty</text>
<line x1="10" y1="659" x2="950" y2="659" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="671" font-family="monospace" font-size="8.5" fill="#a78bfa">owned_files</text>
<text x="175" y="671" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="671" font-family="monospace" font-size="8.5" fill="#94a3b8">任务声明的主要修改文件 JSON,喂 couplingPenalty 文件重叠 + diff 越界闸</text>
<line x1="10" y1="674" x2="950" y2="674" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="686" font-family="monospace" font-size="8.5" fill="#a78bfa">expected_output</text>
<text x="175" y="686" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="686" font-family="monospace" font-size="8.5" fill="#94a3b8">任务级一句话可验证完成标准(per-step 的在 work_plan 内)</text>
<line x1="10" y1="689" x2="950" y2="689" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="701" font-family="monospace" font-size="8.5" fill="#60a5fa">agent_rules ★</text>
<text x="175" y="701" font-family="monospace" font-size="8.5" fill="#94a3b8">projects</text>
<text x="280" y="701" font-family="monospace" font-size="8.5" fill="#94a3b8">项目级 agent 专用规范文本,注入全部 agent prompt 头部(L2 记忆)</text>
<line x1="10" y1="704" x2="950" y2="704" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="716" font-family="monospace" font-size="8.5" fill="#60a5fa">diff_max_files</text>
<text x="175" y="716" font-family="monospace" font-size="8.5" fill="#94a3b8">projects</text>
<text x="280" y="716" font-family="monospace" font-size="8.5" fill="#94a3b8">diff 体量闸阈值(默认 100),执行后改动文件数超限即拦截</text>
<line x1="10" y1="719" x2="950" y2="719" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="731" font-family="monospace" font-size="8.5" fill="#60a5fa">checks</text>
<text x="175" y="731" font-family="monospace" font-size="8.5" fill="#94a3b8">projects</text>
<text x="280" y="731" font-family="monospace" font-size="8.5" fill="#94a3b8">分项静态检查命令 JSON {lint, typecheck, build},任一非 0 硬失败</text>
<line x1="10" y1="734" x2="950" y2="734" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="746" font-family="monospace" font-size="8.5" fill="#60a5fa">budget_usd / budget_period</text>
<text x="175" y="746" font-family="monospace" font-size="8.5" fill="#94a3b8">projects</text>
<text x="280" y="746" font-family="monospace" font-size="8.5" fill="#94a3b8">项目级预算上限(当期 day|month),超额自动 paused 停领(⑥ 护栏)</text>
<line x1="10" y1="749" x2="950" y2="749" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="761" font-family="monospace" font-size="8.5" fill="#4ade80">kind</text>
<text x="175" y="761" font-family="monospace" font-size="8.5" fill="#94a3b8">runs</text>
<text x="280" y="761" font-family="monospace" font-size="8.5" fill="#94a3b8">run 类型,对齐 work_typeplan|code|review|pr|conflict|research + interactive(人工接管,非 work_type</text>
<line x1="10" y1="764" x2="950" y2="764" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="776" font-family="monospace" font-size="8.5" fill="#4ade80">trace_id</text>
<text x="175" y="776" font-family="monospace" font-size="8.5" fill="#94a3b8">runs</text>
<text x="280" y="776" font-family="monospace" font-size="8.5" fill="#94a3b8">跨 run 追踪 ID,关联同一任务的多次运行</text>
<line x1="10" y1="779" x2="950" y2="779" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="791" font-family="monospace" font-size="8.5" fill="#4ade80">usage / cost_usd</text>
<text x="175" y="791" font-family="monospace" font-size="8.5" fill="#94a3b8">runs</text>
<text x="280" y="791" font-family="monospace" font-size="8.5" fill="#94a3b8">每次模型调用 token 用量 + 折算成本(模型×token 价目),汇总到 task / project</text>
<line x1="10" y1="794" x2="950" y2="794" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="806" font-family="monospace" font-size="8.5" fill="#4ade80">last_seq</text>
<text x="175" y="806" font-family="monospace" font-size="8.5" fill="#94a3b8">runs</text>
<text x="280" y="806" font-family="monospace" font-size="8.5" fill="#94a3b8">outbox.ndjson 已消费行号,daemon ingest 幂等重试游标</text>
<line x1="10" y1="809" x2="950" y2="809" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="821" font-family="monospace" font-size="8.5" fill="#a78bfa">attachments</text>
<text x="175" y="821" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="821" font-family="monospace" font-size="8.5" fill="#94a3b8">提交附件 [{name,type,path}](图片/文件),喂 agent 多模态 / Read</text>
<line x1="10" y1="824" x2="950" y2="824" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="836" font-family="monospace" font-size="8.5" fill="#a78bfa">source / created_by</text>
<text x="175" y="836" font-family="monospace" font-size="8.5" fill="#94a3b8">tasks</text>
<text x="280" y="836" font-family="monospace" font-size="8.5" fill="#94a3b8">任务来源 human|ai|sync|github + 创建者,AI 提的默认进待确认闸</text>
<line x1="10" y1="839" x2="950" y2="839" stroke="#1a2440" stroke-width="0.5"/>
<text x="20" y="851" font-family="monospace" font-size="8.5" fill="#f87171">payload</text>
<text x="175" y="851" font-family="monospace" font-size="8.5" fill="#94a3b8">events</text>
<text x="280" y="851" font-family="monospace" font-size="8.5" fill="#94a3b8">事件附加数据 JSON,审计日志 + WebSocket 实时推送源</text>
</svg>
</div>
<div class="card">
<div class="card-title">核心表说明</div>
<table class="tbl">
<thead><tr><th></th><th>行数量级</th><th>作用</th><th>关键约束</th></tr></thead>
<tbody>
<tr><td><code>projects</code></td><td>少量(&lt;100</td><td>项目配置、自动化策略、并发/超时参数</td><td>repo_path UNIQUE</td></tr>
<tr><td><code>tasks</code></td><td>中量(&lt;10k</td><td>任务主表,三类任务(hard/medium/easy)均在此表,以 complexity+status+run.kind 区分</td><td>FK project_id, parent_id(自引用)</td></tr>
<tr><td><code>approvals</code></td><td>中量</td><td>审批闸记录(plan/spec/exec),reject 必须带 reason</td><td>FK task_id</td></tr>
<tr><td><code>runs</code></td><td>中量(&lt;50k</td><td>每次 agent 运行(planner/executor/reviewer/security/conflict),含 transcript 路径和 ingest 游标</td><td>FK task_id</td></tr>
<tr><td><code>events</code></td><td>大量(仅追加)</td><td>审计日志 + WebSocket 实时推送源,无 FK 约束(高性能追加)</td><td>INDEX (project_id, at)</td></tr>
</tbody>
</table>
</div>
<div class="card">
<div class="card-title">★ 本方案新增列</div>
<div class="grid-2">
<div class="box amber">
<h4>projects.agent_rules (L2)</h4>
<div class="row">TEXT,项目级 agent 专用规范正文(markdown</div>
<div class="row">daemon 在 claimOne 写 job.json 时注入全部 4 类 agent 的 prompt 头部</div>
<div class="row">文件:schema.sql + types.ts + mappers.ts + store.ts</div>
</div>
<div class="box cyan">
<h4>非表字段(JobSpec.context</h4>
<div class="row">protocol.ts 新增可选字段,由 daemon 组装、随 job.json 下发</div>
<div class="row">内容:parentPlan(父任务拆解意图)+ siblings(兄弟任务状态摘要)</div>
<div class="row">worker 读 job.json 即可,无需访问 DB</div>
</div>
</div>
</div>
<!-- ══════════ ④ Agent 记忆注入 ══════════ -->
<a class="chapter-anchor" id="d-memory"></a>
<div class="chapter-hdr"><span></span> Agent 记忆注入<span class="phase mix">P1 核心(L1) · P2(L2L4)</span></div>
<div class="card" style="border-left:3px solid var(--red)">
<div class="card-title">当前缺口</div>
<div class="grid-2">
<div>
<div class="row"><code>buildPrompt</code> executortitle/id + operations/spec/plan + 执行约束。<span style="color:var(--red)">无 lastRunError、无项目规范。</span></div>
<div class="row"><code>buildPlannerPrompt</code> plannertitle/id + 草稿 + depth。<span style="color:var(--red)">无 lastRunError、无项目规范。</span></div>
<div class="row"><code>buildReviewPrompt</code> reviewer:任务要求 + 执行者自述 + diff 命令。<span style="color:var(--red)">无项目规范。</span></div>
</div>
<div>
<div class="row"><code>cc.ts:80</code> <code>settingSources:['project']</code> → SDK 自动读项目 CLAUDE.md(现已生效 ✓)</div>
<div class="row"><code>task.lastRunError</code> 字段已存在于 Task 类型,但<span style="color:var(--red)">从未注入任何 prompt</span></div>
<div class="row"><code>orchestrator</code> claim 时拿到的 task 对象<span style="color:var(--red)">不带 last_run_error</span>(仅审核 UI 子查询)</div>
</div>
</div>
</div>
<div class="card" style="border-left:3px solid var(--cyan)">
<div class="card-title">数据流铁律 → 决定方案形态</div>
<div class="row">worker 进程<strong>完全不碰 DB</strong>,只读 <code>runs/&lt;runId&gt;/job.json</code>protocol.ts 铁律)</div>
<div class="row">所有来自 DB 的记忆,必须由 <strong>daemon 在 claimOne 写 job.json 时组装进去</strong></div>
<div class="row">JobSpec 已携带完整 task + project 对象 → lastRunError 和 agentRules 天然可随 job.json 下发</div>
<div class="row">L3 跨任务上下文 → 扩展 <code>JobSpec.context?</code> 可选字段</div>
</div>
<div class="grid-2" style="margin-bottom:20px">
<div class="box green">
<h4>L1 — 注入 lastRunErrorROI 最高)</h4>
<div class="row">现状:last_run_error 是 pendingApprovals 子查询,claim 时的 task 不带它</div>
<div class="row">改 store.ts:抽 <code>lastRunErrorOf(taskId)</code> 方法(复用现有子查询 SQL</div>
<div class="row">改 orchestrator.tsclaimOne 写 job 前赋值 <code>task.lastRunError</code></div>
<div class="row">改 runner.ts3 处 build*Prompt 插「## 上次失败原因(重试任务)」段</div>
<div class="row" style="color:var(--green)">5 行代码,零协议改动(字段已在 Task 上)</div>
</div>
<div class="box amber">
<h4>L2 — 项目级 agent 规范(存 DB projects 表)</h4>
<div class="row">schema.sqlprojects 表加 <code>agent_rules TEXT</code> 列 + 幂等迁移</div>
<div class="row">types.ts/mappers.tsProject 加 <code>agentRules?</code>、row ↔ 字段映射</div>
<div class="row">project 已随 job.json 下发 → worker 端 4 处 prompt 插「## 项目规范」头部</div>
<div class="row">API/Web 编辑入口后续补;本期先打通存储+注入</div>
</div>
</div>
<div class="grid-2" style="margin-bottom:20px">
<div class="box violet">
<h4>L3 — 跨任务上下文</h4>
<div class="row">protocol.ts JobSpec 加 <code>context?: &#123; parentPlan?: string; siblings?: &#123;...&#125;[] &#125;</code></div>
<div class="row">store.ts:取 parent.plan + 同 parentId 兄弟摘要的方法</div>
<div class="row">orchestrator.ts claimOne:若 task.parentId,组装 job.context 再下发</div>
<div class="row">runner.ts buildPrompt:注入「## 拆解背景(父任务意图 + 兄弟状态)」</div>
<div class="row" style="color:var(--text3)">仅做 DB 读;不做 git 演化检索/RAG(留后续)</div>
</div>
<div class="box cyan">
<h4>L4 — 全局 agent 规范(maestro 自维护)</h4>
<div class="row"><strong>不读</strong> user 全局 ~/.claude/CLAUDE.md(避免个人习惯干扰受控执行)</div>
<div class="row"><code>settingSources:['project']</code> <strong>保持不变</strong></div>
<div class="row">maestro 维护 <code>~/.maestro/agent-global.md</code></div>
<div class="row">daemon 启动时读一次缓存,claimOne 随 job.json 下发,4 处 prompt 头部注入</div>
<div class="row" style="color:var(--text3)">文件不存在则跳过(可选,缺省空)</div>
</div>
</div>
<div class="card">
<div class="card-title">注入顺序(prompt 内记忆段排布)</div>
<div style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--text2);line-height:2">
<span style="color:var(--cyan)">[L4] ## 全局执行规范</span>
<span style="color:var(--amber)">[L2] ## 项目规范(必须遵守)</span>
<span style="color:var(--violet)">[L3] ## 拆解背景(父任务意图 + 兄弟状态)</span>
<span style="color:var(--text)">## 任务内容</span>
<span style="color:var(--red)">[L1] ## 上次失败原因(重试任务)</span>
<span style="color:var(--text)">## 执行约束</span>
</div>
<div class="row" style="margin-top:8px;color:var(--text3)">项目 CLAUDE.md 由 SDK 经 settingSources:['project'] 自动进 <strong>system prompt</strong>,不在 user prompt 里重复。</div>
</div>
<a class="chapter-anchor" id="d-agentspec"></a>
<div class="chapter-hdr"><span></span> Agent 规格与注册表<span class="phase p1">Phase 1</span></div>
<div class="card">
<div class="card-title">Agent 注册表模型(work_type → AgentSpec,可动态扩展)</div>
<p class="lead" style="font-size:12px;margin:0 0 12px;color:var(--text2)">每个 <code>work_type</code> 对应一条 <strong>AgentSpec</strong>。Orchestrator 派发 = <strong>注册表查表</strong>,而非写死 <code>switch</code>。新增一种 agent = 注册一条 AgentSpec<strong>调度内核 / 状态机零改动</strong>。六个维度——流程 / 每步模型 / skills+subagents / prompt / 上下文 / 输出——全部声明在 AgentSpec 里。</p>
<pre><span class="kw">interface</span> <span class="tp">AgentSpec</span> {
workType: <span class="tp">WorkType</span>; <span class="cm">// 'plan'|'code'|'review'|'pr'|'conflict'|'research'|'writing'|…</span>
runKind: <span class="tp">string</span>; <span class="cm">// 落 runs.kind</span>
pickModel: (t, p) =&gt; <span class="tp">ModelId</span>; <span class="cm">// 每步档位(可按 complexity 分档)</span>
tools: <span class="tp">ToolPattern</span>[]; <span class="cm">// 工具白名单(∩ 全局允许超集;红线永不入集)</span>
skills?: <span class="tp">SkillId</span>[]; <span class="cm">// 注入的 skill(如 writing-plans / elements-of-style</span>
subagents?: <span class="tp">AgentType</span>[]; <span class="cm">// 可调度的 subagent(如 Explore / general-purpose</span>
needsWorktree: <span class="tp">boolean</span>; <span class="cm">// 写类 true(隔离改动)</span>
buildPrompt: (ctx: <span class="tp">AgentContext</span>) =&gt; <span class="tp">string</span>; <span class="cm">// 含 L1L4 记忆</span>
parseOutput: (raw: <span class="tp">string</span>) =&gt; <span class="tp">StepResult</span>; <span class="cm">// 输出解析</span>
gate?: <span class="str">'plan'</span> | <span class="str">'review'</span> | <span class="kw">null</span>; <span class="cm">// 完成后的人工闸</span>
maxTurns: <span class="tp">number</span>;
preempt?: <span class="tp">boolean</span>; <span class="cm">// 抢占调度(如 conflict</span>
}
<span class="cm">// 派发 = 查表,不是写死 switch</span>
<span class="kw">const</span> spec = <span class="tp">AGENT_REGISTRY</span>[task.work_type];
<span class="kw">await</span> spec.<span class="fn">run</span>(task); <span class="cm">// run 内:buildPrompt → cc(model,tools,skills) → parseOutput → gate</span></pre>
<p style="font-size:12px;margin:10px 0 4px;color:var(--text2)"><strong>AgentContext(上下文,daemon 在 claim 时组装进 job.json</strong></p>
<pre>{
task, project, <span class="cm">// job.json 携带</span>
step, <span class="cm">// work_plan 当前步 {type, spec, owned_files, expected_output}</span>
memory: { global, <span class="cm">// L4 全局规范</span>
projectRules,<span class="cm">// L2 项目 agent_rules</span>
decompCtx, <span class="cm">// L3 父任务意图 + 兄弟状态</span>
lastError },<span class="cm">// L1 上次失败</span>
diff?, reviewerNotes? <span class="cm">// review / pr 用</span>
}</pre>
</div>
<div class="card">
<div class="card-title"><span class="badge" style="color:#f0a060;border-color:#d6783b">plan</span> Planner Agent</div>
<table class="tbl">
<tr><th style="width:96px">维度</th><th>内容</th></tr>
<tr><td>触发</td><td><code>(ready, plan)</code>Hard→decompose / Medium→spec<strong>只读、不建 worktree</strong></td></tr>
<tr><td>流程</td><td>buildPlannerPrompt → runPlanner(CC 只读) → 解析输出 → <code>(waiting, plan)</code> 人审</td></tr>
<tr><td>模型/档</td><td>easy=<code>sonnet-4-6</code> · medium=<code>opus-4-8</code> · hard=<code>opus-4-8</code>(最强档)</td></tr>
<tr><td>skills/subagent</td><td>skill <code>superpowers:writing-plans</code>(拆解质量);subagent <code>Explore</code>(大范围只读检索代码演化)</td></tr>
<tr><td>tools</td><td>Read / Glob / Grep + Bash(git log/diff/show)maxTurns 90/60/40</td></tr>
<tr><td>prompt</td><td>title/id + spec/plan 草稿 + <code>depth</code> + 剩余可拆层(≤3) + 「产<strong>子任务表格(人审)</strong> + 末尾 fenced JSON」</td></tr>
<tr><td>上下文</td><td>L4 全局 → L2 项目规范 → L3 拆解背景 → 任务内容 → L1 上次失败;project CLAUDE.md 经 settingSources 入 system</td></tr>
<tr><td>输出</td><td>decompose: 表格 + <code>JSON{title,complexity,priority,deps,owned_files,expected_output}</code>;spec: 三段 md(改动/为什么/验收)</td></tr>
</table>
</div>
<div class="card">
<div class="card-title"><span class="badge violet">code</span> Executor Agent</div>
<table class="tbl">
<tr><th style="width:96px">维度</th><th>内容</th></tr>
<tr><td>触发</td><td><code>(ready, code)</code><strong>建 worktree</strong>(隔离写)</td></tr>
<tr><td>流程</td><td>syncMain → createWorktree → buildPrompt → runTask(CC, acceptEdits) → <strong>必须 commit</strong> → verify → checks 闸 → diff 体量闸</td></tr>
<tr><td>模型/档</td><td>easy=<code>sonnet-4-6</code> · medium/hard=<code>opus-4-8</code></td></tr>
<tr><td>skills/subagent</td><td>subagent <code>general-purpose</code> / <code>Explore</code>(并行只读调研、省主上下文);skill:项目可配领域 skill</td></tr>
<tr><td>tools</td><td>Read/Edit/Write/Glob/Grep + Bash(git <strong>无 push</strong>, test) + WebSearchmaxTurns 100</td></tr>
<tr><td>prompt</td><td>title/id + <code>operations??spec??plan</code> + owned_files 范围 + expected_output + 执行约束(禁越界 / 必 commit 含任务 ID</td></tr>
<tr><td>上下文</td><td>L1L4 + work_plan 当前 <code>step.spec / step.owned_files</code></td></tr>
<tr><td>输出</td><td>执行自述 + 含任务 ID 的 commit</td></tr>
</table>
</div>
<div class="card">
<div class="card-title"><span class="badge cyan">review</span> Reviewer Agent</div>
<table class="tbl">
<tr><th style="width:96px">维度</th><th>内容</th></tr>
<tr><td>触发</td><td><code>(ready, review)</code>code 完成自动串联;<strong>独立 run、只读</strong>、可选</td></tr>
<tr><td>流程</td><td><code>Promise.all(reviewCode, reviewSecurity)</code> 并行 → 合并 verdict → reject 硬拦截(回 code) / pass → <code>(waiting, review)</code></td></tr>
<tr><td>模型/档</td><td>三档统一 <code>opus-4-8</code>(最强,<strong>不被 project.model 降档</strong>,保自审严格度)</td></tr>
<tr><td>skills/subagent</td><td>skillcode-review 清单 / 安全审计清单;<strong>无 subagent</strong>(聚焦审单一 diff</td></tr>
<tr><td>tools</td><td>Read/Glob/Grep + Bash(git diff/show/log)maxTurns 40;不写、复用 code 的 worktree 只读</td></tr>
<tr><td>prompt</td><td>任务要求 + 执行者自述 + worktreeDiff + 「输出 <code>VERDICT</code> 模板(pass/reject + 理由)」</td></tr>
<tr><td>上下文</td><td>任务 spec + code 自述 + diff + L2 项目规范</td></tr>
<tr><td>输出</td><td>双 verdict + 意见;任一 reject → 硬退回 code(带意见)</td></tr>
</table>
</div>
<div class="card">
<div class="card-title"><span class="badge green">pr</span> PR Agent</div>
<table class="tbl">
<tr><th style="width:96px">维度</th><th>内容</th></tr>
<tr><td>触发</td><td>review 通过后 <code>(ready, pr)</code></td></tr>
<tr><td>流程</td><td>创建 PR(gh) → <code>--no-ff</code> 合并 → 冲突? → 建 conflict run : done</td></tr>
<tr><td>模型/档</td><td><code>opus-4-8</code>(PR 文案 / 合并决策;多为确定性操作)</td></tr>
<tr><td>skills/subagent</td><td>skillPR 描述模板;无 subagent</td></tr>
<tr><td>tools</td><td>Bash(git, gh —<strong>受控</strong>:建 PR / 合并,<strong>禁强推</strong>);不建新 worktree</td></tr>
<tr><td>prompt</td><td>任务摘要 + commit 列表 + 关联 issue + PR 模板</td></tr>
<tr><td>上下文</td><td>任务 + 分支 + diff 摘要 + review 结论</td></tr>
<tr><td>输出</td><td>PR url + merge 结果(成功 → done / 冲突 → conflict run</td></tr>
</table>
</div>
<div class="card">
<div class="card-title"><span class="badge red">conflict</span> Conflict Agent</div>
<table class="tbl">
<tr><th style="width:96px">维度</th><th>内容</th></tr>
<tr><td>触发</td><td>pr 合并冲突自动建(<strong>抢占调度</strong>,不排队等 score</td></tr>
<tr><td>流程</td><td>补救 worktree <code>git merge</code> 制造冲突 → CC 解(保两边意图) → 跑测试 → commit → 双复审</td></tr>
<tr><td>模型/档</td><td><code>opus-4-8</code> 固定(最强,不随原任务复杂度降档)</td></tr>
<tr><td>skills/subagent</td><td>skill:解冲突纪律(禁丢任一方改动 / 解完验证);subagent <code>Explore</code>(查两边意图)</td></tr>
<tr><td>tools</td><td>Read/Edit/Write + Bash(<code>git merge</code><strong>仅此分支</strong>白名单, test)</td></tr>
<tr><td>prompt</td><td>专用解冲突模板(保留两边意图 + 禁丢改动 + 解完跑测试 + commit 沿用合并信息)</td></tr>
<tr><td>上下文</td><td>两分支 diff + 冲突文件 + 原任务意图 + L1 上次失败</td></tr>
<tr><td>输出</td><td>解冲突自述(解了哪些文件 / 取舍 / 测试结果)+ commit</td></tr>
</table>
<div class="note" style="margin-top:8px;font-size:11px">另有 <code>research</code>(纯只读调研,不写、无 gate)与 <code>interactive</code>(人工接管,见「交互平面」专题)两个 work_type,规格同样登记在 AGENT_REGISTRY。</div>
</div>
<div class="card">
<div class="card-title">动态新增 Agent(写作 / 视频发布 示例)</div>
<p class="lead" style="font-size:12px;margin:0 0 10px;color:var(--text2)"><strong>三步加一个 agent</strong>:① <code>work_type</code> / <code>runs.kind</code> 加枚举值(TEXT 兼容、无需改表结构)→ ② 写一条 AgentSpec 注册进 <code>AGENT_REGISTRY</code> → ③(可选)模型档位表 + 工具允许超集补该角色。<strong>调度器 / 状态机零改动</strong>——一旦某任务的 <code>work_plan</code> 含该 work_typeOrchestrator 自动按注册表派发。</p>
<pre><span class="cm">// 示例 1:写作 agent(后期加内容生产能力)</span>
<span class="tp">AGENT_REGISTRY</span>[<span class="str">'writing'</span>] = {
workType:<span class="str">'writing'</span>, runKind:<span class="str">'writing'</span>,
pickModel: () =&gt; <span class="str">'claude-opus-4-8'</span>,
tools:[<span class="str">'Read'</span>,<span class="str">'Write'</span>,<span class="str">'WebSearch'</span>], needsWorktree:<span class="kw">true</span>,
skills:[<span class="str">'elements-of-style:writing-clearly-and-concisely'</span>],
subagents:[<span class="str">'Explore'</span>], <span class="cm">// 查资料</span>
buildPrompt: ctx =&gt; 写作 brief + <span class="cm">L2 项目文风规范</span> + 素材,
gate:<span class="str">'review'</span>, <span class="cm">// 产出走人审</span>
maxTurns:60,
};
<span class="cm">// 示例 2:自动发视频 agent(对外副作用,须显式授权)</span>
<span class="tp">AGENT_REGISTRY</span>[<span class="str">'video-publish'</span>] = {
workType:<span class="str">'video-publish'</span>, runKind:<span class="str">'video-publish'</span>,
pickModel: () =&gt; <span class="str">'claude-opus-4-8'</span>,
tools:[<span class="str">'Read'</span>,<span class="str">'Bash(ffmpeg:*)'</span>,<span class="str">'Bash(youtube-upload:*)'</span>], <span class="cm">// 受控发布工具</span>
needsWorktree:<span class="kw">false</span>,
buildPrompt: ctx =&gt; 视频元数据 + 平台 + 发布策略,
gate:<span class="str">'review'</span>, <span class="cm">// 对外发布强制人审闸</span>
maxTurns:30,
};</pre>
<div class="note" style="margin-top:8px">🛡 <strong>红线护栏</strong>:对外产生副作用的 agent(发布 / 部署 / push)默认强制 <code>gate:'review'</code> 人工闸,且工具受<strong>允许超集</strong>约束——「未审不发」是硬约束,新 agent 不能绕过。纯生产/只读 agentwriting 草稿、research)可配 <code>gate:null</code> 自动放行。</div>
</div>
<a class="chapter-anchor" id="d-ops"></a>
<div class="chapter-hdr"><span></span> 运维与护栏(Ops &amp; Guardrails<span class="phase p2">Phase 2</span></div>
<div class="card">
<div class="card-title">成本与预算治理(精确到项目 · 按 模型 × token 计费)</div>
<p class="lead" style="font-size:12px;margin:0 0 10px;color:var(--text2)">自治系统自动 spawn 付费 agent<strong>必须有成本闸防失控烧钱</strong>。每个 run 从 Agent SDK 抓 token 用量 → 按<strong>每模型</strong>价目算成本 → 落库 → 汇总到 task / project → <strong>项目级预算闸</strong>超额自动停领。</p>
<pre><span class="cm">// 1. 每次模型调用的用量落 runs.usagecc.ts 从 SDK message.usage 抓)</span>
runs.usage = [{ model, input_tokens, output_tokens, cache_read, cache_write }, …]
<span class="cm">// 2. 价目表($/Mtok,各模型 in/out/cache 单价不同)</span>
<span class="kw">const</span> <span class="tp">MODEL_PRICES</span> = {
<span class="str">'claude-opus-4-8'</span>: { in:15, out:75, cacheRead:1.5, cacheWrite:18.75 },
<span class="str">'claude-sonnet-4-6'</span>: { in:3, out:15, cacheRead:0.3, cacheWrite:3.75 },
<span class="str">'claude-haiku-4-5'</span>: { in:1, out:5, cacheRead:0.1, cacheWrite:1.25 },
};
<span class="cm">// 3. run 成本 = Σ 每次调用(按该次的 model 单价)</span>
cost_usd = Σ ( in_tok·P.in + out_tok·P.out + cache_read·P.cacheRead + cache_write·P.cacheWrite ) / 1e6
<span class="cm">// 4. 汇总:task.cost = Σ runs.cost_usd project 当期 spend = Σ task.cost(按 budget_period</span>
<span class="cm">// 5. 项目级预算闸(Orchestrator claim 前)</span>
<span class="kw">if</span> (project.budget_usd &amp;&amp; periodSpend(project, project.budget_period) &gt;= project.budget_usd) {
pauseProject(project); <span class="cm">// 新任务不再领取;在途 run 跑完不打断;通知人工</span>
<span class="kw">continue</span>;
}</pre>
<table class="tbl" style="margin-top:10px">
<tr><th>维度</th><th>设计</th></tr>
<tr><td>计费粒度</td><td>per 模型调用 → per run<code>runs.cost_usd</code>)→ per task → <strong>per project(当期)</strong></td></tr>
<tr><td>预算闸</td><td><code>projects.budget_usd</code> + <code>budget_periodday|month</code>;超额 → project 自动 <code>paused</code> 停领,可单独配全局日上限做二级保险</td></tr>
<tr><td>消费端</td><td><code>/api/usage</code>(成本明细:项目/模型/时段)与 <code>/api/metrics</code> 由此填充,不再是空端点</td></tr>
<tr><td>价目维护</td><td>价目表为代码常量,模型变更/调价改一处;缓存命中(cacheRead)显著降本,纳入计算</td></tr>
</table>
</div>
<div class="card">
<div class="card-title">限流与全局并发背压</div>
<table class="tbl">
<tr><th style="width:120px">机制</th><th>设计</th></tr>
<tr><td>两层并发闸</td><td><code>project.concurrency</code>(单项目)<strong>+ 新增 <code>MAESTRO_MAX_INFLIGHT</code>(全局在途上限)</strong>claim 受 <code>min(项目闸, 全局闸)</code> —— 防多项目×并发把 API 打爆</td></tr>
<tr><td>API 429 / overloaded</td><td>cc.ts 捕获 Anthropic 限流 → run 标 <code>throttled</code><strong>不计任务失败</strong>、不消耗 max_retries)→ 退避后重排;与 <code>failAttempt</code> 严格区分</td></tr>
<tr><td>spawn 速率</td><td>令牌桶限制每秒新起 worker 数,避免 tick 一次性雷鸣群(thundering herd)冲击 API + 磁盘</td></tr>
<tr><td>与成本闸协同</td><td>budget 超 → <strong>停领</strong>(不再 claim);429 → <strong>慢领</strong>(退避);两者正交叠加</td></tr>
</table>
</div>
<div class="card">
<div class="card-title">资源生命周期与 GC</div>
<table class="tbl">
<tr><th style="width:120px">资源</th><th>回收策略</th></tr>
<tr><td>worktree</td><td>task done/cancelled/reap 后删 worktree<strong>已合并</strong>的临时分支删除,<strong>未合并</strong>分支保留待人查</td></tr>
<tr><td><code>runs/&lt;runId&gt;/</code></td><td>job.json / outbox / heartbeat / transcript 保留 <code>retention_days</code>(可配)→ 过期归档或删;transcript 大文件单独策略</td></tr>
<tr><td><code>events</code></td><td>append-only → 定期 <strong>rollup</strong>(旧事件按 task 聚合)/ 归档到冷表,防无限增长拖慢看板查询</td></tr>
<tr><td>启动 GC</td><td>daemon 启动扫<strong>孤儿 worktree</strong>(无对应活跃 run)→ 清理;扫残留 runs 目录对账</td></tr>
<tr><td>SQLite</td><td>周期 WAL checkpoint + 低峰 VACUUM,控制 DB 文件膨胀</td></tr>
</table>
</div>
<div class="card">
<div class="card-title">可观测性(metrics / health / trace</div>
<table class="tbl">
<tr><th style="width:120px"></th><th>内容</th></tr>
<tr><td><code>/api/metrics</code></td><td>队列深度(各 <code>schedule_status</code> 计数)· 吞吐(done/h)· agent 时延 + turn 数分布 · review 通过/reject 率 · <strong>成本(per project / model</strong></td></tr>
<tr><td><code>/api/health</code></td><td>daemon 存活 + DB 可写 + 在途 worker 数 + 最近一次 tick 时间(探活/告警用)</td></tr>
<tr><td>trace_id</td><td><code>JobSpec.traceId</code> 贯穿 outbox 每条 → 串起一次任务跨多 runplan→code→review→pr)的全链;<strong>从 Phase 2 提前为一等公民</strong>(成本/排障都依赖它聚合)</td></tr>
<tr><td>审计源</td><td><code>events</code> 表已是审计 + WS 推送源,metrics 在其上做聚合,不另起存储</td></tr>
</table>
</div>
<div class="card">
<div class="card-title">依赖图完整性(环检测 / 死锁)</div>
<table class="tbl">
<tr><th style="width:120px">时机</th><th>设计</th></tr>
<tr><td>decompose 落库时</td><td>planner 产出 deps → 建边前跑<strong>环检测</strong>(拓扑排序 / DFS);发现环 → 拒绝该 decompose 结果 + planner 重试(带「deps 成环,请重排」意见)</td></tr>
<tr><td>运行时死锁</td><td>若存在非终态 task 但<strong>可运行集为空</strong>(全部 blocked 互等)→ 标记死锁 + 通知人工,不空转</td></tr>
<tr><td>防御(prompt</td><td>planner prompt 明确「deps 用子任务序号引用、禁自环 / 互环」,从源头降低成环概率</td></tr>
</table>
</div>
<a class="chapter-anchor" id="d-security"></a>
<div class="chapter-hdr"><span></span> 安全与沙箱(Security &amp; Sandbox<span class="phase p2">Phase 2</span></div>
<div class="card">
<div class="card-title">安全模型(沙箱 · 凭证 · 脱敏 · 对外授权)</div>
<table class="tbl">
<tr><th style="width:130px"></th><th>设计</th></tr>
<tr><td>沙箱</td><td><code>MAESTRO_SANDBOX</code>worker 进程 OS 级<strong>写围栏</strong>(只允许写自己的 worktree + <code>runs/&lt;runId&gt;/</code>+ ulimitsCPU/内存/fd);配置落 <code>sandbox.sb</code>。默认 off<strong>生产建议 on</strong></td></tr>
<tr><td>工具红线(全局)</td><td>任何 agent 不可逾越:<strong>禁 git push / 禁部署 / 禁对外发布</strong>(除非显式授权 agent + 人审闸);实际工具 = <code>AgentSpec.tools ∩ 全局允许超集</code>,红线永不入超集</td></tr>
<tr><td>凭证</td><td>git / API 凭证<strong>不写进 worktree、不入 job.json</strong>;由 daemon 以最小权限注入 worker env,按 run 生命周期回收;解冲突/PR 的 git 操作受控、不外推</td></tr>
<tr><td>secret 脱敏</td><td><code>outbox</code> / <code>transcript</code> / <code>events</code> 落盘前过滤已知 secret 模式(token / key / 凭证),防泄漏到审计层与看板</td></tr>
<tr><td>对外副作用授权</td><td>产生外部副作用的 agent<code>video-publish</code> / deploy)强制 <code>gate:'review'</code> 人审闸 + <strong>项目级显式开关</strong>,未授权不执行——「未审不对外」是硬约束</td></tr>
</table>
<div class="note" style="margin-top:10px">与 ⑤ Agent 规格的工具白名单、交互平面的「有界可配工具」一脉相承:<strong>能力按需授予、红线全局兜底、对外副作用强制人审</strong></div>
</div>
<a class="chapter-anchor" id="d-api"></a>
<div class="chapter-hdr"><span></span> 前端 API 契约(REST + WS<span class="phase mix">P1 现有 · P2 ★新路由</span></div>
<div class="card">
<div class="card-title">统一约定</div>
<table class="tbl">
<tr><th style="width:130px"></th><th>约定</th></tr>
<tr><td>Base URL</td><td><code>http://127.0.0.1:4517</code><code>MAESTRO_PORT</code>);仅本地监听</td></tr>
<tr><td>Content-Type</td><td><code>application/json</code>;附件上传用 <code>multipart/form-data</code>SSE 为 <code>text/event-stream</code></td></tr>
<tr><td>鉴权</td><td>本地无鉴权(仅 127.0.0.1);如需远程暴露另加(本期不含)</td></tr>
<tr><td>错误格式</td><td>统一 <code>{ "error": "&lt;message&gt;" }</code><code>StoreError</code><strong>400</strong>、不存在 → <strong>404</strong>、状态机非法转移 → 400、内部 → <strong>500</strong></td></tr>
<tr><td>时间字段</td><td>ISO8601 字符串(TEXT</td></tr>
</table>
</div>
<div class="card">
<div class="card-title">Projects API</div>
<table class="tbl">
<tr><th style="width:62px">方法</th><th style="width:230px">路径</th><th>请求体(关键字段)</th><th>响应</th></tr>
<tr><td>GET</td><td><code>/api/projects</code></td><td></td><td><code>Project[]</code></td></tr>
<tr><td>POST</td><td><code>/api/projects</code></td><td><code>{name*, repoPath*, defaultBranch?, autonomy?, concurrency?, model?}</code></td><td><code>Project</code></td></tr>
<tr><td>GET</td><td><code>/api/projects/:id</code></td><td></td><td><code>Project</code></td></tr>
<tr><td>PATCH</td><td><code>/api/projects/:id</code></td><td>部分字段:<code>autonomy / concurrency / model / budget_usd / budget_period / checks / agent_rules / diff_max_files / verify_cmd</code></td><td><code>Project</code></td></tr>
<tr><td>DELETE</td><td><code>/api/projects/:id</code></td><td></td><td><code>{ok}</code></td></tr>
<tr><td>POST</td><td><code>/api/projects/:id/sync</code></td><td></td><td><code>{created, done, skipped}</code></td></tr>
<tr><td>POST</td><td><code>/api/projects/reorder</code></td><td><code>{order: id[]}</code></td><td><code>{ok}</code></td></tr>
<tr><td>GET</td><td><code>/api/projects/:id/tasks</code></td><td></td><td><code>Task[]</code>(任务树)</td></tr>
<tr><td>GET</td><td><code>/api/projects/:id/events</code></td><td></td><td><code>Event[]</code></td></tr>
</table>
</div>
<div class="card">
<div class="card-title">Tasks API</div>
<table class="tbl">
<tr><th style="width:62px">方法</th><th style="width:230px">路径</th><th>请求体(关键字段)</th><th>响应</th></tr>
<tr><td>POST</td><td><code>/api/projects/:id/tasks</code></td><td><code>{title*, complexity?(auto|hard|medium|easy), priority?, deps?, parentId?, owned_files?, scope?, expected_output?}</code></td><td><code>Task</code></td></tr>
<tr><td>GET</td><td><code>/api/tasks/:id</code></td><td></td><td><code>Task</code></td></tr>
<tr><td>POST</td><td><code>/api/tasks/:id/plan</code></td><td><code>{plan*}</code></td><td><code>Task</code></td></tr>
<tr><td>POST</td><td><code>/api/tasks/:id/spec</code></td><td><code>{spec*}</code></td><td><code>Task</code></td></tr>
<tr><td>POST</td><td><code>/api/tasks/:id/decide</code></td><td><code>{action: accept|reject, actor?, reason?, merge?}</code>(人工闸)</td><td><code>Task</code></td></tr>
<tr><td>POST</td><td><code>/api/tasks/:id/transition</code></td><td><code>{to*, meta?}</code>(守卫校验合法转移)</td><td><code>Task</code></td></tr>
<tr><td>POST</td><td><code>/api/tasks/:id/requeue</code></td><td></td><td><code>Task</code></td></tr>
<tr><td>POST</td><td><code>/api/tasks/:id/cancel</code></td><td></td><td><code>Task</code></td></tr>
<tr><td>DELETE</td><td><code>/api/tasks/:id</code></td><td></td><td><code>{ok}</code></td></tr>
<tr><td><span class="chg add">POST ★</span></td><td><code>/api/tasks/:id/attachments</code></td><td><code>multipart: files[]</code>(图片/文件)</td><td><code>{attachments:[{name,type,path}]}</code></td></tr>
<tr><td><span class="chg add">POST ★</span></td><td><code>/api/tasks/:id/takeover</code></td><td>—(起交互接管 run</td><td><code>{runId, console}</code></td></tr>
</table>
</div>
<div class="card">
<div class="card-title">Runs / 观测 API</div>
<table class="tbl">
<tr><th style="width:62px">方法</th><th style="width:230px">路径</th><th>请求体 / 查询</th><th>响应</th></tr>
<tr><td>GET</td><td><code>/api/tasks/:id/runs</code></td><td></td><td><code>Run[]</code></td></tr>
<tr><td>GET</td><td><code>/api/runs/:id/transcript</code></td><td></td><td><code>text/ndjson</code>transcript</td></tr>
<tr><td>GET</td><td><code>/api/tasks/:id/events</code></td><td></td><td><code>Event[]</code></td></tr>
<tr><td><span class="chg add">GET ★</span></td><td><code>/api/tasks/:id/stream</code></td><td></td><td><code>text/event-stream</code>agent token SSEPhase 2</td></tr>
<tr><td>GET</td><td><code>/api/agents</code></td><td></td><td><code>AgentSpec[]</code>(注册表)</td></tr>
<tr><td><span class="chg add">GET ★</span></td><td><code>/api/usage</code></td><td><code>?project&amp;period</code></td><td><code>{byProject, byModel, total}</code> 成本明细</td></tr>
<tr><td><span class="chg add">GET ★</span></td><td><code>/api/metrics</code></td><td></td><td><code>{queueDepth, throughput, latency, reviewPassRate, cost}</code></td></tr>
<tr><td><span class="chg add">GET ★</span></td><td><code>/api/health</code></td><td></td><td><code>{ok, db, inflight, lastTickAt}</code></td></tr>
</table>
</div>
<div class="card">
<div class="card-title">WebSocket 事件契约(<code>/ws</code></div>
<p class="lead" style="font-size:12px;margin:0 0 10px;color:var(--text2)">连接 <code>ws://127.0.0.1:4517/ws</code>,连上后<strong>服务端单向推送</strong>(客户端无需发消息)。每条消息 = 一行 <code>events</code> 表记录,前端据 <code>type</code> 增量更新看板,<strong>无需轮询</strong></p>
<pre><span class="cm">// 消息封装(JSON</span>
{ type, projectId, taskId, payload, at }</pre>
<table class="tbl" style="margin-top:10px">
<tr><th style="width:200px">type</th><th>触发</th><th>payload 关键</th></tr>
<tr><td><code>task.created</code></td><td>新任务(任意入口)</td><td>task</td></tr>
<tr><td><code>task.updated</code></td><td>任务字段变更</td><td>task / 变更字段</td></tr>
<tr><td><code>status.changed</code></td><td><code>schedule_status</code> 变更</td><td>from / to / work_type</td></tr>
<tr><td><code>complexity.changed</code></td><td>AUTO 复杂度回填</td><td>complexity / reason</td></tr>
<tr><td><code>run.started</code></td><td>worker 起跑</td><td>runId / kind</td></tr>
<tr><td><code>run.finished</code></td><td>run 结束</td><td>runId / result</td></tr>
<tr><td><code>approval.granted</code> / <code>approval.rejected</code></td><td>人工闸决策</td><td>gate / reason</td></tr>
<tr><td><code>merge.failed</code> / <code>merge.remediated</code></td><td>合并冲突 / 收口</td><td>branch</td></tr>
<tr><td><code>project.synced</code></td><td>todo 同步完成</td><td>created / done / skipped</td></tr>
<tr><td><span class="chg add"><code>run.progress</code></span></td><td>SSE 配套(Phase 2</td><td>runId / token 增量</td></tr>
<tr><td><span class="chg add"><code>budget.exceeded</code></span></td><td>项目当期成本超预算</td><td>projectId / spend / budget</td></tr>
</table>
<div class="note" style="margin-top:8px">★ = 本次重构新增路由 / 事件。完整 <code>Task</code> / <code>Project</code> / <code>Run</code> / <code>Event</code> 结构见 ③ 数据库 Schema 的字段表(响应体即各表行的 camelCase 映射)。</div>
</div>
<!-- ══════════ ⑤ Phase 1 实施计划 ══════════ -->
</div><!-- /.section s-design -->
<div class="section" id="s-impl">
<div class="sec-heading">Phase 1 实施计划</div>
<div class="sec-desc">六大任务(T1–T6)及 checkbox 进度追踪,实施前须完成 ③ 设计方案审阅。</div>
<div style="max-width:1280px;margin:0 auto;padding:0 40px 40px">
<a class="chapter-anchor" id="d-plan"></a>
<div class="chapter-hdr"><span></span> Phase 1 实施计划</div>
<div class="card" style="border-left:3px solid var(--amber)">
<div class="card-title">全局约束</div>
<div class="grid-2">
<div>
<div class="row"><code>npm test</code> 全套测试,使用 <code>new Store(':memory:')</code>,禁止 mock</div>
<div class="row"><code>npm run typecheck</code> 类型检查</div>
<div class="row"><code>npm run build</code> 编译到 dist/</div>
</div>
<div>
<div class="row" style="color:var(--amber)">最强档模型 ID<code>'claude-opus-4-8'</code>fable-5 暂不可用)</div>
<div class="row" style="color:var(--red)">禁止 git push / 禁止部署</div>
<div class="row" style="color:var(--red)">禁止重复实现已有功能(见"已完成项")</div>
</div>
</div>
</div>
<div class="card" style="border-left:3px solid var(--green)">
<div class="card-title">已完成项(禁止重复实现)</div>
<div class="row"><code>verdict→硬闸</code> ingest.ts 149165 行:任一 reject 退回重执行</div>
<div class="row"><code>Promise.all 并行双复审</code> pipeline.ts 154 行</div>
<div class="row"><code>syncMain</code> 执行前同步主分支 pipeline.ts 121127 行</div>
<div class="row"><code>conflict 管道</code> runConflictPipeline pipeline.ts 211273 行</div>
<div class="row"><code>planner maxTurns</code> 按复杂度 90/60/40 runner.ts 168 行</div>
<div class="row"><code>depth 注入</code> 防无限拆 buildPlannerPrompt 133 行</div>
<div class="row"><code>git 只读工具</code> log/diff/show runPlanner 171 行</div>
<div class="row"><code>deps 序号→taskId</code> 映射 ingest.ts 103116 行</div>
<div class="note" style="margin-top:12px"><strong>迁移说明</strong>:以上均实现在<strong>旧扁平 FSM</strong>analyzing/speccing/executing/exec_review)上。重构到 schedule×work_type 时<strong>业务逻辑可复用</strong>(双复审 / 解冲突 / syncMain / planner 档位与 prompt / deps 映射),但<strong>状态机接线须重指向</strong>新 schedule_status×work_type:① review 现为<strong>独立 work_type</strong>,「Promise.all 双复审」须从 executor(code) run <strong>拆出到独立 reviewer run</strong>;② 「verdict→硬闸」「exec_review 转移」重定位到 review 步的 <code>waiting(review)</code> 闸 + <code>pr</code> 步合并。属 T1–T6 的迁移内容,非推倒重写。</div>
</div>
<!-- T1 -->
<div class="task-card" id="tc1">
<div class="task-hdr" onclick="toggleTask('tc1')">
<div class="task-num t1">T1</div>
<div class="task-info"><div class="task-title2">Schema 迁移 + Task 类型扩展</div><div class="task-sub">schema.sql · db.ts · types.ts · mappers.ts · store.ts</div></div>
<div class="task-prog"><span class="dn" id="dn-tc1">0</span>/10</div>
<div class="caret"></div>
</div>
<div class="task-body">
<ul class="steps" id="sl-tc1">
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind test">测试</span>步骤 1:写失败测试</div>
<pre>test(<span class="str">'Task 新字段默认值'</span>, () => {
<span class="kw">const</span> store = <span class="kw">new</span> <span class="tp">Store</span>(<span class="str">':memory:'</span>);
<span class="kw">const</span> p = store.<span class="fn">createProject</span>({ name:<span class="str">'p'</span>, repoPath:<span class="str">'/tmp/t-'</span>+Math.random(), autonomy:<span class="str">'manual'</span> });
<span class="kw">const</span> t = store.<span class="fn">createTask</span>({ projectId: p.id, title:<span class="str">'feat'</span>, complexity:<span class="str">'easy'</span> });
assert.<span class="fn">strictEqual</span>(t.scheduleStatus, <span class="str">'init'</span>);
assert.<span class="fn">deepStrictEqual</span>(t.workPlan, []);
assert.<span class="fn">strictEqual</span>(t.version, <span class="nu">1</span>);
});</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind run">运行</span>步骤 2:确认失败</div>
<pre>tsx --test test/store.test.ts 2>&1 | grep -E "fail|Error" | head -5</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 3schema.sql — tasks 加状态/队列等 9 列,runs 加 trace_id</div>
<pre> schedule_status TEXT NOT NULL DEFAULT <span class="str">'init'</span>,
work_type TEXT,
work_plan TEXT NOT NULL DEFAULT <span class="str">'[]'</span>,
work_history TEXT NOT NULL DEFAULT <span class="str">'[]'</span>,
claimed_at TEXT,
version INTEGER NOT NULL DEFAULT <span class="nu">1</span>,
scope TEXT,
owned_files TEXT NOT NULL DEFAULT <span class="str">'[]'</span>,
expected_output TEXT
<span class="cm">-- runs 表:</span>
trace_id TEXT</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 4db.ts — openDb 末尾追加 10 个 ensureColumn</div>
<pre> <span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'schedule_status'</span>,<span class="str">"schedule_status TEXT NOT NULL DEFAULT 'init'"</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'work_type'</span>,<span class="str">'work_type TEXT'</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'work_plan'</span>,<span class="str">"work_plan TEXT NOT NULL DEFAULT '[]'"</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'work_history'</span>,<span class="str">"work_history TEXT NOT NULL DEFAULT '[]'"</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'claimed_at'</span>,<span class="str">'claimed_at TEXT'</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'version'</span>,<span class="str">'version INTEGER NOT NULL DEFAULT 1'</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'scope'</span>,<span class="str">'scope TEXT'</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'owned_files'</span>,<span class="str">"owned_files TEXT NOT NULL DEFAULT '[]'"</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'tasks'</span>,<span class="str">'expected_output'</span>,<span class="str">'expected_output TEXT'</span>);
<span class="fn">ensureColumn</span>(db,<span class="str">'runs'</span>,<span class="str">'trace_id'</span>,<span class="str">'trace_id TEXT'</span>);</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 5types.ts — 新增 ScheduleStatus/WorkType/WorkStep + Task 接口字段</div>
<pre><span class="kw">export type</span> <span class="tp">ScheduleStatus</span> = <span class="str">'init'</span>|<span class="str">'ready'</span>|<span class="str">'running'</span>|<span class="str">'waiting'</span>|<span class="str">'blocked'</span>|<span class="str">'paused'</span>|<span class="str">'failed'</span>|<span class="str">'stuck'</span>|<span class="str">'done'</span>|<span class="str">'cancelled'</span>;
<span class="kw">export type</span> <span class="tp">WorkType</span> = <span class="str">'plan'</span>|<span class="str">'code'</span>|<span class="str">'review'</span>|<span class="str">'pr'</span>|<span class="str">'conflict'</span>|<span class="str">'research'</span>;
<span class="kw">export type</span> <span class="tp">TaskScope</span> = <span class="str">'file'</span>|<span class="str">'module'</span>|<span class="str">'service'</span>|<span class="str">'cross-service'</span>;
<span class="kw">export interface</span> <span class="tp">WorkStep</span> { type: <span class="tp">WorkType</span>; spec?: <span class="tp">string</span>; ownedFiles?: <span class="tp">string</span>[]; expectedOutput?: <span class="tp">string</span>; }
<span class="cm">// Task 接口追加:</span>
scheduleStatus: <span class="tp">ScheduleStatus</span>; workType: <span class="tp">WorkType</span>|<span class="kw">null</span>;
workPlan: <span class="tp">WorkStep</span>[]; workHistory: <span class="tp">WorkStep</span>[];
scope: <span class="tp">TaskScope</span>|<span class="kw">null</span>; ownedFiles: <span class="tp">string</span>[]; expectedOutput: <span class="tp">string</span>|<span class="kw">null</span>;
claimedAt: <span class="tp">string</span>|<span class="kw">null</span>; version: <span class="tp">number</span>;</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 6mappers.ts — TaskRow 加字段 + rowToTask 映射</div>
<pre><span class="cm">// TaskRow 追加:</span>
schedule_status:<span class="tp">string</span>; work_type:<span class="tp">string</span>|<span class="kw">null</span>; work_plan:<span class="tp">string</span>; work_history:<span class="tp">string</span>; owned_files:<span class="tp">string</span>; claimed_at:<span class="tp">string</span>|<span class="kw">null</span>; version:<span class="tp">number</span>;
<span class="cm">// rowToTask 追加:</span>
scheduleStatus: r.schedule_status ?? <span class="str">'init'</span>,
workType: r.work_type ?? <span class="kw">null</span>,
workPlan: r.work_plan ? JSON.<span class="fn">parse</span>(r.work_plan) : [],
workHistory: r.work_history ? JSON.<span class="fn">parse</span>(r.work_history) : [],
ownedFiles: r.owned_files ? JSON.<span class="fn">parse</span>(r.owned_files) : [],
version: r.version ?? <span class="nu">1</span>,</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 7store.ts — PatchTaskInput 支持新字段(workPlan/workHistory 用 JSON.stringify 序列化)</div>
<div class="step-desc">按已有参数化查询模式处理;禁止字符串拼接传值。</div>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind run">运行</span>步骤 8npm test</div>
<pre>npm test 2>&1 | tail -15</pre>
<div class="step-desc">预期:新测试通过,旧测试不受影响。</div>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind run">运行</span>步骤 9typecheck</div>
<pre>npm run typecheck</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc1')"></div><div class="step-con">
<div class="step-ttl"><span class="kind commit">提交</span>步骤 10</div>
<pre>git add src/store/schema.sql src/store/db.ts src/model/types.ts src/store/mappers.ts src/store/store.ts test/store.test.ts
git commit -m "feat(model): 新增 scheduleStatus/workType/workPlan/workHistory/scope/version 字段 + schema 迁移"</pre>
</div></li>
</ul>
</div></div>
<!-- T2 -->
<div class="task-card" id="tc2">
<div class="task-hdr" onclick="toggleTask('tc2')">
<div class="task-num t2">T2</div>
<div class="task-info"><div class="task-title2">CAS Claim + 执行期锁</div><div class="task-sub">store.ts · orchestrator.ts</div></div>
<div class="task-prog"><span class="dn" id="dn-tc2">0</span>/10</div><div class="caret"></div>
</div>
<div class="task-body">
<ul class="steps" id="sl-tc2">
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con">
<div class="step-ttl"><span class="kind test">测试</span>步骤 1:写失败测试(双重 claim + ready 清零)</div>
<pre>test(<span class="str">'CAS — 同任务不能双重领取'</span>, () => {
assert.<span class="fn">ok</span>(store.<span class="fn">casClaimTask</span>(task.id,<span class="str">'run-1'</span>));
assert.<span class="fn">ok</span>(!store.<span class="fn">casClaimTask</span>(task.id,<span class="str">'run-2'</span>));
});
test(<span class="str">'transition→ready 清空 claimed_at'</span>, () => {
store.<span class="fn">decide</span>(task.id,<span class="str">'reject'</span>,<span class="str">'human'</span>,<span class="str">'改'</span>);
assert.<span class="fn">ok</span>(store.<span class="fn">casClaimTask</span>(task.id,<span class="str">'run-3'</span>));
});</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con">
<div class="step-ttl"><span class="kind run">运行</span>步骤 2:确认失败</div>
<pre>tsx --test test/orchestrator.test.ts 2>&1 | grep casClaimTask | head -3</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 3store.ts — casClaimTask</div>
<pre><span class="fn">casClaimTask</span>(taskId:<span class="tp">string</span>,_runId:<span class="tp">string</span>):<span class="tp">boolean</span> {
<span class="kw">const</span> r=<span class="kw">this</span>.db.<span class="fn">prepare</span>(
<span class="str">`UPDATE tasks SET claimed_at=?,updated_at=? WHERE id=? AND claimed_at IS NULL`</span>
).<span class="fn">run</span>(<span class="fn">now</span>(),<span class="fn">now</span>(),taskId);
<span class="kw">return</span> r.changes===<span class="nu">1</span>;
}</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 4transition() to==='ready' 清 claimed_atrequeueTask 同步清零</div>
<pre><span class="kw">if</span>(to===<span class="str">'ready'</span>){
<span class="kw">this</span>.db.<span class="fn">prepare</span>(<span class="str">`UPDATE tasks SET status=?,claimed_at=NULL,updated_at=? WHERE id=?`</span>).<span class="fn">run</span>(to,<span class="fn">now</span>(),taskId);
}</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 5orchestrator.claimOne — startRun 后接 casClaimTask</div>
<pre><span class="kw">if</span>(!store.<span class="fn">casClaimTask</span>(task.id,run.id)){log.<span class="fn">error</span>(<span class="str">`CAS失败`</span>);<span class="kw">return</span>;}</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 6store.ts — assertNoActiveRun 私有方法</div>
<pre><span class="kw">private</span> <span class="fn">assertNoActiveRun</span>(taskId:<span class="tp">string</span>){
<span class="kw">const</span> {n}=<span class="kw">this</span>.db.<span class="fn">prepare</span>(<span class="str">`SELECT COUNT(*) n FROM runs WHERE task_id=? AND status='started'`</span>).<span class="fn">get</span>(taskId) <span class="kw">as</span> {n:<span class="tp">number</span>};
<span class="kw">if</span>(n><span class="nu">0</span>) <span class="kw">throw new</span> <span class="tp">StoreError</span>(<span class="str">'任务执行中,禁止修改'</span>);
}</pre>
<div class="step-desc">在 setSpec / setPlan / patchTask 入口调用 assertNoActiveRun。</div>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con">
<div class="step-ttl"><span class="kind test">测试</span>步骤 7:补执行期锁测试</div>
<pre>test(<span class="str">'执行中禁止修改 spec'</span>, () => {
store.<span class="fn">startRun</span>(t.id,<span class="str">'planner'</span>);
assert.<span class="fn">throws</span>(()=>store.<span class="fn">setSpec</span>(t.id,<span class="str">'x'</span>),/执行中/);
});</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 8npm test</div><pre>npm test 2>&1 | tail -15</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 9typecheck</div><pre>npm run typecheck</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc2')"></div><div class="step-con">
<div class="step-ttl"><span class="kind commit">提交</span>步骤 10</div>
<pre>git add src/store/store.ts src/daemon/orchestrator.ts test/orchestrator.test.ts
git commit -m "feat(store): CAS claimed_at 防双重 claim + 执行期锁"</pre>
</div></li>
</ul>
</div></div>
<!-- T3 -->
<div class="task-card" id="tc3">
<div class="task-hdr" onclick="toggleTask('tc3')">
<div class="task-num t3">T3</div>
<div class="task-info"><div class="task-title2">调度算法升级(rankU + agingBonus + couplingPenalty</div><div class="task-sub">scoring.ts · orchestrator.ts</div></div>
<div class="task-prog"><span class="dn" id="dn-tc3">0</span>/7</div><div class="caret"></div>
</div>
<div class="task-body">
<ul class="steps" id="sl-tc3">
<li class="step"><div class="chkbox" onclick="ck(this,'tc3')"></div><div class="step-con">
<div class="step-ttl"><span class="kind test">测试</span>步骤 1:写失败测试</div>
<pre>test(<span class="str">'agingBonus — 新建任务为 0'</span>, () =>
assert.<span class="fn">strictEqual</span>(<span class="fn">agingBonus</span>({createdAt:<span class="kw">new</span> Date().<span class="fn">toISOString</span>(),priority:<span class="nu">1</span>} <span class="kw">as</span> <span class="tp">Task</span>,Date.<span class="fn">now</span>()),<span class="nu">0</span>));
test(<span class="str">'couplingPenalty — 1个重叠=-0.5'</span>, () =>
assert.<span class="fn">strictEqual</span>(<span class="fn">couplingPenalty</span>({ownedFiles:[<span class="str">'a.ts'</span>,<span class="str">'b.ts'</span>]} <span class="kw">as</span> <span class="tp">Task</span>,[{ownedFiles:[<span class="str">'a.ts'</span>]} <span class="kw">as</span> <span class="tp">Task</span>]),<span class="nu">0.5</span>));</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc3')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 2:确认失败</div><pre>tsx --test test/orchestrator.test.ts 2>&1 | grep -E "agingBonus|coupling" | head -3</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc3')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 3:重写 scoring.ts — 导出 rankU / agingBonus / couplingPenalty / 新签名 rankByScore</div>
<pre><span class="kw">export function</span> <span class="fn">rankU</span>(id:<span class="tp">string</span>,byId:<span class="tp">Map</span>&lt;<span class="tp">string</span>,<span class="tp">Task</span>&gt;,deps:<span class="tp">Map</span>&lt;<span class="tp">string</span>,<span class="tp">Task</span>[]&gt;,cache:<span class="tp">Map</span>&lt;<span class="tp">string</span>,<span class="tp">number</span>&gt;):<span class="tp">number</span>{
<span class="kw">if</span>(cache.<span class="fn">has</span>(id))<span class="kw">return</span> cache.<span class="fn">get</span>(id)!;
<span class="kw">const</span> t=byId.<span class="fn">get</span>(id); <span class="kw">if</span>(!t){cache.<span class="fn">set</span>(id,<span class="nu">0</span>);<span class="kw">return</span> <span class="nu">0</span>;}
<span class="kw">const</span> v=<span class="fn">baseScore</span>(t.priority)+(deps.<span class="fn">get</span>(id)??[]).<span class="fn">reduce</span>((s,d)=>s+<span class="fn">rankU</span>(d.id,byId,deps,cache),<span class="nu">0</span>);
cache.<span class="fn">set</span>(id,v);<span class="kw">return</span> v;
}
<span class="kw">export const</span> <span class="fn">agingBonus</span>=(t:<span class="tp">Task</span>,nowMs:<span class="tp">number</span>)=>
Math.<span class="fn">min</span>(<span class="fn">baseScore</span>(t.priority),(nowMs-Date.<span class="fn">parse</span>(t.createdAt))/<span class="nu">3_600_000</span>/<span class="nu">48</span>);
<span class="kw">export function</span> <span class="fn">couplingPenalty</span>(t:<span class="tp">Task</span>,inFlight:<span class="tp">Task</span>[]):<span class="tp">number</span>{
<span class="kw">const</span> s=<span class="kw">new</span> Set(t.ownedFiles??[]); <span class="kw">if</span>(!s.size)<span class="kw">return</span> <span class="nu">0</span>;
<span class="kw">let</span> n=<span class="nu">0</span>; <span class="kw">for</span>(<span class="kw">const</span> x <span class="kw">of</span> inFlight)<span class="kw">for</span>(<span class="kw">const</span> f <span class="kw">of</span>(x.ownedFiles??[])) <span class="kw">if</span>(s.<span class="fn">has</span>(f))n++;
<span class="kw">return</span> n*<span class="nu">0.5</span>;
}</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc3')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 4orchestrator.ts — claimable() 传入 inFlight 列表</div>
<pre><span class="kw">const</span> inflightList=tasks.<span class="fn">filter</span>(t=>inflight.<span class="fn">has</span>(t.id));
<span class="kw">return</span> <span class="fn">rankByScore</span>(candidates,tasks,inflightList,d.<span class="fn">nowMs</span>());</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc3')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 5npm test</div><pre>npm test 2>&1 | tail -15</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc3')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 6typecheck</div><pre>npm run typecheck</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc3')"></div><div class="step-con">
<div class="step-ttl"><span class="kind commit">提交</span>步骤 7</div>
<pre>git add src/model/scoring.ts src/daemon/orchestrator.ts test/orchestrator.test.ts
git commit -m "feat(scoring): CPM rankU + agingBonus + couplingPenalty 调度算法"</pre>
</div></li>
</ul>
</div></div>
<!-- T4 -->
<div class="task-card" id="tc4">
<div class="task-hdr" onclick="toggleTask('tc4')">
<div class="task-num t4">T4</div>
<div class="task-info"><div class="task-title2">模型档位升级(统一最强档 opus-4-8)</div><div class="task-sub">models.ts</div></div>
<div class="task-prog"><span class="dn" id="dn-tc4">0</span>/6</div><div class="caret"></div>
</div>
<div class="task-body">
<ul class="steps" id="sl-tc4">
<li class="step"><div class="chkbox" onclick="ck(this,'tc4')"></div><div class="step-con">
<div class="step-ttl"><span class="kind test">测试</span>步骤 1:写失败测试</div>
<pre>test(<span class="str">'planner hard = opus-4-8'</span>, () =>
assert.<span class="fn">strictEqual</span>(<span class="fn">pickModel</span>({complexity:<span class="str">'hard'</span>}<span class="kw">as</span> <span class="tp">Task</span>,{model:<span class="kw">null</span>}<span class="kw">as</span> <span class="tp">Project</span>,<span class="str">'planner'</span>),<span class="str">'claude-opus-4-8'</span>));
test(<span class="str">'reviewer 不被 project.model 降档'</span>, () =>
assert.<span class="fn">strictEqual</span>(<span class="fn">pickModel</span>({complexity:<span class="str">'easy'</span>}<span class="kw">as</span> <span class="tp">Task</span>,{model:<span class="str">'claude-sonnet-4-6'</span>}<span class="kw">as</span> <span class="tp">Project</span>,<span class="str">'reviewer'</span>),<span class="str">'claude-opus-4-8'</span>));
test(<span class="str">'MODEL_FALLBACK_CHAIN[0] = opus-4-8'</span>, () =>
assert.<span class="fn">strictEqual</span>(MODEL_FALLBACK_CHAIN[<span class="nu">0</span>],<span class="str">'claude-opus-4-8'</span>));</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc4')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 2:确认失败</div><pre>tsx --test test/models.test.ts 2>&1 | grep -E 'opus|reviewer' | head -3</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc4')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 3models.ts — 4 处修改</div>
<pre><span class="cm">// 1. 回退链(fable-5 暂不可用,opus-4-8 为最强档)</span>
<span class="kw">export const</span> MODEL_FALLBACK_CHAIN=[<span class="str">'claude-opus-4-8'</span>,<span class="str">'claude-sonnet-4-6'</span>] <span class="kw">as const</span>;
<span class="cm">// 2. planner 分档:easy=sonnetmedium/hard=opushard 不再与 easy 同档)</span>
hard:[<span class="str">'MAESTRO_MODEL_PLAN_HARD'</span>,<span class="str">'claude-opus-4-8'</span>],
<span class="cm">// 3. reviewer 三档统一最强 opus-4-8(不被 project.model 降档)</span>
reviewer:{easy:[...,<span class="str">'claude-opus-4-8'</span>],medium:[...,<span class="str">'claude-opus-4-8'</span>],hard:[...,<span class="str">'claude-opus-4-8'</span>]},
<span class="cm">// 4. conflict 固定最强(pickModel 首行)</span>
<span class="kw">if</span>(role===<span class="str">'conflict'</span>)<span class="kw">return</span> <span class="str">'claude-opus-4-8'</span>;</pre>
<div class="note">⚠ fable-5 是设计目标最强档,暂不可用;当前一律用 opus-4-8 充当,fable-5 恢复后把以上 4 处的 <code>claude-opus-4-8</code> 换回 <code>claude-fable-5</code> 即可,结构不变。</div>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc4')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 4npm test</div><pre>npm test 2>&1 | tail -15</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc4')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 5typecheck</div><pre>npm run typecheck</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc4')"></div><div class="step-con">
<div class="step-ttl"><span class="kind commit">提交</span>步骤 6</div>
<pre>git add src/executor/models.ts test/models.test.ts
git commit -m "feat(models): 复审/解冲突/planner-hard 统一最强档 opus-4-8"</pre>
</div></li>
</ul>
</div></div>
<!-- T5 -->
<div class="task-card" id="tc5">
<div class="task-hdr" onclick="toggleTask('tc5')">
<div class="task-num t5">T5</div>
<div class="task-info"><div class="task-title2">Planner Decompose JSON 扩展(ownedFiles + expectedOutput</div><div class="task-sub">runner.ts · protocol.ts · ingest.ts</div></div>
<div class="task-prog"><span class="dn" id="dn-tc5">0</span>/8</div><div class="caret"></div>
</div>
<div class="task-body">
<ul class="steps" id="sl-tc5">
<li class="step"><div class="chkbox" onclick="ck(this,'tc5')"></div><div class="step-con">
<div class="step-ttl"><span class="kind test">测试</span>步骤 1ingest decompose-result 映射测试</div>
<pre>test(<span class="str">'decompose-result — ownedFiles 落入子任务'</span>, () => {
<span class="fn">appendOutbox</span>(run.id,{type:<span class="str">'decompose-result'</span>,plan:<span class="str">'x'</span>,
subtasks:[{title:<span class="str">'类型'</span>,complexity:<span class="str">'easy'</span>,priority:<span class="nu">0</span>,deps:[],
ownedFiles:[<span class="str">'src/model/types.ts'</span>],expectedOutput:<span class="str">'typecheck通过'</span>}]});
<span class="fn">ingestRun</span>(store,noopLog,run.id);
<span class="kw">const</span> [child]=store.<span class="fn">childrenOf</span>(parent.id);
assert.<span class="fn">deepStrictEqual</span>(child.ownedFiles,[<span class="str">'src/model/types.ts'</span>]);
assert.<span class="fn">strictEqual</span>(child.expectedOutput,<span class="str">'typecheck通过'</span>);
});</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc5')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 2:确认失败</div><pre>tsx --test test/ingest.test.ts 2>&1 | grep -E "ownedFiles|expectedOutput" | head -3</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc5')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 3protocol.ts — decompose-result 子任务加 ownedFiles? / expectedOutput?</div>
<pre>subtasks: <span class="tp">Array</span>&lt;{title:<span class="tp">string</span>;complexity:<span class="tp">Complexity</span>;priority?:<span class="tp">number</span>;deps?:<span class="tp">number</span>[];
ownedFiles?:<span class="tp">string</span>[];expectedOutput?:<span class="tp">string</span>}&gt;;</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc5')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 4runner.ts — buildPlannerPrompt decompose 分支加两条 bullet + 更新 JSON 示例</div>
<pre><span class="str">'- ownedFiles:主要修改文件路径 JSON string[](无则填 []'</span>,
<span class="str">'- expectedOutput:一句话可验证完成标准(如:"typecheck通过"'</span></pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc5')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 5ingest.ts — decompose-result deps 映射之后追加 ownedFiles/expectedOutput 映射循环</div>
<pre><span class="kw">for</span>(<span class="kw">let</span> i=<span class="nu">0</span>;i&lt;rec.subtasks.length;i++){
<span class="kw">const</span> sub=rec.subtasks[i] <span class="kw">as</span> {ownedFiles?:<span class="tp">string</span>[];expectedOutput?:<span class="tp">string</span>};
<span class="kw">if</span>(!createdIds[i])<span class="kw">continue</span>;
<span class="kw">const</span> patch:<span class="tp">Record</span>&lt;<span class="tp">string</span>,<span class="tp">unknown</span>&gt;={};
<span class="kw">if</span>(<span class="tp">Array</span>.<span class="fn">isArray</span>(sub.ownedFiles)&&sub.ownedFiles.length) patch.ownedFiles=sub.ownedFiles;
<span class="kw">if</span>(<span class="kw">typeof</span> sub.expectedOutput===<span class="str">'string'</span>&&sub.expectedOutput.<span class="fn">trim</span>()) patch.expectedOutput=sub.expectedOutput.<span class="fn">trim</span>();
<span class="kw">if</span>(<span class="tp">Object</span>.<span class="fn">keys</span>(patch).length) store.<span class="fn">patchTask</span>(createdIds[i],patch);
}</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc5')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 6npm test</div><pre>npm test 2>&1 | tail -15</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc5')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 7typecheck</div><pre>npm run typecheck</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc5')"></div><div class="step-con">
<div class="step-ttl"><span class="kind commit">提交</span>步骤 8</div>
<pre>git add src/executor/runner.ts src/executor/protocol.ts src/daemon/ingest.ts test/ingest.test.ts
git commit -m "feat(planner): decompose JSON 扩展 ownedFiles/expectedOutput"</pre>
</div></li>
</ul>
</div></div>
<!-- T6 -->
<div class="task-card" id="tc6">
<div class="task-hdr" onclick="toggleTask('tc6')">
<div class="task-num t6">T6</div>
<div class="task-info"><div class="task-title2">管道硬闸(checks 分项 + diff 体量)</div><div class="task-sub">pipeline.ts · schema.sql · db.ts · types.ts · mappers.ts</div></div>
<div class="task-prog"><span class="dn" id="dn-tc6">0</span>/10</div><div class="caret"></div>
</div>
<div class="task-body">
<ul class="steps" id="sl-tc6">
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con">
<div class="step-ttl"><span class="kind test">测试</span>步骤 1:写失败测试(checks + diff 体量)</div>
<pre>test(<span class="str">'checks — lint 失败阻止 exec_review'</span>, <span class="kw">async</span> () => {
job.project.checks=JSON.<span class="fn">stringify</span>({lint:<span class="str">'exit 1'</span>});
<span class="kw">const</span> emitted=[];
<span class="kw">await</span> <span class="fn">runPipeline</span>(job,mockDeps,p=>emitted.<span class="fn">push</span>(p));
assert.<span class="fn">ok</span>(emitted.<span class="fn">find</span>(e=>e.type===<span class="str">'failed'</span>&&e.error.<span class="fn">includes</span>(<span class="str">'lint'</span>)));
});
test(<span class="str">'diff 体量闸 — 超 diffMaxFiles 失败'</span>, <span class="kw">async</span> () => {
job.project.diffMaxFiles=<span class="nu">2</span>;
mockDeps.worktreeDiff=<span class="kw">async</span>()=>({diffSummary:<span class="str">' a.ts | 1\n b.ts | 1\n c.ts | 1'</span>,commits:[]});
<span class="kw">const</span> emitted=[];
<span class="kw">await</span> <span class="fn">runPipeline</span>(job,mockDeps,p=>emitted.<span class="fn">push</span>(p));
assert.<span class="fn">ok</span>(emitted.<span class="fn">find</span>(e=>e.type===<span class="str">'failed'</span>));
});</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 2:确认失败</div><pre>tsx --test test/pipeline.test.ts 2>&1 | grep -E "diff|checks" | head -3</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 3schema.sql + db.ts — projects 加 diff_max_files</div>
<pre><span class="cm">-- schema.sql</span>
diff_max_files INTEGER NOT NULL DEFAULT <span class="nu">100</span>
<span class="cm">-- db.ts</span>
<span class="fn">ensureColumn</span>(db,<span class="str">'projects'</span>,<span class="str">'diff_max_files'</span>,<span class="str">'diff_max_files INTEGER NOT NULL DEFAULT 100'</span>);</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 4types.ts + mappers.ts — Project 加 diffMaxFiles</div>
<pre><span class="cm">// types.ts</span> diffMaxFiles: <span class="tp">number</span>;
<span class="cm">// rowToProject</span> diffMaxFiles: r.diff_max_files ?? <span class="nu">100</span>,</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 5PipelineDeps 加可选 runCheck 注入点(测试用)</div>
<pre>runCheck?: (cmd:<span class="tp">string</span>, cwd:<span class="tp">string</span>) => <span class="kw">void</span>;</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 6pipeline.ts — verify 之后插入 checks 分项闸</div>
<pre><span class="kw">if</span>(job.project.checks){
<span class="kw">let</span> obj={}; <span class="kw">try</span>{obj=JSON.<span class="fn">parse</span>(job.project.checks)}<span class="kw">catch</span>{}
<span class="kw">for</span>(<span class="kw">const</span> [name,cmd] <span class="kw">of</span> Object.<span class="fn">entries</span>(obj <span class="kw">as</span> Record&lt;string,string&gt;)){
<span class="kw">try</span>{ (deps.runCheck??<span class="fn">defaultRunCheck</span>)(cmd,wt.dir); }
<span class="kw">catch</span>(e){ emit({type:<span class="str">'failed'</span>,error:<span class="str">`[${name}]失败`</span>,...}); <span class="kw">return</span>; }
}
}</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con">
<div class="step-ttl"><span class="kind impl">实现</span>步骤 7pipeline.ts — worktreeDiff 之后插入 diff 体量闸</div>
<pre><span class="kw">const</span> maxF=job.project.diffMaxFiles??<span class="nu">100</span>;
<span class="kw">if</span>(maxF><span class="nu">0</span>){
<span class="kw">const</span> fc=diff.diffSummary.<span class="fn">split</span>(<span class="str">'\n'</span>).<span class="fn">filter</span>(l=>l.<span class="fn">includes</span>(<span class="str">'|'</span>)).length;
<span class="kw">if</span>(fc>maxF){ emit({type:<span class="str">'failed'</span>,error:<span class="str">`diff超限:${fc}>${maxF}文件`</span>,...}); <span class="kw">return</span>; }
}</pre>
</div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 8npm test</div><pre>npm test 2>&1 | tail -15</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con"><div class="step-ttl"><span class="kind run">运行</span>步骤 9typecheck + build</div><pre>npm run typecheck && npm run build</pre></div></li>
<li class="step"><div class="chkbox" onclick="ck(this,'tc6')"></div><div class="step-con">
<div class="step-ttl"><span class="kind commit">提交</span>步骤 10</div>
<pre>git add src/executor/pipeline.ts src/store/schema.sql src/store/db.ts src/model/types.ts src/store/mappers.ts test/pipeline.test.ts
git commit -m "feat(pipeline): checks 分项闸 + diff 体量闸 (diffMaxFiles)"</pre>
</div></li>
</ul>
</div></div>
<div class="card">
<div class="card-title">Agent 记忆注入(L1–L4)— 涉及文件</div>
<div class="grid-3">
<div>
<div class="row"><code>src/store/schema.sql</code> <span class="chg mod"></span> projects 加 agent_rules</div>
<div class="row"><code>src/store/store.ts</code> <span class="chg mod"></span> lastRunErrorOf + agentRules</div>
<div class="row"><code>src/model/types.ts</code> <span class="chg mod"></span> Project.agentRules</div>
<div class="row"><code>src/store/mappers.ts</code> <span class="chg mod"></span> agent_rules 映射</div>
</div>
<div>
<div class="row"><code>src/executor/protocol.ts</code> <span class="chg mod"></span> JobSpec.context</div>
<div class="row"><code>src/daemon/orchestrator.ts</code> <span class="chg mod"></span> claimOne 组装记忆</div>
<div class="row"><code>src/executor/runner.ts</code> <span class="chg mod"></span> 3 处 build*Prompt</div>
<div class="row"><code>src/executor/reviewer.ts</code> <span class="chg mod"></span> buildReviewPrompt</div>
</div>
<div>
<div class="row"><code>src/executor/cc.ts</code> <span class="chg add">不改</span> settingSources 保持 project</div>
<div class="row"><code>~/.maestro/agent-global.md</code> <span class="chg add">新建</span> 全局规范模板</div>
<div class="row">测试:build*Prompt 纯函数单测(有/无各记忆字段时段落正确)</div>
<div class="row">验证:failed→retry 任务 transcript 含「上次失败原因」</div>
</div>
</div>
</div>
</div>
</div><!-- /.section s-impl -->
<!-- /s-design -->
</div><!-- /.content -->
</div><!-- /.main-layout -->
</body>
</html>