From 217eb6aa1fcff320e901e289b8578f27155059e8 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 13 Jun 2026 09:13:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(merge):=20=E8=87=AA=E5=8A=A8=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E5=A4=B1=E8=B4=A5=E6=97=B6=E5=BB=BA=E6=9C=80=E9=AB=98?= =?UTF-8?q?=E4=BC=98=E5=85=88=E7=BA=A7=E8=A1=A5=E6=95=91=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=9D=A5=E5=AE=8C=E6=88=90=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/api/server.ts | 8 ++++++- src/daemon/orchestrator.ts | 1 + src/model/types.ts | 1 + src/store/mappers.ts | 1 + src/store/store.ts | 43 ++++++++++++++++++++++++++++++++++++++ test/orchestrator.test.ts | 1 + test/store.test.ts | 43 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/api/server.ts b/src/api/server.ts index 8b76090..f918337 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -240,7 +240,13 @@ export function buildServer(opts: ApiOptions): FastifyInstance { if (!project) throw new StoreError(`项目不存在: ${task.projectId}`); const { branch, worktree } = task.result; const mr = await mergeBranch(project.repoPath, branch, project.defaultBranch, task.id); - if (!mr.ok) throw new StoreError(`合并失败:${mr.error}(任务保留在审核闸)`); + if (!mr.ok) { + // 自动合并失败(多为冲突)→ 建/复用最高优先级补救任务来完成合并,原任务留在审核闸 + const rem = store.ensureMergeRemediationTask(task.id, { + branch, targetBranch: project.defaultBranch, conflictError: mr.error ?? '未知冲突', + }); + throw new StoreError(`合并失败:${mr.error}(原任务保留在审核闸;已建最高优先级补救任务 ${rem.id}「${rem.title}」来完成合并)`); + } store.decide(id, 'accept', b.actor ?? 'user', b.reason ?? null); // 合并产物记录:复用 prUrl 字段写 merged: diff --git a/src/daemon/orchestrator.ts b/src/daemon/orchestrator.ts index 7487e5a..6bf418e 100644 --- a/src/daemon/orchestrator.ts +++ b/src/daemon/orchestrator.ts @@ -140,6 +140,7 @@ export function createOrchestrator(store: Store, log: OrchestratorLogger, deps: diffSummary: diff.diffSummary, commits: diff.commits, prUrl: null, summary: code.summary, verdict: code.verdict, securitySummary: sec.summary, securityVerdict: sec.verdict, + mergeTaskId: null, }); store.transition(task.id, 'exec_review', { by: 'orchestrator', runId: run.id }); store.finishRun(run.id, 'succeeded', { diff --git a/src/model/types.ts b/src/model/types.ts index 87ca9ce..75b3c48 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -47,6 +47,7 @@ export interface TaskResult { verdict: ReviewVerdict | null; // code review 结论,解析不到 / 复审失败 = null securitySummary: string | null; // 安全审计报告(markdown),旧数据缺省 null securityVerdict: ReviewVerdict | null; // 安全审计结论,解析不到 / 审计失败 = null + mergeTaskId: string | null; // 自动合并失败时建的最高优先级补救任务 id(幂等标记) } export interface Task { diff --git a/src/store/mappers.ts b/src/store/mappers.ts index fd413cc..77d91de 100644 --- a/src/store/mappers.ts +++ b/src/store/mappers.ts @@ -53,6 +53,7 @@ function parseResult(json: string): TaskResult { verdict: verdict(raw.verdict), securitySummary: raw.securitySummary ?? null, securityVerdict: verdict(raw.securityVerdict), + mergeTaskId: raw.mergeTaskId ?? null, }; } diff --git a/src/store/store.ts b/src/store/store.ts index 1e41a0e..ca1a2d0 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -257,6 +257,49 @@ export class Store { return this.getTask(taskId)!; } + /** + * 自动合并失败时,为原任务建一个最高优先级(P0)补救任务来完成合并。 + * 幂等:原任务 result.mergeTaskId 已指向一个未结束(非 done/cancelled)的任务则直接复用,不重复建。 + * 补救任务为 easy(直达 ready,自治项目会自动领取去解冲突),operations 写明分支/目标/冲突详情与步骤。 + */ + ensureMergeRemediationTask( + taskId: string, + opts: { branch: string; targetBranch: string; conflictError: string }, + ): Task { + const orig = this.getTask(taskId); + if (!orig) throw new StoreError(`任务不存在: ${taskId}`); + + const existingId = orig.result?.mergeTaskId; + if (existingId) { + const existing = this.getTask(existingId); + if (existing && existing.status !== 'done' && existing.status !== 'cancelled') return existing; + } + + const rem = this.createTask({ + projectId: orig.projectId, + title: `合并冲突待解决:${orig.title}`, + complexity: 'easy', + priority: 0, // 最高优先级 + }); + this.setOperations( + rem.id, + `## 操作(自动生成 · 合并冲突补救)\n` + + `原任务「${orig.title}」(${orig.id}) 的分支 \`${opts.branch}\` 自动合并到 \`${opts.targetBranch}\` 时发生冲突,需解决冲突并完成合并。\n\n` + + `### 冲突详情\n${opts.conflictError}\n\n` + + `### 步骤(在本任务 worktree 内,已从 ${opts.targetBranch} 检出)\n` + + `1. \`git merge ${opts.branch}\` 触发冲突;\n` + + `2. 逐个解决冲突文件,保留两边意图(不要丢任一方改动);\n` + + `3. \`git add -A && git commit\`(沿用默认合并信息即可);\n` + + `4. 本任务进 exec_review 后,「通过并合并」会把已解决的结果干净并入 ${opts.targetBranch};\n` + + `5. 完成后,原任务 ${orig.id} 可用「仅通过」标记 done(其改动已随本任务并入)。`, + ); + + // 幂等标记写回原任务 result(保留原有结果字段) + if (orig.result) this.setResult(orig.id, { ...orig.result, mergeTaskId: rem.id }); + this.emit(orig.projectId, rem.id, 'task.updated', { field: 'merge-remediation', forTask: orig.id }); + return this.getTask(rem.id)!; + } + setResult(taskId: string, result: TaskResult): Task { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); diff --git a/test/orchestrator.test.ts b/test/orchestrator.test.ts index f5e1a9d..47829f1 100644 --- a/test/orchestrator.test.ts +++ b/test/orchestrator.test.ts @@ -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'); diff --git a/test/store.test.ts b/test/store.test.ts index 3749a36..327aa6d 100644 --- a/test/store.test.ts +++ b/test/store.test.ts @@ -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(); +});