feat(merge): 自动合并失败时建最高优先级补救任务来完成合并

decide 端 exec_review accept→merge 冲突失败时,不再只是报错留在闸:
自动建一个 P0 easy 补救任务(operations 写明分支/目标/冲突详情与解冲突步骤),
自治项目会自动领取去解决。幂等:补救任务 id 记到原任务 result.mergeTaskId,
已有未结束补救任务则复用不重复建;结束(done/cancelled)后允许新建。

- model/types: TaskResult 加 mergeTaskId 字段
- mappers/orchestrator: 同步透传/补 null
- store.ensureMergeRemediationTask + store.test 覆盖(P0/easy/ready/operations/幂等/可重建)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 09:13:30 +08:00
parent 47eeaf1902
commit 217eb6aa1f
7 changed files with 97 additions and 1 deletions
+1
View File
@@ -184,6 +184,7 @@ test('成功路径:状态流转 + setResult(四字段) + executor/reviewer/sec
verdict: 'approve',
securitySummary: '## 安全审计\nmock 审计通过',
securityVerdict: 'approve',
mergeTaskId: null,
});
assert.equal(codeReportSeen, '执行自述:改了 README'); // runner finalText 传给两个复审作执行者自述
assert.equal(secReportSeen, '执行自述:改了 README');
+43
View File
@@ -208,3 +208,46 @@ test('容器收口:已拆解 Hard 的子任务全 done → 容器自动 done
assert.equal(s.getTask(root.id).status, 'done'); // 子全 done → 容器自动 done
s.close();
});
test('合并失败补救:建最高优先级 easy 任务,且幂等不重复建', () => {
const s = freshStore();
const p = s.createProject({ name: 'm', repoPath: '/tmp/m-' + Math.random() });
// 造一个进入 exec_review、带分支结果的原任务
const t = s.createTask({ projectId: p.id, title: '配置面板补 model', complexity: 'easy', priority: 1 });
s.setOperations(t.id, 'op');
s.transition(t.id, 'queued');
s.transition(t.id, 'executing');
s.setResult(t.id, {
branch: 'maestro/' + t.id, worktree: '/tmp/wt', diffSummary: '', commits: [], prUrl: null,
summary: null, verdict: null, securitySummary: null, securityVerdict: null, mergeTaskId: null,
});
s.transition(t.id, 'exec_review');
const rem = s.ensureMergeRemediationTask(t.id, {
branch: 'maestro/' + t.id, targetBranch: 'main', conflictError: '冲突文件:web/app.js',
});
assert.equal(rem.complexity, 'easy');
assert.equal(rem.priority, 0, '补救任务应为最高优先级 P0');
assert.equal(rem.status, 'ready', 'easy 无依赖 → 直达 ready,自治项目可领');
assert.match(rem.title, /合并冲突待解决/);
assert.match(rem.operations ?? '', /git merge maestro\//, 'operations 应含合并步骤');
assert.match(rem.operations ?? '', /web\/app\.js/, 'operations 应含冲突详情');
// 幂等标记写回原任务
assert.equal(s.getTask(t.id).result?.mergeTaskId, rem.id);
// 再次调用 → 复用同一补救任务,不新建
const again = s.ensureMergeRemediationTask(t.id, {
branch: 'maestro/' + t.id, targetBranch: 'main', conflictError: '再次冲突',
});
assert.equal(again.id, rem.id, '已有未结束补救任务 → 复用');
const remCount = s.listTasks(p.id).filter((x) => x.title.includes('合并冲突待解决')).length;
assert.equal(remCount, 1, '不应重复建补救任务');
// 补救任务结束(cancelled)后再调用 → 允许新建
s.transition(rem.id, 'cancelled');
const fresh = s.ensureMergeRemediationTask(t.id, {
branch: 'maestro/' + t.id, targetBranch: 'main', conflictError: '又冲突',
});
assert.notEqual(fresh.id, rem.id, '旧补救任务已结束 → 建新的');
s.close();
});