feat(agent): ② approve 前硬闸——分项检查(lint/typecheck/build) + diff 体量闸

- checks.ts:runChecks 按 lint→typecheck→build 逐项跑(worktree 内,各 10min,
  任一非0=硬失败报哪项挂);parseChecks 解析 project.checks JSON
- diffSizeGate:git diff --numstat 统计文件数+增删行数,超阈值=硬失败(提示拆解);
  阈值 env 可配 MAESTRO_DIFF_MAX_FILES/LINES(默认 60/3000,0=不限);git 出错不拦截
- pipeline:verify 后插 checks+diff 闸,任一失败 emit failed 不进复审;
  conflict 管线只跑 checks(diff 含整条原分支,体量闸会误伤)
- 注:diff「声明外文件」闸未做——task 无声明文件范围字段

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-24 22:53:56 +08:00
parent ceb4f9e9d2
commit 2d127698d1
4 changed files with 300 additions and 0 deletions
+29
View File
@@ -152,6 +152,35 @@ test('verify 失败:runner ok 但 verify !ok → emit failed(沿用 executor
assert.equal(emitted.at(-1)?.type, 'done', '末尾应为 done');
});
test('checks 闸失败:verify 过但分项检查 !ok → emit failed(报哪项挂)+ done,无 result,不复审', async () => {
const { emitted, emit } = collector();
let reviewed = false;
await runPipeline(fakeJob, mockDeps({
checks: async () => ({ ok: false, failed: 'typecheck', logRef: '/tmp/c.log', error: 'typecheck 检查失败(exit 1):tsc' }),
reviewCode: async () => { reviewed = true; return okReview; },
}), emit);
assert.equal(find(emitted, 'result'), undefined, 'checks 失败不应 emit result');
assert.equal(reviewed, false, 'checks 失败不应进入复审');
const failed = find(emitted, 'failed');
assert.ok(failed, '应 emit failed');
assert.match(failed.error!, /typecheck 检查失败/);
assert.equal(emitted.at(-1)?.type, 'done', '末尾应为 done');
});
test('diff 体量闸失败:emit failed(体量超阈值)+ done,无 result', async () => {
const { emitted, emit } = collector();
await runPipeline(fakeJob, mockDeps({
diffGate: async () => ({ ok: false, files: 99, lines: 9999, error: '改动体量超阈值(文件数 99 > 60):任务可能过大,建议拆解后重做' }),
}), emit);
assert.equal(find(emitted, 'result'), undefined, 'diff 闸失败不应 emit result');
const failed = find(emitted, 'failed');
assert.ok(failed, '应 emit failed');
assert.match(failed.error!, /体量超阈值/);
assert.equal(emitted.at(-1)?.type, 'done', '末尾应为 done');
});
test('复审抛错不挡:reviewCode 抛错 → result 仍 emitcode.verdict=null 且 summary 含「自动复审失败」', async () => {
const { emitted, emit } = collector();
await runPipeline(fakeJob, mockDeps({