feat: 合并失败→补救→完成 全程事件轨迹与自动收口 [tsk_BagzbKZNAHXV]

不新增顶层状态,用 events + result 字段记录完整轨迹:
- 新增 merge.failed / merge.remediated 两类事件
- merge 失败时在原任务时间线记 merge.failed 节点(结构化冲突文件 + 补救任务链接)
- 补救任务成功合并后,findMergeOriginTask 反查原任务,remediateOrigin 自动把原任务标 done
  并记 merge.remediated 节点,无需人工 merge:false
- 看板:归档时间线渲染失败/补救节点;结果评审闸显示「合并失败 → 补救任务」横幅

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 15:04:39 +08:00
parent f020e15137
commit b1d881a4b4
7 changed files with 149 additions and 8 deletions
+45 -2
View File
@@ -298,7 +298,7 @@ export class Store {
*/
ensureMergeRemediationTask(
taskId: string,
opts: { branch: string; targetBranch: string; conflictError: string },
opts: { branch: string; targetBranch: string; conflictError: string; conflictFiles?: string[] },
): Task {
const orig = this.getTask(taskId);
if (!orig) throw new StoreError(`任务不存在: ${taskId}`);
@@ -306,7 +306,14 @@ export class Store {
const existingId = orig.result?.mergeTaskId;
if (existingId) {
const existing = this.getTask(existingId);
if (existing && existing.status !== 'done' && existing.status !== 'cancelled') return existing;
if (existing && existing.status !== 'done' && existing.status !== 'cancelled') {
// 复用既有补救任务:仍记一次「合并失败」节点(每次失败的合并尝试都是真实事件)
this.emit(orig.projectId, orig.id, 'merge.failed', {
remediationTaskId: existing.id, branch: opts.branch, targetBranch: opts.targetBranch,
conflictFiles: opts.conflictFiles ?? [], error: opts.conflictError, reused: true,
});
return existing;
}
}
const rem = this.createTask({
@@ -331,9 +338,45 @@ export class Store {
// 幂等标记写回原任务 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 });
// 在原任务时间线上记一个「合并失败 → 补救任务」节点
this.emit(orig.projectId, orig.id, 'merge.failed', {
remediationTaskId: rem.id, branch: opts.branch, targetBranch: opts.targetBranch,
conflictFiles: opts.conflictFiles ?? [], error: opts.conflictError,
});
return this.getTask(rem.id)!;
}
/**
* 反查某补救任务对应的原任务(其 result.mergeTaskId 指向该补救任务)。
* 用于补救任务成功合并后自动收口原任务。无 → null。
*/
findMergeOriginTask(remediationTaskId: string): Task | null {
const row = this.db.prepare(
`SELECT * FROM tasks WHERE json_extract(result, '$.mergeTaskId') = ? LIMIT 1`,
).get(remediationTaskId) as TaskRow | undefined;
return row ? this.getTask(row.id)! : null;
}
/**
* 补救完成自动收口:补救任务成功合并到默认分支后,把原任务标 done(原任务改动已随补救任务并入),
* 在原任务时间线追加 merge.remediated 事件「补救完成 → 完成」,无需人工对原任务 merge:false。
* 原任务须仍在 exec_review(合并闸);否则不动(已被人工处理 / 已结束)。
*/
remediateOrigin(originId: string, remediationTaskId: string, mergeCommit?: string): Task | null {
const row = this.getTaskRow(originId);
if (!row) return null;
if (row.status !== 'exec_review') return this.getTask(originId)!; // 已被人工处理 / 非合并闸 → 不动
// 合并产物记录:复用 prUrl 字段标记「经补救任务并入」
const cur = this.getTask(originId)!;
if (cur.result) {
this.setResult(originId, { ...cur.result, prUrl: `merged-via:${remediationTaskId}${mergeCommit ? ':' + mergeCommit : ''}` });
}
this.emit(row.project_id, originId, 'merge.remediated', { remediationTaskId, mergeCommit: mergeCommit ?? null });
// exec_review → done(合法流转):触发 afterDone 放行依赖 / 容器收口
this.transition(originId, 'done', { by: 'merge-remediation', auto: 'merge-remediated', remediationTaskId });
return this.getTask(originId)!;
}
setResult(taskId: string, result: TaskResult): Task {
const row = this.getTaskRow(taskId);
if (!row) throw new StoreError(`任务不存在: ${taskId}`);