645d36323e
手工正确合并(破除补救死循环):带上 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>
135 lines
9.0 KiB
Markdown
135 lines
9.0 KiB
Markdown
# maestro
|
||
|
||
多项目 TODO 管理与 Agent 执行系统(本地优先 daemon · Node/TypeScript)。
|
||
|
||
按任务复杂度(Hard / Medium / Easy)分级驱动审批闸,用后台 agent(Claude 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.ts(daemon↔worker 文件协议:job/outbox/heartbeat/判活)
|
||
worker.ts(独立 worker 进程入口)、pipeline.ts(纯执行管线,不碰 DB)
|
||
daemon/ maestrod 入口、编排器(监工:spawn/ingest/回收)、ingest(outbox→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 Code(MCP)
|
||
|
||
先 `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,带 seq,daemon 按 `last_seq` 幂等 ingest)/ `heartbeat`(worker 每 10s 刷 mtime,daemon 判活)/ `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 keychain,60s 缓存。
|
||
|
||
### 自动合并
|
||
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` 放行。
|