Files
wangjia 724cebb653 merge: 合并冲突解决 —— worker 池化评估 + SDK resume 抗抖动 [tsk_xbtd6GpS4NYg]
将 maestro/tsk_74Acz1Nh4K1J 分支的 DESIGN.md 改动手动合并:
1. §19 分期状态:新增「🟡 worker 池化(已评估,暂缓)」说明
2. §20 风险与注意事项:新增「SDK 抖动 → resume 续跑」条目

保留了 HEAD 中已有的更详细版本的「自动执行安全」和「中断恢复(多进程 re-adopt)」条目。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 16:46:50 +08:00

251 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Maestro — 多项目 TODO 管理与 Agent 执行系统 · 设计方案 v0.2
本地优先的 daemon:管理多个本地 git 项目的任务,按复杂度分级驱动审批闸,并用后台 agent(Claude Code)自动执行可执行任务。
> **当前实现状态(2026-06**Phase 1(核心 + 看板 + MCP)与 Phase 2(编排器 + 执行器 + 双复审 + 自动合并)已全面落地;Phase 3 通知、额度透传、归档详情已一并完成,并把执行从 **in-process** 重构为 **多进程解耦**daemon 监工 + 独立 worker 进程 + 文件通讯 + 重启 re-adopt,见 §7)。本文档对应已实现代码,不再是纯设计预稿。
## Context(为什么做)
前身是 `~/.claude/skills/todo`JSON + HTML 单机原型):单项目、无真正后端、确认靠临时 http hack、无自动执行。它验证了「复杂度分级 + 确认闸 + 看板」的形态,但不可扩展。Maestro 把它升级为正式系统:**多项目、任务层级化、复杂度驱动的审批闸、完整状态机、本地常驻 daemon、每项目一个后台执行 agent、与 Claude Code 深度集成(MCP + Agent SDK**。
## 0. 关键决策
| 维度 | 决策 |
|---|---|
| 运行时 | Node / TypeScript |
| 部署 | 本地优先 daemon(跑在用户机器,管理本地 git 项目) |
| Claude Code 集成 | MCP 服务(任务读写)+ Claude Agent SDKheadless 自动执行) |
| 自动合并边界 | PR accept 默认自动 `--no-ff` 合并(`merge:false` 逃生口:主检出脏时跳合并只通过) |
## 1. 系统总览
```
maestrod(核心 daemon,常驻 · 唯一 DB 写者 · 监工)
├─ 任务存储 SQLite ├─ 项目注册表
├─ REST + WebSocket API ├─ MCP server(给任意 Claude Code 会话)
├─ 编排器 Orchestrator └─ 监工:spawn worker / ingest outbox / 回收死 worker
│ (领取 + 文件通讯 + 判活,自身不跑 agent)
└── worker 进程(每任务一个,独立 OS 进程,哑执行器·完全不碰 DB)
执行 agentexecutor run):Agent SDK 在 worktree 跑 headless CC
code review agentreviewer run):只读,审 diff 产出报告 + verdict
安全审计 agentsecurity run):只读,仅审安全问题
↕ 文件通讯:job.jsondaemon→worker/ outbox.ndjson + heartbeatworker→daemon/ SIGTERM(取消)
Web 看板(多项目·实时·页面内 accept/reject
```
闭环:建任务 →(Hard 分析拆解 / Medium 写方案 / Easy 写操作)→ 前置审批闸 → ready → 编排器 score 调度领取 → worktree 起 headless CC 执行 → verify → 双复审(code review + 安全审计)→ exec_review(带复审报告)→ accept 自动合并 → done。
## 2. 复杂度分级
Hard / Medium / Easy ↔ 旧 tier 1/2/3。每个任务(含子任务)都带 `complexity`,决定它走哪条「产出 + 审批」路径。(见 `src/model/complexity.ts`
## 3. 任务层级
`parent_id` + `depth`;默认 ≤3 层,极复杂 ≤4 层(创建第 5 层时拒绝)。子任务各自带 complexity。**叶子任务才可执行**;被拆解的 Hard 是容器,进度由子任务汇总。
## 4. 三条产出路径与审批闸
| 复杂度 | 产出(执行前) | 前置闸 | reject 规则 |
|---|---|---|---|
| **Hard** | 强制进 plan 分析 → 产出「分析 + 任务拆解(子任务,各带复杂度)」 | `plan_review`accept 才进下一步 | **必须带改进意见** → 退回重做并附 feedback |
| **Medium** | 写「具体方案 = 改动内容 + 为什么这么做」 | `spec_review`accept/reject | 同上(必带意见) |
| **Easy** | 写清「将执行的操作」记录在任务上 | 无前置闸,直接可执行 | — |
**结果闸 `exec_review`**(所有产生改动的执行后、合并前):
- 展示 code review 报告(含 verdictapprove/reject+ 安全审计报告(含 securityVerdict
- `accept` 默认触发 `--no-ff` 自动合并(见 §11),成功后任务状态 → `done`,异步清理 worktree + 任务分支
- `accept` + `merge:false`(逃生口):仅通过审批,不触发合并(手动处理冲突或工作区脏时用)
- `reject`(必带改进意见)→ 返工,`ready` → 等下一轮编排器重新领取
## 5. 状态机(见 `src/model/status.ts`
`init` · `analyzing` · `plan_review` · `decomposed` · `speccing` · `spec_review` · `ready` · `blocked` · `queued` · `executing` · `exec_review` · `failed` · `needs_attention` · `done` · `paused` · `cancelled`
主要流转:
- `init` →(Hard`analyzing``plan_review` —accept→ `decomposed` —子全 done→ `done`;—reject(+意见)→ `analyzing`
- `init` →(Medium`speccing``spec_review` —accept→ `ready`;—reject(+意见)→ `speccing`
- `init` →(Easy,写完 operations)→ `ready`
- `ready` —deps 满足→ `queued``executing``exec_review` —accept(auto-merge)→ `done`;—reject(+意见)→ `ready`(返工)
- `executing``failed` —自动重试 ≤2 次→ `queued` / `needs_attention`3 次失败后)
合法流转表与守卫在 `TRANSITIONS` / `canTransition()`
## 6. 数据模型(见 `src/model/types.ts` 与 `src/store/schema.sql`
- **Project**repo_path、default_branch、verify_cmd、autonomy、model、concurrency、status、logo、sort_order、last_sync_at
- **Task**project_id、parent_id、depth、complexity、status、priority(P0/P1/P2)、deps[]、plan/spec/operations、result(含 summary/verdict/securitySummary/securityVerdict/branch/worktree/commits/prUrl)、assignee、source_ref(旧 todo 映射用)、next_eligible_at(持久化退避)、retry_baseline(手动重投基线)
- **ApprovalRecord**gate(plan/spec/exec)、action、actor、reason(reject 必填)、at
- **Run**kind(planner/executor/reviewer/security)、worktree、branch、status、transcript_ref、claude_session_id、error、worker_pid(多进程执行的 worker pid,判活用)、last_seqoutbox ingest 幂等游标)
- **Event**append-only):审计 + 实时看板推送源
`TaskResult.prUrl` 在合并后写 `merged:<mergeCommit>`,复用字段记录合并产物。
## 7. 多进程执行架构:监工 + worker(见 `src/daemon/orchestrator.ts`、`src/daemon/ingest.ts`、`src/executor/{protocol,worker,pipeline}.ts`
执行已从早期的 **in-process**(编排器自己 `await` 跑 agent)重构为 **多进程解耦**。核心铁律两条:
- **daemon = 唯一 DB 写者 + 监工**:只负责领任务、spawn worker、把 worker 的产出摄取入库、回收死 worker。**所有** DB 写(含执行进度/结果落库)都在 daemon 进程内,故 WS 推送照常经 `store.subscribe` 触发。
- **worker = 哑执行器**:每个执行任务 spawn **一个独立 OS 进程**`node dist/executor/worker.js <runId>`),它跑「worktree → 执行 → verify → 双复审 → 产出」全流程,但**完全不碰 DB(连读都不碰)**,也不做任何重试 / `needs_attention` 决策。重试、退避、转终态全归 daemon。
二者只经 **文件 + 进程信号** 通讯,故天然跨 daemon 重启持久、无内存在途态:
| 方向 | 载体 | 说明 |
|---|---|---|
| daemon → worker | `runs/<runId>/job.json` | worker 的**唯一输入**:完整 `Task` + `Project` + 确定性 worktree 路径/分支。worker 据它自行 `pickModel`/`buildPrompt`,无需查 DB。 |
| daemon → worker | `SIGTERM` | 取消 / 超时。worker 兜底报 `failed` + `done` 后干净退出。 |
| worker → daemon | `runs/<runId>/outbox.ndjson` | 追加式、带单调递增 `seq` 的进度/结果流(`started`/`phase`/`failed`/`result`/`done`)。daemon **幂等 ingest**(按 `run.lastSeq` 游标,只消费 `seq>lastSeq`)。 |
| worker → daemon | `runs/<runId>/heartbeat` | 心跳文件,worker 每 10s `touch`(刷 mtime)。daemon 据其 mtime「年龄」判活。 |
**领取规则**(编排循环由 `MAESTRO_ORCH_INTERVAL` 秒驱动,默认 15`0` 关闭;每轮 `ingest → reap → claim`):
- 候选 = 叶子(非容器)、status=`ready``queued`(重试/重启遗留)、deps 全 `done`
- `auto-easy` 模式只领 `easy` 任务;`auto-approved` 领全部
- 并发闸读 `store.countExecuting`(每个 `executing` 对应一个活 worker),退避读持久化的 `task.nextEligibleAt`(早于它不领)
- 按 §8 Score 降序排列,同分创建早的优先
**领取一个任务**`claimOne`):算分支/worktree 路径 → `executing``startRun(executor)` → 写 `job.json``spawnWorker`(记 `worker_pid`)。spawn 用 `detached:false`(保持与 daemon 同会话,worker 内起的 `claude` 才能沿用 macOS keychain 鉴权)+ `unref`daemon 退出后 worker 作为孤儿被 launchd 收养、继续跑)。`env` 用**黑名单**收敛:剥离 `*_SECRET/*_TOKEN/*_KEY/AWS_/CF_/GH_` 等真密钥,保留会话变量(keychain 需要)与 `ANTHROPIC_/CLAUDE_` 鉴权变量。
**worker 执行管线**`runPipeline`,纯执行、全程经 `emit` 写 outbox):
1. `createWorktree``emit phase:executing``runTask`;失败 → `emit failed` + `done`,返回(不抛)
2. `runVerify`(项目 `verify_cmd`);失败 → `emit failed` + `done`
3. `worktreeDiff``emit phase:reviewing` → 双复审顺序执行(code review → 安全审计,任一失败折叠为 `verdict=null`、不挡任务)
4. 成功 → `emit result`branch/worktree/diffSummary/commits/executor/code/security+ `done`
**daemon ingest**`ingest.ts`,把 outbox 映射为 DB 写):
- `result` → 建 reviewer/security 两条复审 run + `setResult`(四字段)+ 转 `exec_review` + 收尾 executor run
- `failed``finishRun(failed)` + `failTaskAttempt`(失败/重试策略,见下)
- 幂等:每条处理后 `setLastSeq(seq)`;崩溃重启续读时已落库的 `seq` 不重做,终态记录只消费一次
**失败/重试/退避**`store.failTaskAttempt`,是 ingest 的 `failed`、reaper 判死、reconcile 判死的**唯一落点**):净失败次数(相对 `retry_baseline`)未到 `project.maxRetries`(默认 2)→ `queued` + 持久化退避 `next_eligible_at = now + min(30s·2^(n-1), 10min)`;到上限 → `needs_attention`(清退避)。
**回收死 worker**reaper,每轮 tick):对每个 `executing` 任务按 `worker_pid + 心跳` 判活,死 → `failTaskAttempt` 回收重试。
**重启 re-adopt / 回收**`reconcileInterrupted`daemon 启动时):逐个 `executing` 任务判活——
- **活**(心跳新鲜,或 boot 窗口内 pid 仍在)→ **re-adopt**:保留 `executing`daemon 续 ingest 它的 outboxworker 全程没停);
- **死** → `failTaskAttempt` 回收重试。
**判活逻辑**`isWorkerAlive`,见 `protocol.ts`):主信号=心跳新鲜(年龄 < `HEARTBEAT_GRACE_MS=60s`);`started``BOOT_GRACE_MS=30s` 启动空窗内额外接受 `pid` 存活兜底;窗口后仅认心跳——规避 pid 复用误判(被复用的 pid 不会更新本 run 的心跳)。
## 8. Score 调度(见 `src/model/scoring.ts`
在领取那一刻现算(不落库):
```
score = baseScore(priority) // 自身分:P0=3 / P1=2 / P2=1
+ Σ baseScore(dep.priority) for dep where dep.status=done // 链条惯性:前置投入越重越优先出活
+ Σ baseScore(w.priority) for w where w.status=blocked // 解锁效应:等我的人越多越优先
```
- 依赖与解锁均只算一层(不递归):deps 在创建时即固定且只能引用已存在任务,依赖图天然无环;递归会让深链分数膨胀失控,故不递归。
- 同分时按 `createdAt` 升序(先建先出)。
- `GET /api/agents` 暴露 `scheduling: "score"` 字段,前端可展示当前调度模式。
## 9. 双复审 PR 流程(见 `src/executor/reviewer.ts`
执行成功 + verify 通过后,编排器顺序起两个独立的只读 headless Claude Code
| Run | kind | 产出 | 工具白名单 |
|---|---|---|---|
| Code Review | `reviewer` | 做了什么/怎么做/测试情况/逐条问题/结论 + `VERDICT: approve\|reject` | Read/Grep/Glob + `git diff/log/show/status` |
| 安全审计 | `security` | 凭证泄露/注入/权限/UI红线词/部署风险 + `VERDICT: approve\|reject` | 同上(只读) |
- 提示词强制限制:不得修改任何文件,不得执行除 git 只读命令之外的命令,核对执行者自述与实际 diff 一致性。
- 机器解析 `VERDICT` 行(最后一个匹配项),去掉 VERDICT 行后的全文作为 `summary`
- 任一复审失败(抛错或无法起动)→ `summary=失败原因``verdict=null`,不挡主任务进 `exec_review`
- 看板在 `exec_review` 卡片展示两份 markdown 报告 + verdict 徽章(建议通过 / 建议拒绝 / 未复审)。
## 10. 模型分级与回退(见 `src/executor/models.ts`
| 角色 | Easy | Medium | Hard |
|---|---|---|---|
| executor(执行改动) | claude-sonnet-4-6 | claude-opus-4-8 | claude-fable-5 |
| reviewercode review + 安全审计) | claude-sonnet-4-6 | claude-opus-4-8 | claude-opus-4-8 |
优先级(高→低):
1. `project.model`(若设置,executor/reviewer 均用此模型)
2. 环境变量覆盖(`MAESTRO_MODEL_EASY/MEDIUM/HARD``MAESTRO_MODEL_REVIEW_EASY/MEDIUM/HARD`
3. 上表默认值
**回退链**`claude-opus-4-8``claude-sonnet-4-6`。模型不可用(not_found/invalid/permission 等错误含 `model` 关键词)时,同一 run 内自动切换回退模型重试一次;链上全失败才抛错。
## 11. 自动合并边界(见 `src/executor/merge.ts` + `src/api/server.ts`
`POST /api/tasks/:id/decide { action:"accept" }``exec_review` 时默认触发 `mergeBranch`
**两条路径**(避免 git 重复检出问题):
1. `defaultBranch` 正被主检出(`git rev-parse HEAD` 一致)→ 直接在主检出里 `git merge --no-ff`。要求工作区干净,否则拒绝并提示「先提交/暂存,或使用 merge:false 逃生口」。
2. 否则 → 在 `<dataDir>/worktrees/_merge/<taskId>/` 建临时 worktree 检出 defaultBranch 后合并,合并后无论成败都删临时 worktree。
**冲突处理**`git merge --abort` 后复原,error 返回冲突文件列表;两侧分支均无损,任务保留在 `exec_review`
**合并成功后**(异步):删任务分支 `git branch -d <branch>` + 回收任务 worktree(失败只记日志,不影响响应)。
**`merge:false` 逃生口**`POST /decide { action:"accept", merge:false }`,仅通过审批,不合并分支(用于主检出有未提交改动时手动处理)。
## 12. 归档与运行历史(见 `src/api/server.ts` + `src/store/store.ts`
- `GET /api/tasks/:id/events`:单任务全量事件升序(状态流转/审批/运行历史),供归档详情时间线。
- `GET /api/tasks/:id/runs`:该任务全部 run 记录(kind/status/started_at/ended_at/transcript_ref/error)。
- `GET /api/tasks/:id`:含 `result`branch/commits/diffSummary/summary/verdict/securitySummary/securityVerdict/prUrl)。
- 看板归档详情面板:状态流转时间线 + run 列表 + code review / 安全审计报告展开。
## 13. macOS 通知(见 `src/daemon/notify.ts`
daemon 启动时订阅 Store 事件流,符合条件时用 `osascript display notification` 发原生通知:
| 触发事件 | 通知文案 |
|---|---|
| status → `plan_review` / `spec_review` | ⏳ `<任务名>` 等待审核(拆解/方案) |
| status → `exec_review`(复审均通过) | ⏳ `<任务名>` 等待审核(PR |
| status → `exec_review`(任一 verdict=reject | ⚠ `<任务名>` 复审建议拒绝(PR 待审核) |
| status → `needs_attention` | ❌ `<任务名>` 连续失败需人工 |
| `approval.granted` gate=exec | ✅ `<任务名>` 已合并完成 |
- 同一任务同类型通知 60s 内只发一次(抑制重复触发)。
- `MAESTRO_NOTIFY=0` 关闭;非 macOS 平台自动不启用。
- osascript 字符串注入防护:去换行、转义反斜杠/双引号、截断 80 字符。
## 14. 额度透传(见 `src/daemon/usage.ts`
`GET /api/agents` 响应中附带 `usage` 字段,展示 Claude 订阅额度消耗:
```jsonc
{
"usage": {
"session": { "percent": 42, "resetsAt": "2025-06-13T18:00:00Z" }, // 5 小时滚动窗口
"weekly": { "percent": 18, "resetsAt": "2025-06-16T00:00:00Z" } // 7 天窗口
}
}
```
- 数据源:`GET https://api.anthropic.com/api/oauth/usage`,与 Claude Code statusline `rate_limits` 同一端点。
- 凭证:优先 `~/.claude/.credentials.json`OAuth accessToken),其次 macOS keychainservice "Claude Code-credentials")。token 只进内存,绝不写日志/响应。
- 60s 内存缓存 + 并发去重(in-flight 共享);任何失败(无凭证/网络/非200)→ `null`,前端降级显示。
## 15. 后端架构
- 存储 SQLitebetter-sqlite3,单文件零运维);run 转录日志存 JSONL 文件,Run 表引用路径。**daemon 是唯一 DB 读写者**worker 进程完全不碰 DB,见 §7)。
- APIRESTCRUD+ WebSocket`/ws`,Store 事件广播,驱动看板实时刷新)。
- 项目绑定:`maestro project add <repo>` 记录 repo 路径/默认分支/verify/autonomy/model/并发;daemon 中央 DB 行。
- Worktree:执行在 repo 的 git worktree(隔离分支),合并由 §11 逻辑控制,不自动 push。
- 进程间通讯目录:`<dataDir>/runs/<runId>/``job.json` / `outbox.ndjson` / `heartbeat`),是 daemon 与 worker 的唯一共享面,跨重启持久(见 §7)。`worker_pid``last_seq``next_eligible_at` 三列落 DB,使在途态/退避无需内存即可恢复。
## 16. Web 看板(`web/`
多项目切换(侧栏拖动排序)/ 全局总览;任务树(层级折叠)、复杂度徽章、状态、内联审批闸(accept/reject,reject 必填意见)、实时执行状态、`exec_review` 的 code review + 安全审计报告 + diff/分支信息。实时 WS 驱动。
项目支持自定义 logo(仓库内文件路径或外链),无 logo 时取首字母徽章兜底。
## 17. Claude Code 集成
- **MCP server(交互面)**:项目里的 Claude Code 配 `.mcp.json``maestro-mcp`。工具:`list_projects` / `create_project` / `list_tasks` / `get_task` / `create_task` / `decompose_task` / `write_plan` / `write_spec` / `write_operations` / `update_status` / `get_next_executable` / `get_pending_approvals`accept/reject 在 Web 看板操作)。
- **Agent SDK(自动面)**:编排器 spawn 的独立 worker 进程用 Claude Agent SDK 起 headless CC 跑 executor / reviewer / security run(见 §7 多进程架构)。
- approve/reject 是用户动作,走看板按钮(或直接 `POST /api/tasks/:id/decide`)。
## 18. 从现有 skill 迁移
`todo.mjs`/JSON 作为 v0 原型被取代。同步引擎(`src/sync/todo-sync.ts`):读旧 `todo/todo.json`tier1/2/3 → hard/medium/easy,单向幂等同步(`POST /api/projects/:id/sync`)。旧侧 done/accepted 任务自动沿合法路径推到 maestro `done`
## 19. 分期实施状态
-**Phase 1(核心,已完成)**daemon + SQLite + 项目注册 + 任务模型 + 状态机 + Web 看板 + MCP(读写 + 审批)+ CLI + todo 同步引擎。
-**Phase 2(自动化,已完成)**:编排器 + score 调度 + Agent SDK executorEasy/Medium/Hard worktree 自动执行)+ verify + 双复审(code review + 安全审计)+ 自动合并(--no-ff + 逃生口)。
-**Phase 3(打磨,核心已完成)**:多进程执行解耦(daemon 监工 + 独立 worker 进程 + job.json/outbox/heartbeat 文件通讯 + 重启 re-adopt,见 §7+ macOS 原生通知 + Claude 订阅额度透传 + 归档详情时间线 + 模型分级(复杂度→模型映射 + env 覆盖 + 回退链)+ 项目 logo + 侧栏拖动排序。
- 🔲 **后续**:并发资源精细监控 / 指标面板 / 可选远程看板 / PR 平台集成(GitHub PR 链接)。
- 🟡 **worker 池化(已评估,暂缓)**:长驻 worker 从队列拉任务、省 spawn 冷启动开销,仅在 spawn 量很大时才有收益。当前模型是干净的「一 run ↔ 一 pid」:`setWorkerPid`/`isWorkerAlive`/reaper/`reconcileInterrupted` 全靠「每 run 一个 pid + 一份心跳」判活与恢复,且 worker 与 DB 完全隔离 + 重启 re-adopt 天然得到崩溃恢复。池化会打破该不变量(一个池 worker 在跑哪个 run?单进程崩溃波及 N 个任务、共享 worktree 冲突),收益只在高并发量下兑现。现规模下**保持「每任务一进程 + 失败从头重跑」的简单模型**,不引入池化。
## 20. 风险与注意事项
- **Agent SDK headless 执行可靠性**verify_cmd 把关 + 双复审 + needs_attention 人工兜底。executor run 最多 100 轮、30min 超时兜底;reviewer run 最多 40 轮、15min 超时兜底。
- **SDK 抖动 → resume 续跑**`cc.ts`):单次会话若因流式异常中断(流断 / 没收到 result,**非**超时取消、**非** max_turns 等终态),且已拿到 sessionId,则在 worker 进程内用 `resume` 接着原会话续跑一次(保留已干的活,不从头重来),剩余预算不足时至少留 60s。这是 worker 内部健壮性小优化,与「重启靠 re-adopt、整体失败靠 daemon 从头重跑」**正交、不替代**它们;`MAESTRO_SDK_RESUME=0` 可关闭。与模型不可用回退(换模型重开会话)互斥。
- **自动执行安全**:worktree 隔离(改动不进主检出)+ 不自动 push + merge 需 exec_review accept + 安全审计 run 专门检查凭证/注入/权限问题;worker 进程不碰 DB(无法越权改任务状态),spawn 时黑名单剥离真密钥类 env。
- **中断恢复(多进程 re-adopt**daemon 重启后 `reconcileInterrupted()``worker_pid + 心跳` 真判活——worker 仍活则 **re-adopt**(保留 executing,续 ingest 其 outbox,不打断正在跑的 agent);已死则 `failTaskAttempt` 回收重试。daemon 与 worker 解耦后,daemon 崩溃/重启不再丢失在途执行。
- **长任务上下文超限**100 turns / 30min 兜底,超时后转 failed → 重试。
- **worktree 泄漏**:合并后异步清理,daemon 启动时 `git worktree prune` 清残留。