From 24d78816a09c372a66f13084d6512f818014766d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 29 Jun 2026 11:24:41 -0700 Subject: [PATCH] fix(FN-7233): route workflow reviews through remediation nodes --- .changeset/fn-workflow-remediation-nodes.md | 7 ++ .../builtin-code-review-group.test.ts | 4 +- .../builtin-coding-workflow-ir.test.ts | 5 +- .../src/__tests__/builtin-workflows.test.ts | 19 +++++- .../core/src/builtin-coding-workflow-ir.ts | 14 +++- .../core/src/builtin-plan-review-group.ts | 2 +- .../builtin-stepwise-coding-workflow-ir.ts | 14 +++- .../src/builtin-workflow-remediation-nodes.ts | 52 +++++++++++++++ packages/core/src/builtin-workflows.ts | 40 +++++++++++- .../workflow-graph-optional-group.test.ts | 6 +- .../engine/src/workflow-graph-executor.ts | 64 ++++++++++++++++++- 11 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 .changeset/fn-workflow-remediation-nodes.md create mode 100644 packages/core/src/builtin-workflow-remediation-nodes.ts diff --git a/.changeset/fn-workflow-remediation-nodes.md b/.changeset/fn-workflow-remediation-nodes.md new file mode 100644 index 0000000000..fa363f6657 --- /dev/null +++ b/.changeset/fn-workflow-remediation-nodes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show workflow review failures as explicit replan and remediation nodes. +category: fix +dev: Built-in review gate failures now route to graph-owned remediation nodes before executor scheduling fallback. diff --git a/packages/core/src/__tests__/builtin-code-review-group.test.ts b/packages/core/src/__tests__/builtin-code-review-group.test.ts index 4e61c54f6e..ee0d00bb83 100644 --- a/packages/core/src/__tests__/builtin-code-review-group.test.ts +++ b/packages/core/src/__tests__/builtin-code-review-group.test.ts @@ -76,12 +76,12 @@ describe("built-in coding + stepwise workflows wire code-review as a default-ON expect(group?.config?.defaultOn).toBe(true); expect(group?.column).toBe("in-progress"); - // Pre-merge wiring: ... → browser-verification → code-review → completion-summary; failure → end. + // Pre-merge wiring: ... → browser-verification → code-review → completion-summary; failure → remediation node. expect(ir.edges).toEqual( expect.arrayContaining([ expect.objectContaining({ from: "browser-verification", to: "code-review", condition: "success" }), expect.objectContaining({ from: "code-review", to: "completion-summary", condition: "success" }), - expect.objectContaining({ from: "code-review", to: "end", condition: "failure" }), + expect.objectContaining({ from: "code-review", to: "code-review-remediation", condition: "failure" }), ]), ); diff --git a/packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts b/packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts index 745dcbe591..5aacff6035 100644 --- a/packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts +++ b/packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts @@ -84,14 +84,15 @@ describe("builtin coding workflow ir", () => { gateMode: "gate", }); // execute → browser-verification → code-review → completion-summary → review on the success path; the - // pre-merge code-review optional-group sits next to browser-verification. failure → end. + // pre-merge code-review optional-group sits next to browser-verification. failures route to remediation nodes. expect(BUILTIN_CODING_WORKFLOW_IR.edges).toEqual( expect.arrayContaining([ expect.objectContaining({ from: "execute", to: "browser-verification", condition: "success" }), expect.objectContaining({ from: "browser-verification", to: "code-review", condition: "success" }), expect.objectContaining({ from: "code-review", to: "completion-summary", condition: "success" }), expect.objectContaining({ from: "completion-summary", to: "review", condition: "success" }), - expect.objectContaining({ from: "browser-verification", to: "end", condition: "failure" }), + expect.objectContaining({ from: "browser-verification", to: "browser-verification-remediation", condition: "failure" }), + expect.objectContaining({ from: "code-review", to: "code-review-remediation", condition: "failure" }), ]), ); // The legacy optionalSteps declaration is gone (the group replaces it). diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index 71b57e34ca..2f5058ef7b 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -431,7 +431,7 @@ describe("built-in workflows", () => { expect(byId.get("plan-review")?.column).toBe("in-progress"); expect(planReviewInnerConfig(ir)).toMatchObject({ toolMode: "readonly", - gateMode: "advisory", + gateMode: "gate", }); expect(byId.get("parse")?.column).toBe("in-progress"); expect(byId.get("steps")?.column).toBe("in-progress"); @@ -515,7 +515,19 @@ describe("built-in workflows", () => { expect(() => parseWorkflowIr(design!.ir)).not.toThrow(); const authoredNodeIds = design!.ir.nodes.filter((node) => node.id !== "start" && node.id !== "end").map((node) => node.id); - expect(authoredNodeIds).toEqual(["plan-review", "execute", "browser-verification", "code-review", "design-review", "review", "completion-summary", "merge"]); + expect(authoredNodeIds).toEqual([ + "plan-review", + "execute", + "browser-verification", + "code-review", + "design-review", + "review", + "completion-summary", + "merge", + "plan-replan", + "browser-verification-remediation", + "code-review-remediation", + ]); const execute = design!.ir.nodes.find((node) => node.id === "execute"); expect(execute?.config?.seam).toBe("execute"); @@ -746,6 +758,9 @@ describe("built-in workflows", () => { "completion-summary", "merge", "document", + "plan-replan", + "browser-verification-remediation", + "code-review-remediation", ]); expect(ce.ir.nodes.some((node) => node.config?.seam === "review")).toBe(false); diff --git a/packages/core/src/builtin-coding-workflow-ir.ts b/packages/core/src/builtin-coding-workflow-ir.ts index 8d81950651..bf2395a436 100644 --- a/packages/core/src/builtin-coding-workflow-ir.ts +++ b/packages/core/src/builtin-coding-workflow-ir.ts @@ -6,6 +6,11 @@ import { browserVerificationOptionalGroupNode } from "./builtin-browser-verifica import { codeReviewOptionalGroupNode } from "./builtin-code-review-group.js"; import { completionSummaryNode } from "./builtin-completion-summary-node.js"; import { planReviewOptionalGroupNode } from "./builtin-plan-review-group.js"; +import { + browserVerificationRemediationNode, + codeReviewRemediationNode, + planReplanNode, +} from "./builtin-workflow-remediation-nodes.js"; /** * The built-in default workflow as a v2 IR. Its six columns have ids that are @@ -71,6 +76,7 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = { config: builtinPromptConfig("planning", "Plan / specify"), }, planReviewOptionalGroupNode("in-progress"), + planReplanNode("triage"), { id: "execute", kind: "prompt", @@ -79,12 +85,14 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = { }, // Pre-merge optional browser-verification (optional-group, default OFF). browserVerificationOptionalGroupNode("in-progress"), + browserVerificationRemediationNode("in-progress"), // FNXC:CodeReviewStep 2026-06-25-15:00: // Pre-merge Code Review as a DEFAULT-ON optional-group (blocking gate), on the success path // between browser-verification and review (execute → browser-verification → // code-review → review). Runs for every coding task by default (defaultOn:true) but is // toggleable off per task; disabled → byte-inert pass-through. codeReviewOptionalGroupNode("in-progress"), + codeReviewRemediationNode("in-progress"), completionSummaryNode("in-review"), { id: "review", kind: "prompt", column: "in-review", config: builtinPromptConfig("review", "Review") }, { id: "merge-gate", kind: "merge-gate", column: "in-review", config: { gate: "auto-merge" } }, @@ -132,10 +140,10 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = { { from: "merge-attempt", to: "merge-manual-hold", condition: "outcome:manual-required" }, { from: "recovery-router", to: "merge-attempt", condition: "outcome:wake-merge", kind: "rework" }, { from: "planning", to: "end", condition: "failure" }, - { from: "plan-review", to: "end", condition: "failure" }, + { from: "plan-review", to: "plan-replan", condition: "failure" }, { from: "execute", to: "end", condition: "failure" }, - { from: "browser-verification", to: "end", condition: "failure" }, - { from: "code-review", to: "end", condition: "failure" }, + { from: "browser-verification", to: "browser-verification-remediation", condition: "failure" }, + { from: "code-review", to: "code-review-remediation", condition: "failure" }, { from: "review", to: "end", condition: "failure" }, { from: "merge-attempt", to: "end", condition: "failure" }, ], diff --git a/packages/core/src/builtin-plan-review-group.ts b/packages/core/src/builtin-plan-review-group.ts index e19e21b06d..664c08dd7f 100644 --- a/packages/core/src/builtin-plan-review-group.ts +++ b/packages/core/src/builtin-plan-review-group.ts @@ -60,7 +60,7 @@ export function planReviewOptionalGroupNode( description: PLAN_REVIEW_DESCRIPTION, prompt: PLAN_REVIEW_PROMPT, toolMode: "readonly", - gateMode: "advisory", + gateMode: "gate", }, }, ], diff --git a/packages/core/src/builtin-stepwise-coding-workflow-ir.ts b/packages/core/src/builtin-stepwise-coding-workflow-ir.ts index 3b860947f2..0c977db01b 100644 --- a/packages/core/src/builtin-stepwise-coding-workflow-ir.ts +++ b/packages/core/src/builtin-stepwise-coding-workflow-ir.ts @@ -6,6 +6,11 @@ import { browserVerificationOptionalGroupNode } from "./builtin-browser-verifica import { codeReviewOptionalGroupNode } from "./builtin-code-review-group.js"; import { completionSummaryNode } from "./builtin-completion-summary-node.js"; import { planReviewOptionalGroupNode } from "./builtin-plan-review-group.js"; +import { + browserVerificationRemediationNode, + codeReviewRemediationNode, + planReplanNode, +} from "./builtin-workflow-remediation-nodes.js"; /** * The built-in **stepwise** coding workflow (KTD-9) — the demonstration of step @@ -76,6 +81,7 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = { // Planning seam: produces PROMPT.md (the declared step-source artifact). { id: "plan", kind: "prompt", column: "in-progress", config: builtinPromptConfig("planning", "Plan") }, planReviewOptionalGroupNode("in-progress"), + planReplanNode("triage"), // KTD-12: parse the planned PROMPT.md into the task step list. This node must // dominate the foreach (validator-enforced). { @@ -143,6 +149,7 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = { // disabled the group passes through inert. Both the normal foreach-success path // and the rework-exhausted manual-release path flow through this node. browserVerificationOptionalGroupNode("in-progress"), + browserVerificationRemediationNode("in-progress"), // FNXC:CodeReviewStep 2026-06-25-15:00: // Pre-merge Code Review as a DEFAULT-ON optional-group (blocking gate), on the post-foreach // success path between browser-verification and review (steps → browser-verification → @@ -151,6 +158,7 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = { // release paths flow through it. Runs for every task by default (defaultOn:true) but is // toggleable off per task; disabled → byte-inert pass-through. codeReviewOptionalGroupNode("in-progress"), + codeReviewRemediationNode("in-progress"), completionSummaryNode("in-review"), { id: "review", kind: "prompt", column: "in-review", config: builtinPromptConfig("review", "Review") }, { id: "merge-gate", kind: "merge-gate", column: "in-review", config: { gate: "auto-merge" } }, @@ -177,7 +185,7 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = { { from: "plan", to: "plan-review", condition: "success" }, { from: "plan", to: "end", condition: "failure" }, { from: "plan-review", to: "parse", condition: "success" }, - { from: "plan-review", to: "end", condition: "failure" }, + { from: "plan-review", to: "plan-replan", condition: "failure" }, { from: "parse", to: "steps", condition: "success" }, // parse-steps no-steps defaults to success; route it explicitly to the foreach // (zero steps → foreach no-ops through its success edge, KTD-8/R8). @@ -197,8 +205,8 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = { { from: "browser-verification", to: "code-review", condition: "success" }, { from: "code-review", to: "completion-summary", condition: "success" }, { from: "completion-summary", to: "review", condition: "success" }, - { from: "browser-verification", to: "end", condition: "failure" }, - { from: "code-review", to: "end", condition: "failure" }, + { from: "browser-verification", to: "browser-verification-remediation", condition: "failure" }, + { from: "code-review", to: "code-review-remediation", condition: "failure" }, { from: "steps", to: "end", condition: "failure" }, { from: "review", to: "merge-gate", condition: "success" }, { from: "review", to: "end", condition: "failure" }, diff --git a/packages/core/src/builtin-workflow-remediation-nodes.ts b/packages/core/src/builtin-workflow-remediation-nodes.ts new file mode 100644 index 0000000000..2313fc6f5a --- /dev/null +++ b/packages/core/src/builtin-workflow-remediation-nodes.ts @@ -0,0 +1,52 @@ +import type { WorkflowIrNode } from "./workflow-ir-types.js"; + +/* +FNXC:WorkflowRemediation 2026-06-29-16:18: +Review-gate remediation must be visible in the workflow graph instead of living only in executor fallback branches. These prompt nodes are graph-owned lifecycle policy: Plan Review failure routes to an automatic replan handoff, while Code Review and Browser Verification failures route to implementation remediation handoffs. The engine still enforces the handoff mechanics and budgets. +*/ + +export const PLAN_REPLAN_NODE_ID = "plan-replan"; +export const BROWSER_VERIFICATION_REMEDIATION_NODE_ID = "browser-verification-remediation"; +export const CODE_REVIEW_REMEDIATION_NODE_ID = "code-review-remediation"; + +export function planReplanNode(column = "triage"): WorkflowIrNode { + return { + id: PLAN_REPLAN_NODE_ID, + kind: "prompt", + column, + config: { + name: "Plan Replan", + workflowAction: "plan-replan", + forWorkflowStepId: "plan-review", + toolMode: "readonly", + }, + }; +} + +export function browserVerificationRemediationNode(column = "in-progress"): WorkflowIrNode { + return { + id: BROWSER_VERIFICATION_REMEDIATION_NODE_ID, + kind: "prompt", + column, + config: { + name: "Browser Verification Remediation", + workflowAction: "pre-merge-remediation", + forWorkflowStepId: "browser-verification", + toolMode: "coding", + }, + }; +} + +export function codeReviewRemediationNode(column = "in-progress"): WorkflowIrNode { + return { + id: CODE_REVIEW_REMEDIATION_NODE_ID, + kind: "prompt", + column, + config: { + name: "Code Review Remediation", + workflowAction: "pre-merge-remediation", + forWorkflowStepId: "code-review", + toolMode: "coding", + }, + }; +} diff --git a/packages/core/src/builtin-workflows.ts b/packages/core/src/builtin-workflows.ts index 203836d3db..3496cca37b 100644 --- a/packages/core/src/builtin-workflows.ts +++ b/packages/core/src/builtin-workflows.ts @@ -10,6 +10,11 @@ import { browserVerificationOptionalGroupNode } from "./builtin-browser-verifica import { codeReviewOptionalGroupNode } from "./builtin-code-review-group.js"; import { completionSummaryNode } from "./builtin-completion-summary-node.js"; import { planReviewOptionalGroupNode } from "./builtin-plan-review-group.js"; +import { + browserVerificationRemediationNode, + codeReviewRemediationNode, + planReplanNode, +} from "./builtin-workflow-remediation-nodes.js"; import type { WorkflowDefinition } from "./workflow-definition-types.js"; import type { WorkflowIr, WorkflowIrColumn, WorkflowIrNode } from "./workflow-ir-types.js"; import { parseWorkflowIr } from "./workflow-ir.js"; @@ -121,19 +126,39 @@ function linear(spec: BuiltinSpec): WorkflowDefinition { ? withEngineeringOptionalGroups(spec.nodes, spec.engineeringOptionalGroups) : spec.nodes; const workflowNodes = withCompletionSummaryNode(specNodes); + const hasPlanReview = workflowNodes.some((node) => node.id === "plan-review"); + const hasBrowserVerification = workflowNodes.some((node) => node.id === "browser-verification"); + const hasCodeReview = workflowNodes.some((node) => node.id === "code-review"); + const remediationNodes = hasPlanReview || hasBrowserVerification || hasCodeReview + ? [ + ...(hasPlanReview ? [planReplanNode("triage")] : []), + ...(hasBrowserVerification ? [browserVerificationRemediationNode("in-progress")] : []), + ...(hasCodeReview ? [codeReviewRemediationNode("in-progress")] : []), + ] + : []; const nodes: WorkflowIr["nodes"] = [ { id: "start", kind: "start" }, ...workflowNodes, + ...remediationNodes, { id: "end", kind: "end" }, ]; const edges: WorkflowIr["edges"] = []; - for (let i = 0; i < nodes.length - 1; i += 1) { - edges.push({ from: nodes[i].id, to: nodes[i + 1].id, condition: "success" }); + const successPathNodes: WorkflowIr["nodes"] = [{ id: "start", kind: "start" }, ...workflowNodes, { id: "end", kind: "end" }]; + for (let i = 0; i < successPathNodes.length - 1; i += 1) { + edges.push({ from: successPathNodes[i].id, to: successPathNodes[i + 1].id, condition: "success" }); } // Seam nodes also fail straight to end (mirrors the legacy pipeline). for (const node of workflowNodes) { if ((typeof node.config?.seam === "string" || node.kind === "optional-group") && node.config?.summaryTarget !== "task") { - edges.push({ from: node.id, to: "end", condition: "failure" }); + const failureTarget = + node.id === "plan-review" + ? "plan-replan" + : node.id === "browser-verification" + ? "browser-verification-remediation" + : node.id === "code-review" + ? "code-review-remediation" + : "end"; + edges.push({ from: node.id, to: failureTarget, condition: "failure" }); } } const layout: Record = {}; @@ -223,10 +248,13 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ start: { x: 60, y: 160 }, plan: { x: 230, y: 160 }, "plan-review": { x: 400, y: 160 }, + "plan-replan": { x: 400, y: 320 }, parse: { x: 570, y: 160 }, steps: { x: 740, y: 160 }, "browser-verification": { x: 910, y: 160 }, + "browser-verification-remediation": { x: 910, y: 320 }, "code-review": { x: 1080, y: 160 }, + "code-review-remediation": { x: 1080, y: 320 }, "completion-summary": { x: 1250, y: 160 }, "merge-gate": { x: 1420, y: 160 }, "branch-group-member-integration": { x: 1590, y: 80 }, @@ -254,9 +282,12 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ start: { x: 60, y: 160 }, planning: { x: 230, y: 160 }, "plan-review": { x: 400, y: 160 }, + "plan-replan": { x: 400, y: 320 }, execute: { x: 570, y: 160 }, "browser-verification": { x: 740, y: 160 }, + "browser-verification-remediation": { x: 740, y: 320 }, "code-review": { x: 910, y: 160 }, + "code-review-remediation": { x: 910, y: 320 }, "completion-summary": { x: 1080, y: 160 }, review: { x: 1250, y: 160 }, "merge-gate": { x: 1420, y: 160 }, @@ -463,11 +494,14 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ start: { x: 60, y: 160 }, plan: { x: 230, y: 160 }, "plan-review": { x: 400, y: 160 }, + "plan-replan": { x: 400, y: 320 }, parse: { x: 570, y: 160 }, steps: { x: 740, y: 160 }, "rework-hold": { x: 740, y: 320 }, "browser-verification": { x: 910, y: 160 }, + "browser-verification-remediation": { x: 910, y: 320 }, "code-review": { x: 1080, y: 160 }, + "code-review-remediation": { x: 1080, y: 320 }, "completion-summary": { x: 1250, y: 160 }, review: { x: 1420, y: 160 }, "merge-gate": { x: 1590, y: 160 }, diff --git a/packages/engine/src/__tests__/workflow-graph-optional-group.test.ts b/packages/engine/src/__tests__/workflow-graph-optional-group.test.ts index 0f96e96441..edb8b5d8ce 100644 --- a/packages/engine/src/__tests__/workflow-graph-optional-group.test.ts +++ b/packages/engine/src/__tests__/workflow-graph-optional-group.test.ts @@ -758,7 +758,11 @@ describe("WorkflowGraphExecutor optional-group", () => { expect(node?.config?.phase).toBeUndefined(); expect(ir.edges).toEqual(expect.arrayContaining([ expect.objectContaining({ from: groupId, to: groupId === "browser-verification" ? "code-review" : "completion-summary", condition: "success" }), - expect.objectContaining({ from: groupId, to: "end", condition: "failure" }), + expect.objectContaining({ + from: groupId, + to: groupId === "browser-verification" ? "browser-verification-remediation" : "code-review-remediation", + condition: "failure", + }), ])); } } diff --git a/packages/engine/src/workflow-graph-executor.ts b/packages/engine/src/workflow-graph-executor.ts index c8cf6238cf..70e123ff78 100644 --- a/packages/engine/src/workflow-graph-executor.ts +++ b/packages/engine/src/workflow-graph-executor.ts @@ -55,6 +55,16 @@ export interface WorkflowNodeResult { contextPatch?: Record; } +interface PreMergeOptionalStepFailureContext { + stepName: string; + feedback: string; + phase: WorkflowStepResult["phase"]; + status: WorkflowStepResult["status"]; + verdict?: string; + nodeId?: string; + maxRevisions?: unknown; +} + export interface WorkflowTaskProjection { modifiedFiles?: string[]; mergeDetails?: { @@ -233,6 +243,10 @@ function isMergeRegionKind(kind: WorkflowIrNodeKind): boolean { return MERGE_REGION_KINDS.has(kind); } +function optionalStepFailureContextKey(stepId: string): string { + return `workflow:optional-step-failure:${stepId}`; +} + function normalizeTouchedFile(value: unknown): string | undefined { if (typeof value === "string") { const trimmed = value.trim().replaceAll("\\", "/").replace(/^\.\//, ""); @@ -743,7 +757,7 @@ export class WorkflowGraphExecutor { || (node.id === PLAN_REVIEW_GROUP_ID ? "Plan Review failed before execution. Re-run triage to revise PROMPT.md before implementation continues." : "(no feedback captured)"); - const fixScheduled = await this.deps.requestPreMergeOptionalStepFix?.(task.id, { + const failureContext: PreMergeOptionalStepFailureContext = { stepName: groupName, feedback, phase: stepPhase, @@ -751,7 +765,27 @@ export class WorkflowGraphExecutor { verdict: verdict ?? (node.id === PLAN_REVIEW_GROUP_ID ? "REVISE" : undefined), nodeId: node.id, maxRevisions: node.config?.maxRevisions, + }; + context[optionalStepFailureContextKey(node.id)] = failureContext; + /* + * FNXC:WorkflowRemediation 2026-06-29-16:22: + * New built-in and custom workflows can author an explicit failure edge + * from an optional review gate to a remediation/replan node. When such a + * node exists, traversal owns the handoff so the workflow definition shows + * the lifecycle policy. Older stored specs without that node keep the + * compatibility scheduler here. + */ + const remediationRouteSource: WorkflowNodeResult = { outcome: "failure", value: result.value }; + const explicitWorkflowRemediationRoute = (outgoingMap.get(node.id) ?? []).some((edge) => { + if (!this.shouldTraverseEdge(edge, remediationRouteSource)) return false; + const target = nodeMap.get(edge.to); + const action = target?.config?.workflowAction; + return action === "plan-replan" || action === "pre-merge-remediation"; }); + if (explicitWorkflowRemediationRoute) { + return await traverseChildren(node, remediationRouteSource); + } + const fixScheduled = await this.deps.requestPreMergeOptionalStepFix?.(task.id, failureContext); if (fixScheduled) { context[`node:${node.id}:fixScheduled`] = true; return { outcome: "success", value: "pre-merge-optional-step-fix-scheduled" }; @@ -760,6 +794,34 @@ export class WorkflowGraphExecutor { return await traverseChildren(node, result); } + const workflowAction = node.config?.workflowAction; + if (workflowAction === "plan-replan" || workflowAction === "pre-merge-remediation") { + const stepId = typeof node.config?.forWorkflowStepId === "string" + ? node.config.forWorkflowStepId + : undefined; + const failureContext = stepId + ? context[optionalStepFailureContextKey(stepId)] as PreMergeOptionalStepFailureContext | undefined + : undefined; + if (!failureContext) { + return { outcome: "failure", value: "missing-remediation-context" }; + } + const scheduled = await this.deps.requestPreMergeOptionalStepFix?.(task.id, failureContext); + if (!scheduled) { + return { outcome: "failure", value: "remediation-not-scheduled" }; + } + /* + * FNXC:WorkflowRemediation 2026-06-29-16:27: + * A remediation/replan node schedules asynchronous task work rather than + * fixing the branch inside this graph call. Stop traversal after a successful + * handoff so the rerun starts from fresh task state instead of immediately + * re-reviewing unchanged PROMPT.md or unchanged code. + */ + if (failureContext.nodeId) context[`node:${failureContext.nodeId}:fixScheduled`] = true; + context[`node:${node.id}:outcome`] = "success"; + context[`node:${node.id}:value`] = "remediation-scheduled"; + return { outcome: "success", value: "remediation-scheduled" }; + } + const result = await this.executeNodeWithRetries(node, task, settings, context, ir, this.deps.signal); if (result.contextPatch) Object.assign(context, result.contextPatch); context[`node:${node.id}:outcome`] = result.outcome;