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
+53 -1
View File
@@ -4,7 +4,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import { parseChecks, runChecks, diffSizeGate, globToRegExp, matchesAnyGlob, scopeFileGate } from '../src/executor/checks.js';
import { parseChecks, runChecks, diffSizeGate, globToRegExp, matchesAnyGlob, scopeFileGate, reevalScopeGate } from '../src/executor/checks.js';
import type { Project } from '../src/model/types.js';
function withTmpData(cb: (dir: string) => Promise<void> | void): Promise<void> | void {
@@ -145,3 +145,55 @@ test('scopeFileGategit 出错 → 不拦截', async () => {
const r = await scopeFileGate('/nonexistent-repo-xyz', 'a', 'b', ['src/**']);
assert.equal(r.ok, true);
});
test('reevalScopeGate(规则13):main 动了声明范围内文件 → overlap 非空;范围外 → 空;空声明 → 空', async () => {
const repo = mkdtempSync(join(tmpdir(), 'maestro-reeval-'));
const g = (args: string[]) => execFileSync('git', args, { cwd: repo }).toString();
g(['init', '-q', '-b', 'main']);
g(['config', 'user.email', 't@t']); g(['config', 'user.name', 't']);
execFileSync('mkdir', ['-p', join(repo, 'src')]);
writeFileSync(join(repo, 'src', 'a.ts'), 'a\n');
writeFileSync(join(repo, 'docs.md'), 'd\n');
g(['add', '-A']); g(['commit', '-qm', 'base']);
// 任务分支从此刻 main 切出(尚无任务提交,模拟 sync 前的 worktree HEAD
g(['checkout', '-q', '-b', 'feature']);
// main 在任务排队期间前移:改了 src/a.ts(声明范围内)
g(['checkout', '-q', 'main']);
writeFileSync(join(repo, 'src', 'a.ts'), 'a changed by main\n');
g(['add', '-A']); g(['commit', '-qm', 'main moved']);
g(['checkout', '-q', 'feature']); // worktree HEAD 回到分支点
try {
// 声明 src/**main 动了 src/a.ts → 撞上,触发重评估
const hit = await reevalScopeGate(repo, 'main', ['src/**']);
assert.deepEqual(hit.overlap, ['src/a.ts']);
// 声明 docs.mdmain 只动了 src/a.ts → 不撞,不触发
const miss = await reevalScopeGate(repo, 'main', ['docs.md']);
assert.deepEqual(miss.overlap, []);
// 空声明 → 不评估
const skip = await reevalScopeGate(repo, 'main', []);
assert.deepEqual(skip.overlap, []);
} finally {
rmSync(repo, { recursive: true, force: true });
}
});
test('reevalScopeGate(规则13):main 无前移 → 空 overlapgit 出错 → 空', async () => {
const repo = mkdtempSync(join(tmpdir(), 'maestro-reeval2-'));
const g = (args: string[]) => execFileSync('git', args, { cwd: repo }).toString();
g(['init', '-q', '-b', 'main']);
g(['config', 'user.email', 't@t']); g(['config', 'user.name', 't']);
writeFileSync(join(repo, 'src.ts'), 'x\n');
g(['add', '-A']); g(['commit', '-qm', 'base']);
g(['checkout', '-q', '-b', 'feature']); // 分支点 = main tipmain 未前移
try {
const r = await reevalScopeGate(repo, 'main', ['**']);
assert.deepEqual(r.overlap, []);
} finally {
rmSync(repo, { recursive: true, force: true });
}
const err = await reevalScopeGate('/nonexistent-repo-xyz', 'main', ['src/**']);
assert.deepEqual(err.overlap, []);
});