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

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

518 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 算法设计
**CPMCritical Path Method)后向传播** 替代简单优先级排序:
```typescript
// src/model/scoring.ts
/** 递归计算任务向后传播的 unlock 价值 */
function rankU(taskId: string, cache: Map<string, number>): 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-6env 三档可覆盖) |
| 输出格式 | 子任务表格(人审) + 扩展 JSON{title,complexity,priority,deps,ownedFiles,expectedOutput} |
| 审批 | 默认人审;可选 auto-approved+全 easy+改动小 自动放行(默认关) |
### 4.2 Executor(代码执行)
| 维度 | 决策 |
|---|---|
| 双复审 | code + security 改 `Promise.all` **并行**(≤15min 替代 ≤30min |
| 复审模型 | 统一 fable-5,不被 project.model 降档 |
| 新增硬闸 | 分项 checkslint/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 runrun 结束时清理对应 emitter。
**选型理由**SSE 天然支持重连+Last-Event-ID(断网续读),无需引入 gRPC/额外 WebSocket。
### 5.2 类 Chat 任务(执行中注入指令)
```
POST /api/tasks/:id/inject { message: string }
→ daemon 写入 runs/<runId>/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 输出 <ask>...</ask>)
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<void | { cancel: string }>;
'after:planner:output'?: (task: Task, plan: string) => Promise<string>; // 可改 plan
'before:execute'?: (task: Task, job: JobSpec) => Promise<void | { cancel: string }>;
'before:merge'?: (task: Task, branch: string) => Promise<void | { cancel: string }>;
'on:conflict'?: (task: Task, files: string[]) => Promise<'auto' | 'manual'>;
'before:exec-review'?: (task: Task, result: TaskResult) => Promise<void | { cancel: string }>;
}
```
- 超时 5s → 等同于 cancel
- Shell script 方式:`.maestro/hooks/before-execute.sh`exit != 0 = cancelstdout = 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/<runId>/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(全局 storeprojects/tasks/ws 连接) |
| 样式 | CSS Modules + IBM Plex MonoClaude Design System 字体) |
| 国际化 | i18next5 语言: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 事件 → 看板实时刷新
- SSETaskDetail 右侧面板显示 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→硬闸 | — |
| P0Task 模型) | 新增 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 | — |
| P2Hook | MaestroHooks 契约 + shell/TS 两种实现 | — |
| P2Trace 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 1Task 分类与自动拆解(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-loopSSE / clarify / Hook 契约 / Trace ID
- Agent 5AI Agent 编排最佳实践(expected_output / stages.json / Reflect 阶段)