feat: Phase2 完整管线——score 调度 + 自动复审 + 模型分级 + 归档与详情

调度:
- model/scoring.ts: score = 自身分(P0=3/P1=2/P2=1) + 已完成依赖分(链条惯性)
  + 等待解锁的 blocked 任务分(解锁加权),编排器与 nextExecutable 同一打分
- createTask 校验 deps 存在且同项目(依赖图天然无环)
- daemon 重启中断自愈: executing 任务标 failed run 后重新入队(reconcileInterrupted)

执行管线:
- executor/cc.ts: 公共 headless CC 执行器(转录/超时/模型回退重试)
- executor/reviewer.ts: 执行后自动复审(只读 CC 审 diff),固定模板 summary
  (做了什么/怎么做/测试/CodeReview/安全Review/结论) + VERDICT 解析
- executor/models.ts: 按复杂度选模型(easy→sonnet/medium→opus/hard→fable5),
  env 可覆盖、project.model 最优先、不可用自动回退链
- runner: 测试/构建命令白名单(npm/go/shellcheck/make/pytest),prompt 要求实跑测试
- TaskResult 加 summary/verdict; RunKind 加 reviewer
- 容器收口: 已拆解 Hard 子任务全 done → 容器自动 done(afterDone 逐级向上)

看板:
- 五徽章组(待审批/待执行/执行中/被阻塞/总量,hover 展开,均不含已完成)
- 归档区: 深度1整树完成沉底,时间倒序分页(10/20/50/100 chip 选择)
- 归档详情对话框: 全属性/执行历史与时长/审批记录/状态流转时间线(含相关人或事)
- Agent 面板显示调度模式 + 各复杂度实际模型
- 结果闸展示复审 summary + 建议通过/拒绝徽章
- 筛选修复(组选与单选分离、已拆解移出进行中)、同步按钮收进配置面板、
  保存配置自动收起、预览全宽、被依赖阻塞→被阻塞
- API: GET /api/tasks/:id/events(任务级事件时间线)、/api/agents 带 scheduling/models

测试: 49/49(新增 scoring/复审/模型/容器收口/deps 校验/中断恢复)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 02:46:37 +08:00
parent fa472a06a7
commit f18db021c3
25 changed files with 2215 additions and 56 deletions
+92
View File
@@ -0,0 +1,92 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createWorktree, worktreeDiff, removeWorktree, worktreeDirFor, branchFor } from '../src/executor/worktree.js';
function gitSync(cwd: string, args: string[]): string {
return execFileSync('git', args, { cwd, encoding: 'utf8' });
}
/** /tmp 下建一个带 1 个 commit 的真实 git repomain 分支) */
function makeRepo(): string {
const repo = mkdtempSync(join(tmpdir(), 'maestro-wt-repo-'));
gitSync(repo, ['init', '-b', 'main']);
gitSync(repo, ['config', 'user.name', 'maestro-test']);
gitSync(repo, ['config', 'user.email', 'test@maestro.local']);
writeFileSync(join(repo, 'README.md'), '# demo\n');
gitSync(repo, ['add', '-A']);
gitSync(repo, ['commit', '-m', 'init']);
return repo;
}
test('worktreecreate / diff / remove 在真实 git repo 上工作', async (t) => {
// worktree 根目录隔离到临时 MAESTRO_DATA_DIR,不碰 ~/.maestro
const dataDir = mkdtempSync(join(tmpdir(), 'maestro-wt-data-'));
const prevDataDir = process.env.MAESTRO_DATA_DIR;
process.env.MAESTRO_DATA_DIR = dataDir;
const repo = makeRepo();
t.after(() => {
if (prevDataDir === undefined) delete process.env.MAESTRO_DATA_DIR;
else process.env.MAESTRO_DATA_DIR = prevDataDir;
rmSync(dataDir, { recursive: true, force: true });
rmSync(repo, { recursive: true, force: true });
});
const taskId = 'tsk_test123';
// 1) 创建:目录与分支符合约定
const wt = await createWorktree(repo, taskId, 'main');
assert.equal(wt.branch, branchFor(taskId));
assert.equal(wt.dir, worktreeDirFor(repo, taskId));
assert.ok(existsSync(join(wt.dir, 'README.md')), 'worktree 应检出 README.md');
const head = gitSync(wt.dir, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
assert.equal(head, wt.branch);
// 2) 空分支 diff:无 commit、无摘要
const empty = await worktreeDiff(repo, wt.dir, wt.branch, 'main');
assert.equal(empty.commits.length, 0);
assert.equal(empty.diffSummary, '');
// 3) 在 worktree 提交一笔改动 → diff 能看到 commit 与 --stat 摘要
writeFileSync(join(wt.dir, 'README.md'), '# demo\nhello maestro\n');
gitSync(wt.dir, ['add', '-A']);
gitSync(wt.dir, ['commit', '-m', `maestro(${taskId}): tweak readme`]);
const diff = await worktreeDiff(repo, wt.dir, wt.branch, 'main');
assert.equal(diff.commits.length, 1);
assert.ok(diff.commits[0].includes(taskId), `commit 行应含任务 id${diff.commits[0]}`);
assert.ok(diff.diffSummary.includes('README.md'), `diffSummary 应含文件名:${diff.diffSummary}`);
// 4) 已存在同名 worktree/分支时重建(重试场景):从 main 干净重来
const wt2 = await createWorktree(repo, taskId, 'main');
assert.equal(wt2.dir, wt.dir);
const diff2 = await worktreeDiff(repo, wt2.dir, wt2.branch, 'main');
assert.equal(diff2.commits.length, 0, '重建后分支应回到 baseBranch,无旧 commit');
// 5) remove:目录与注册项移除,主 repo 不受影响
await removeWorktree(repo, wt2.dir);
assert.ok(!existsSync(wt2.dir), 'worktree 目录应被移除');
const list = gitSync(repo, ['worktree', 'list']);
assert.ok(!list.includes(taskId), 'git worktree list 不应再含该任务');
assert.ok(existsSync(join(repo, 'README.md')), '主 repo 不受影响');
});
test('worktree:错误透传(不存在的 baseBranch', async (t) => {
const dataDir = mkdtempSync(join(tmpdir(), 'maestro-wt-data2-'));
const prevDataDir = process.env.MAESTRO_DATA_DIR;
process.env.MAESTRO_DATA_DIR = dataDir;
const repo = makeRepo();
t.after(() => {
if (prevDataDir === undefined) delete process.env.MAESTRO_DATA_DIR;
else process.env.MAESTRO_DATA_DIR = prevDataDir;
rmSync(dataDir, { recursive: true, force: true });
rmSync(repo, { recursive: true, force: true });
});
await assert.rejects(
() => createWorktree(repo, 'tsk_badbase', 'no-such-branch'),
/git worktree add/,
);
});