feat(rule13): 执行前分歧重评估闸——main 撞声明范围→needs_attention 重评估

通用规则13的(B)半边:sync main 之前先跑 reevalScopeGate,
以 git diff HEAD...origin/main 取 main 自分支点以来改动的文件,
与任务 scopeFiles 求交集;非空即 emit needs-reeval →
daemon markReeval 直接转 needs_attention(不重试/不计失败/run收尾cancelled)。

- checks.ts: + reevalScopeGate / ReevalGateFn / ReevalGateResult
- pipeline.ts: createWorktree 后、syncMain 前插入重评估闸 + reevalGate 依赖注入
- protocol.ts: + needs-reeval OutboxRecord/Payload 事件
- ingest.ts: + case 'needs-reeval' → store.markReeval
- store.ts: + markReeval(executing→needs_attention,不进重试链)
- status.ts: executing 合法转移 + needs_attention
- 测试 +5:reevalScopeGate(命中/未命中/空/无前移/git错) · pipeline(命中emit/未命中续跑) · store.markReeval;全套 242 绿
- docs: optimization-plan.html 规则13 标  已实现

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-25 20:15:02 +08:00
parent 6bf2b3619c
commit fa4ba3db28
10 changed files with 194 additions and 6 deletions
+19
View File
@@ -144,6 +144,25 @@ test('exec_review 结果闸:accept → done', () => {
s.close();
});
test('markReeval(规则13):executing → needs_attention,收尾 run 为 cancelled,不计执行失败次数', () => {
const s = freshStore();
const p = s.createProject({ name: 'rev', repoPath: '/tmp/rev-' + Math.random() });
const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy', scopeFiles: ['src/**'] });
s.setOperations(t.id, 'op');
s.transition(t.id, 'queued');
s.transition(t.id, 'executing');
const run = s.startRun(t.id, 'executor');
const after = s.markReeval(t.id, run.id, '执行前同步发现 main 已改动声明范围内文件');
assert.equal(after.status, 'needs_attention', '应直接转 needs_attention');
assert.equal(s.getRun(run.id)!.status, 'cancelled', 'run 收尾为 cancelled(非 failed');
// 没有任何 executor run 被标 failed(重评估不计失败次数 / 不进重试链)
const failedRuns = s.listRuns(t.id).filter((r) => r.kind === 'executor' && r.status === 'failed');
assert.equal(failedRuns.length, 0, 'markReeval 不应产生 failed 的 executor run');
s.close();
});
test('reconcileInterrupted:执行中被 cancel 的任务仍挂 started run → 仅收尾不崩(绝不非法 cancelled→failed', () => {
const s = freshStore();
const p = s.createProject({ name: 'recon', repoPath: '/tmp/recon-' + Math.random() });