fix(FN-7233): route workflow reviews through remediation nodes
This commit is contained in:
7
.changeset/fn-workflow-remediation-nodes.md
Normal file
7
.changeset/fn-workflow-remediation-nodes.md
Normal file
@@ -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.
|
||||
@@ -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" }),
|
||||
]),
|
||||
);
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
|
||||
@@ -60,7 +60,7 @@ export function planReviewOptionalGroupNode(
|
||||
description: PLAN_REVIEW_DESCRIPTION,
|
||||
prompt: PLAN_REVIEW_PROMPT,
|
||||
toolMode: "readonly",
|
||||
gateMode: "advisory",
|
||||
gateMode: "gate",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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" },
|
||||
|
||||
52
packages/core/src/builtin-workflow-remediation-nodes.ts
Normal file
52
packages/core/src/builtin-workflow-remediation-nodes.ts
Normal file
@@ -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",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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<string, { x: number; y: number }> = {};
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,16 @@ export interface WorkflowNodeResult {
|
||||
contextPatch?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user