diff --git a/docs/architecture.html b/docs/architecture.html new file mode 100644 index 0000000..9cf4b99 --- /dev/null +++ b/docs/architecture.html @@ -0,0 +1,440 @@ + + + + + +MAESTRO · 架构设计 + + + + +
+ +
+ +
架构设计 · ARCHITECTURE DESIGN v2.0 DRAFT
+ +
+ + +
全景架构 · SYSTEM OVERVIEW
+ +
+ +
+
前端层 · FRONTEND — React 18 + TypeScript + Vite
+
+
Design System 组件从 claude.ai/design 迁移 · TSX
+
三栏看板Sidebar · Main · EventPanel · 拖拽宽度
+
Mobile Tab390px · 底部四 Tab 导航
+
Zustand客户端状态管理
+
i18n · 5 语言zh / en / es / ja / fr
+
WebSocket Client实时看板刷新
+
Vite Dev Server开发时 API 代理 · HMR
+
dist/web/构建产物 → Fastify 托管
+
+
+ +
+
REST · /api/*
+
WebSocket · /ws
+
Auth Token(预留)
+
+ + +
+
API 层 · Fastify(路由拆分 + Schema 校验)
+
+
routes/projectsCRUD · logo · sync · reorder
+
routes/tasksCRUD · decide · requeue · transition
+
routes/runstranscript · active runs
+
routes/approvalspendingApprovals
+
routes/metrics健康度 · 成本聚合
+
ws.tsStore 事件 → 广播所有客户端
+
middleware/auth.tsno-op · 预留 JWT 插槽
+
schemas/Fastify JSON Schema 请求校验
+
+
+ + +
+
数据层 · DB ABSTRACTION
+
+
DBAdapter 接口统一抽象
+
SqliteAdapter当前实现
+
PostgresAdapter未来多租户
+
ProjectRepo
+
TaskRepo
+
RunRepo
+
ApprovalRepo
+
EventRepo
+
MetricsRepo
+
+
+ +
+
读写 DB
+
事件订阅 → WS 推送
+
调度查询
+
+ + +
+
Daemon 层(拆分 4 职责)
+
+
Orchestrator协调者 · tick 主循环
+
Scheduler纯调度逻辑 · score → 选任务
+
WorkerManagerspawn / reap / pid 管理
+
Ingestoroutbox.ndjson → DB · 幂等摄取
+
MergeCoordinatormerge-resolve 池 + settle
+
config.ts
+
notify.ts
+
+
+ + +
+
Worker 进程(按类型拆分)
+
+
pipelines/executor
+
pipelines/planner
+
pipelines/conflict
+
pipelines/merge-resolve
+
runners/executorprompt + runTask
+
runners/plannerprompt + runPlanner
+
runners/conflict
+
runners/merge-resolve
+
文件协议(保留不变)job.json → outbox.ndjson → heartbeat
+
+
+ +
+ + +
任务执行数据流 · EXECUTION FLOW
+ +
+
+
01 · 触发
+
用户 / API
+
POST /api/.../tasks
创建任务 → init 态
+
+
+
+
02 · 调度
+
Scheduler
+
score 排序 → claimable
ready → queued → executing
+
+
+
+
03 · 起进程
+
WorkerManager
+
spawn worker 进程
写 job.json · 记 pid
+
+
+
+
04 · 执行
+
Worker Pipeline
+
CC Agent 执行任务
追加 outbox.ndjson
+
+
+
+
05 · 摄取
+
Ingestor
+
outbox → DB
→ WS 推送 → 审核闸
+
+
+ + +
重构变更对照 · WHAT CHANGES
+ +
+
+

✕ 删除

+
    +
  • web/app.js (2473 行)替换为 React + TypeScript 前端
  • +
  • web/style.css (1333 行)设计 token 迁移到 frontend/src/tokens/
  • +
  • web/index.htmlVite 入口替代
  • +
+
+ +
+

✓ 保留不动

+
    +
  • 文件协议 — job.json / outbox.ndjson / heartbeat
  • +
  • 状态机 — src/model/status.ts · TRANSITIONS
  • +
  • Score 调度算法 — src/model/scoring.ts
  • +
  • CC 封装 — src/executor/cc.ts
  • +
  • Schema SQL — src/store/schema.sql
  • +
  • MCP server — src/mcp/
  • +
  • CLI — src/cli/
  • +
+
+ +
+

⟳ 拆分重构

+
    +
  • store.ts (1227行) → 6 Repo + DBAdapter 接口
  • +
  • orchestrator.ts (426行) → Scheduler + WorkerManager + Ingestor + MergeCoordinator
  • +
  • runner.ts (353行) → runners/ 4 个独立文件
  • +
  • pipeline.ts (358行) → pipelines/ 4 个独立文件
  • +
  • server.ts (401行) → routes/ 5 个路由模块 + Schema
  • +
+
+ +
+

✦ 新增

+
    +
  • frontend/ — React + TypeScript + Vite 工程
  • +
  • frontend/components/ — claude design → TSX 组件库
  • +
  • src/store/db.ts — DBAdapter 抽象接口
  • +
  • src/store/repos/ — 6 个职责单一的 Repository
  • +
  • src/api/middleware/auth.ts — Auth 插槽(no-op)
  • +
  • src/api/schemas/ — Fastify JSON Schema 校验
  • +
+
+
+ + +
目录结构 · FILE STRUCTURE
+ +
+
+
+
frontend/ ← 全新
+
src/
+
main.tsx
+
App.tsx 三栏布局 · 状态根
+
components/ ← claude design 迁移
+
core/ Button · StatusChip · ...
+
forms/ Input · Select · ...
+
surfaces/ Panel · GateCard · ...
+
ui/ 页面级组件
+
Sidebar.tsx
+
Topbar.tsx
+
GateSection.tsx
+
TaskTree.tsx
+
EventPanel.tsx
+
ArchiveSection.tsx
+
api/ HTTP + WS 客户端
+
store/ Zustand 状态管理
+
i18n/ 5 语言字符串
+
tokens/ CSS 自定义属性
+
index.html
+
vite.config.ts
+
tsconfig.json
+
+
web/ 整体删除
+
app.js 2473 行
+
style.css 1333 行
+
index.html
+
+
+
src/
+
api/
+
server.ts 入口(精简为注册插件)
+
routes/ ← 拆分
+
projects.ts · tasks.ts · runs.ts
+
approvals.ts · metrics.ts
+
schemas/ JSON Schema 请求校验
+
middleware/auth.ts no-op 插槽
+
+
store/
+
db.ts DBAdapter 接口
+
sqlite.ts 当前实现
+
repos/ ← 拆分自 store.ts
+
ProjectRepo · TaskRepo · RunRepo
+
ApprovalRepo · EventRepo · MetricsRepo
+
index.ts Store 聚合(外部接口不变)
+
mappers.ts 保留
+
+
daemon/
+
scheduler.ts ← 拆分 · 纯调度逻辑
+
worker-manager.ts ← 拆分 · 进程管理
+
ingest.ts 保留接口不变
+
merge-coordinator.ts ← 拆分
+
orchestrator.ts 精简为协调者
+
+
executor/
+
runners/ ← 拆分自 runner.ts
+
executor · planner · conflict · merge-resolve
+
pipelines/ ← 拆分自 pipeline.ts
+
executor · planner · conflict · merge-resolve
+
cc.ts · models.ts · worktree.ts 保留
+
+
+
+ +
+ MAESTRO ARCHITECTURE · v2.0 DRAFT · 仅设计文档,尚未实现 · 保留 · 拆分重构 · 新增 · 删除 +
+ +
+ + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..5c162e7 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,96 @@ + + + + + +Maestro · 文档索引 + + + + +
+ MAESTRO + · 文档索引 / docs index +
+
+

Maestro 项目全部文档的单一入口。按 设计方案 / 实现计划 / 知识库调研 / 排障 Runbook 分类汇总(HTML 与历史 MD 都列)。新增文档须同时登记此处。

+ + +
+
设计 设计方案 / 架构设计 · 1
+ +
MAESTRO · 架构设计
+
docs/architecture.html
+
系统整体架构设计:进程模型(daemon 唯一 DB 写者 / worker 文件协议)、前后端、数据流、调度与执行管道总览。
+
架构HTML
+
+
+ + +
+
计划 实现计划 · 2
+ +
Maestro 大重构全景 · 现状 → 目标 → 计划
+
docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.html
+
扁平 FSM → schedule_status × work_type 正交模型的完整技术蓝图(HTML 阅读版)。四标签:现状架构 / 重构目标 / 设计方案 / 具体实现。专题含:状态设计、任务提交与入口(网页/MCP/todo 同步三入口·图片文件附件·AI 提任务)、并发调度(CAS·couplingPenalty)、交互平面·stuck 人工接管、数据库 Schema(ER 图)、Agent 记忆注入 L1–L4、Agent 规格与注册表(每 agent 流程/模型/skill/prompt/上下文 + 动态扩展)运维与护栏(成本预算治理·限流背压·资源 GC·可观测·依赖环检测)安全与沙箱前端 API 契约(REST 全量 + WS 事件)、Phase 1 逐步实施计划。全部图表为深色内联 SVG(自包含、无 CDN)。
+
实现计划设计HTML 阅读版
+
+ +
同上 · Markdown 执行真相源
+
docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.md
+
上面 HTML 的同源 .md,保留 - [ ] checkbox 供 executing-plans / subagent-driven-development 驱动执行跟踪。.md 是执行真相源,.html 仅供阅读。
+
执行真相源MD
+
+
+ + +
+
调研 知识库调研 · 0
+
(暂无)
+
+ + +
+
排障 排障 Runbook · 0
+
(暂无)
+
+ +
单一入口 · 直接 file:// 打开 · 新增 / 迁移文档请同步登记本页。
+
+ + diff --git a/docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.html b/docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.html new file mode 100644 index 0000000..d0d21d4 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.html @@ -0,0 +1,2638 @@ + + + + + +Maestro 重构全景 · 现状→目标→计划 + + + + + +
+ MAESTRO · 重构全景 + 2026-06-22 · 现状 → 目标 → 计划 + +
+ + +
+ +
+ + + + + +
+
现状架构
+
Maestro 当前系统全景——进程模型、前后端、数据流、调度算法、执行管道与模型档位。
+ +
+
系统总览
+ +MAESTRO 系统架构(现状) + +⚠ 现状问题点 / 重构待改 + + + +用户端 + +Browser — Vanilla JS SPA +web/index.html + app.js + +多项目看板 +任务列表实时刷新 + +任务详情 +plan/spec/events/runs + +审批 +approve / reject + +⚠ 无流式输出 +只能看 transcript + + + +Daemon 进程(唯一 DB 写者) + +Fastify API Server :4517 +REST 40+ · WS /ws +src/api/server.ts + +Orchestrator Loop · 每 15s +ingest → reap → claim +src/daemon/orchestrator.ts + +⚠ rankByScore:简单优先级加总,无 CPM / 无防饥饿 / 忽略文件耦合 +⚠ claimOne 无 CAS → 并发可双重领取同一任务 + +SQLite DB +~/.maestro/maestro.db +WAL + 外键 + +tables(5 张) +projects · tasks · runs · events · approvals +task 用单一扁平 status 列建模 + +⚠ 缺列:claimed_at · task_type · scope · owned_files · version +⚠ 状态用扁平 FSM(16 个混合 status),无 schedule × work_type 正交建模 + + + +Worker 进程(每任务独立 · 不接触 DB) +executor pipeline(复审内联、串行) + +syncMain + + +createWorktree + + +runTask (CC) + + +verify 可选 + + +worktreeDiff + + +⚠ reviewCode 串行 + + +⚠ reviewSec 串行 + + +exec_review +planner pipeline(只读) + +buildPlannerPrompt + + +runPlanner (CC 只读) + + +⚠ decompose JSON 缺 ownedFiles +conflict pipeline(无专用 agent) + +git merge + + +⚠ CC 自己猜着解 + + +双复审 + + + +Claude Code Agent(Headless) + +cc.ts · @anthropic-ai/claude-code SDK +for-await 流式(仅落 transcript) + +executor 白名单 +Read/Edit/Write/Glob/Grep · Bash(git,test) · WebSearch + +planner 白名单 +Read/Glob/Grep · Bash(git log/diff/show) + +src/mcp/ +MCP + +src/sync/ +todo 同步 + +
+ +
+
+
前端(现状)
+ + + + + + + + + +
维度现状
技术栈Vanilla JS + CSS,无构建工具
入口web/index.html 静态文件,Fastify 直接 serve
状态管理全局 JS 变量 + 手动 DOM 操作
实时通信WebSocket 收事件后手动更新 DOM
流式输出无(只看完成后 transcript)
代码分割单文件 app.js (~1500 行)
类型安全无(纯 JS)
+
+
+
进程模型(现状)
+ + + + + + +
角色职责
Daemon唯一 DB 写者;API+WS 服务;Orchestrator 调度循环;ingest worker 产出
Worker每任务独立 OS 进程;不碰 DB;纯文件通信
CC Agent在 worker 内 SDK headless 运行;工具白名单隔离
通信file + SIGTERM(可存活 daemon crash)
+
+
+ +
+
REST API 主要路由(重构后 · 完整契约见 ③设计方案 ⑧ 前端 API 契约)
+
+

Projects

+
GET /api/projects
+
POST /api/projects
+
GET /api/projects/:id
+
PATCH /api/projects/:id
+
DELETE /api/projects/:id
+
POST /api/projects/:id/sync
+
POST /api/projects/reorder
+
+

Tasks

+
GET /api/projects/:id/tasks
+
POST /api/projects/:id/tasks
+
GET /api/tasks/:id
+
POST /api/tasks/:id/plan
+
POST /api/tasks/:id/spec
+
POST /api/tasks/:id/decide
+
POST /api/tasks/:id/requeue
+
POST /api/tasks/:id/cancel
+
DELETE /api/tasks/:id
+
POST /api/tasks/:id/attachments
+
POST /api/tasks/:id/takeover
+
+

Runs / Events

+
GET /api/tasks/:id/runs
+
GET /api/runs/:id/transcript
+
GET /api/tasks/:id/events
+
GET /api/projects/:id/events
+
GET /api/agents
+
GET /api/usage
+
GET /api/metrics
+
GET /api/tasks/:id/stream
+
GET /api/health
+
WS /ws
+
+
+
+ +
+
数据库 Schema(现状)
+
projects id · name · repo_path · autonomy · concurrency · max_retries · timeout_ms + verify_cmd · model · checks · auto_approve_plan · auto_approve_exec + +tasks 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 + ⚠ 缺少:schedule_status · work_type · work_plan · work_history · claimed_at · scope · expected_output · version + +runs id · task_id · kind(planner/executor/conflict) · status · worktree · branch + transcript_ref · session_id · worker_pid · last_seq · started_at · ended_at + ⚠ 缺少:trace_id + +events id · project_id · task_id · type · payload(JSON) · at +approvals id · task_id · gate(plan/spec/exec) · action · actor · reason · at
+
+ +
+
任务状态机(生命周期)
+ + + + + + + + + + +Maestro 任务状态机(现状 · 扁平 FSM) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Hard +Medium +Easy +cancel +拆解完成 +accept +reject +重拆 +子任务全 done +方案完成 +accept +reject +claim +退回 +worker 启动 +执行完成 +accept(合并) +依赖未满足 +依赖满足 +执行失败 +失败 +reject(返工) +重试 +超阈值 +失败超限 +失败超限 +重排队 +重新分析 +重写方案 +暂停 +恢复 + + +init + +analyzing + +plan_review + +decomposed + +speccing + +spec_review + +ready + +queued + +executing + +exec_review + +done + +blocked + +failed + +stuck + +paused + +cancelled + +Hard 拆解 +Medium 方案 +执行调度 +Agent 运行 +异常/失败 +暂停(虚线) +注:paused 与所有活跃态双向互通;cancelled 任意态可达(略去以保主干清晰)。本图为重构前扁平 FSM,对照「重构目标」的 schedule × work_type 正交模型。 + +
+ +
+
调度算法(现状)
+
+

评分公式

+
+score(t) = baseScore(priority) P0=3,P1=2,P2=1
+         + Σ baseScore(d) d=已完成依赖
+         + Σ baseScore(b) b=被本任务阻塞 +
+
⚠ 无防饥饿 · 忽略文件耦合 · 无 CAS
+
+

调度流程

+
ingest 读 outbox → 写 DB
+
reap 心跳超时 → failAttempt
+
claim rankByScore → startRun
+
⚠ 并发 tick 可双重领取
+
+
+
+ +
+
执行管道(现状)
+
+
syncMain
+
createWorktree
+
runTask (CC)
+
verify 可选
+
worktreeDiff
+
+
+
+
reviewCode 串行
+
reviewSecurity 串行
+
exec_review
+
+
⚠ 串行复审(耗时翻倍) ·  verdict=reject 只是建议不挡人工审核 ·  无分项检查闸 ·  无 diff 体量限制
+
+ +
+
模型档位(现状)
+ + + + + + + +
角色EasyMediumHard
executorclaude-sonnet-4-6claude-opus-4-8claude-opus-4-8
plannerclaude-sonnet-4-6claude-opus-4-8claude-opus-4-8 ⚠
reviewerclaude-sonnet-4-6claude-opus-4-8claude-opus-4-8 ⚠
conflictclaude-opus-4-8 ⚠(不够强)
回退链['claude-opus-4-8', 'claude-sonnet-4-6'](缺 fable-5)
+
+ +
+ + + +
+
重构后架构
+
Phase 1(本次实施)完成后的目标态。绿=新增 琥珀=变更 红=移除
+ +
+
系统总览(重构后)
+ +MAESTRO 系统架构(重构后目标态) + +★ Phase 1 新增/变更 +★★ Phase 2 规划 + + + +用户端 + +Browser — Vanilla JS SPA +web/index.html + app.js +(Phase 1 不变) + +★★ React 18 + Vite +Zustand · IBM Plex Mono +[Phase 2] + +★★ SSE 流式 token 输出 +GET /api/tasks/:id/stream +[Phase 2] + + + +Daemon 进程(唯一 DB 写者) + +Fastify API Server :4517 +REST 40+ 路由 · WS /ws +src/api/server.ts(沿用) + +Orchestrator Loop · 每 15s +ingest → reap → claim + +★ 调度重构(scoring.ts) +rankU = CPM 最长链 ++ agingBonus(防饥饿) +− couplingPenalty(文件耦合) +★ casClaimTask:CAS 防双 claim +★ 按 work_type dispatch agent + +SQLite DB +~/.maestro/maestro.db +WAL + 外键 + +★ tasks:schedule_status × work_type(取代扁平 status) ++ work_plan / work_history 队列 · owned_files · scope · expected_output · claimed_at · version + +★ runs.kind 扩 plan|code|review|pr|conflict|research · + trace_id ★ projects + checks / diff_max_files / agent_rules +⛔ 已弃用:task_type / sub_status / state_snapshot / parent_version_id(被 schedule_status + work_plan 队列取代) + +work_plan 队列(线性、运行时决定执行路径) +[ {type, spec, owned_files, expected_output}, … ] ← Orchestrator pop 队头 → (ready, 下一 work_type) +waiting(plan|review) 为人工闸,不占并发名额 + + + +Worker 进程(每任务独立 · 不接触 DB) — 按 work_type 拆独立 run +plan run(Planner Agent) + +runPlanner +maxTurns 90/60/40 + + +★ decompose JSON ++ ownedFiles / expectedOutput + + +ingest → 子任务 work_plan +① code run(Executor Agent) + +syncMain + + +createWorktree + + +runTask (CC) + + +verify ✓ + + +★ checks 分项闸 + + +worktreeDiff + + +★ diff 体量闸 + + +commit +② review run(Reviewer Agent · 独立、可选) + +★ Promise.all 双复审(code + security) + + +★ verdict=reject → 硬拦截(回 code) + + +waiting(review) 人审 +③ pr run(PR Agent · review 通过后串联) + +创建 PR + + +--no-ff 合并 + + +冲突 → conflict run + + +done ✓ +conflict run(Conflict Agent · 抢占调度 · opus-4-8) + +git merge 制造冲突 + + +CC 解冲突 + + +★ 双复审 + + +commit + + + +Claude Code Agent(Headless) + +cc.ts +@anthropic-ai/ +claude-code SDK +for-await 流式 + +★ 模型档位(统一最强档) +executor: sonnet / opus / opus +planner-hard · reviewer · conflict → opus-4-8 +回退链 [opus-4-8, sonnet-4-6] +(fable-5 暂不可用,opus 充当最强档) + +★ Agent 记忆注入 L1–L4 +L4 全局规范 · L2 项目 agent_rules +L3 拆解背景(父/兄弟)· L1 上次失败 +daemon claim 时组装进 job.json + +★★ onToken callback +→ EventEmitter → SSE +[Phase 2] + +
+ +
+
Phase 1 变更清单
+ + + + + + + + + + + + +
文件变更内容类型
schema.sql + db.tstasks 状态列重构:status→schedule_status + work_type/work_plan/work_history/scope/owned_files/expected_output/claimed_at/version;runs.kind 扩 review|pr|conflict + trace_id;projects 加 diff_max_files/checks/agent_rules;ensureColumn 向后兼容旧库变更
types.ts新增 ScheduleStatus / WorkType / WorkStep 类型;Task 接口以 schedule_status × work_type + work_plan 队列取代旧 status 单态新增
mappers.tsTaskRow/ProjectRow 追加字段;rowToTask/rowToProject 映射变更
store.tscasClaimTask(CAS);transition→ready 清 claimed_at;requeueTask 清零;assertNoActiveRun 执行期锁变更
orchestrator.tsclaimOne 接 casClaimTask;rankByScore 传 inFlight+nowMs;按 work_type dispatch agent(code/plan/review/pr/conflict)变更
scoring.tsrankU(CPM) + agingBonus(防饥饿) + couplingPenalty(文件耦合) + 新签名 rankByScore变更
models.tsplanner-hard/reviewer/conflict → opus-4-8(最强档);回退链 [opus-4-8, sonnet-4-6](fable-5 暂不可用,恢复后改档位表即可)变更
runner.ts + protocol.tsdecompose JSON 扩展 ownedFiles/expectedOutput;prompt 更新变更
ingest.tsdecompose-result 映射新字段到子任务变更
pipeline.tscode run:checks 分项闸(verify 后逐项执行)+ diff 体量闸(超 diffMaxFiles 硬失败);复审从 code pipeline 拆出为独立 reviewer run;新增 pr / conflict run 分支新增
+
+ +
+
+

调度算法(重构前)

+
score(t) = baseScore(priority)
+         + Σ baseScore(d)  // 已完成依赖
+         + Σ baseScore(b)  // 被阻塞任务
+
+无防饥饿(低优先永远靠后)
+忽略文件耦合
+无 CAS 防双重 claim
+
+
+

调度算法(重构后)

+
score(t) = rankU(t)        // CPM 关键路径(最长链)
+         + agingBonus(t)  // 防饥饿,48h 线性到 base
+         - couplingPenalty(t) // 未合并 worktree 文件重叠 -0.5/个
+
+rankU(t) = baseScore(t.priority)
+         + max(rankU(d) | d 直接依赖我)  // 取最长,非求和
+// 叶子无 dependent → 仅 baseScore;最长链封顶,不膨胀
+agingBonus = base × min(1, waitHours/48)  // 48h 达 base
+casClaimTask: WHERE claimed_at IS NULL
+
+
+ +
+
执行管道(重构后)— 按 work_type 拆独立 run
+
每个 work_type 是一次独立 Worker run,run 间由 work_plan 队列串联——复审不再内联进 executor,而是独立 reviewer run;合并独立成 pr run。
+ +
① code run
+
+
syncMain
+
createWorktree
+
runTask (CC)
+
verify ✓
+
checks分项闸 ★
+
worktreeDiff
+
diff体量闸 ★
+
commit
+
+ +
② review run(独立 reviewer run,可选)
+
+
Promise.all 双复审(code + security)★
+
verdict=reject → 硬拦截(回 code)★
+
waiting(review) 人审
+
+ +
③ pr run(review 通过后串联)
+
+
创建 PR
+
--no-ff 合并
+
冲突 → conflict run
+
done ✓
+
+
★=新增硬闸 · 复审为独立 reviewer run(非 code 内联),run 内 code+security 仍 Promise.all 并行 · work_plan 不含 review 时整段可跳过、code 完成直接进 pr
+
+ +
+
模型档位(重构后)
+ + + + + + + +
角色EasyMediumHard备注
executorclaude-sonnet-4-6claude-opus-4-8claude-opus-4-8不变
plannerclaude-sonnet-4-6claude-opus-4-8claude-opus-4-8Hard 升最强档
reviewerclaude-opus-4-8claude-opus-4-8claude-opus-4-8三档统一最强,不被 project.model 降档
conflictclaude-opus-4-8(固定,不降档)
回退链['claude-opus-4-8', 'claude-sonnet-4-6']
+
⚠ 设计目标是「最强档」;claude-fable-5 暂不可用,当前一律以 claude-opus-4-8 充当最强档(commit a5a3c96 已全量替换)。fable-5 恢复后只需改 models.ts 档位表,结构不变。
+
+ +
+
Phase 2 预告(本次不含)
+
+

SSE 流式

+
cc.ts onToken callback
+
→ daemon EventEmitter
+
→ GET /api/tasks/:id/stream
+
前端实时看 token
+
+

Hook 合约

+
.maestro/hooks.ts
+
MaestroHooks 接口
+
5s 超时取消
+
+

Trace ID

+
JobSpec.traceId
+
outbox 每条携带
+
跨进程可观测
+
+

React 前端

+
React 18 + Vite
+
Zustand 状态管理
+
IBM Plex Mono 设计
+
SSE token 显示
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + +
+
+
完整设计方案
+
Maestro 大重构的完整技术蓝图:schedule × work_type 状态机架构 · 数据库 Schema 变更 · Agent 记忆注入 · Phase 1 实施路线。
+ + + +
+ + + +
架构决策总览
+
🗂 分期标注:各章头部 Phase 1 = 本期核心重构(对应 ④具体实现 T1–T6);Phase 2 = 能力扩展(成本预算 / 安全沙箱 / 交互接管 / SSE 等);混合 = 章内分属两期,以卡片内标注为准。标注为建议范围,最终以实施计划为准。
+ +
+
六大核心决策
+ + + + + + + + +
#决策选择关键理由
D1状态机架构schedule_status × work_type 双轴,替代平铺 FSMschedule_status 对齐 OS 进程模型(OS 五态),描述资源调度;work_type 描述当前执行何种工作(plan/code/review/pr…);执行路径由混合模式动态决定,无需静态枚举所有路径
D2任务类型扩展work_type 注册表(新类型 = 加 agent + dispatcher 分支)核心代码零改动;schedule_status 通用状态(ready/running/waiting…)完全复用,无需改状态机
D3DB 状态存储schedule_status + work_type + work_plan 队列(JSON)schedule_status / work_type 双列供 SQL 索引快速过滤与调度;work_plan 队列自带恢复位置,不引入 XState snapshot
D4Agent 记忆四层注入(L1 失败原因 / L2 项目规范 / L3 跨任务上下文 / L4 全局规范)worker 不碰 DB,所有记忆由 daemon claimOne 组装进 job.json;ROI 由高到低
D5流程硬闸复审 reject 变硬闸 + 分项 checks + diff 体量闸消除"复审仅建议"漏洞;静态检查前置防止无效执行进审批
D6并发调度CAS claimed_at + couplingPenalty(文件重叠降分)防双重 claim 竞态;相同文件的任务并发执行概率降低,冲突减少
+
+ +
+
重构前后对比快览
+
+
+

现有设计(平铺 FSM)

+
• 16 状态平铺,全局 TRANSITIONS 表
+• CANCEL/FAIL 每个子状态重复声明
+• code + security 复审串行
+• 暂停恢复需 paused_return_to 字段
+• 新任务类型 = 修改核心转移表
+• claimed_at 无 CAS,存在双重领取竞态
+• reviewer reject 不挡流,仅建议
+• agent prompt 无失败记忆 / 无规范注入
+
+
+

重构后(schedule × work_type + 四层记忆)

+
• schedule_status × work_type 双轴正交状态
+• work_plan 队列动态决定执行路径(混合模式)
+• waiting 人工闸不占并发名额,running 才占
+• (paused, work_type) 自带恢复位置,无需额外字段
+• 新 work_type = 加 agent + dispatcher 分支,零状态机改动
+• CAS claimed_at + couplingPenalty 调度
+• reviewer reject = 硬闸,回退重执行
+• L1-L4 记忆注入 + 项目/全局规范
+
+
+
+ +
+
进程模型铁律(不改)
+
+
+

Daemon — 唯一 DB 写者

+
REST + WebSocket API(:4517)
+
Orchestrator 调度循环(ingest → reap → claim)
+
写 job.json 时组装全部记忆(L1-L4)
+
读 outbox.ndjson 入 DB,状态机 send(event)
+
+
+

Worker — 不碰 DB

+
读 runs/<runId>/job.json
+
执行 agent pipeline,追加 outbox.ndjson
+
任何 DB 数据 → daemon 组装进 job.json 下发
+
状态机 actor 在 daemon 侧管理
+
+
+
+ + + +
状态设计(schedule × work_type)Phase 1
+ +
+
任务提交与入口(Ingestion)
+

提交方式不变——主入口仍是网页表单。提交后任务落 (init, 无 work_type),再由系统赋 work_type / 产 work_plan 进入调度。

+ +

三条入口

+ + + + + + +
入口路径用途
网页表单「+ 新建任务」→ POST /api/projects/:id/tasks人手动提交(主入口
MCP 工具create_task / decompose_taskClaude Code 会话 / agent 编程建任务
todo.json 同步src/sync/ 单向同步legacy /todo 数据自动导入
GitHub issue(未来)webhook 入站 → 建任务TODO #1 双向同步,本期不含
+ +

提交字段(极简 + 可选增强)

+ + + + + + +
字段必填?说明
title✅ 必填唯一硬要求
complexity默认 autoauto = LLM 异步分类回填(hard/medium/easy);也可手填
priority / deps可选缺省 P2 / 无依赖
owned_files / scope / expected_output可选新模型增强:喂调度 couplingPenalty + 给 planner 更准起点;缺省由 planner 补全
+
设计取向:提交极简(只逼 title),质量靠 planner 兜;可选字段让愿意填的人给更强的调度/拆解信号,但从不强制。
+ +

提交后流转

+
+
提交
+
(init, 无 work_type)
+
plan run:planner 产 work_plan
+
(ready, 首 work_type)
+
调度
+
+
Hard/Medium 走 plan(planner 拆解 / 写 spec 产 work_plan 队列);Easy 跳过 plan,直接 work_plan=[code, review, pr] → ready。autonomy(manual / auto-easy / auto-approved)决定提交后是自动起跑还是等人放行
+ +

附件支持(图片 / 文件)Phase 2

+ + + + + +
设计
表单「+ 新建任务」支持拖拽 / 粘贴截图 + 上传文件(bug 截图、设计稿、规格文档、数据样本)
API / 存储POST /api/tasks/:id/attachments(multipart)→ 存 tasks/<taskId>/attachments/tasks.attachments JSON 存 [{name, type, path}]
喂给 agent附件路径随 job.json 下发 → worker 把图片直接喂进 agent 多模态输入、文件作为 Read 目标;附件是任务上下文的一等部分(并入 ⑤ AgentContext)
+ +

AI 提任务(agent-initiated)Phase 2

+

已部分存在(MCP create_task + planner decompose 产子任务)。扩展为一等能力:

+ + + + + +
来源设计
执行中衍生agent 执行时发现额外工作 → 经 MCP 提关联任务(parent / sibling),不打断当前 run
巡检 agent可注册的 groomer agent(work_type,呼应 ⑤ 动态扩展)周期扫 repo / backlog / issue → 提议任务
来源标记tasks.source(human|ai|sync|github)+ created_by(提交它的 agent run id),区分人提 vs AI 提
+
🛡 护栏:AI 提的任务默认进「待确认新任务」人工闸(不自动起跑),人审后才入调度——防 AI 自我增殖任务失控烧钱(呼应 ⑥ 预算护栏)。是否需人审由项目 autonomy + AI-task 开关决定;纯子任务拆解(planner decompose)已有 plan_review 闸覆盖。
+
+ + +
+
schedule_status(调度状态,对齐 OS 进程模型)
+ + + + + + + + + + + + +
状态OS 类比含义可用 work_type
initNew任务刚创建,尚未确定执行路径,无 work_type
readyReady有 work_type,等待 Orchestrator 调度任意
runningRunningWorker 正在执行当前 work_type 对应的 agent任意
waitingBlocked (I/O)等待人工输入(审批/拒绝),无 agent 在运行保持完成步骤的 work_type(如 plan / review)
blockedBlocked (deps)等待任务依赖完成,deps 满足后自动回 ready保持上次 work_type
pausedStopped人工暂停,恢复时回到 ready(保持 work_type)保持上次 work_type
failedZombie当前步骤失败,退避后自动重试(回 ready);超重试上限 → stuck同失败时 work_type
stuckStopped (wait human)任务卡住需人工介入:①重试耗尽 ②agent 主动上报歧义/不可解 ③审批超时兜底。自动化停止,等人决策保持卡住时 work_type
doneTerminatedwork_plan 队列全部完成,终态
cancelledKilled人工取消,终态
+
stuck 是一等 schedule_status 值,直接存 DB 列——可用 WHERE schedule_status = 'stuck' 直接查,无需 retry_count 联合判断。人工处理完成后可转 ready / done / cancelled。
+
+ + +
+
work_type(工作类型,决定用哪个 Agent)
+
+
+

Agent 类型(schedule = ready → running)

+ + + + + + + + +
work_type执行者主要工作
planPlanner Agent分析任务,产出初始 work_plan(步骤队列 + 每步 spec)
codeExecutor Agent在 worktree 里实现代码变更,commit
reviewReviewer Agent代码审查 + 安全审计(run 内 Promise.all 并行),输出 verdict。独立 run、不并入 code;可选(work_plan 可省略)
prPR Agentreview 通过后创建 PR + 完成合并(冲突 → conflict)
conflictConflict Agentgit merge 制造冲突 → CC 解 → 复审
researchResearch Agent技术调研,产出报告(无代码修改)
+
+
+

人工闸(schedule = waiting,无 Agent 运行)

+
+

waiting 的 work_type 直接沿用上一步完成时的值,不引入新枚举——waiting(plan) 等人审 plan 输出,waiting(review) 等人审代码复审结果。

+
+ + + + +
当前 work_type等待内容通过 → 下一步拒绝 → 下一步
plan人审 Planner 产出的 work_planpop 队头,进 ready(下一 work_type)重回 ready(plan),带拒绝原因重规划
review人审执行结果 + 复审 verdictpop → pr 步(提 PR + 合并)→ doneready(code),带拒绝原因重做
+
+

为什么 waiting ≠ running

+

人工闸没有 worker 进程在运行,Orchestrator 不应把并发名额算在它上面;running 才占名额,waiting 不占。

+
+
+
+
+ + +
+
混合模式:下一步如何决定(Hybrid Next-Step)
+
+
+

Plan 阶段:产出初始 work_plan 队列

+
{
+  "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)
+
+
+

执行阶段:每步可修改剩余队列

+
{
+  "type": "step-complete",
+  "result": "发现 auth 复杂,拆成2步",
+  "update_plan": [          // ← 可选,修改剩余队列
+    { "type": "code",
+      "spec": "先实现 session 层" },
+    { "type": "code",       // ← 动态插入新步骤
+      "spec": "再实现 token 层" },
+    { "type": "review" }
+  ]
+}
+// 无 update_plan → pop 队头继续
+// update_plan 非空 → 替换剩余队列
+
+
+ +

队列为空时

+
+
+

step-complete 后 work_plan 队列为空 → schedule_status = done,任务完成。

+
+
+

Easy 任务无需 plan 步骤:init 时 Orchestrator 直接写入默认队列 [code, review],跳过 plan。

+
+
+
+ + +
+
完整转移规则
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +init + +ready + +running + +waiting + +done + +paused + +blocked + +failed + +stuck + +cancelled + + +写入队列 +claim +plan/review done +review approve→pr +approve / reject +pr merge +code→review (auto) +error +retry +retry 耗尽 +agent 上报 +人工: 重试 +人工: 完成 +人工: 取消 +deps⬇ +deps✓ +暂停 +恢复 + + + +自动 + +人工 + +异常 + +自动串联 + + +
+ + +
+
执行路径示例
+
+
+

Hard 任务(需规划)

+
+(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 完成(commit;run 结束) +(running, review) ← 自动串联(独立 reviewer run,非 code 内联) + ↓ review 完成(run 内并行双复审 verdict) +(waiting, review) + ↓ 人 approve +(running, pr) ← approve 后串联 + ↓ pr agent 提 PR + 合并(冲突→conflict run) +done ✓ +
+
+
+

Easy 任务(跳过规划)

+
+(init, -) + ↓ Orchestrator 写队列 [code, review, pr] +(ready, code) + ↓ worker 启动 +(running, code) + ↓ code agent 完成 +(running, review) ← 自动串联(独立 reviewer run) + ↓ review agent 完成 +(waiting, review) + ↓ 人 approve +(running, pr) → 提 PR + 合并 → done ✓ +
+

review 可选:work_plan 不含 review 时,code 完成 → 直接 (running, pr);纯只读任务可仅 [research],无 code/review/pr。

+

动态拆步(运行中修改计划)

+
+(running, code) + ↓ agent 发现比预期复杂 + ↓ step-complete + update_plan: + [code(token层), review] +(ready, code) ← 新步骤(session 层) +(running, code) ← 再次 code(token 层) +(running, review) → (waiting, review) → done ✓ +
+
+
+
+ + +
+
DB 字段(tasks 表关键变更)
+
+
+ + + + + + + +
字段类型说明
schedule_statusTEXT取代旧 status:init|ready|running|waiting|blocked|paused|failed|stuck|done|cancelled
work_typeTEXT当前工作类型:plan|code|review|pr|conflict|research|null
work_planTEXT JSON剩余步骤队列 [{type, spec, owned_files, expected_output}]
work_historyTEXT JSON已完成步骤记录(审计+重试上下文)
claimed_atTEXTCAS 锁,NULL=可领取,非NULL=已锁
+
+
+

Orchestrator 关键查询

+
-- 可调度任务(schedule 维度)
+SELECT * FROM tasks
+WHERE schedule_status = 'ready'
+  AND claimed_at IS NULL
+  AND (next_eligible_at IS NULL
+       OR next_eligible_at <= datetime('now'))
+ORDER BY score DESC;
+
+-- 派发 agent(work_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);
+}
+
+
+
waiting(人工闸)不占并发名额:Orchestrator 的 inflight 计数只统计 schedule_status=running 的任务;waiting 的任务不计入,也不占 project.concurrency
+
+ + +
+
与旧设计对比
+ + + + + + + + + + + +
维度旧设计(平铺 FSM)新设计(schedule × work_type)
状态数量16 个混合状态(analyzing/speccing/ready/executing/exec_review…)10 个 schedule_status × N 个 work_type,正交组合
执行路径静态,TRANSITIONS 表写死动态,work_plan 队列运行时决定
新增工作类型改 TRANSITIONS 表 + 加 status 枚举加 work_type 枚举 + 加 agent dispatcher 分支
人工审批闸plan_review/spec_review/exec_review 是独立 status(额外枚举)waiting(plan) / waiting(review),直接沿用卡住时 work_type,无新枚举
stuck逻辑态:failed + retry_count ≥ max_retries 联合推导,无独立 DB 值一等状态:独立 DB 值,直接查询;人工处理后出 ready / done / cancelled
并发占用所有非终态都计入 inflight只有 running 计入 inflight;waiting / stuck 不占名额
暂停恢复需 paused_return_to 字段记录位置(paused, work_type) 自带位置,恢复即 (ready, same work_type)
可观测性一个 status 字段,需要枚举含义两个字段,(running, code) 直接表达"正在写代码"
实现复杂度中(平铺 transition 守卫)低(两字段 + 队列 pop/push)
+
+ + +
+
并发调度(调度单位 · CAS 防双领 · couplingPenalty 错峰)
+ + + + + + + + +
维度决策
最小调度单位task(一次一个 work_step);并发发生在不同 task 之间,不在 task 内部。work_plan 队列严格线性
阶段内并行判据 = 是否需要独立 worktree / 写权限:只读·汇总型 → agent 内 Promise.all(占 1 槽);需独立写 → 拆子任务交调度器(占 N 槽)
并行建模并行 = 拆子任务parent decomposed → 子全 done → done 做 join;不引入队列内并行分支节点(保队列模型简洁)
防双重 claimCAS claimed_at(见下);即「执行期锁」的 DB 落地
文件错峰couplingPenalty(见下):与所有持未合并 worktree 的任务(running + waiting(review) + 带改动 failed)的 owned_files 重叠扣分,软错峰非硬禁;agingBonus 兜底防饥饿
并发预算槽只认 running;agent 内并行不额外占槽,调度器并行占 N 槽,project.concurrency 封顶
+ +
+
+

CAS 防双重 claim

+
-- 抢占:仅当未被锁定才成功
+UPDATE tasks SET claimed_at = :now
+WHERE id = :id AND claimed_at IS NULL;
+-- changes==1 → 抢到;==0 → 已被占,放弃
+
单条 SQL 行级原子,无论多少路径同时领同一任务,最多一个成功。run 结束(finishRun / failAttempt / reap)置 NULL 释放。比"用 run.status 反推"更直接、可索引。
+
+
+

couplingPenalty 文件错峰

+
score(t) = rankU(t) + agingBonus(t)
+         − 0.5 × | t.owned_files ∩
+            (未合并 worktree 任务的 owned_files 并集) |
+
重叠越多分越低 → 越靠后领 → 大概率等冲突任务先跑完。按任务级 owned_files 并集算,覆盖所有分支上有未合并提交的任务(running + waiting(review) 待合并 + 带改动 failed),不只 running——否则 code 完进 waiting 等人审时分支已有提交,只看 running 会漏算、照样撞冲突。软错峰:实在没别的可干仍会跑,agingBonus 防永久饥饿。
+
+
+ +

阶段内并行:放调度器还是放 agent?

+ + + + +
并行的性质放哪占槽例子
共享同一 worktree、只读 / 汇总(不互相写)agent 自己并行(worker 内 Promise.all)1 槽双复审 code + security(都只读同一 diff)
各自要写、可隔离(不同文件 / 模块)拆子任务交调度器(各自 worktree)N 槽feature 拆「auth 层」+「UI 层」
+ +
🔗 闭环:planner 拆并行子任务时须给互斥的 owned_files,它们才真并行;给重叠了 couplingPenalty 自动把它们串起来——拆解质量与调度错峰相互校正。
反模式:同一 worktree 内多个写手并发(git index / 文件竞态)。要并行写就必须隔离 worktree,而隔离 worktree 即「子任务」——于是天然回到调度器并行。
+
+ + +
+
交互平面 · stuck 任务人工接管(interactive run)Phase 2
+

自治流水线之外的第二执行平面:任务卡在 stuck 时,人可「接管」,像终端一样与带完整上下文的 agent 长对话改完,再交回自治管道。不改造自治 Worker,作为并列能力接入。

+ +

两个执行平面

+ + + + + + + +
维度自治平面(现有)交互平面(新增)
触发orchestrator 按 score 调度人显式接管(on-demand,POST /api/tasks/:id/takeover
传输纯文件(job.json / outbox / heartbeat)实时双向runs/<runId>/console.sock(unix socket)↔ daemon ↔ 客户端 WS
节奏跑完即退、可 reap人驱动、按对话推进
重启可恢复✅ 文件协议保证❌ 牺牲,改由 session resume 找回
run kindplan / code / review / pr / conflictinteractive
+
两平面共享 worktree / transcript+session / DB 状态(经 daemon)/ task 模型,只在传输 + 生命周期触发上不同——自治内核保持纯净,交互作为边界清晰的独立能力。
+ +

接管会话流程

+
+
stuck
+
人点接管 /takeover
+
spawn interactive run(复用 worktree + resume session)
+
人 ↔ agent 长对话
+
人选出口
+
+ +
+

接管期间

+
schedule_status=running + run.kind=interactive
+
work_type 保持卡住时的值(不污染枚举)
+
reaper 对 interactive 关执行超时,heartbeat 仍发
+
idle-TTL 30min 无活动 → checkpoint session id → 退出 → 回 stuck;重连即 resume(挂起/恢复无缝)
+
+

结束出口(人选)

+
修好 → 交回:commit WIP,回 ready 进下一 work_type(review/pr)
+
修好 → 直接完成:走 pr / 合并 → done
+
放弃 → 回 stuck / cancelled
+
session id 落 work_history → 后续自治 run 可 resume,接管对话喂 L1–L4 记忆
+
+
+ +

关键决策(已定)

+ + + + + + +
决策点取定
schedule_status复用 running + run.kind=interactive,不加状态枚举;reaper 单点特判
工具权限有界可配:项目级自定义白名单,但只能在允许超集内勾选——effective = 用户选 ∩ 允许超集git push / 部署等红线永不入超集
idle-TTL30 分钟挂起 + resume 重连
并发名额占 1 个 project.concurrency(在用 worktree + agent),可单独配小池避免抢占自治吞吐
+ +
📦 Schema 影响极小runs.kind 扩枚举加 interactive(TEXT 兼容、非 work_type);session id 复用 work_historyconsole.sockruns/<runId>/ 下的文件——无需新增列。新增的是 daemon 的 takeover API + WS↔socket 桥接 + reaper 对 interactive 的特判 + 项目级 interactive_tools 配置(受允许超集约束)。
+
+ + + +
数据库 Schema 变更Phase 1
+ +
+
★ 本方案新增 / 变更字段汇总(schedule × work_type 模型)
+

核心是 tasks 表的状态建模:用 schedule_status + work_type + work_plan 队列 取代旧 status 单字段;状态机以「队列驱动」实现(非 XState 快照),故 state_snapshot / sub_status 不引入。

+ + + + + + + + + + + + + + + + + + + + + + + +
字段类型用途性质
tasksschedule_statusTEXT调度状态机:init|ready|running|waiting|blocked|paused|failed|stuck|done|cancelled,取代旧 status取代 status
taskswork_typeTEXT当前工作类型,决定 dispatch 哪个 agent:plan|code|review|pr|conflict|research|null新增
taskswork_planTEXT JSON剩余步骤队列 [{type, spec, owned_files, expected_output}],运行时动态可改新增
taskswork_historyTEXT JSON已完成步骤记录(审计 + 重试上下文)新增
tasksclaimed_atTEXTCAS 锁,NULL = 可领取、非 NULL = 已锁定(防双重领取)新增
tasksversionINTEGER乐观锁版本号,执行期任务修改检测新增
tasksscopeTEXTfile / module / service / cross-service 改动范围,喂调度 couplingPenalty新增
tasksowned_filesTEXT JSON任务声明的主要修改文件路径,喂 couplingPenalty 文件重叠 + diff 越界闸;planner decompose 给子任务赋值新增
tasksexpected_outputTEXT任务级一句话可验证完成标准(per-step 的在 work_plan 里)新增
tasksattachmentsTEXT JSON提交附件 [{name, type, path}](图片/文件);存 tasks/<id>/attachments/,喂 agent 多模态/Read新增
taskssourceTEXT任务来源 human|ai|sync|github;AI 提的默认进待确认闸新增
taskscreated_byTEXT创建者(人 / 提交它的 agent run id),配合 source 审计新增
projectsagent_rulesTEXT项目级 agent 专用规范(markdown),L2 记忆注入新增
projectsdiff_max_filesINTEGERdiff 体量闸阈值(默认 100 文件)新增
projectschecksTEXT JSON分项检查命令 {lint, typecheck, build}新增
projectsbudget_usdREAL项目级预算上限(当期);当期 spend 超额 → 自动 paused 停领(见 ⑥ 护栏)新增
projectsbudget_periodTEXT预算周期 day|month(配合 budget_usd)新增
runskindTEXT对齐 work_type:plan|code|review|pr|conflict|research,外加 interactive(人工接管 run,非 work_type、不进调度队列)扩枚举
runstrace_idTEXT跨 run 追踪 ID,关联同任务多次运行新增
runsusageTEXT JSON每次模型调用的用量 [{model, input_tokens, output_tokens, cache_read, cache_write}],喂成本计算新增
runscost_usdREAL本 run 折算成本(按 模型×token 价目表);汇总到 task / project新增
+
已弃用(早期 XState 草案)state_snapshot / sub_status(被 schedule_status + work_plan 队列取代)、task_type(任务性质由 work_plan 步骤组成表达,并入 work_type)、parent_version_id(无对应场景)。
注:owned_files 同时存在「任务级」(声明级,喂调度/diff 闸)与「work_plan 每步级」(执行级,当前步声明),两者并存不冲突。
+
+ +
+
Entity Relationship 图(数据库表结构与关系)
+ + + + + + + + + +projects + +🔑 id TEXT PK + name TEXT + repo_path TEXT UNIQUE + default_branch TEXT + verify_cmd TEXT + autonomy TEXT + model TEXT + concurrency INTEGER + max_retries INTEGER + timeout_ms INTEGER +★ checks TEXT (JSON) +★ diff_max_files INTEGER +★ budget_usd REAL +★ budget_period TEXT + auto_approve_plan INTEGER + auto_approve_exec INTEGER +★ agent_rules TEXT + status TEXT + sort_order INTEGER + logo TEXT + last_sync_at TEXT + created_at TEXT + + + + +tasks + +🔑 id TEXT PK +🔗 project_id → projects.id +🔗 parent_id → tasks.id (可空) + depth INTEGER + title TEXT + complexity TEXT +★ schedule_status TEXT +★ work_type TEXT +★ work_plan TEXT (JSON) +★ work_history TEXT (JSON) + priority INTEGER + deps TEXT (JSON) +★ scope TEXT +★ owned_files TEXT (JSON) +★ expected_output TEXT +★ claimed_at TEXT +★ version INTEGER +★ attachments TEXT (JSON) +★ source TEXT +★ created_by TEXT + result TEXT (JSON) + assignee TEXT + retry_baseline INTEGER + next_eligible_at TEXT + source_ref TEXT + created_at / updated_at TEXT + + + + +approvals + +🔑 id TEXT PK +🔗 task_id → tasks.id + gate TEXT (plan|review) + action TEXT (accept|reject) + actor TEXT + reason TEXT + at TEXT + + + + +runs + +🔑 id TEXT PK +🔗 task_id → tasks.id + kind TEXT (plan|code|review|pr|conflict|research) +★ trace_id TEXT +★ usage TEXT (JSON) +★ cost_usd REAL + worktree TEXT + branch TEXT + status TEXT + started_at / ended_at TEXT + transcript_ref TEXT + claude_session_id TEXT + error TEXT + worker_pid INTEGER + last_seq INTEGER + + + + +events + +🔑 id TEXT PK +🔗 project_id (无 FK 约束) +🔗 task_id (可空, 无 FK 约束) + type TEXT + payload TEXT (JSON) + at TEXT + + + + +1:N + +1:N + +1:N + +1:N + +0..1:N (可空) + +parent + + + + +图 例 +★ = 本方案新增列 +🔗 = 外键引用 🔑 = 主键 + +有 FK 约束 + +弱引用(无 FK) + + + + +关键字段说明 + +字段 +所属表 +含义 + + +schedule_status +tasks +调度状态机,取代旧 status:init|ready|running|waiting|blocked|paused|failed|stuck|done|cancelled + +work_type +tasks +当前工作类型,决定 dispatch 哪个 agent:plan|code|review|pr|conflict|research|null + +work_plan +tasks +剩余步骤队列 [{type, spec, owned_files, expected_output}],运行时动态可改 + +work_history +tasks +已完成步骤记录,审计 + 重试上下文 + +claimed_at +tasks +CAS 锁,NULL=可领取、非 NULL=已锁定,防 Orchestrator 双重领取 + +version +tasks +乐观锁版本号,执行期任务修改检测(D6) + +scope +tasks +改动范围 file/module/service/cross-service,喂调度 couplingPenalty + +owned_files +tasks +任务声明的主要修改文件 JSON,喂 couplingPenalty 文件重叠 + diff 越界闸 + +expected_output +tasks +任务级一句话可验证完成标准(per-step 的在 work_plan 内) + +agent_rules ★ +projects +项目级 agent 专用规范文本,注入全部 agent prompt 头部(L2 记忆) + +diff_max_files +projects +diff 体量闸阈值(默认 100),执行后改动文件数超限即拦截 + +checks +projects +分项静态检查命令 JSON {lint, typecheck, build},任一非 0 硬失败 + +budget_usd / budget_period +projects +项目级预算上限(当期 day|month),超额自动 paused 停领(⑥ 护栏) + +kind +runs +run 类型,对齐 work_type:plan|code|review|pr|conflict|research + interactive(人工接管,非 work_type) + +trace_id +runs +跨 run 追踪 ID,关联同一任务的多次运行 + +usage / cost_usd +runs +每次模型调用 token 用量 + 折算成本(模型×token 价目),汇总到 task / project + +last_seq +runs +outbox.ndjson 已消费行号,daemon ingest 幂等重试游标 + +attachments +tasks +提交附件 [{name,type,path}](图片/文件),喂 agent 多模态 / Read + +source / created_by +tasks +任务来源 human|ai|sync|github + 创建者,AI 提的默认进待确认闸 + +payload +events +事件附加数据 JSON,审计日志 + WebSocket 实时推送源 + + +
+ +
+
核心表说明
+ + + + + + + + + +
行数量级作用关键约束
projects少量(<100)项目配置、自动化策略、并发/超时参数repo_path UNIQUE
tasks中量(<10k)任务主表,三类任务(hard/medium/easy)均在此表,以 complexity+status+run.kind 区分FK project_id, parent_id(自引用)
approvals中量审批闸记录(plan/spec/exec),reject 必须带 reasonFK task_id
runs中量(<50k)每次 agent 运行(planner/executor/reviewer/security/conflict),含 transcript 路径和 ingest 游标FK task_id
events大量(仅追加)审计日志 + WebSocket 实时推送源,无 FK 约束(高性能追加)INDEX (project_id, at)
+
+ +
+
★ 本方案新增列
+
+
+

projects.agent_rules (L2)

+
TEXT,项目级 agent 专用规范正文(markdown)
+
daemon 在 claimOne 写 job.json 时注入全部 4 类 agent 的 prompt 头部
+
文件:schema.sql + types.ts + mappers.ts + store.ts
+
+
+

非表字段(JobSpec.context)

+
protocol.ts 新增可选字段,由 daemon 组装、随 job.json 下发
+
内容:parentPlan(父任务拆解意图)+ siblings(兄弟任务状态摘要)
+
worker 读 job.json 即可,无需访问 DB
+
+
+
+ + + + +
Agent 记忆注入P1 核心(L1) · P2(L2–L4)
+ +
+
当前缺口
+
+
+
buildPrompt executor:title/id + operations/spec/plan + 执行约束。无 lastRunError、无项目规范。
+
buildPlannerPrompt planner:title/id + 草稿 + depth。无 lastRunError、无项目规范。
+
buildReviewPrompt reviewer:任务要求 + 执行者自述 + diff 命令。无项目规范。
+
+
+
cc.ts:80 settingSources:['project'] → SDK 自动读项目 CLAUDE.md(现已生效 ✓)
+
task.lastRunError 字段已存在于 Task 类型,但从未注入任何 prompt
+
orchestrator claim 时拿到的 task 对象不带 last_run_error(仅审核 UI 子查询)
+
+
+
+ +
+
数据流铁律 → 决定方案形态
+
worker 进程完全不碰 DB,只读 runs/<runId>/job.json(protocol.ts 铁律)
+
所有来自 DB 的记忆,必须由 daemon 在 claimOne 写 job.json 时组装进去
+
JobSpec 已携带完整 task + project 对象 → lastRunError 和 agentRules 天然可随 job.json 下发
+
L3 跨任务上下文 → 扩展 JobSpec.context? 可选字段
+
+ +
+
+

L1 — 注入 lastRunError(ROI 最高)

+
现状:last_run_error 是 pendingApprovals 子查询,claim 时的 task 不带它
+
改 store.ts:抽 lastRunErrorOf(taskId) 方法(复用现有子查询 SQL)
+
改 orchestrator.ts:claimOne 写 job 前赋值 task.lastRunError
+
改 runner.ts:3 处 build*Prompt 插「## 上次失败原因(重试任务)」段
+
5 行代码,零协议改动(字段已在 Task 上)
+
+
+

L2 — 项目级 agent 规范(存 DB projects 表)

+
schema.sql:projects 表加 agent_rules TEXT 列 + 幂等迁移
+
types.ts/mappers.ts:Project 加 agentRules?、row ↔ 字段映射
+
project 已随 job.json 下发 → worker 端 4 处 prompt 插「## 项目规范」头部
+
API/Web 编辑入口后续补;本期先打通存储+注入
+
+
+ +
+
+

L3 — 跨任务上下文

+
protocol.ts JobSpec 加 context?: { parentPlan?: string; siblings?: {...}[] }
+
store.ts:取 parent.plan + 同 parentId 兄弟摘要的方法
+
orchestrator.ts claimOne:若 task.parentId,组装 job.context 再下发
+
runner.ts buildPrompt:注入「## 拆解背景(父任务意图 + 兄弟状态)」
+
仅做 DB 读;不做 git 演化检索/RAG(留后续)
+
+
+

L4 — 全局 agent 规范(maestro 自维护)

+
不读 user 全局 ~/.claude/CLAUDE.md(避免个人习惯干扰受控执行)
+
settingSources:['project'] 保持不变
+
maestro 维护 ~/.maestro/agent-global.md
+
daemon 启动时读一次缓存,claimOne 随 job.json 下发,4 处 prompt 头部注入
+
文件不存在则跳过(可选,缺省空)
+
+
+ +
+
注入顺序(prompt 内记忆段排布)
+
+[L4] ## 全局执行规范 → +[L2] ## 项目规范(必须遵守) → +[L3] ## 拆解背景(父任务意图 + 兄弟状态) → +## 任务内容 → +[L1] ## 上次失败原因(重试任务) → +## 执行约束 +
+
项目 CLAUDE.md 由 SDK 经 settingSources:['project'] 自动进 system prompt,不在 user prompt 里重复。
+
+ + +
Agent 规格与注册表Phase 1
+ +
+
Agent 注册表模型(work_type → AgentSpec,可动态扩展)
+

每个 work_type 对应一条 AgentSpec。Orchestrator 派发 = 注册表查表,而非写死 switch。新增一种 agent = 注册一条 AgentSpec,调度内核 / 状态机零改动。六个维度——流程 / 每步模型 / skills+subagents / prompt / 上下文 / 输出——全部声明在 AgentSpec 里。

+
interface AgentSpec {
+  workType:    WorkType;        // 'plan'|'code'|'review'|'pr'|'conflict'|'research'|'writing'|…
+  runKind:     string;          // 落 runs.kind
+  pickModel:   (t, p) => ModelId; // 每步档位(可按 complexity 分档)
+  tools:       ToolPattern[];     // 工具白名单(∩ 全局允许超集;红线永不入集)
+  skills?:     SkillId[];         // 注入的 skill(如 writing-plans / elements-of-style)
+  subagents?:  AgentType[];       // 可调度的 subagent(如 Explore / general-purpose)
+  needsWorktree: boolean;        // 写类 true(隔离改动)
+  buildPrompt: (ctx: AgentContext) => string;  // 含 L1–L4 记忆
+  parseOutput: (raw: string) => StepResult;    // 输出解析
+  gate?:       'plan' | 'review' | null;        // 完成后的人工闸
+  maxTurns:    number;
+  preempt?:    boolean;          // 抢占调度(如 conflict)
+}
+
+// 派发 = 查表,不是写死 switch
+const spec = AGENT_REGISTRY[task.work_type];
+await spec.run(task);  // run 内:buildPrompt → cc(model,tools,skills) → parseOutput → gate
+

AgentContext(上下文,daemon 在 claim 时组装进 job.json)

+
{
+  task, project,        // job.json 携带
+  step,                 // work_plan 当前步 {type, spec, owned_files, expected_output}
+  memory: { global,     // L4 全局规范
+            projectRules,// L2 项目 agent_rules
+            decompCtx,  // L3 父任务意图 + 兄弟状态
+            lastError },// L1 上次失败
+  diff?, reviewerNotes? // review / pr 用
+}
+
+ +
+
plan Planner Agent
+ + + + + + + + + + +
维度内容
触发(ready, plan);Hard→decompose / Medium→spec;只读、不建 worktree
流程buildPlannerPrompt → runPlanner(CC 只读) → 解析输出 → (waiting, plan) 人审
模型/档easy=sonnet-4-6 · medium=opus-4-8 · hard=opus-4-8(最强档)
skills/subagentskill superpowers:writing-plans(拆解质量);subagent Explore(大范围只读检索代码演化)
toolsRead / Glob / Grep + Bash(git log/diff/show);maxTurns 90/60/40
prompttitle/id + spec/plan 草稿 + depth + 剩余可拆层(≤3) + 「产子任务表格(人审) + 末尾 fenced JSON」
上下文L4 全局 → L2 项目规范 → L3 拆解背景 → 任务内容 → L1 上次失败;project CLAUDE.md 经 settingSources 入 system
输出decompose: 表格 + JSON{title,complexity,priority,deps,owned_files,expected_output};spec: 三段 md(改动/为什么/验收)
+
+ +
+
code Executor Agent
+ + + + + + + + + + +
维度内容
触发(ready, code)建 worktree(隔离写)
流程syncMain → createWorktree → buildPrompt → runTask(CC, acceptEdits) → 必须 commit → verify → checks 闸 → diff 体量闸
模型/档easy=sonnet-4-6 · medium/hard=opus-4-8
skills/subagentsubagent general-purpose / Explore(并行只读调研、省主上下文);skill:项目可配领域 skill
toolsRead/Edit/Write/Glob/Grep + Bash(git 无 push, test) + WebSearch;maxTurns 100
prompttitle/id + operations??spec??plan + owned_files 范围 + expected_output + 执行约束(禁越界 / 必 commit 含任务 ID)
上下文L1–L4 + work_plan 当前 step.spec / step.owned_files
输出执行自述 + 含任务 ID 的 commit
+
+ +
+
review Reviewer Agent
+ + + + + + + + + + +
维度内容
触发(ready, review);code 完成自动串联;独立 run、只读、可选
流程Promise.all(reviewCode, reviewSecurity) 并行 → 合并 verdict → reject 硬拦截(回 code) / pass → (waiting, review)
模型/档三档统一 opus-4-8(最强,不被 project.model 降档,保自审严格度)
skills/subagentskill:code-review 清单 / 安全审计清单;无 subagent(聚焦审单一 diff)
toolsRead/Glob/Grep + Bash(git diff/show/log);maxTurns 40;不写、复用 code 的 worktree 只读
prompt任务要求 + 执行者自述 + worktreeDiff + 「输出 VERDICT 模板(pass/reject + 理由)」
上下文任务 spec + code 自述 + diff + L2 项目规范
输出双 verdict + 意见;任一 reject → 硬退回 code(带意见)
+
+ +
+
pr PR Agent
+ + + + + + + + + + +
维度内容
触发review 通过后 (ready, pr)
流程创建 PR(gh) → --no-ff 合并 → 冲突? → 建 conflict run : done
模型/档opus-4-8(PR 文案 / 合并决策;多为确定性操作)
skills/subagentskill:PR 描述模板;无 subagent
toolsBash(git, gh —受控:建 PR / 合并,禁强推);不建新 worktree
prompt任务摘要 + commit 列表 + 关联 issue + PR 模板
上下文任务 + 分支 + diff 摘要 + review 结论
输出PR url + merge 结果(成功 → done / 冲突 → conflict run)
+
+ +
+
conflict Conflict Agent
+ + + + + + + + + + +
维度内容
触发pr 合并冲突自动建(抢占调度,不排队等 score)
流程补救 worktree git merge 制造冲突 → CC 解(保两边意图) → 跑测试 → commit → 双复审
模型/档opus-4-8 固定(最强,不随原任务复杂度降档)
skills/subagentskill:解冲突纪律(禁丢任一方改动 / 解完验证);subagent Explore(查两边意图)
toolsRead/Edit/Write + Bash(git merge仅此分支白名单, test)
prompt专用解冲突模板(保留两边意图 + 禁丢改动 + 解完跑测试 + commit 沿用合并信息)
上下文两分支 diff + 冲突文件 + 原任务意图 + L1 上次失败
输出解冲突自述(解了哪些文件 / 取舍 / 测试结果)+ commit
+
另有 research(纯只读调研,不写、无 gate)与 interactive(人工接管,见「交互平面」专题)两个 work_type,规格同样登记在 AGENT_REGISTRY。
+
+ +
+
动态新增 Agent(写作 / 视频发布 示例)
+

三步加一个 agent:① work_type / runs.kind 加枚举值(TEXT 兼容、无需改表结构)→ ② 写一条 AgentSpec 注册进 AGENT_REGISTRY → ③(可选)模型档位表 + 工具允许超集补该角色。调度器 / 状态机零改动——一旦某任务的 work_plan 含该 work_type,Orchestrator 自动按注册表派发。

+
// 示例 1:写作 agent(后期加内容生产能力)
+AGENT_REGISTRY['writing'] = {
+  workType:'writing', runKind:'writing',
+  pickModel: () => 'claude-opus-4-8',
+  tools:['Read','Write','WebSearch'], needsWorktree:true,
+  skills:['elements-of-style:writing-clearly-and-concisely'],
+  subagents:['Explore'],          // 查资料
+  buildPrompt: ctx => 写作 brief + L2 项目文风规范 + 素材,
+  gate:'review',                  // 产出走人审
+  maxTurns:60,
+};
+
+// 示例 2:自动发视频 agent(对外副作用,须显式授权)
+AGENT_REGISTRY['video-publish'] = {
+  workType:'video-publish', runKind:'video-publish',
+  pickModel: () => 'claude-opus-4-8',
+  tools:['Read','Bash(ffmpeg:*)','Bash(youtube-upload:*)'], // 受控发布工具
+  needsWorktree:false,
+  buildPrompt: ctx => 视频元数据 + 平台 + 发布策略,
+  gate:'review',                  // 对外发布强制人审闸
+  maxTurns:30,
+};
+
🛡 红线护栏:对外产生副作用的 agent(发布 / 部署 / push)默认强制 gate:'review' 人工闸,且工具受允许超集约束——「未审不发」是硬约束,新 agent 不能绕过。纯生产/只读 agent(writing 草稿、research)可配 gate:null 自动放行。
+
+ + + +
运维与护栏(Ops & Guardrails)Phase 2
+ +
+
成本与预算治理(精确到项目 · 按 模型 × token 计费)
+

自治系统自动 spawn 付费 agent,必须有成本闸防失控烧钱。每个 run 从 Agent SDK 抓 token 用量 → 按每模型价目算成本 → 落库 → 汇总到 task / project → 项目级预算闸超额自动停领。

+
// 1. 每次模型调用的用量落 runs.usage(cc.ts 从 SDK message.usage 抓)
+runs.usage = [{ model, input_tokens, output_tokens, cache_read, cache_write }, …]
+
+// 2. 价目表($/Mtok,各模型 in/out/cache 单价不同)
+const MODEL_PRICES = {
+  'claude-opus-4-8':   { in:15, out:75, cacheRead:1.5, cacheWrite:18.75 },
+  'claude-sonnet-4-6': { in:3,  out:15, cacheRead:0.3, cacheWrite:3.75  },
+  'claude-haiku-4-5':  { in:1,  out:5,  cacheRead:0.1, cacheWrite:1.25  },
+};
+// 3. run 成本 = Σ 每次调用(按该次的 model 单价)
+cost_usd = Σ ( in_tok·P.in + out_tok·P.out + cache_read·P.cacheRead + cache_write·P.cacheWrite ) / 1e6
+// 4. 汇总:task.cost = Σ runs.cost_usd ; project 当期 spend = Σ task.cost(按 budget_period)
+
+// 5. 项目级预算闸(Orchestrator claim 前)
+if (project.budget_usd && periodSpend(project, project.budget_period) >= project.budget_usd) {
+  pauseProject(project);  // 新任务不再领取;在途 run 跑完不打断;通知人工
+  continue;
+}
+ + + + + + +
维度设计
计费粒度per 模型调用 → per run(runs.cost_usd)→ per task → per project(当期)
预算闸projects.budget_usd + budget_period(day|month);超额 → project 自动 paused 停领,可单独配全局日上限做二级保险
消费端/api/usage(成本明细:项目/模型/时段)与 /api/metrics 由此填充,不再是空端点
价目维护价目表为代码常量,模型变更/调价改一处;缓存命中(cacheRead)显著降本,纳入计算
+
+ +
+
限流与全局并发背压
+ + + + + + +
机制设计
两层并发闸project.concurrency(单项目)+ 新增 MAESTRO_MAX_INFLIGHT(全局在途上限);claim 受 min(项目闸, 全局闸) —— 防多项目×并发把 API 打爆
API 429 / overloadedcc.ts 捕获 Anthropic 限流 → run 标 throttled不计任务失败、不消耗 max_retries)→ 退避后重排;与 failAttempt 严格区分
spawn 速率令牌桶限制每秒新起 worker 数,避免 tick 一次性雷鸣群(thundering herd)冲击 API + 磁盘
与成本闸协同budget 超 → 停领(不再 claim);429 → 慢领(退避);两者正交叠加
+
+ +
+
资源生命周期与 GC
+ + + + + + + +
资源回收策略
worktreetask done/cancelled/reap 后删 worktree;已合并的临时分支删除,未合并分支保留待人查
runs/<runId>/job.json / outbox / heartbeat / transcript 保留 retention_days(可配)→ 过期归档或删;transcript 大文件单独策略
eventsappend-only → 定期 rollup(旧事件按 task 聚合)/ 归档到冷表,防无限增长拖慢看板查询
启动 GCdaemon 启动扫孤儿 worktree(无对应活跃 run)→ 清理;扫残留 runs 目录对账
SQLite周期 WAL checkpoint + 低峰 VACUUM,控制 DB 文件膨胀
+
+ +
+
可观测性(metrics / health / trace)
+ + + + + + +
内容
/api/metrics队列深度(各 schedule_status 计数)· 吞吐(done/h)· agent 时延 + turn 数分布 · review 通过/reject 率 · 成本(per project / model)
/api/healthdaemon 存活 + DB 可写 + 在途 worker 数 + 最近一次 tick 时间(探活/告警用)
trace_idJobSpec.traceId 贯穿 outbox 每条 → 串起一次任务跨多 run(plan→code→review→pr)的全链;从 Phase 2 提前为一等公民(成本/排障都依赖它聚合)
审计源events 表已是审计 + WS 推送源,metrics 在其上做聚合,不另起存储
+
+ +
+
依赖图完整性(环检测 / 死锁)
+ + + + + +
时机设计
decompose 落库时planner 产出 deps → 建边前跑环检测(拓扑排序 / DFS);发现环 → 拒绝该 decompose 结果 + planner 重试(带「deps 成环,请重排」意见)
运行时死锁若存在非终态 task 但可运行集为空(全部 blocked 互等)→ 标记死锁 + 通知人工,不空转
防御(prompt)planner prompt 明确「deps 用子任务序号引用、禁自环 / 互环」,从源头降低成环概率
+
+ + +
安全与沙箱(Security & Sandbox)Phase 2
+ +
+
安全模型(沙箱 · 凭证 · 脱敏 · 对外授权)
+ + + + + + + +
设计
沙箱MAESTRO_SANDBOX:worker 进程 OS 级写围栏(只允许写自己的 worktree + runs/<runId>/)+ ulimits(CPU/内存/fd);配置落 sandbox.sb。默认 off,生产建议 on
工具红线(全局)任何 agent 不可逾越:禁 git push / 禁部署 / 禁对外发布(除非显式授权 agent + 人审闸);实际工具 = AgentSpec.tools ∩ 全局允许超集,红线永不入超集
凭证git / API 凭证不写进 worktree、不入 job.json;由 daemon 以最小权限注入 worker env,按 run 生命周期回收;解冲突/PR 的 git 操作受控、不外推
secret 脱敏outbox / transcript / events 落盘前过滤已知 secret 模式(token / key / 凭证),防泄漏到审计层与看板
对外副作用授权产生外部副作用的 agent(video-publish / deploy)强制 gate:'review' 人审闸 + 项目级显式开关,未授权不执行——「未审不对外」是硬约束
+
与 ⑤ Agent 规格的工具白名单、交互平面的「有界可配工具」一脉相承:能力按需授予、红线全局兜底、对外副作用强制人审
+
+ + + +
前端 API 契约(REST + WS)P1 现有 · P2 ★新路由
+ +
+
统一约定
+ + + + + + + +
约定
Base URLhttp://127.0.0.1:4517MAESTRO_PORT);仅本地监听
Content-Typeapplication/json;附件上传用 multipart/form-data;SSE 为 text/event-stream
鉴权本地无鉴权(仅 127.0.0.1);如需远程暴露另加(本期不含)
错误格式统一 { "error": "<message>" }StoreError400、不存在 → 404、状态机非法转移 → 400、内部 → 500
时间字段ISO8601 字符串(TEXT)
+
+ +
+
Projects API
+ + + + + + + + + + + +
方法路径请求体(关键字段)响应
GET/api/projectsProject[]
POST/api/projects{name*, repoPath*, defaultBranch?, autonomy?, concurrency?, model?}Project
GET/api/projects/:idProject
PATCH/api/projects/:id部分字段:autonomy / concurrency / model / budget_usd / budget_period / checks / agent_rules / diff_max_files / verify_cmdProject
DELETE/api/projects/:id{ok}
POST/api/projects/:id/sync{created, done, skipped}
POST/api/projects/reorder{order: id[]}{ok}
GET/api/projects/:id/tasksTask[](任务树)
GET/api/projects/:id/eventsEvent[]
+
+ +
+
Tasks API
+ + + + + + + + + + + + + +
方法路径请求体(关键字段)响应
POST/api/projects/:id/tasks{title*, complexity?(auto|hard|medium|easy), priority?, deps?, parentId?, owned_files?, scope?, expected_output?}Task
GET/api/tasks/:idTask
POST/api/tasks/:id/plan{plan*}Task
POST/api/tasks/:id/spec{spec*}Task
POST/api/tasks/:id/decide{action: accept|reject, actor?, reason?, merge?}(人工闸)Task
POST/api/tasks/:id/transition{to*, meta?}(守卫校验合法转移)Task
POST/api/tasks/:id/requeueTask
POST/api/tasks/:id/cancelTask
DELETE/api/tasks/:id{ok}
POST ★/api/tasks/:id/attachmentsmultipart: files[](图片/文件){attachments:[{name,type,path}]}
POST ★/api/tasks/:id/takeover—(起交互接管 run){runId, console}
+
+ +
+
Runs / 观测 API
+ + + + + + + + + + +
方法路径请求体 / 查询响应
GET/api/tasks/:id/runsRun[]
GET/api/runs/:id/transcripttext/ndjson(transcript)
GET/api/tasks/:id/eventsEvent[]
GET ★/api/tasks/:id/streamtext/event-stream(agent token SSE,Phase 2)
GET/api/agentsAgentSpec[](注册表)
GET ★/api/usage?project&period{byProject, byModel, total} 成本明细
GET ★/api/metrics{queueDepth, throughput, latency, reviewPassRate, cost}
GET ★/api/health{ok, db, inflight, lastTickAt}
+
+ +
+
WebSocket 事件契约(/ws
+

连接 ws://127.0.0.1:4517/ws,连上后服务端单向推送(客户端无需发消息)。每条消息 = 一行 events 表记录,前端据 type 增量更新看板,无需轮询

+
// 消息封装(JSON)
+{ type, projectId, taskId, payload, at }
+ + + + + + + + + + + + + +
type触发payload 关键
task.created新任务(任意入口)task
task.updated任务字段变更task / 变更字段
status.changedschedule_status 变更from / to / work_type
complexity.changedAUTO 复杂度回填complexity / reason
run.startedworker 起跑runId / kind
run.finishedrun 结束runId / result
approval.granted / approval.rejected人工闸决策gate / reason
merge.failed / merge.remediated合并冲突 / 收口branch
project.syncedtodo 同步完成created / done / skipped
run.progressSSE 配套(Phase 2)runId / token 增量
budget.exceeded项目当期成本超预算projectId / spend / budget
+
★ = 本次重构新增路由 / 事件。完整 Task / Project / Run / Event 结构见 ③ 数据库 Schema 的字段表(响应体即各表行的 camelCase 映射)。
+
+ + + +
+ +
+
Phase 1 实施计划
+
六大任务(T1–T6)及 checkbox 进度追踪,实施前须完成 ③ 设计方案审阅。
+
+ +
Phase 1 实施计划
+ +
+
全局约束
+
+
+
npm test 全套测试,使用 new Store(':memory:'),禁止 mock
+
npm run typecheck 类型检查
+
npm run build 编译到 dist/
+
+
+
最强档模型 ID:'claude-opus-4-8'(fable-5 暂不可用)
+
禁止 git push / 禁止部署
+
禁止重复实现已有功能(见"已完成项")
+
+
+
+ +
+
已完成项(禁止重复实现)
+
verdict→硬闸 ingest.ts 149–165 行:任一 reject 退回重执行
+
Promise.all 并行双复审 pipeline.ts 154 行
+
syncMain 执行前同步主分支 pipeline.ts 121–127 行
+
conflict 管道 runConflictPipeline pipeline.ts 211–273 行
+
planner maxTurns 按复杂度 90/60/40 runner.ts 168 行
+
depth 注入 防无限拆 buildPlannerPrompt 133 行
+
git 只读工具 log/diff/show runPlanner 171 行
+
deps 序号→taskId 映射 ingest.ts 103–116 行
+
迁移说明:以上均实现在旧扁平 FSM(analyzing/speccing/executing/exec_review)上。重构到 schedule×work_type 时业务逻辑可复用(双复审 / 解冲突 / syncMain / planner 档位与 prompt / deps 映射),但状态机接线须重指向新 schedule_status×work_type:① review 现为独立 work_type,「Promise.all 双复审」须从 executor(code) run 拆出到独立 reviewer run;② 「verdict→硬闸」「exec_review 转移」重定位到 review 步的 waiting(review) 闸 + pr 步合并。属 T1–T6 的迁移内容,非推倒重写。
+
+ + +
+
+
T1
+
Schema 迁移 + Task 类型扩展
schema.sql · db.ts · types.ts · mappers.ts · store.ts
+
0/10
+
+
+
+
    +
  • +
    测试步骤 1:写失败测试
    +
    test('Task 新字段默认值', () => {
    +  const store = new Store(':memory:');
    +  const p = store.createProject({ name:'p', repoPath:'/tmp/t-'+Math.random(), autonomy:'manual' });
    +  const t = store.createTask({ projectId: p.id, title:'feat', complexity:'easy' });
    +  assert.strictEqual(t.scheduleStatus, 'init');
    +  assert.deepStrictEqual(t.workPlan, []);
    +  assert.strictEqual(t.version, 1);
    +});
    +
  • +
  • +
    运行步骤 2:确认失败
    +
    tsx --test test/store.test.ts 2>&1 | grep -E "fail|Error" | head -5
    +
  • +
  • +
    实现步骤 3:schema.sql — tasks 加状态/队列等 9 列,runs 加 trace_id
    +
      schedule_status   TEXT NOT NULL DEFAULT 'init',
    +  work_type         TEXT,
    +  work_plan         TEXT NOT NULL DEFAULT '[]',
    +  work_history      TEXT NOT NULL DEFAULT '[]',
    +  claimed_at        TEXT,
    +  version           INTEGER NOT NULL DEFAULT 1,
    +  scope             TEXT,
    +  owned_files       TEXT NOT NULL DEFAULT '[]',
    +  expected_output   TEXT
    +  -- runs 表:
    +  trace_id          TEXT
    +
  • +
  • +
    实现步骤 4:db.ts — openDb 末尾追加 10 个 ensureColumn
    +
      ensureColumn(db,'tasks','schedule_status',"schedule_status TEXT NOT NULL DEFAULT 'init'");
    +  ensureColumn(db,'tasks','work_type','work_type TEXT');
    +  ensureColumn(db,'tasks','work_plan',"work_plan TEXT NOT NULL DEFAULT '[]'");
    +  ensureColumn(db,'tasks','work_history',"work_history TEXT NOT NULL DEFAULT '[]'");
    +  ensureColumn(db,'tasks','claimed_at','claimed_at TEXT');
    +  ensureColumn(db,'tasks','version','version INTEGER NOT NULL DEFAULT 1');
    +  ensureColumn(db,'tasks','scope','scope TEXT');
    +  ensureColumn(db,'tasks','owned_files',"owned_files TEXT NOT NULL DEFAULT '[]'");
    +  ensureColumn(db,'tasks','expected_output','expected_output TEXT');
    +  ensureColumn(db,'runs','trace_id','trace_id TEXT');
    +
  • +
  • +
    实现步骤 5:types.ts — 新增 ScheduleStatus/WorkType/WorkStep + Task 接口字段
    +
    export type ScheduleStatus = 'init'|'ready'|'running'|'waiting'|'blocked'|'paused'|'failed'|'stuck'|'done'|'cancelled';
    +export type WorkType = 'plan'|'code'|'review'|'pr'|'conflict'|'research';
    +export type TaskScope = 'file'|'module'|'service'|'cross-service';
    +export interface WorkStep { type: WorkType; spec?: string; ownedFiles?: string[]; expectedOutput?: string; }
    +// Task 接口追加:
    +  scheduleStatus: ScheduleStatus; workType: WorkType|null;
    +  workPlan: WorkStep[]; workHistory: WorkStep[];
    +  scope: TaskScope|null; ownedFiles: string[]; expectedOutput: string|null;
    +  claimedAt: string|null; version: number;
    +
  • +
  • +
    实现步骤 6:mappers.ts — TaskRow 加字段 + rowToTask 映射
    +
    // TaskRow 追加:
    +  schedule_status:string; work_type:string|null; work_plan:string; work_history:string; owned_files:string; claimed_at:string|null; version:number;
    +// rowToTask 追加:
    +  scheduleStatus: r.schedule_status ?? 'init',
    +  workType: r.work_type ?? null,
    +  workPlan: r.work_plan ? JSON.parse(r.work_plan) : [],
    +  workHistory: r.work_history ? JSON.parse(r.work_history) : [],
    +  ownedFiles: r.owned_files ? JSON.parse(r.owned_files) : [],
    +  version: r.version ?? 1,
    +
  • +
  • +
    实现步骤 7:store.ts — PatchTaskInput 支持新字段(workPlan/workHistory 用 JSON.stringify 序列化)
    +
    按已有参数化查询模式处理;禁止字符串拼接传值。
    +
  • +
  • +
    运行步骤 8:npm test
    +
    npm test 2>&1 | tail -15
    +
    预期:新测试通过,旧测试不受影响。
    +
  • +
  • +
    运行步骤 9:typecheck
    +
    npm run typecheck
    +
  • +
  • +
    提交步骤 10
    +
    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 迁移"
    +
  • +
+
+ + +
+
+
T2
+
CAS Claim + 执行期锁
store.ts · orchestrator.ts
+
0/10
+
+
+
    +
  • +
    测试步骤 1:写失败测试(双重 claim + ready 清零)
    +
    test('CAS — 同任务不能双重领取', () => {
    +  assert.ok(store.casClaimTask(task.id,'run-1'));
    +  assert.ok(!store.casClaimTask(task.id,'run-2'));
    +});
    +test('transition→ready 清空 claimed_at', () => {
    +  store.decide(task.id,'reject','human','改');
    +  assert.ok(store.casClaimTask(task.id,'run-3'));
    +});
    +
  • +
  • +
    运行步骤 2:确认失败
    +
    tsx --test test/orchestrator.test.ts 2>&1 | grep casClaimTask | head -3
    +
  • +
  • +
    实现步骤 3:store.ts — casClaimTask
    +
    casClaimTask(taskId:string,_runId:string):boolean {
    +  const r=this.db.prepare(
    +    `UPDATE tasks SET claimed_at=?,updated_at=? WHERE id=? AND claimed_at IS NULL`
    +  ).run(now(),now(),taskId);
    +  return r.changes===1;
    +}
    +
  • +
  • +
    实现步骤 4:transition() to==='ready' 清 claimed_at;requeueTask 同步清零
    +
    if(to==='ready'){
    +  this.db.prepare(`UPDATE tasks SET status=?,claimed_at=NULL,updated_at=? WHERE id=?`).run(to,now(),taskId);
    +}
    +
  • +
  • +
    实现步骤 5:orchestrator.claimOne — startRun 后接 casClaimTask
    +
    if(!store.casClaimTask(task.id,run.id)){log.error(`CAS失败`);return;}
    +
  • +
  • +
    实现步骤 6:store.ts — assertNoActiveRun 私有方法
    +
    private assertNoActiveRun(taskId:string){
    +  const {n}=this.db.prepare(`SELECT COUNT(*) n FROM runs WHERE task_id=? AND status='started'`).get(taskId) as {n:number};
    +  if(n>0) throw new StoreError('任务执行中,禁止修改');
    +}
    +
    在 setSpec / setPlan / patchTask 入口调用 assertNoActiveRun。
    +
  • +
  • +
    测试步骤 7:补执行期锁测试
    +
    test('执行中禁止修改 spec', () => {
    +  store.startRun(t.id,'planner');
    +  assert.throws(()=>store.setSpec(t.id,'x'),/执行中/);
    +});
    +
  • +
  • 运行步骤 8:npm test
    npm test 2>&1 | tail -15
  • +
  • 运行步骤 9:typecheck
    npm run typecheck
  • +
  • +
    提交步骤 10
    +
    git add src/store/store.ts src/daemon/orchestrator.ts test/orchestrator.test.ts
    +git commit -m "feat(store): CAS claimed_at 防双重 claim + 执行期锁"
    +
  • +
+
+ + +
+
+
T3
+
调度算法升级(rankU + agingBonus + couplingPenalty)
scoring.ts · orchestrator.ts
+
0/7
+
+
+
    +
  • +
    测试步骤 1:写失败测试
    +
    test('agingBonus — 新建任务为 0', () =>
    +  assert.strictEqual(agingBonus({createdAt:new Date().toISOString(),priority:1} as Task,Date.now()),0));
    +test('couplingPenalty — 1个重叠=-0.5', () =>
    +  assert.strictEqual(couplingPenalty({ownedFiles:['a.ts','b.ts']} as Task,[{ownedFiles:['a.ts']} as Task]),0.5));
    +
  • +
  • 运行步骤 2:确认失败
    tsx --test test/orchestrator.test.ts 2>&1 | grep -E "agingBonus|coupling" | head -3
  • +
  • +
    实现步骤 3:重写 scoring.ts — 导出 rankU / agingBonus / couplingPenalty / 新签名 rankByScore
    +
    export function rankU(id:string,byId:Map<string,Task>,deps:Map<string,Task[]>,cache:Map<string,number>):number{
    +  if(cache.has(id))return cache.get(id)!;
    +  const t=byId.get(id); if(!t){cache.set(id,0);return 0;}
    +  const v=baseScore(t.priority)+(deps.get(id)??[]).reduce((s,d)=>s+rankU(d.id,byId,deps,cache),0);
    +  cache.set(id,v);return v;
    +}
    +export const agingBonus=(t:Task,nowMs:number)=>
    +  Math.min(baseScore(t.priority),(nowMs-Date.parse(t.createdAt))/3_600_000/48);
    +export function couplingPenalty(t:Task,inFlight:Task[]):number{
    +  const s=new Set(t.ownedFiles??[]); if(!s.size)return 0;
    +  let n=0; for(const x of inFlight)for(const f of(x.ownedFiles??[])) if(s.has(f))n++;
    +  return n*0.5;
    +}
    +
  • +
  • +
    实现步骤 4:orchestrator.ts — claimable() 传入 inFlight 列表
    +
    const inflightList=tasks.filter(t=>inflight.has(t.id));
    +return rankByScore(candidates,tasks,inflightList,d.nowMs());
    +
  • +
  • 运行步骤 5:npm test
    npm test 2>&1 | tail -15
  • +
  • 运行步骤 6:typecheck
    npm run typecheck
  • +
  • +
    提交步骤 7
    +
    git add src/model/scoring.ts src/daemon/orchestrator.ts test/orchestrator.test.ts
    +git commit -m "feat(scoring): CPM rankU + agingBonus + couplingPenalty 调度算法"
    +
  • +
+
+ + +
+
+
T4
+
模型档位升级(统一最强档 opus-4-8)
models.ts
+
0/6
+
+
+
    +
  • +
    测试步骤 1:写失败测试
    +
    test('planner hard = opus-4-8', () =>
    +  assert.strictEqual(pickModel({complexity:'hard'}as Task,{model:null}as Project,'planner'),'claude-opus-4-8'));
    +test('reviewer 不被 project.model 降档', () =>
    +  assert.strictEqual(pickModel({complexity:'easy'}as Task,{model:'claude-sonnet-4-6'}as Project,'reviewer'),'claude-opus-4-8'));
    +test('MODEL_FALLBACK_CHAIN[0] = opus-4-8', () =>
    +  assert.strictEqual(MODEL_FALLBACK_CHAIN[0],'claude-opus-4-8'));
    +
  • +
  • 运行步骤 2:确认失败
    tsx --test test/models.test.ts 2>&1 | grep -E 'opus|reviewer' | head -3
  • +
  • +
    实现步骤 3:models.ts — 4 处修改
    +
    // 1. 回退链(fable-5 暂不可用,opus-4-8 为最强档)
    +export const MODEL_FALLBACK_CHAIN=['claude-opus-4-8','claude-sonnet-4-6'] as const;
    +// 2. planner 分档:easy=sonnet,medium/hard=opus(hard 不再与 easy 同档)
    +  hard:['MAESTRO_MODEL_PLAN_HARD','claude-opus-4-8'],
    +// 3. reviewer 三档统一最强 opus-4-8(不被 project.model 降档)
    +  reviewer:{easy:[...,'claude-opus-4-8'],medium:[...,'claude-opus-4-8'],hard:[...,'claude-opus-4-8']},
    +// 4. conflict 固定最强(pickModel 首行)
    +if(role==='conflict')return 'claude-opus-4-8';
    +
    ⚠ fable-5 是设计目标最强档,暂不可用;当前一律用 opus-4-8 充当,fable-5 恢复后把以上 4 处的 claude-opus-4-8 换回 claude-fable-5 即可,结构不变。
    +
  • +
  • 运行步骤 4:npm test
    npm test 2>&1 | tail -15
  • +
  • 运行步骤 5:typecheck
    npm run typecheck
  • +
  • +
    提交步骤 6
    +
    git add src/executor/models.ts test/models.test.ts
    +git commit -m "feat(models): 复审/解冲突/planner-hard 统一最强档 opus-4-8"
    +
  • +
+
+ + +
+
+
T5
+
Planner Decompose JSON 扩展(ownedFiles + expectedOutput)
runner.ts · protocol.ts · ingest.ts
+
0/8
+
+
+
    +
  • +
    测试步骤 1:ingest decompose-result 映射测试
    +
    test('decompose-result — ownedFiles 落入子任务', () => {
    +  appendOutbox(run.id,{type:'decompose-result',plan:'x',
    +    subtasks:[{title:'类型',complexity:'easy',priority:0,deps:[],
    +      ownedFiles:['src/model/types.ts'],expectedOutput:'typecheck通过'}]});
    +  ingestRun(store,noopLog,run.id);
    +  const [child]=store.childrenOf(parent.id);
    +  assert.deepStrictEqual(child.ownedFiles,['src/model/types.ts']);
    +  assert.strictEqual(child.expectedOutput,'typecheck通过');
    +});
    +
  • +
  • 运行步骤 2:确认失败
    tsx --test test/ingest.test.ts 2>&1 | grep -E "ownedFiles|expectedOutput" | head -3
  • +
  • +
    实现步骤 3:protocol.ts — decompose-result 子任务加 ownedFiles? / expectedOutput?
    +
    subtasks: Array<{title:string;complexity:Complexity;priority?:number;deps?:number[];
    +  ownedFiles?:string[];expectedOutput?:string}>;
    +
  • +
  • +
    实现步骤 4:runner.ts — buildPlannerPrompt decompose 分支加两条 bullet + 更新 JSON 示例
    +
    '- ownedFiles:主要修改文件路径 JSON string[](无则填 [])',
    +'- expectedOutput:一句话可验证完成标准(如:"typecheck通过")'
    +
  • +
  • +
    实现步骤 5:ingest.ts — decompose-result deps 映射之后追加 ownedFiles/expectedOutput 映射循环
    +
    for(let i=0;i<rec.subtasks.length;i++){
    +  const sub=rec.subtasks[i] as {ownedFiles?:string[];expectedOutput?:string};
    +  if(!createdIds[i])continue;
    +  const patch:Record<string,unknown>={};
    +  if(Array.isArray(sub.ownedFiles)&&sub.ownedFiles.length) patch.ownedFiles=sub.ownedFiles;
    +  if(typeof sub.expectedOutput==='string'&&sub.expectedOutput.trim()) patch.expectedOutput=sub.expectedOutput.trim();
    +  if(Object.keys(patch).length) store.patchTask(createdIds[i],patch);
    +}
    +
  • +
  • 运行步骤 6:npm test
    npm test 2>&1 | tail -15
  • +
  • 运行步骤 7:typecheck
    npm run typecheck
  • +
  • +
    提交步骤 8
    +
    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"
    +
  • +
+
+ + +
+
+
T6
+
管道硬闸(checks 分项 + diff 体量)
pipeline.ts · schema.sql · db.ts · types.ts · mappers.ts
+
0/10
+
+
+
    +
  • +
    测试步骤 1:写失败测试(checks + diff 体量)
    +
    test('checks — lint 失败阻止 exec_review', async () => {
    +  job.project.checks=JSON.stringify({lint:'exit 1'});
    +  const emitted=[];
    +  await runPipeline(job,mockDeps,p=>emitted.push(p));
    +  assert.ok(emitted.find(e=>e.type==='failed'&&e.error.includes('lint')));
    +});
    +test('diff 体量闸 — 超 diffMaxFiles 失败', async () => {
    +  job.project.diffMaxFiles=2;
    +  mockDeps.worktreeDiff=async()=>({diffSummary:' a.ts | 1\n b.ts | 1\n c.ts | 1',commits:[]});
    +  const emitted=[];
    +  await runPipeline(job,mockDeps,p=>emitted.push(p));
    +  assert.ok(emitted.find(e=>e.type==='failed'));
    +});
    +
  • +
  • 运行步骤 2:确认失败
    tsx --test test/pipeline.test.ts 2>&1 | grep -E "diff|checks" | head -3
  • +
  • +
    实现步骤 3:schema.sql + db.ts — projects 加 diff_max_files
    +
    -- schema.sql:
    +  diff_max_files INTEGER NOT NULL DEFAULT 100
    +-- db.ts:
    +  ensureColumn(db,'projects','diff_max_files','diff_max_files INTEGER NOT NULL DEFAULT 100');
    +
  • +
  • +
    实现步骤 4:types.ts + mappers.ts — Project 加 diffMaxFiles
    +
    // types.ts: diffMaxFiles: number;
    +// rowToProject: diffMaxFiles: r.diff_max_files ?? 100,
    +
  • +
  • +
    实现步骤 5:PipelineDeps 加可选 runCheck 注入点(测试用)
    +
    runCheck?: (cmd:string, cwd:string) => void;
    +
  • +
  • +
    实现步骤 6:pipeline.ts — verify 之后插入 checks 分项闸
    +
    if(job.project.checks){
    +  let obj={}; try{obj=JSON.parse(job.project.checks)}catch{}
    +  for(const [name,cmd] of Object.entries(obj as Record<string,string>)){
    +    try{ (deps.runCheck??defaultRunCheck)(cmd,wt.dir); }
    +    catch(e){ emit({type:'failed',error:`[${name}]失败`,...}); return; }
    +  }
    +}
    +
  • +
  • +
    实现步骤 7:pipeline.ts — worktreeDiff 之后插入 diff 体量闸
    +
    const maxF=job.project.diffMaxFiles??100;
    +if(maxF>0){
    +  const fc=diff.diffSummary.split('\n').filter(l=>l.includes('|')).length;
    +  if(fc>maxF){ emit({type:'failed',error:`diff超限:${fc}>${maxF}文件`,...}); return; }
    +}
    +
  • +
  • 运行步骤 8:npm test
    npm test 2>&1 | tail -15
  • +
  • 运行步骤 9:typecheck + build
    npm run typecheck && npm run build
  • +
  • +
    提交步骤 10
    +
    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)"
    +
  • +
+
+ + + + +
+
Agent 记忆注入(L1–L4)— 涉及文件
+
+
+
src/store/schema.sql projects 加 agent_rules
+
src/store/store.ts lastRunErrorOf + agentRules
+
src/model/types.ts Project.agentRules
+
src/store/mappers.ts agent_rules 映射
+
+
+
src/executor/protocol.ts JobSpec.context
+
src/daemon/orchestrator.ts claimOne 组装记忆
+
src/executor/runner.ts 3 处 build*Prompt
+
src/executor/reviewer.ts buildReviewPrompt
+
+
+
src/executor/cc.ts 不改 settingSources 保持 project
+
~/.maestro/agent-global.md 新建 全局规范模板
+
测试:build*Prompt 纯函数单测(有/无各记忆字段时段落正确)
+
验证:failed→retry 任务 transcript 含「上次失败原因」
+
+
+
+ +
+
+ + +
+
+ + diff --git a/docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.md b/docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.md new file mode 100644 index 0000000..2074ed5 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-maestro-refactor-phase1.md @@ -0,0 +1,1150 @@ +# Maestro Refactor Phase 1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Upgrade Task model (taskType/ownedFiles/expectedOutput), CAS claim scheduling, CPM-based scoring with agingBonus + couplingPenalty, fable-5 model tiers, planner decompose JSON extension, and pipeline hardgates (checks分项/diff体量). + +**Architecture:** Bottom-up: schema + types first (Task 1), then store correctness (Task 2), then scheduling intelligence (Task 3), model upgrades (Task 4), planner output enrichment (Task 5), pipeline hardgates (Task 6). Each task produces independently testable changes. Later tasks reference types from earlier ones. + +**Tech Stack:** TypeScript 5, better-sqlite3, Node.js ≥22, tsx (dev runner), `node --test` via `npm test` + +## Global Constraints + +- Run all tests: `npm test` +- Run single test file: `tsx --test test/.test.ts` +- Type-check (no build): `npm run typecheck` +- Build: `npm run build` +- Tests use real in-memory SQLite (`new Store(':memory:')`) — no SQLite mocking +- Test imports: `import { test } from 'node:test'; import assert from 'node:assert/strict';` +- Store imported as: `import { Store } from '../src/store/index.js'` +- Do NOT push to remote or deploy +- fable-5 model ID: `'claude-fable-5'` + +## Pre-flight: What's already done + +The following were already implemented — do NOT re-implement: +- `verdict → hardgate` in `/Users/wangjia/code/maestro/src/daemon/ingest.ts` (lines 149–165) +- Parallel `Promise.all` reviews in `/Users/wangjia/code/maestro/src/executor/pipeline.ts` (line 154) +- `syncMain` before execution in `/Users/wangjia/code/maestro/src/executor/pipeline.ts` (lines 121–127) +- `conflict` pipeline in `/Users/wangjia/code/maestro/src/executor/pipeline.ts` (lines 211–273) +- Planner `maxTurns` by complexity (90/60/40) in `/Users/wangjia/code/maestro/src/executor/runner.ts` (line 168) +- Planner `depth` injection in `buildPlannerPrompt` decompose branch (line 133) +- Planner git read-only tools (log/diff/show) in `runPlanner` (line 171) +- Decompose `deps` sequence→taskId mapping in `/Users/wangjia/code/maestro/src/daemon/ingest.ts` (lines 103–116) +- `checks` + `auto_approve_plan`/`auto_approve_exec` columns in projects schema + +--- + +## Task 1: Schema Migration + Task Model Types + +**Files:** +- Modify: `/Users/wangjia/code/maestro/src/store/schema.sql` +- Modify: `/Users/wangjia/code/maestro/src/store/db.ts` +- Modify: `/Users/wangjia/code/maestro/src/model/types.ts` +- Modify: `/Users/wangjia/code/maestro/src/store/mappers.ts` +- Test: `/Users/wangjia/code/maestro/test/store.test.ts` + +**Interfaces:** +- Produces: `TaskType = 'feature' | 'bugfix' | 'refactor' | 'chore' | 'docs'` +- Produces: `TaskScope = 'file' | 'module' | 'service' | 'cross-service'` +- Produces: `Task.taskType`, `Task.scope`, `Task.ownedFiles: string[]`, `Task.expectedOutput: string | null`, `Task.parentVersionId: string | null`, `Task.version: number` +- Consumed by: Tasks 2, 3, 5 + +- [ ] **Step 1: Write failing test for new Task fields** + +Add this test to `/Users/wangjia/code/maestro/test/store.test.ts`: + +```typescript +test('Task 新字段默认值', () => { + const store = new Store(':memory:'); + const p = store.createProject({ name: 'p', repoPath: '/tmp/t1-' + Math.random(), autonomy: 'manual' }); + const t = store.createTask({ projectId: p.id, title: 'feat', complexity: 'easy' }); + assert.strictEqual(t.taskType, null); + assert.strictEqual(t.scope, null); + assert.deepStrictEqual(t.ownedFiles, []); + assert.strictEqual(t.expectedOutput, null); + assert.strictEqual(t.parentVersionId, null); + assert.strictEqual(t.version, 1); +}); + +test('patchTask 更新 ownedFiles 与 taskType', () => { + const store = new Store(':memory:'); + const p = store.createProject({ name: 'p', repoPath: '/tmp/t1b-' + Math.random(), autonomy: 'manual' }); + const t = store.createTask({ projectId: p.id, title: 'feat', complexity: 'easy' }); + const updated = store.patchTask(t.id, { + taskType: 'feature', + scope: 'module', + ownedFiles: ['src/foo.ts', 'src/bar.ts'], + expectedOutput: 'all tests pass', + }); + assert.strictEqual(updated.taskType, 'feature'); + assert.strictEqual(updated.scope, 'module'); + assert.deepStrictEqual(updated.ownedFiles, ['src/foo.ts', 'src/bar.ts']); + assert.strictEqual(updated.expectedOutput, 'all tests pass'); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +tsx --test test/store.test.ts 2>&1 | grep -E "fail|error|FAIL|Error" | head -5 +``` + +Expected: fails with `t.taskType is not a property` or similar. + +- [ ] **Step 3: Update schema.sql — add new columns to tasks CREATE TABLE** + +In `/Users/wangjia/code/maestro/src/store/schema.sql`, find the `CREATE TABLE IF NOT EXISTS tasks` block and add 6 new columns before the closing `)`: + +```sql + task_type TEXT, -- feature|bugfix|refactor|chore|docs + scope TEXT, -- file|module|service|cross-service + owned_files TEXT NOT NULL DEFAULT '[]', -- JSON string[] 声明的文件所有权 + expected_output TEXT, -- "done" 的可验证描述(exec_review 验收用) + parent_version_id TEXT, -- 版本链:reject 后新建版指向前一版 + version INTEGER NOT NULL DEFAULT 1 -- 任务版本号(每次 reject 递增) +``` + +Also add to runs table before closing `)`: + +```sql + trace_id TEXT -- Trace ID(跨进程可观测性) +``` + +- [ ] **Step 4: Update db.ts — add ensureColumn migrations** + +In `/Users/wangjia/code/maestro/src/store/db.ts`, after line 34 (`ensureColumn(db, 'runs', 'last_seq', ...)`), add: + +```typescript + ensureColumn(db, 'tasks', 'task_type', 'task_type TEXT'); + ensureColumn(db, 'tasks', 'scope', 'scope TEXT'); + ensureColumn(db, 'tasks', 'owned_files', "owned_files TEXT NOT NULL DEFAULT '[]'"); + ensureColumn(db, 'tasks', 'expected_output', 'expected_output TEXT'); + ensureColumn(db, 'tasks', 'parent_version_id', 'parent_version_id TEXT'); + ensureColumn(db, 'tasks', 'version', 'version INTEGER NOT NULL DEFAULT 1'); + ensureColumn(db, 'tasks', 'claimed_at', 'claimed_at TEXT'); // CAS claim(Task 2 用) + ensureColumn(db, 'runs', 'trace_id', 'trace_id TEXT'); +``` + +- [ ] **Step 5: Update types.ts — add TaskType, TaskScope and Task fields** + +In `/Users/wangjia/code/maestro/src/model/types.ts`, add after the imports block: + +```typescript +export type TaskType = 'feature' | 'bugfix' | 'refactor' | 'chore' | 'docs'; +export type TaskScope = 'file' | 'module' | 'service' | 'cross-service'; +``` + +In the `Task` interface, add after `lastRunError`: + +```typescript + taskType: TaskType | null; + scope: TaskScope | null; + ownedFiles: string[]; + expectedOutput: string | null; + parentVersionId: string | null; + version: number; +``` + +- [ ] **Step 6: Update mappers.ts — TaskRow + rowToTask** + +In `/Users/wangjia/code/maestro/src/store/mappers.ts`, add to `TaskRow` interface (after `last_run_error`): + +```typescript + task_type: string | null; + scope: string | null; + owned_files: string; + expected_output: string | null; + parent_version_id: string | null; + version: number; + claimed_at: string | null; // CAS claim(Task 2 用) +``` + +Add to `RunRow` interface (after `last_seq`): + +```typescript + trace_id: string | null; +``` + +Update `rowToTask` function body (add before closing `}`): + +```typescript + taskType: (r.task_type as TaskType | null) ?? null, + scope: (r.scope as TaskScope | null) ?? null, + ownedFiles: r.owned_files ? (JSON.parse(r.owned_files) as string[]) : [], + expectedOutput: r.expected_output ?? null, + parentVersionId: r.parent_version_id ?? null, + version: r.version ?? 1, +``` + +Also add import at top of mappers.ts: `import type { TaskType, TaskScope } from '../model/types.js';` + +- [ ] **Step 7: Update store.ts — patchTask to handle new fields** + +In `/Users/wangjia/code/maestro/src/store/store.ts`, find the `patchTask` method. Update its accepted fields to include the new ones. Find the `PatchTaskInput` type (or wherever patchTask's input type is declared) and add: + +```typescript +taskType?: TaskType | null; +scope?: TaskScope | null; +ownedFiles?: string[]; +expectedOutput?: string | null; +``` + +In the `patchTask` implementation, map these new fields into the SQL UPDATE. The method needs to build the SET clause dynamically — add handling for each new field that serializes `ownedFiles` as JSON: + +```typescript +if ('taskType' in patch) sets.push(`task_type = '${patch.taskType ?? null}'`) // use parameterized query +if ('ownedFiles' in patch) sets.push(/* JSON.stringify(patch.ownedFiles) */) +``` + +> **Note:** Look at how patchTask currently handles existing optional fields (title, priority, deps, etc.) and follow the same pattern for parameterized queries. Do NOT use string interpolation for values. + +- [ ] **Step 8: Run tests** + +```bash +npm test 2>&1 | tail -20 +``` + +Expected: the two new store tests pass; all existing tests still pass. + +- [ ] **Step 9: Typecheck** + +```bash +npm run typecheck +``` + +Expected: no errors. + +- [ ] **Step 10: Commit** + +```bash +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): add taskType/scope/ownedFiles/expectedOutput/version fields + schema migration" +``` + +--- + +## Task 2: CAS Claim + Execution Lock + +**Files:** +- Modify: `/Users/wangjia/code/maestro/src/store/store.ts` +- Modify: `/Users/wangjia/code/maestro/src/daemon/orchestrator.ts` +- Test: `/Users/wangjia/code/maestro/test/orchestrator.test.ts` + +**Interfaces:** +- Consumes: `claimed_at` column from Task 1 +- Produces: `store.claimTask(taskId, toStatus)` — CAS claim that sets `claimed_at`; `→ ready` transition clears `claimed_at = NULL` +- Produces: execution lock — any task with a `started` run rejects mutations on title/spec/plan/deps/complexity + +- [ ] **Step 1: Write failing tests** + +Add to `/Users/wangjia/code/maestro/test/orchestrator.test.ts`: + +```typescript +test('CAS claim — 同一任务不能被双重领取', (t) => { + const store = new Store(':memory:'); + const p = store.createProject({ name: 'cas', repoPath: '/tmp/cas-' + Math.random(), autonomy: 'auto-easy' }); + const task = store.createTask({ projectId: p.id, title: 'x', complexity: 'easy' }); + store.setOperations(task.id, 'op'); + store.transition(task.id, 'queued', { by: 'test' }); + store.transition(task.id, 'executing', { by: 'test' }); + + // 第一次 claim 成功(CAS) + const ok1 = store.casClaimTask(task.id, 'run-001'); + assert.ok(ok1, '第一次 claim 应成功'); + + // 第二次 claim 同一任务失败 + const ok2 = store.casClaimTask(task.id, 'run-002'); + assert.ok(!ok2, '双重 claim 应失败'); +}); + +test('transition → ready 清空 claimed_at', (t) => { + const store = new Store(':memory:'); + const p = store.createProject({ name: 'cas2', repoPath: '/tmp/cas2-' + Math.random(), autonomy: 'auto-easy' }); + const task = store.createTask({ projectId: p.id, title: 'y', complexity: 'easy' }); + store.setOperations(task.id, 'op'); + store.transition(task.id, 'queued', { by: 'test' }); + store.transition(task.id, 'executing', { by: 'test' }); + store.casClaimTask(task.id, 'run-x'); + + // reject → ready 应清空 claimed_at,使下次 CAS 可成功 + store.transition(task.id, 'exec_review', { by: 'test' }); + store.decide(task.id, 'reject', 'human', '需要修改'); + const ok = store.casClaimTask(task.id, 'run-y'); + assert.ok(ok, 'ready 后应可再次 claim'); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +tsx --test test/orchestrator.test.ts 2>&1 | grep -E "fail|Error|casClaimTask" | head -5 +``` + +Expected: `store.casClaimTask is not a function` + +- [ ] **Step 3: Add casClaimTask to store.ts** + +In `/Users/wangjia/code/maestro/src/store/store.ts`, add this new method to the `Store` class (near the `claimTick`-adjacent logic): + +```typescript +/** + * CAS 领取:仅当 claimed_at IS NULL 时才 SET claimed_at=now(),防止双重领取。 + * 返回 true=领取成功,false=已被其他线程/进程抢先。 + */ +casClaimTask(taskId: string, runId: string): boolean { + const result = this.db + .prepare(`UPDATE tasks SET claimed_at = ?, updated_at = ? WHERE id = ? AND claimed_at IS NULL`) + .run(now(), now(), taskId); + return result.changes === 1; +} +``` + +- [ ] **Step 4: Clear claimed_at when transitioning to ready** + +In `/Users/wangjia/code/maestro/src/store/store.ts`, find the `transition` method (around line 899 or `UPDATE tasks SET status = ?, updated_at = ?`). When the target status is `'ready'`, also clear `claimed_at`: + +```typescript +// In transition() — when to === 'ready', clear claimed_at +if (to === 'ready') { + this.db.prepare(`UPDATE tasks SET status = ?, claimed_at = NULL, updated_at = ? WHERE id = ?`).run(to, now(), taskId); +} else { + this.db.prepare(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`).run(to, now(), taskId); +} +``` + +Also clear in `decide()` when transitioning back to `ready` (exec reject), `analyzing` (plan reject), `speccing` (spec reject) — all these pass through `transition()` already, so the check above covers them. + +Also ensure `requeueTask()` (lines 967–979) clears `claimed_at`: + +```typescript +// After updating retry_baseline in requeueTask: +this.db.prepare(`UPDATE tasks SET claimed_at = NULL, updated_at = ? WHERE id = ?`).run(now(), taskId); +``` + +- [ ] **Step 5: Use casClaimTask in orchestrator.claimOne** + +In `/Users/wangjia/code/maestro/src/daemon/orchestrator.ts`, in `claimOne()` (around line 168), after `store.transition(task.id, 'executing', ...)` succeeds, add a CAS check: + +```typescript +// After both transitions (queued → executing), verify CAS claim +if (!store.casClaimTask(task.id, run.id)) { + // Another process already claimed this task — abort and clean up + log.error(`任务 ${task.id} CAS claim 失败(并发冲突),放弃本次领取`); + return; +} +``` + +> Place this after `store.startRun()` and before `d.writeJobSpec()`. + +- [ ] **Step 6: Run tests** + +```bash +npm test 2>&1 | tail -20 +``` + +Expected: new CAS tests pass; all existing tests still pass. + +- [ ] **Step 7: Typecheck** + +```bash +npm run typecheck +``` + +- [ ] **Step 8: Add execution lock to mutation methods in store.ts** + +在途任务(有 `status=started` 的 run)应拒绝修改 title/spec/plan/complexity/deps。在 store.ts 中新增内部校验辅助方法: + +```typescript +/** 若任务有在途 run(status=started),抛出 StoreError(执行期锁)。 */ +private assertNoActiveRun(taskId: string): void { + const row = this.db.prepare( + `SELECT COUNT(*) AS n FROM runs WHERE task_id = ? AND status = 'started'`, + ).get(taskId) as { n: number }; + if (row.n > 0) throw new StoreError('任务执行中,禁止修改(等 run 结束后再操作)'); +} +``` + +在以下方法开头加 `this.assertNoActiveRun(taskId)` 调用: +- `setSpec(taskId, ...)` +- `setPlan(taskId, ...)` +- `setOperations(taskId, ...)` +- `patchTask(taskId, patch)` — 仅当 patch 含 title/complexity/deps/priority 时 + +在 `patchField` 私有方法里统一加也可以(因为 setSpec/setPlan/setOperations 都走它)。 + +- [ ] **Step 9: Write test for execution lock** + +```typescript +test('执行期间禁止修改 spec(执行期锁)', () => { + const store = new Store(':memory:'); + const p = store.createProject({ name: 'lock', repoPath: '/tmp/lock-' + Math.random(), autonomy: 'manual' }); + const t = store.createTask({ projectId: p.id, title: 'x', complexity: 'medium' }); + store.transition(t.id, 'speccing', { by: 'test' }); + const run = store.startRun(t.id, 'planner'); + + // 有在途 run 时修改 spec 应抛出 + assert.throws(() => store.setSpec(t.id, 'new spec'), /执行中/); + + // run 结束后可以修改 + store.finishRun(run.id, 'failed', { error: 'test' }); + assert.doesNotThrow(() => store.setSpec(t.id, 'new spec')); +}); +``` + +- [ ] **Step 10: Commit** + +```bash +git add src/store/store.ts src/daemon/orchestrator.ts test/orchestrator.test.ts +git commit -m "feat(store): CAS claimed_at 防双重 claim + transition→ready 清零 + 执行期锁" +``` + +--- + +## Task 3: Scheduling Upgrade (rankU + agingBonus + couplingPenalty) + +**Files:** +- Modify: `/Users/wangjia/code/maestro/src/model/scoring.ts` +- Modify: `/Users/wangjia/code/maestro/src/daemon/orchestrator.ts` +- Test: `/Users/wangjia/code/maestro/test/orchestrator.test.ts` + +**Interfaces:** +- Consumes: `Task.ownedFiles` from Task 1; `Task.createdAt` +- Produces: `rankU(taskId, allByIdMap, dependentsMap, cache): number` — recursive CPM unlock value +- Produces: `agingBonus(task, nowMs): number` +- Produces: `couplingPenalty(task, inFlight): number` +- Produces: new `rankByScore(candidates, all, inFlight, nowMs)` signature (adds `inFlight` and `nowMs`) +- Consumed by: `orchestrator.claimable()` + +- [ ] **Step 1: Write failing tests** + +Add to `/Users/wangjia/code/maestro/test/orchestrator.test.ts`: + +```typescript +import { rankByScore, agingBonus, couplingPenalty } from '../src/model/scoring.js'; + +test('agingBonus — 新建任务无加成', () => { + const task = { createdAt: new Date().toISOString(), priority: 1 } as Task; + assert.strictEqual(agingBonus(task, Date.now()), 0); +}); + +test('agingBonus — 等待 48h 达到 baseScore 上限', () => { + const fortyEightHoursAgo = new Date(Date.now() - 48 * 3600 * 1000).toISOString(); + const task = { createdAt: fortyEightHoursAgo, priority: 1 } as Task; + // P1 baseScore = 2, bonus after 48h = min(2, 48/48) = min(2,1) = 1 + assert.ok(agingBonus(task, Date.now()) >= 0.99); + assert.ok(agingBonus(task, Date.now()) <= 1.01); +}); + +test('couplingPenalty — 无重叠文件时为 0', () => { + const candidate = { ownedFiles: ['src/a.ts'] } as Task; + const inFlight = [{ ownedFiles: ['src/b.ts'] } as Task]; + assert.strictEqual(couplingPenalty(candidate, inFlight), 0); +}); + +test('couplingPenalty — 每个重叠文件 -0.5', () => { + const candidate = { ownedFiles: ['src/a.ts', 'src/b.ts', 'src/c.ts'] } as Task; + const inFlight = [{ ownedFiles: ['src/a.ts', 'src/b.ts'] } as Task]; + assert.strictEqual(couplingPenalty(candidate, inFlight), 1.0); +}); + +test('rankByScore — agingBonus 提升得分', () => { + const old = { id: 'old', priority: 2, createdAt: new Date(Date.now() - 96 * 3600 * 1000).toISOString(), + ownedFiles: [], deps: [], status: 'ready' } as unknown as Task; + const fresh = { id: 'fresh', priority: 2, createdAt: new Date().toISOString(), + ownedFiles: [], deps: [], status: 'ready' } as unknown as Task; + const ranked = rankByScore([old, fresh], [old, fresh], [], Date.now()); + assert.strictEqual(ranked[0].task.id, 'old', '等待更久的应排前'); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +tsx --test test/orchestrator.test.ts 2>&1 | grep -E "agingBonus|couplingPenalty" | head -5 +``` + +Expected: `agingBonus is not exported` + +- [ ] **Step 3: Update scoring.ts** + +Replace `/Users/wangjia/code/maestro/src/model/scoring.ts` content: + +```typescript +import type { Task } from './types.js'; + +/** P0=3 / P1=2 / P2=1 */ +export function baseScore(priority: number): number { + return Math.max(1, 3 - priority); +} + +/** 反向索引:taskId → 直接依赖它的任务列表 */ +export function buildDependentsIndex(tasks: Task[]): Map { + const idx = new Map(); + for (const t of tasks) { + for (const d of t.deps) { + const list = idx.get(d) ?? []; + list.push(t); + idx.set(d, list); + } + } + return idx; +} + +/** + * CPM 后向传播:递归累计"解锁价值"。 + * cache 防止 DAG 中重复计算(deps 无环保证终止)。 + */ +export function rankU( + taskId: string, + byId: Map, + dependents: Map, + cache: Map, +): number { + if (cache.has(taskId)) return cache.get(taskId)!; + const task = byId.get(taskId); + if (!task) { cache.set(taskId, 0); return 0; } + const base = baseScore(task.priority); + const unlockValue = (dependents.get(taskId) ?? []) + .reduce((sum, dep) => sum + rankU(dep.id, byId, dependents, cache), 0); + const result = base + unlockValue; + cache.set(taskId, result); + return result; +} + +/** + * 老化加成:防止低优先级任务因文件耦合被长期回避。 + * waitHours / 48 线性增长,上限 = baseScore(priority)(即 P0 最多 +3 点,P2 最多 +1 点)。 + */ +export function agingBonus(task: Task, nowMs: number): number { + const base = baseScore(task.priority); + const waitHours = (nowMs - Date.parse(task.createdAt)) / 3_600_000; + return Math.min(base, waitHours / 48); +} + +/** + * 耦合惩罚:ownedFiles 与在途任务有交集时,每个重叠文件 -0.5。 + * 不硬 block(防饥饿),agingBonus 会最终覆盖惩罚。 + */ +export function couplingPenalty(task: Task, inFlight: Task[]): number { + const files = new Set(task.ownedFiles ?? []); + if (files.size === 0) return 0; + let overlaps = 0; + for (const t of inFlight) { + for (const f of (t.ownedFiles ?? [])) { + if (files.has(f)) overlaps++; + } + } + return overlaps * 0.5; +} + +/** + * 综合调度得分(在 claim 时现算,不落库): + * rankU(自身 + 递归解锁效应) + * + chainInertia(已完成依赖的 baseScore 之和,链条惯性) + * + agingBonus(等待越久加分越多) + * - couplingPenalty(文件重叠减分,防并发冲突) + */ +export function scoreTask( + t: Task, + byId: Map, + dependents: Map, + inFlight: Task[], + nowMs: number, + rankUCache: Map, +): number { + const chainInertia = t.deps.reduce((s, d) => { + const dep = byId.get(d); + return dep?.status === 'done' ? s + baseScore(dep.priority) : s; + }, 0); + return rankU(t.id, byId, dependents, rankUCache) + + chainInertia + + agingBonus(t, nowMs) + - couplingPenalty(t, inFlight); +} + +/** + * 候选任务按综合得分降序排(同分则创建时间早优先)。 + * inFlight: 当前在途任务列表(用于耦合惩罚)。 + * nowMs: 当前时间(便于测试注入可控时钟)。 + */ +export function rankByScore( + candidates: Task[], + all: Task[], + inFlight: Task[], + nowMs: number = Date.now(), +): Array<{ task: Task; score: number }> { + const byId = new Map(all.map((t) => [t.id, t])); + const dependents = buildDependentsIndex(all); + const cache = new Map(); + return candidates + .map((task) => ({ task, score: scoreTask(task, byId, dependents, inFlight, nowMs, cache) })) + .sort((a, b) => b.score - a.score || a.task.createdAt.localeCompare(b.task.createdAt)); +} +``` + +- [ ] **Step 4: Update orchestrator.ts to pass inFlight to rankByScore** + +In `/Users/wangjia/code/maestro/src/daemon/orchestrator.ts`, in the `claimable()` function (around line 159), change: + +```typescript +return rankByScore(candidates, tasks); +``` + +to: + +```typescript +const inflightList = tasks.filter((t) => inflight.has(t.id)); +return rankByScore(candidates, tasks, inflightList, d.nowMs()); +``` + +- [ ] **Step 5: Run tests** + +```bash +npm test 2>&1 | tail -20 +``` + +Expected: new scoring tests pass; all existing tests pass. + +- [ ] **Step 6: Typecheck** + +```bash +npm run typecheck +``` + +- [ ] **Step 7: Commit** + +```bash +git add src/model/scoring.ts src/daemon/orchestrator.ts test/orchestrator.test.ts +git commit -m "feat(scoring): CPM rankU + agingBonus + couplingPenalty 调度算法升级" +``` + +--- + +## Task 4: Model Tier Upgrade (fable-5) + +**Files:** +- Modify: `/Users/wangjia/code/maestro/src/executor/models.ts` +- Test: `/Users/wangjia/code/maestro/test/models.test.ts` + +**Interfaces:** +- Produces: `MODEL_FALLBACK_CHAIN = ['claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-4-6']` +- Produces: planner hard → `'claude-fable-5'`; reviewer all → `'claude-fable-5'`; conflict all → `'claude-fable-5'` + +- [ ] **Step 1: Write failing tests** + +Add to `/Users/wangjia/code/maestro/test/models.test.ts`: + +```typescript +import { pickModel, MODEL_FALLBACK_CHAIN, pickFallbackModel } from '../src/executor/models.js'; + +test('planner hard 用 fable-5', () => { + const task = { complexity: 'hard' } as Task; + const project = { model: null } as Project; + assert.strictEqual(pickModel(task, project, 'planner'), 'claude-fable-5'); +}); + +test('reviewer 不受 project.model 影响,固定 fable-5', () => { + const task = { complexity: 'easy' } as Task; + const project = { model: 'claude-sonnet-4-6' } as Project; + assert.strictEqual(pickModel(task, project, 'reviewer'), 'claude-fable-5'); +}); + +test('conflict 固定 fable-5', () => { + const task = { complexity: 'easy' } as Task; + const project = { model: null } as Project; + assert.strictEqual(pickModel(task, project, 'conflict'), 'claude-fable-5'); +}); + +test('MODEL_FALLBACK_CHAIN 首档为 fable-5', () => { + assert.strictEqual(MODEL_FALLBACK_CHAIN[0], 'claude-fable-5'); +}); + +test('pickFallbackModel — fable-5 回退到 opus-4-8', () => { + assert.strictEqual(pickFallbackModel('claude-fable-5'), 'claude-opus-4-8'); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +tsx --test test/models.test.ts 2>&1 | grep -E "fable|fail" | head -5 +``` + +Expected: assertions fail (current hard=opus, reviewer=opus). + +- [ ] **Step 3: Update models.ts** + +In `/Users/wangjia/code/maestro/src/executor/models.ts`: + +1. Change `MODEL_FALLBACK_CHAIN`: +```typescript +export const MODEL_FALLBACK_CHAIN = ['claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-4-6'] as const; +``` + +2. Change `MODEL_TABLE.planner.hard`: +```typescript +hard: ['MAESTRO_MODEL_PLAN_HARD', 'claude-fable-5'], +``` + +3. Change all three `MODEL_TABLE.reviewer` entries to fable-5: +```typescript +reviewer: { + easy: ['MAESTRO_MODEL_REVIEW_EASY', 'claude-fable-5'], + medium: ['MAESTRO_MODEL_REVIEW_MEDIUM', 'claude-fable-5'], + hard: ['MAESTRO_MODEL_REVIEW_HARD', 'claude-fable-5'], +}, +``` + +4. Change `MODEL_TABLE.conflict` and the `pickModel` conflict early return: +```typescript +// In MODEL_TABLE.conflict (占位,实际由 pickModel 直接返回 fable-5) +conflict: { + easy: ['', 'claude-fable-5'], + medium: ['', 'claude-fable-5'], + hard: ['', 'claude-fable-5'], +}, +``` + +5. Update `pickModel` conflict return: +```typescript +if (role === 'conflict') return 'claude-fable-5'; +``` + +- [ ] **Step 4: Run tests** + +```bash +npm test 2>&1 | tail -20 +``` + +Expected: new model tests pass; all existing tests pass. Note: existing tests that assert `'claude-opus-4-8'` for reviewer/conflict/planner-hard will need to be updated to `'claude-fable-5'`. + +- [ ] **Step 5: Typecheck** + +```bash +npm run typecheck +``` + +- [ ] **Step 6: Commit** + +```bash +git add src/executor/models.ts test/models.test.ts +git commit -m "feat(models): fable-5 为 planner-hard/reviewer/conflict 最强档;回退链首档改 fable-5" +``` + +--- + +## Task 5: Planner Decompose JSON Extension (ownedFiles + expectedOutput) + +**Files:** +- Modify: `/Users/wangjia/code/maestro/src/executor/runner.ts` +- Modify: `/Users/wangjia/code/maestro/src/daemon/ingest.ts` +- Test: `/Users/wangjia/code/maestro/test/ingest.test.ts` + +**Interfaces:** +- Consumes: `Task.ownedFiles`, `Task.expectedOutput` from Task 1 +- Produces: `buildPlannerPrompt` decompose branch requires `ownedFiles` and `expectedOutput` in subtask JSON +- Produces: `ingest.ts` decompose-result handler maps subtask `ownedFiles`/`expectedOutput` via `store.patchTask` + +- [ ] **Step 1: Write failing test** + +Add to `/Users/wangjia/code/maestro/test/ingest.test.ts`: + +```typescript +test('decompose-result — ownedFiles/expectedOutput 落入子任务', () => { + withTmpDataDir(() => { + const store = new Store(':memory:'); + const p = store.createProject({ + name: 'p', repoPath: '/tmp/dr-' + Math.random(), autonomy: 'auto-approved', + }); + const parent = store.createTask({ projectId: p.id, title: 'big feat', complexity: 'hard' }); + store.transition(parent.id, 'analyzing', { by: 'test' }); + const run = store.startRun(parent.id, 'planner', { worktree: p.repoPath }); + + const dir = runDir(run.id); + mkdirSync(dir, { recursive: true }); + appendOutbox(run.id, { + type: 'decompose-result', + plan: 'big feature', + subtasks: [ + { + title: '类型定义', + complexity: 'easy' as const, + priority: 0, + deps: [], + ownedFiles: ['src/model/types.ts'], + expectedOutput: 'typecheck 通过', + }, + { + title: '实现层', + complexity: 'medium' as const, + priority: 1, + deps: [0], + ownedFiles: ['src/store/taskRepo.ts'], + expectedOutput: '单测通过', + }, + ], + transcriptRef: null, + sessionId: null, + }); + ingestRun(store, noopLog, run.id); + + const children = store.childrenOf(parent.id); + assert.strictEqual(children.length, 2); + assert.deepStrictEqual(children[0].ownedFiles, ['src/model/types.ts']); + assert.strictEqual(children[0].expectedOutput, 'typecheck 通过'); + assert.deepStrictEqual(children[1].ownedFiles, ['src/store/taskRepo.ts']); + assert.strictEqual(children[1].expectedOutput, '单测通过'); + }); +}); +``` + +> Note: you'll need to import `runDir` from protocol.ts and `mkdirSync` from fs. Check existing ingest.test.ts imports to see what's already imported. + +- [ ] **Step 2: Run test to verify it fails** + +```bash +tsx --test test/ingest.test.ts 2>&1 | grep -E "ownedFiles|expectedOutput|fail" | head -5 +``` + +Expected: assertion fails (ownedFiles is `[]`, expectedOutput is `null`). + +- [ ] **Step 3: Update buildPlannerPrompt in runner.ts** + +In `/Users/wangjia/code/maestro/src/executor/runner.ts`, find `buildPlannerPrompt` (line 114), in the decompose branch, update the JSON example in the prompt: + +Change this line (around line 150): +```typescript +'{"plan":"一句话分析与拆解理由","subtasks":[{"title":"子任务标题","complexity":"easy","priority":1,"deps":[]}]}', +``` + +To: +```typescript +'{"plan":"一句话分析与拆解理由","subtasks":[{"title":"子任务标题","complexity":"easy","priority":1,"deps":[],"ownedFiles":["src/example.ts"],"expectedOutput":"相关测试通过"}]}', +``` + +Also update the subtasks description bullet points (around line 138–139) to add: +```typescript +'- ownedFiles:该子任务主要修改的文件路径列表(JSON string[],无则填 [])', +'- expectedOutput:一句话描述该子任务"完成"的可验证标准(例:"typecheck 通过")', +``` + +Also update the table header line to add columns: +```typescript +'| # | 子任务标题 | 复杂度 | 优先级 | 依赖序号 | 主要文件 | 验收标准 |', +'|---|---|---|---|---|---|---|', +``` + +- [ ] **Step 4: Update ingest.ts decompose-result handler** + +In `/Users/wangjia/code/maestro/src/daemon/ingest.ts`, in the `decompose-result` case (around line 83), update the subtask creation loop. After the `store.patchTask(createdIds[i], { deps: depIds })` call for deps, add ownedFiles/expectedOutput mapping: + +```typescript +// After the deps mapping loop (around line 116), add: +for (let i = 0; i < rec.subtasks.length; i++) { + const sub = rec.subtasks[i] as { + ownedFiles?: string[]; + expectedOutput?: string; + deps?: number[]; + }; + if (!createdIds[i]) continue; + const patch: Record = {}; + if (Array.isArray(sub.ownedFiles) && sub.ownedFiles.length > 0) { + patch.ownedFiles = sub.ownedFiles; + } + if (typeof sub.expectedOutput === 'string' && sub.expectedOutput.trim()) { + patch.expectedOutput = sub.expectedOutput.trim(); + } + if (Object.keys(patch).length > 0) { + try { + store.patchTask(createdIds[i], patch); + } catch (e) { + log.error(`decompose: 更新子任务「${rec.subtasks[i].title}」元数据失败:${(e as Error).message}`); + } + } +} +``` + +> **Important:** Place this loop AFTER the deps mapping loop (not inside it). Both loops iterate the same `rec.subtasks` array independently. + +- [ ] **Step 5: Update OutboxPayload type in protocol.ts** + +In `/Users/wangjia/code/maestro/src/executor/protocol.ts`, find the `decompose-result` payload type. The `subtasks` array currently has `{ title: string; complexity: Complexity }`. Extend it: + +```typescript +// In the decompose-result payload subtasks item type: +subtasks: Array<{ + title: string; + complexity: Complexity; + priority?: number; + deps?: number[]; + ownedFiles?: string[]; // 新增 + expectedOutput?: string; // 新增 +}>; +``` + +- [ ] **Step 6: Run tests** + +```bash +npm test 2>&1 | tail -20 +``` + +Expected: new ingest test passes; all existing tests pass. + +- [ ] **Step 7: Typecheck** + +```bash +npm run typecheck +``` + +- [ ] **Step 8: Commit** + +```bash +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,落入子任务" +``` + +--- + +## Task 6: Checks分项闸 + Diff体量闸 + +**Files:** +- Modify: `/Users/wangjia/code/maestro/src/executor/pipeline.ts` +- Modify: `/Users/wangjia/code/maestro/src/store/schema.sql` +- Modify: `/Users/wangjia/code/maestro/src/store/db.ts` +- Modify: `/Users/wangjia/code/maestro/src/model/types.ts` +- Modify: `/Users/wangjia/code/maestro/src/store/mappers.ts` +- Test: `/Users/wangjia/code/maestro/test/pipeline.test.ts` + +**Interfaces:** +- Consumes: `project.checks: string | null` (existing field, JSON `{lint:"...",typecheck:"..."}`) +- Produces: new `Project.diffMaxFiles: number` (default 100) +- Produces: `runPipeline` emits `failed` if any check has non-zero exit OR if changed file count > `diffMaxFiles` + +- [ ] **Step 1: Write failing tests** + +Add to `/Users/wangjia/code/maestro/test/pipeline.test.ts`: + +```typescript +test('checks分项闸 — lint 失败阻止进入 exec_review', async () => { + const job = makeJob({ complexity: 'easy' }); // use existing test helper + job.project.checks = JSON.stringify({ lint: 'exit 1' }); // always-fail lint + + const emitted: OutboxPayload[] = []; + const mockDeps = makeMockDeps({ + // runTask succeeds and commits + runTask: async () => ({ ok: true, transcriptRef: null, sessionId: null, finalText: 'ok' }), + // verify passes + verify: async () => ({ ok: true }), + // no reviews needed — should not reach them + reviewCode: async () => { throw new Error('should not reach review'); }, + reviewSecurity: async () => { throw new Error('should not reach review'); }, + }); + await runPipeline(job, mockDeps, (p) => emitted.push(p)); + + const failed = emitted.find((e) => e.type === 'failed'); + assert.ok(failed, '应 emit failed'); + assert.ok((failed as { type: 'failed'; error: string }).error.includes('lint'), 'error 应说明 lint 失败'); +}); + +test('diff体量闸 — 超过 diffMaxFiles 时阻止进入 exec_review', async () => { + const job = makeJob({ complexity: 'easy' }); + job.project.diffMaxFiles = 2; // 限制 2 个文件 + + const emitted: OutboxPayload[] = []; + const mockDeps = makeMockDeps({ + runTask: async () => ({ ok: true, transcriptRef: null, sessionId: null, finalText: 'ok' }), + verify: async () => ({ ok: true }), + // worktreeDiff returns a summary with 5 files changed + worktreeDiff: async () => ({ + diffSummary: ' a.ts | 1+\n b.ts | 1+\n c.ts | 1+\n d.ts | 1+\n e.ts | 1+\n 5 files changed, 5 insertions(+)', + commits: ['abc foo'], + }), + reviewCode: async () => { throw new Error('should not reach'); }, + reviewSecurity: async () => { throw new Error('should not reach'); }, + }); + await runPipeline(job, mockDeps, (p) => emitted.push(p)); + + const failed = emitted.find((e) => e.type === 'failed'); + assert.ok(failed, '应 emit failed'); + assert.ok((failed as { error: string }).error.includes('diff'), 'error 应说明 diff 超限'); +}); +``` + +> Check the existing pipeline.test.ts structure for `makeJob` and `makeMockDeps` helpers — use the same pattern. + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +tsx --test test/pipeline.test.ts 2>&1 | grep -E "fail|Error" | head -5 +``` + +- [ ] **Step 3: Add diffMaxFiles to schema.sql and db.ts** + +In `/Users/wangjia/code/maestro/src/store/schema.sql`, add to `CREATE TABLE IF NOT EXISTS projects`: +```sql + diff_max_files INTEGER NOT NULL DEFAULT 100 -- diff 体量闸:超过此文件数则硬失败(0=禁用) +``` + +In `/Users/wangjia/code/maestro/src/store/db.ts`, add: +```typescript +ensureColumn(db, 'projects', 'diff_max_files', 'diff_max_files INTEGER NOT NULL DEFAULT 100'); +``` + +- [ ] **Step 4: Add diffMaxFiles to types.ts and mappers.ts** + +In `/Users/wangjia/code/maestro/src/model/types.ts`, in `Project` interface add: +```typescript +diffMaxFiles: number; // diff 体量闸(默认 100,0=禁用) +``` + +In `/Users/wangjia/code/maestro/src/store/mappers.ts`, in `ProjectRow` add: +```typescript +diff_max_files: number; +``` + +In `rowToProject` add: +```typescript +diffMaxFiles: r.diff_max_files ?? 100, +``` + +- [ ] **Step 5: Add checks分项闸 to pipeline.ts** + +In `/Users/wangjia/code/maestro/src/executor/pipeline.ts`, in the `runPipeline` function, after the verify step (around line 146), add: + +```typescript + // 3b. 分项 checks(lint/typecheck/build 等)— project.checks 是 JSON "{lint:'cmd', typecheck:'cmd'}" + if (job.project.checks) { + let checksObj: Record; + try { + checksObj = JSON.parse(job.project.checks) as Record; + } catch { + checksObj = {}; + } + for (const [name, cmd] of Object.entries(checksObj)) { + emit({ type: 'phase', phase: `checking:${name}` }); + try { + const { execSync } = await import('node:child_process'); + execSync(cmd, { cwd: wt.dir, stdio: 'pipe', timeout: 60_000 }); + } catch (e) { + const msg = (e as { stderr?: Buffer; message?: string }).stderr?.toString().trim() + || (e as Error).message || `${name} 检查失败`; + emit({ type: 'failed', error: `分项检查 [${name}] 失败:${msg.slice(0, 500)}`, transcriptRef: rr.transcriptRef, sessionId: rr.sessionId }); + emit({ type: 'done' }); + return; + } + } + } +``` + +- [ ] **Step 6: Add diff体量闸 to pipeline.ts** + +In `/Users/wangjia/code/maestro/src/executor/pipeline.ts`, after the `worktreeDiff` call (around line 149), add: + +```typescript + // 4b. diff 体量闸(文件数超限时硬失败) + const maxFiles = job.project.diffMaxFiles ?? 100; + if (maxFiles > 0) { + const fileCount = diff.diffSummary.split('\n').filter((l) => l.includes('|')).length; + if (fileCount > maxFiles) { + emit({ + type: 'failed', + error: `diff 体量超限:改动 ${fileCount} 个文件(上限 ${maxFiles}),请缩小任务范围`, + transcriptRef: rr.transcriptRef, + sessionId: rr.sessionId, + }); + emit({ type: 'done' }); + return; + } + } +``` + +- [ ] **Step 7: Update PipelineDeps to make checks injectable for tests** + +The `execSync` in checks must be injectable for tests. Instead of importing inline, add an optional `runCheck` to `PipelineDeps`: + +```typescript +// In PipelineDeps interface: +runCheck?: (cmd: string, cwd: string) => void; // throws on non-zero exit +``` + +```typescript +// Default implementation in realDeps: +runCheck: (cmd, cwd) => { + const { execSync } = require('node:child_process'); + execSync(cmd, { cwd, stdio: 'pipe', timeout: 60_000 }); +}, +``` + +Update the checks loop in pipeline.ts to use `deps.runCheck ?? defaultRunCheck`. + +- [ ] **Step 8: Run tests** + +```bash +npm test 2>&1 | tail -20 +``` + +Expected: new pipeline tests pass; all existing tests pass. + +- [ ] **Step 9: Typecheck** + +```bash +npm run typecheck +``` + +- [ ] **Step 10: Commit** + +```bash +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)" +``` + +--- + +## Final Verification + +- [ ] **Run full test suite** + +```bash +npm test +``` + +Expected: all tests pass. + +- [ ] **Run typecheck** + +```bash +npm run typecheck +``` + +Expected: no errors. + +- [ ] **Build** + +```bash +npm run build +``` + +Expected: compiles cleanly to `dist/`. + +- [ ] **Smoke test — start daemon and confirm it starts** + +```bash +MAESTRO_ORCH_INTERVAL=0 npm run dev & +sleep 3 +curl -s http://127.0.0.1:4517/api/projects | head -5 +kill %1 +``` + +Expected: daemon starts, returns JSON for projects endpoint. + +--- + +## What Phase 2 Covers (not in this plan) + +- SSE streaming (`cc.ts onToken → daemon EventEmitter → GET /api/tasks/:id/stream`) +- Hook contract (`.maestro/hooks.ts` + shell hooks) +- Trace ID propagation (`JobSpec.traceId`, outbox携带) +- Phase events → WebSocket broadcast +- Backend restructuring (store Repos / daemon split / API routes) +- Frontend migration (React 18 + Vite + Zustand) diff --git a/docs/superpowers/specs/2026-06-22-maestro-refactor-design.md b/docs/superpowers/specs/2026-06-22-maestro-refactor-design.md new file mode 100644 index 0000000..048dd4b --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-maestro-refactor-design.md @@ -0,0 +1,517 @@ +# Maestro 重构设计文档 + +> 状态:草稿 · 2026-06-22 +> 范围:Task 模型 · 调度算法 · 流式通信 · Hook 可观测性 · 前端重构 · 后端拆分 + +--- + +## 1. 背景与目标 + +Maestro 是一个本地优先的 Git 任务编排守护进程,驱动多项目的 headless Claude Code agent 自动执行代码任务。当前版本(约 1.0)已具备基础的"创建 → 方案 → 执行 → 审批 → 合并"完整闭环,但存在以下痛点需要通过重构解决: + +| 类别 | 问题 | +|---|---| +| Task 模型 | 缺少 `task_type`/`scope`/`ownedFiles`/`expected_output`,拆解产物元数据不足 | +| 调度算法 | 简单优先级排序,缺少依赖链 unlock 价值、饥饿保护、文件耦合感知 | +| 执行管道 | 并发 Code+Security 审查是串行的;verify 过于单一;审查 verdict=reject 不挡 | +| 流式通信 | agent 执行中实时 token 无法到达前端;无 human-in-the-loop 动态暂停 | +| 可观测性 | outbox phase 不广播 WS;无 Trace ID 跨进程;transcript 无 Web 检索 | +| 代码组织 | `store.ts`(1227 行) God Object、`orchestrator.ts`(426 行)、`web/app.js`(2473 行) 亟需拆分 | +| 前端 | 纯原生 JS SPA,无组件化,无类型安全,与 Claude Design System 脱节 | + +**重构目标**: +1. Task 模型增强(分类/文件所有权/期望产出) +2. 调度算法升级为 CPM-based rankU + 老化加成 + 耦合感知 +3. 执行管道三类优化(已在 plan 中详细定稿) +4. SSE 流式通信 + Hook 拦截 + Trace ID 传播 +5. 后端按职责拆分(store Repos / daemon 四组件 / API routes) +6. 前端迁移至 React 18 + TypeScript + Vite + Zustand,对接 Claude Design System + +--- + +## 2. Task 模型增强 + +### 2.1 新增字段 + +```typescript +// src/model/types.ts — Task 接口扩展 +export type TaskType = 'feature' | 'bugfix' | 'refactor' | 'chore' | 'docs'; +export type TaskScope = 'file' | 'module' | 'service' | 'cross-service'; + +export interface Task { + // === 现有字段(保留)=== + id, projectId, parentId, depth, title, complexity, status, + priority, deps, plan, spec, operations, approvals, result, + assignee, retryBaseline, nextEligibleAt, lastRunError, createdAt, updatedAt + + // === 新增字段 === + taskType: TaskType | null; // 任务分类(feature/bugfix/refactor/chore/docs) + scope: TaskScope | null; // 改动范围维度 + ownedFiles: string[]; // 声明的文件所有权(冲突检测用) + expectedOutput: string | null; // "done" 的可验证描述,供 exec_review 对照 + parentVersionId: string | null; // reject 后新建版本指向前一版,构成版本链 + version: number; // 任务版本号(每次 reject 递增) +} +``` + +### 2.2 ownedFiles 冲突检测 + +在 `orchestrator.ts` 的 `claimable()` 中增加文件交集检查: + +```typescript +// 已声明 ownedFiles 的在途任务集合 +function hasFileConflict(candidate: Task, inFlight: Task[]): boolean { + if (!candidate.ownedFiles?.length) return false; + const candidateSet = new Set(candidate.ownedFiles); + return inFlight.some(t => + t.ownedFiles?.some(f => candidateSet.has(f)) + ); +} +``` + +- 文件交集 → 降低 score(-0.5/个重叠文件),不硬 block(防饥饿) +- `agingBonus` 兜底:等待超 48h 的任务最多加 1 点,确保不被永久回避 + +### 2.3 自动元数据填充(MCP 工具) + +新增 MCP 工具 `suggest_task_metadata`:人工输入 `title` 后,调用 sonnet 分析仓库上下文自动推断 `taskType`/`scope`/`ownedFiles`/`expectedOutput`,人工确认后写入。触发:MCP 工具调用 or UI "智能填充"按钮。 + +### 2.4 planner 输出扩展 + +Hard/Medium 任务 planner 的 decompose JSON 格式扩展: + +```json +{ + "plan": "...(分析正文)...", + "subtasks": [ + { + "title": "类型定义与接口", + "complexity": "easy", + "priority": 0, + "deps": [], + "ownedFiles": ["src/model/types.ts"], + "expectedOutput": "类型文件通过 typecheck" + }, + { + "title": "TaskRepo 实现", + "complexity": "medium", + "priority": 1, + "deps": [0], + "ownedFiles": ["src/store/taskRepo.ts"], + "expectedOutput": "TaskRepo CRUD 方法通过单测" + } + ] +} +``` + +- 正文先输出 Markdown 子任务表格(供 plan_review 人审) +- JSON 块作为机器解析源,`deps` 用子任务数组序号引用 +- daemon `ingest.ts` 在子任务全建好后做"序号→taskId"二次映射 + +--- + +## 3. 调度算法升级(CPM-based rankU) + +### 3.1 算法设计 + +用 **CPM(Critical Path Method)后向传播** 替代简单优先级排序: + +```typescript +// src/model/scoring.ts + +/** 递归计算任务向后传播的 unlock 价值 */ +function rankU(taskId: string, cache: Map): number { + if (cache.has(taskId)) return cache.get(taskId)!; + const task = getTask(taskId); + const base = baseScore(task); // P0=3, P1=2, P2=1 + const unlockValue = dependents(taskId) + .reduce((sum, dep) => sum + rankU(dep.id, cache), 0); + const result = base + unlockValue; + cache.set(taskId, result); + return result; +} + +/** 老化加成:等待越久加分越多,防饥饿 */ +function agingBonus(task: Task): number { + const base = baseScore(task); + const waitHours = (Date.now() - new Date(task.createdAt).getTime()) / 3600000; + return Math.min(base, waitHours / 48); // 48h 达到 base 上限 +} + +/** 耦合惩罚:文件重叠 */ +function couplingPenalty(task: Task, inFlight: Task[]): number { + const overlaps = inFlight.reduce((sum, t) => { + const shared = (task.ownedFiles ?? []).filter(f => t.ownedFiles?.includes(f)); + return sum + shared.length; + }, 0); + return overlaps * 0.5; +} + +/** 最终调度得分 */ +function scheduleScore(task: Task, completedDeps: Task[], inFlight: Task[]): number { + return rankU(task.id, new Map()) + + completedDeps.reduce((s, d) => s + baseScore(d), 0) // 链惯性 + + agingBonus(task) + - couplingPenalty(task, inFlight); +} +``` + +### 3.2 CAS 防双重 claim + +```sql +-- orchestrator claim 阶段 +UPDATE tasks +SET status = 'queued', claimed_at = datetime('now') +WHERE id = ? AND status = 'ready' AND claimed_at IS NULL +``` + +**重要**:所有使 task 回到 `ready` 的转移(reject/requeue/retry)必须同时清空 `claimed_at`: + +```sql +UPDATE tasks SET status = 'ready', claimed_at = NULL WHERE id = ? +``` + +否则 retry 任务永远命中 `claimed_at IS NULL` = false,无法再被 claim。 + +### 3.3 调度决策日志 + +每次 claim 记录结构化日志: +```json +{ + "taskId": "t-001", + "score": 4.5, + "breakdown": { + "rankU": 3.0, + "chainInertia": 1.0, + "agingBonus": 0.5, + "couplingPenalty": 0.0 + }, + "competitors": [...] +} +``` + +--- + +## 4. 三类执行管道优化(定稿,已在 plan 中逐维确认) + +见 `.claude/plans/tidy-jumping-shell.md` 的完整定稿小结,此处仅列关键决策: + +### 4.1 Planner(任务拆解) + +| 维度 | 决策 | +|---|---| +| 触发/执行期锁 | 在途即锁只读:禁改、禁再调度,run 结束解锁 | +| 模型/档位 | hard=fable-5 / medium=opus-4-8 / easy=sonnet-4-6(env 三档可覆盖) | +| 输出格式 | 子任务表格(人审) + 扩展 JSON{title,complexity,priority,deps,ownedFiles,expectedOutput} | +| 审批 | 默认人审;可选 auto-approved+全 easy+改动小 自动放行(默认关) | + +### 4.2 Executor(代码执行) + +| 维度 | 决策 | +|---|---| +| 双复审 | code + security 改 `Promise.all` **并行**(≤15min 替代 ≤30min) | +| 复审模型 | 统一 fable-5,不被 project.model 降档 | +| 新增硬闸 | 分项 checks(lint/typecheck/build)+ diff 越界/体量闸 + verdict=reject 变硬闸 | +| 执行前同步 | createWorktree 后先 merge main;分歧超阈值 → needs_attention 重评估 | + +### 4.3 ConflictResolver(解冲突) + +| 维度 | 决策 | +|---|---| +| 架构 | 新增 `runKind=conflict`;专用 pipeline:真正 git merge → CC 解 → commit → 复审 | +| 模型 | 固定 fable-5,不随原任务复杂度降档 | +| 调度 | 插队/预留名额,优先于普通 executor | +| 白名单 | 仅 conflict pipeline 开放 `Bash(git merge:*)` | + +### 4.4 通用规则(摘录) + +- **执行期锁**:在途 run → task 只读,run 结束解锁 +- **复审独立性**:复审只读、用最强模型;verdict=reject 变硬闸 +- **模型回退链**:`fable-5 → opus-4-8 → sonnet-4-6`(单 run 只重试一次) +- **Reflect 阶段**:连续失败 2 次,agent 先分析失败原因再 retry(非盲目重试) +- **stages.json 检查点**:pipeline 各阶段写入完成状态,重启后跳过已完成阶段 + +--- + +## 5. 流式通信(Q7) + +### 5.1 SSE 实时 token 推送 + +**架构**:`cc.ts for-await` → `onToken 回调` → `daemon EventEmitter (per runId)` → `SSE /api/tasks/:id/stream` + +```typescript +// cc.ts - 已有 for await,增加 onToken 钩子 +for await (const message of q) { + out.write(JSON.stringify(message) + '\n'); // 保留:持久化到 transcript + options.onToken?.(message); // 新增:实时推送回调 +} + +// server.ts - 新增 SSE 端点(以 taskId 为索引,内部映射到当前活跃 runId) +fastify.get('/api/tasks/:taskId/stream', (req, reply) => { + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + // 查找该 task 当前 in-progress 的 run,按 runId 注册 emitter + const activeRunId = store.getActiveRunId(req.params.taskId); + const emitter = activeRunId ? tokenEmitters.get(activeRunId) : null; + emitter?.on('token', (msg) => reply.raw.write(`data: ${JSON.stringify(msg)}\n\n`)); +}); +``` + +**端点设计**:URL 用 taskId(对前端友好),内部 `tokenEmitters` 以 runId 为键。一个 task 可有多次 run,端点始终映射到最新 in-progress run;run 结束时清理对应 emitter。 + +**选型理由**:SSE 天然支持重连+Last-Event-ID(断网续读),无需引入 gRPC/额外 WebSocket。 + +### 5.2 类 Chat 任务(执行中注入指令) + +``` +POST /api/tasks/:id/inject { message: string } + → daemon 写入 runs//inbox.json + → worker 在 pipeline 断点轮询 inbox.json + → cc.ts resume() 带入新内容 +``` + +不做真正交互式会话——worker 是 subprocess,双向实时通道复杂度过高。实际方案:中途补充指令 → 追加到 next prompt turn。 + +### 5.3 动态暂停(Human-in-the-loop 增强) + +新增 OutboxRecord 类型: + +```typescript +// src/executor/protocol.ts +type OutboxRecord = + | { type: 'phase'; phase: string } + | { type: 'result'; ... } + | { type: 'clarify'; question: string; runId: string } // 新增 + | { type: 'error'; ... } +``` + +状态机新增 `awaiting_input`(在 `executing` 和 `exec_review` 之间): + +``` +executing → awaiting_input (agent 输出 ...) +awaiting_input → executing (POST /api/tasks/:id/reply 提供答复) +``` + +--- + +## 6. Hook 拦截与可观测性(Q8) + +### 6.1 Hook 契约 + +```typescript +// .maestro/hooks.ts(项目级)或 ~/.maestro/hooks.ts(全局级) +export interface MaestroHooks { + 'before:task:claim'?: (task: Task) => Promise; + 'after:planner:output'?: (task: Task, plan: string) => Promise; // 可改 plan + 'before:execute'?: (task: Task, job: JobSpec) => Promise; + 'before:merge'?: (task: Task, branch: string) => Promise; + 'on:conflict'?: (task: Task, files: string[]) => Promise<'auto' | 'manual'>; + 'before:exec-review'?: (task: Task, result: TaskResult) => Promise; +} +``` + +- 超时 5s → 等同于 cancel +- Shell script 方式:`.maestro/hooks/before-execute.sh`(exit != 0 = cancel,stdout = reason) +- 项目级优先,全局兜底 + +### 6.2 Trace ID 传播 + +```typescript +// src/executor/protocol.ts - JobSpec 新增 +interface JobSpec { + runId: string; + taskId: string; + traceId: string; // 新增:daemon 写 job.json 时 crypto.randomUUID() + // ... +} +``` + +- Worker 所有 `appendOutbox` 记录携带 `traceId` +- Dashboard 可按 traceId 聚合 plan → execute → review → merge 的完整链路 + +### 6.3 Phase 事件广播 + +`ingest.ts` 处理 `phase` 记录时,增加 WebSocket 广播: + +```typescript +case 'phase': + log.info({ taskId, runId, phase: record.phase }, 'pipeline phase'); + store.broadcast({ type: 'run.phase', taskId, runId, phase: record.phase }); // 新增 + break; +``` + +前端看板实时显示"分析中 / 执行中 / 验证中 / 复审中"进度条。 + +### 6.4 Transcript 回放 + +``` +GET /api/tasks/:id/transcript → 流式返回 runs//transcript.jsonl +GET /api/tasks/:id/transcript?q=keyword → 服务端 grep 返回匹配行 +``` + +无需 ElasticSearch,本地 JSONL grep 即可。 + +--- + +## 7. 后端代码结构重构 + +### 7.1 store.ts 拆分 + +``` +src/store/ +├── db.ts # DBAdapter 接口(SqliteAdapter / future PostgresAdapter) +├── store.ts # 入口(组合所有 Repos,提供 subscribe/broadcast) +├── projectRepo.ts # Project CRUD +├── taskRepo.ts # Task CRUD + 状态机守卫 +├── runRepo.ts # Run CRUD +├── approvalRepo.ts # ApprovalRecord +├── eventRepo.ts # Event 追加 + 查询 +└── metricsRepo.ts # 聚合指标查询 +``` + +### 7.2 daemon 拆分 + +``` +src/daemon/ +├── orchestrator.ts # 入口:tick = Scheduler.claim → WorkerManager.spawn/reap → Ingestor.ingest +├── scheduler.ts # 纯调度逻辑(scheduleScore/claimable/claim CAS) +├── workerManager.ts # spawn/reap/heartbeat 检测 +├── ingestor.ts # outbox.ndjson → DB 事件(原 ingest.ts) +└── mergeCoordinator.ts # merge-resolve 任务池 + 收口原任务 +``` + +### 7.3 API 拆分 + +``` +src/api/ +├── server.ts # Fastify 初始化 + 路由注册 + WS 挂载 +├── middleware/ +│ └── auth.ts # No-op 占位(future JWT) +├── routes/ +│ ├── projects.ts +│ ├── tasks.ts +│ ├── runs.ts +│ ├── approvals.ts +│ └── metrics.ts +└── schemas/ # Fastify JSON Schema 校验 +``` + +### 7.4 executor 拆分 + +``` +src/executor/ +├── protocol.ts # 文件协议(含 clarify 类型、traceId) +├── cc.ts # Claude Agent SDK wrapper(含 onToken 钩子) +├── pipelines/ +│ ├── executor.ts # 代码执行 pipeline +│ ├── planner.ts # 任务拆解 pipeline +│ ├── conflict.ts # 解冲突 pipeline(新增) +│ └── reviewer.ts # 复审 pipeline(并行 code+security) +└── worker.ts # 入口(读 job.json → 分发到对应 pipeline) +``` + +--- + +## 8. 前端重构 + +### 8.1 技术栈 + +| 层 | 选型 | +|---|---| +| 框架 | React 18 + TypeScript | +| 构建 | Vite | +| 状态管理 | Zustand(全局 store:projects/tasks/ws 连接) | +| 样式 | CSS Modules + IBM Plex Mono(Claude Design System 字体) | +| 国际化 | i18next(5 语言:zh/en/es/ja/fr) | + +### 8.2 目录结构 + +``` +web/ +├── index.html +├── vite.config.ts +├── src/ +│ ├── main.tsx +│ ├── App.tsx +│ ├── store/ # Zustand stores +│ ├── api/ # REST + SSE + WS client +│ ├── components/ # 通用组件(Button/Badge/Modal...) +│ ├── screens/ # 页面(Dashboard/TaskDetail/Settings) +│ └── i18n/ +└── public/ +``` + +### 8.3 布局 + +3 列布局(对齐 Claude Design System): +- 左侧:项目列表(侧栏) +- 中间:任务看板(按状态分组) +- 右侧:任务详情(审批/流式输出/transcript) + +### 8.4 实时特性 + +- WebSocket:任务状态变更 + Phase 事件 → 看板实时刷新 +- SSE:TaskDetail 右侧面板显示 agent 实时 token 输出 +- 审批闸:plan_review / spec_review / exec_review → 内联 approve/reject + +--- + +## 9. Schema 变更(src/store/schema.sql) + +```sql +-- tasks 表新增列 +ALTER TABLE tasks ADD COLUMN task_type TEXT; -- feature/bugfix/refactor/chore/docs +ALTER TABLE tasks ADD COLUMN scope TEXT; -- file/module/service/cross-service +ALTER TABLE tasks ADD COLUMN owned_files TEXT; -- JSON string[] +ALTER TABLE tasks ADD COLUMN expected_output TEXT; -- 验收描述 +ALTER TABLE tasks ADD COLUMN parent_version_id TEXT; -- 版本链前驱 +ALTER TABLE tasks ADD COLUMN version INTEGER DEFAULT 1; +ALTER TABLE tasks ADD COLUMN claimed_at TEXT; -- CAS claim 时间戳 + +-- runs 表 kind 新增 'conflict'(TEXT 无 CHECK,兼容) +-- runs 表新增 trace_id +ALTER TABLE runs ADD COLUMN trace_id TEXT; + +-- 新状态 'awaiting_input' 已在 status.ts TRANSITIONS 中处理,无需 schema 改动 +``` + +--- + +## 10. 实现优先级 + +| 阶段 | 内容 | 依赖 | +|---|---|---| +| P0(核心正确性) | CAS claim / 执行期锁 / 分项 checks 闸 / verdict→硬闸 | — | +| P0(Task 模型) | 新增 owned_files/expected_output/task_type 字段 + Schema | — | +| P1(调度升级) | rankU + agingBonus + couplingPenalty | Task 模型 | +| P1(管道优化) | 复审并行 / Planner 分档 / stages.json 检查点 | — | +| P1(解冲突) | conflict runKind + 专用 pipeline | — | +| P2(流式通信) | cc.ts onToken + SSE endpoint + Phase WS | — | +| P2(Hook) | MaestroHooks 契约 + shell/TS 两种实现 | — | +| P2(Trace ID) | JobSpec.traceId 传播 + outbox 携带 | — | +| P3(后端拆分) | store Repos / daemon 四组件 / API routes | P0-P1 稳定后 | +| P3(前端迁移) | React+Vite + Zustand + 实时流 | P2 SSE/WS | + +--- + +## 附录 A:状态机新增状态 + +``` +awaiting_input (新增) + ← executing (clarify 记录触发) + → executing (POST /reply 恢复) + → cancelled +``` + +完整状态机见 `src/model/status.ts`。 + +## 附录 B:调研来源 + +- Agent 1:Task 分类与自动拆解(Plan-and-Execute / DAG vs Tree / MCP auto-fill) +- Agent 2:文件冲突最小化(ownedFiles / interface-first / madge 分析) +- Agent 3:调度算法(CPM rankU / agingBonus / CAS) +- Agent 4:流式通信与 Human-in-the-loop(SSE / clarify / Hook 契约 / Trace ID) +- Agent 5:AI Agent 编排最佳实践(expected_output / stages.json / Reflect 阶段)