FN-6028: add retries to builtin execute node

Ensure the built-in coding workflow keeps an explicit retry budget on execute.

- set `maxRetries: 2` on the built-in coding workflow execute prompt node
- add IR-level tests that assert only the execute seam carries the retry budget
- add built-in workflow registry tests that preserve the execute retry setting through parse/serialize round-trips

Files changed:
 .../__tests__/builtin-coding-workflow-ir.test.ts   | 31 ++++++++++++++++++++++
 .../core/src/__tests__/builtin-workflows.test.ts   | 27 +++++++++++++++++++
 packages/core/src/builtin-coding-workflow-ir.ts    |  7 ++++-
 3 files changed, 64 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-6028

Fusion-Task-Lineage: cd9fb5ac-c593-438d-aeff-423b4a95fc31
This commit is contained in:
gsxdsm
2026-06-08 12:34:42 -07:00
parent f77aa073c1
commit 7d8ae5333e
3 changed files with 64 additions and 1 deletions

View File

@@ -6,6 +6,17 @@ import {
serializeWorkflowIr,
} from "../index.js";
const EXECUTE_NODE_MAX_RETRIES = 2;
function executeNodeConfig(ir = BUILTIN_CODING_WORKFLOW_IR): Record<string, unknown> {
const executeNodes = ir.nodes.filter((node) => node.id === "execute" && node.config?.seam === "execute");
expect(executeNodes).toHaveLength(1);
const config = executeNodes[0].config;
expect(config).toBeDefined();
expect(Object.keys(config ?? {})).not.toHaveLength(0);
return config ?? {};
}
describe("builtin coding workflow ir", () => {
it("parses and round-trips", () => {
const parsed = parseWorkflowIr(BUILTIN_CODING_WORKFLOW_IR);
@@ -65,4 +76,24 @@ describe("builtin coding workflow ir", () => {
expect(byId.get("review")?.config?.name).toBe("Review");
expect(byId.get("merge")?.config?.name).toBe("Merge boundary");
});
it("declares a bounded retry budget only on the execute seam", () => {
const config = executeNodeConfig();
expect(config.maxRetries).toBe(EXECUTE_NODE_MAX_RETRIES);
expect(Number.isInteger(config.maxRetries)).toBe(true);
expect(config.maxRetries).toBeGreaterThanOrEqual(1);
expect(config.maxRetries).toBeLessThanOrEqual(10);
const byId = new Map(BUILTIN_CODING_WORKFLOW_IR.nodes.map((n) => [n.id, n]));
expect(byId.get("review")?.config?.name).toBe("Review");
expect(byId.get("merge")?.config?.name).toBe("Merge boundary");
expect(byId.get("review")?.config?.maxRetries).toBeUndefined();
expect(byId.get("merge")?.config?.maxRetries).toBeUndefined();
});
it("preserves the execute retry declaration through parse/serialize round-trip", () => {
const reparsed = parseWorkflowIr(serializeWorkflowIr(BUILTIN_CODING_WORKFLOW_IR));
const config = executeNodeConfig(reparsed);
expect(config.maxRetries).toBe(EXECUTE_NODE_MAX_RETRIES);
});
});

View File

@@ -7,6 +7,8 @@ import { compileWorkflowToSteps } from "../workflow-compiler.js";
import { DEFAULT_WORKFLOW_COLUMN_IDS, parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.js";
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
const EXECUTE_NODE_MAX_RETRIES = 2;
describe("built-in workflows", () => {
// Graph-only built-ins (step inversion, KTD-9) model branching/foreach/rework
// structure the linear compiler cannot lower to a step list — they run only
@@ -87,6 +89,31 @@ describe("built-in workflows", () => {
]);
});
it("builtin:coding exposes execute retries after registry lookup and parse round-trip", () => {
const coding = getBuiltinWorkflow("builtin:coding");
expect(coding).toBeDefined();
const ir = parseWorkflowIr(coding!.ir);
const reparsed = parseWorkflowIr(serializeWorkflowIr(ir));
for (const candidate of [ir, reparsed]) {
const executeNodes = candidate.nodes.filter((node) => node.id === "execute" && node.config?.seam === "execute");
expect(executeNodes).toHaveLength(1);
const executeConfig = executeNodes[0].config;
expect(executeConfig).toBeDefined();
expect(Object.keys(executeConfig ?? {})).not.toHaveLength(0);
expect(executeConfig?.maxRetries).toBe(EXECUTE_NODE_MAX_RETRIES);
expect(Number.isInteger(executeConfig?.maxRetries)).toBe(true);
expect(executeConfig?.maxRetries).toBeGreaterThanOrEqual(1);
expect(executeConfig?.maxRetries).toBeLessThanOrEqual(10);
const byId = new Map(candidate.nodes.map((node) => [node.id, node]));
expect(byId.get("review")?.config?.name).toBe("Review");
expect(byId.get("merge")?.config?.name).toBe("Merge boundary");
expect(byId.get("review")?.config?.maxRetries).toBeUndefined();
expect(byId.get("merge")?.config?.maxRetries).toBeUndefined();
}
});
it("builtin:coding exposes merge-blocker and human-review traits on in-review", () => {
const coding = getBuiltinWorkflow("builtin:coding");
expect(coding).toBeDefined();

View File

@@ -47,7 +47,12 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = {
],
nodes: [
{ id: "start", kind: "start", column: "triage" },
{ id: "execute", kind: "prompt", column: "in-progress", config: builtinPromptConfig("execute", "Execute") },
{
id: "execute",
kind: "prompt",
column: "in-progress",
config: { ...builtinPromptConfig("execute", "Execute"), maxRetries: 2 },
},
{ id: "review", kind: "prompt", column: "in-review", config: builtinPromptConfig("review", "Review") },
{ id: "merge", kind: "prompt", column: "in-review", config: builtinPromptConfig("merge", "Merge boundary") },
{ id: "end", kind: "end", column: "done" },