Files
wangjia 645d36323e merge: worker 池化 + SDK resume 抗抖动 [tsk_74Acz1Nh4K1J]
手工正确合并(破除补救死循环):带上 src/executor/cc.ts 池化 + SDK resume + test/cc.test.ts(typecheck + 189 测试全绿)。DESIGN.md 冲突取 main 新版(多进程 re-adopt / worker 不碰 DB 描述),保留 246 行 SDK-resume 新增条目,丢弃分支过时描述。

此前补救任务只啃了 DESIGN.md 冲突文件、没带 cc.ts 代码,导致真代码一直没落地、原任务卡 exec_review、每次重试又生新补救(xbtd6GpS4NYg→Yb8eFAX2VUxZ)的死循环,至此终结。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:32:10 +08:00

135 lines
9.0 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 执行系统(本地优先 daemon · Node/TypeScript)。
按任务复杂度(Hard / Medium / Easy)分级驱动审批闸,用后台 agentClaude Code)在隔离 git worktree 自动执行可执行任务,执行后自动跑 code review + 安全审计,accept 时 `--no-ff` 自动合并,改动不自动 push。
执行是**多进程解耦**的:daemon 是唯一 DB 写者兼监工,每个执行任务 spawn 一个**独立 worker 进程**(哑执行器,完全不碰 DB),二者只经文件 + 进程信号通讯(`job.json` / `outbox.ndjson` / `heartbeat` / `SIGTERM`),daemon 重启可按心跳判活 re-adopt 仍在跑的 worker。详见 [DESIGN.md](./DESIGN.md) §7。
- 完整设计见 **[DESIGN.md](./DESIGN.md)**。
- 当前状态:**Phase 1 + Phase 2 + Phase 3 核心均已落地**(编排器 + score 调度 + 双复审 + 自动合并 + 多进程执行解耦 + macOS 通知 + 额度透传 + 归档)。
## 结构
```
src/
model/ 复杂度、状态机、实体类型、score 调度算法
store/ SQLite schema + 访问层(状态机守卫 + 审批闸)
api/ REST + WebSocket API + logo 解析
mcp/ MCP server(给 Claude Code
executor/ Agent SDK 封装(cc.ts)、worktree 管理、verify、runner、reviewer(双复审)、merge、模型分级
protocol.tsdaemon↔worker 文件协议:job/outbox/heartbeat/判活)
worker.ts(独立 worker 进程入口)、pipeline.ts(纯执行管线,不碰 DB
daemon/ maestrod 入口、编排器(监工:spawn/ingest/回收)、ingestoutbox→DB)、
macOS 通知、额度透传、配置
sync/ 旧 todo.json → maestro 单向同步引擎
web/ 看板(多项目·实时 WS·内联审批·归档详情)
scripts/ poc-exec.mjs(执行闭环验证脚本)
```
## 开发
```bash
npm install
npm run typecheck # tsc --noEmit
npm run build # → dist/
npm run dev # 启动 daemon(默认 http://127.0.0.1:4517
npm test # 运行全量测试(node --test
```
## 接入 Claude CodeMCP
`npm run build`,并保证 maestrod 在跑(`npm run dev`,默认 http://127.0.0.1:4517)。两种接法:
```bash
# 方式一:claude mcp add(在要接入的项目里执行)
claude mcp add maestro -e MAESTRO_URL=http://127.0.0.1:4517 -- node /绝对路径/maestro/dist/mcp/index.js
```
```jsonc
// 方式二:项目根放 .mcp.json(模板见本仓库 .mcp.json.example
{ "mcpServers": { "maestro": {
"command": "node",
"args": ["/绝对路径/maestro/dist/mcp/index.js"],
"env": { "MAESTRO_URL": "http://127.0.0.1:4517" } } } }
```
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 看板操作)。
## 核心功能速览
### 多进程执行(监工 + worker
- **daemon = 唯一 DB 写者 + 监工**:领任务 → 写 `job.json` → spawn worker → ingest 其 outbox → 回收死 worker。每轮 tick 顺序 `ingest → reap → claim`
- **worker = 哑执行器**`node dist/executor/worker.js <runId>`,每任务一个独立进程,跑 worktree→执行→verify→双复审→产出,**完全不碰 DB**,进度/结果只追加进 `outbox.ndjson`
- **通讯**`runs/<runId>/job.json`daemon→worker 唯一输入)/ `outbox.ndjson`worker→daemon,带 seqdaemon 按 `last_seq` 幂等 ingest/ `heartbeat`worker 每 10s 刷 mtimedaemon 判活)/ `SIGTERM`(取消/超时)。
- **重启 re-adopt**daemon 启动 `reconcileInterrupted()``worker_pid + 心跳`判活——活则 re-adopt 续 ingest(不打断 agent),死则回收重试(持久化退避 `next_eligible_at = min(30s·2^(n-1), 10min)`,超 `maxRetries``needs_attention`)。
### Score 调度
编排器按 score 降序领任务(不抢占,领取时现算):
```
score = P(自身优先级) // P0=3 / P1=2 / P2=1
+ Σ P(已完成依赖) // 链条惯性:前置投入越重越优先出活
+ Σ P(等我解锁的 blocked 任务) // 解锁效应:解锁面越广越优先
```
### 双复审 PR 流程
执行成功后顺序起两个只读 headless Claude Code
1. **Code Review**kind=reviewer):正确性/可维护性逐条问题 → `VERDICT: approve|reject`
2. **安全审计**kind=security):凭证泄露/注入/权限/UI红线词/部署风险 → `VERDICT: approve|reject`
任一复审失败不挡任务进 `exec_review`;看板展示两份 markdown 报告 + verdict 徽章。
### 模型分级
| 角色 | Easy | Medium | Hard |
|---|---|---|---|
| executor | claude-sonnet-4-6 | claude-opus-4-8 | claude-fable-5 |
| reviewer | claude-sonnet-4-6 | claude-opus-4-8 | claude-opus-4-8 |
`project.model`(看板设置)最高优先;环境变量次之(见下);内置默认最低。模型不可用时自动回退 `opus-4-8 → sonnet-4-6`
### 归档与运行历史
- `GET /api/tasks/:id/events` — 状态流转时间线
- `GET /api/tasks/:id/runs` — executor / reviewer / security run 历史(含转录引用)
### macOS 通知
进入审核闸(plan/spec/exec_review)、执行失败、复审建议拒绝、合并完成时推送原生通知。`MAESTRO_NOTIFY=0` 关闭。
### 额度透传
`GET /api/agents` 附带 Claude 订阅额度(5h 窗口 + 7天窗口),读取 `~/.claude/.credentials.json` 或 macOS keychain60s 缓存。
### 自动合并
exec_review accept 时自动 `--no-ff` 合并:若 defaultBranch 在主检出且工作区干净 → 直接合并;否则临时 worktree 合并后销毁。冲突时 abort 返回冲突文件列表,任务保留在 exec_review。`merge:false` 逃生口可仅通过不合并。
## 环境变量
| 变量 | 说明 | 默认 |
|---|---|---|
| `MAESTRO_URL` | daemon 地址(MCP 客户端侧) | `http://127.0.0.1:4517` |
| `MAESTRO_PORT` | daemon 监听端口 | `4517` |
| `MAESTRO_DATA_DIR` | SQLite + worktree 数据目录 | `~/.maestro` |
| `MAESTRO_ORCH_INTERVAL` | 编排器轮询间隔(秒),`0`=关闭 | `15` |
| `MAESTRO_WORKER_CMD` | 覆盖 worker 进程启动命令(空格分隔),供 tsx 跑 .ts 入口/测试用 | `node dist/executor/worker.js` |
| `MAESTRO_NOTIFY` | `0` 关闭 macOS 通知 | 开启 |
| `MAESTRO_SDK_RESUME` | `0` 关闭 SDK 抖动时的 session resume 续跑(worker 内部健壮性优化) | 开启 |
| `MAESTRO_MODEL_EASY` | executor Easy 模型覆盖 | `claude-sonnet-4-6` |
| `MAESTRO_MODEL_MEDIUM` | executor Medium 模型覆盖 | `claude-opus-4-8` |
| `MAESTRO_MODEL_HARD` | executor Hard 模型覆盖 | `claude-fable-5` |
| `MAESTRO_MODEL_REVIEW_EASY` | reviewer Easy 模型覆盖 | `claude-sonnet-4-6` |
| `MAESTRO_MODEL_REVIEW_MEDIUM` | reviewer Medium 模型覆盖 | `claude-opus-4-8` |
| `MAESTRO_MODEL_REVIEW_HARD` | reviewer Hard 模型覆盖 | `claude-opus-4-8` |
| `MAESTRO_SANDBOX` | worker OS 级沙箱总开关(`on`/`off` | 关闭 |
| `MAESTRO_SANDBOX_PROFILE` | 是否套 `sandbox-exec`(仅 macOS`off` 则仅留 ulimit | 开启 |
| `MAESTRO_SANDBOX_NET` | 是否放行网络(SDK 需连 Anthropic`off` 隔离) | 放行 |
| `MAESTRO_SANDBOX_CPU` | CPU 时间上限(秒),`0`=不限 | `0` |
| `MAESTRO_SANDBOX_MEM` | 虚拟内存上限(MB),`0`=不限(macOS 对 `-v` 支持不稳定) | `0` |
| `MAESTRO_SANDBOX_NOFILE` | 打开文件描述符上限 | `4096` |
| `MAESTRO_SANDBOX_FSIZE` | 单文件大小上限(MB),`0`=不限 | `0` |
| `MAESTRO_SANDBOX_WRITE_EXTRA` | 额外放行写的绝对路径(冒号分隔) | 空 |
## worker 沙箱(OS 级硬隔离,可选)
进程隔离(独立 worker 进程 + env 收敛,密钥/DB/API 与 daemon 隔开)之上,再加一层 OS 级硬隔离,**默认关闭**,开启后对每个 worker 生效:
- **文件写围栏**macOS `sandbox-exec`):基线 `allow default`(放行读 / exec / 网络 / mach——keychain 鉴权所需),仅对**写**收口,逐条放行 worker 必需的写目标:任务 worktree、主仓 `.git`/`node_modules``runs/<runId>``transcripts``$TMPDIR` 与常用工具缓存(`~/.npm``~/Library/Caches``GOCACHE` 等)。越界写(改系统、污染其它仓库、动 maestro 自身 DB)被内核拒绝。为什么不 `deny default` 全围栏:合法的 git/npm/go/node 需读海量系统/缓存路径,全围栏极易误伤——真正的破坏向量是「写」。
- **资源上限**`ulimit`):CPU 时间 / 虚拟内存 / 打开文件数 / 单文件大小,防失控 agent 拖垮机器;超限由内核杀进程,daemon 的 reaper 据此回收并重试/转 `needs_attention`
- 生效的策略会在 daemon 启动日志打一行;每个 run 的实际 profile 落在 `runs/<runId>/sandbox.sb`(清晰记录)。
**先在一个项目灰度**:开 `MAESTRO_SANDBOX=on` 后跑一轮含 `npm/go/git` 的真实任务,确认能跑通、越界写被拒、超限被杀且日志清晰,再逐步铺开。个别项目若有特殊写路径,用 `MAESTRO_SANDBOX_WRITE_EXTRA` 放行。