diff --git a/.changeset/ce-review-handoff-step-projection.md b/.changeset/ce-review-handoff-step-projection.md new file mode 100644 index 0000000000..0a229e0d61 --- /dev/null +++ b/.changeset/ce-review-handoff-step-projection.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Graph-native workflows now reconcile completed task steps after review handoff reaches the merge column. +category: fix +dev: `ensureWorkflowMergeBoundaryTask` evaluates successful node-result proof and projects it onto the legacy checklist before applying its already-at-merge-column no-op. This prevents Compound Engineering tasks from reaching approved review at `0/N`, failing merge with `task has incomplete steps`, and deadlock-pausing. diff --git a/packages/engine/src/__tests__/executor-graph-boundary.test.ts b/packages/engine/src/__tests__/executor-graph-boundary.test.ts index 2d6d297657..dee50bb853 100644 --- a/packages/engine/src/__tests__/executor-graph-boundary.test.ts +++ b/packages/engine/src/__tests__/executor-graph-boundary.test.ts @@ -36,7 +36,36 @@ function benchmarkIr(): WorkflowIr { } as WorkflowIr; } -function makeExecutor(opts: { selection?: { workflowId: string; stepIds: string[] }; ir?: WorkflowIr; taskColumn?: string }) { +function executeIr(): WorkflowIr { + return { + version: "v2", + name: "execute then merge", + columns: [ + { id: "in-progress", name: "In progress", traits: [] }, + { id: "in-review", name: "In review", traits: [{ trait: "merge" }, { trait: "merge-blocker" }] }, + ], + nodes: [ + { id: "execute", kind: "prompt", column: "in-progress", config: { seam: "execute" } }, + { id: "merge", kind: "merge-gate", column: "in-review" }, + ], + edges: [{ from: "execute", to: "merge", condition: "success" }], + } as WorkflowIr; +} + +function makeExecutor(opts: { + selection?: { workflowId: string; stepIds: string[] }; + ir?: WorkflowIr; + taskColumn?: string; + steps?: Array<{ id: string; title: string; status: "pending" | "done" }>; + workflowStepResults?: Array<{ + workflowStepId: string; + workflowStepName: string; + source: "node"; + phase: "pre-merge"; + status: "passed"; + completedAt: string; + }>; +}) { const store = createMockStore() as unknown as Record; const liveTask = { id: "FN-B1", @@ -44,7 +73,8 @@ function makeExecutor(opts: { selection?: { workflowId: string; stepIds: string[ description: "", column: opts.taskColumn ?? "in-review", dependencies: [], - steps: [], + steps: opts.steps ?? [], + workflowStepResults: opts.workflowStepResults ?? [], currentStep: 0, log: [], prompt: "# t", @@ -109,4 +139,44 @@ describe("U5a — IR-driven merge boundary (scenario 1)", () => { const moveTask = store.moveTask as ReturnType; expect(moveTask).not.toHaveBeenCalled(); }); + + /* + FNXC:WorkflowLifecycle 2026-07-26-22:59: + Successful pre-merge proof must still project graph-native results onto legacy steps after review handoff has already moved the card into the merge column; the projection must not trigger a redundant move. + */ + it("projects graph-native completion after review handoff already moved the card to the merge column", async () => { + const pendingSteps = [ + { id: "0", title: "Preflight", status: "pending" as const }, + { id: "1", title: "Implement", status: "pending" as const }, + ]; + const { executor, store, liveTask } = makeExecutor({ + selection: { workflowId: "custom:execute", stepIds: [] }, + ir: executeIr(), + taskColumn: "in-review", + steps: pendingSteps, + workflowStepResults: [{ + workflowStepId: "execute", + workflowStepName: "Execute", + source: "node", + phase: "pre-merge", + status: "passed", + completedAt: new Date().toISOString(), + }], + }); + + await executor.ensureWorkflowMergeBoundaryTask( + liveTask, + { reason: "workflow-merge-boundary", nodeId: "merge", workflowId: "custom:execute", runId: "r1" }, + ); + + expect(store.updateTask).toHaveBeenCalledWith( + "FN-B1", + { + steps: pendingSteps.map((step) => ({ ...step, status: "done" })), + currentStep: 1, + }, + undefined, + ); + expect(store.moveTask).not.toHaveBeenCalled(); + }); }); diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index bdad63a116..bd10a9bf4c 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -7623,10 +7623,12 @@ export class TaskExecutor { */ const targetColumn = await this.resolveMergeBoundaryColumn(task.id, metadata.nodeId); - // Already at the merge column, or in the terminal (done/complete) column: - // nothing to do. builtin:coding's targetColumn is `in-review`, so this stays - // byte-identical to the pre-cutover `in-review || done` guard. - if (live.column === targetColumn || live.column === "done") return live; + /* + FNXC:WorkflowMerge 2026-07-26-22:59: + A prior review handoff can move a graph-native workflow into its merge column before this boundary projects successful node results onto the legacy checklist. Preserve the no-move behavior, but do not return until the projection has run. + */ + const alreadyAtMergeColumn = live.column === targetColumn; + if (live.column === "done") return live; if (live.paused || live.userPaused) return live; /* @@ -7669,6 +7671,7 @@ export class TaskExecutor { this.getRunContextFor(live.id), ); } + if (alreadyAtMergeColumn) return live; const moveOptions = { preserveProgress: true, moveSource: "engine" as const,