diff --git a/.changeset/fn-7224-builtin-workflow-options.md b/.changeset/fn-7224-builtin-workflow-options.md new file mode 100644 index 0000000000..5ff64ef7c2 --- /dev/null +++ b/.changeset/fn-7224-builtin-workflow-options.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Add consistent Plan Review, Code Review, and Browser Verification toggles to engineering workflows. +category: fix +dev: Quick Fix seeds all three optional groups off; other engineering built-ins seed plan/code review on and browser verification off. diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index c4a555e02c..68ff9e470d 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -10,6 +10,7 @@ import { } from "../builtin-workflows.js"; import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; import { BROWSER_VERIFICATION_GROUP_ID, BROWSER_VERIFICATION_STEP_NODE_ID } from "../builtin-browser-verification-group.js"; +import { CODE_REVIEW_STEP_NODE_ID } from "../builtin-code-review-group.js"; import { PLAN_REVIEW_GROUP_ID, PLAN_REVIEW_STEP_NODE_ID } from "../builtin-plan-review-group.js"; import { builtinPromptConfig, BUILTIN_SEAM_PROMPTS } from "../builtin-workflow-prompts.js"; import { BUILTIN_WORKFLOW_SETTINGS } from "../builtin-workflow-settings.js"; @@ -52,7 +53,11 @@ describe("built-in workflows", () => { const NON_COMPILABLE_BUILTIN_IDS = new Set([ "builtin:coding", "builtin:legacy-coding", + "builtin:quick-fix", + "builtin:review-heavy", + "builtin:design", "builtin:marketing", + "builtin:compound-engineering", "builtin:stepwise-coding", "builtin:pr-workflow", ]); @@ -68,6 +73,34 @@ describe("built-in workflows", () => { } }); + it("engineering built-ins expose plan, code, and browser optional groups with expected defaults", () => { + const expectedDefaults: Record> = { + "builtin:coding": { "plan-review": true, "code-review": true, "browser-verification": false }, + "builtin:legacy-coding": { "plan-review": true, "code-review": true, "browser-verification": false }, + "builtin:quick-fix": { "plan-review": false, "code-review": false, "browser-verification": false }, + "builtin:review-heavy": { "plan-review": true, "code-review": true, "browser-verification": false }, + "builtin:design": { "plan-review": true, "code-review": true, "browser-verification": false }, + "builtin:compound-engineering": { "plan-review": true, "code-review": true, "browser-verification": false }, + "builtin:stepwise-coding": { "plan-review": true, "code-review": true, "browser-verification": false }, + }; + + for (const [workflowId, defaults] of Object.entries(expectedDefaults)) { + const workflow = getBuiltinWorkflow(workflowId)!; + const byId = new Map(workflow.ir.nodes.map((node) => [node.id, node])); + for (const [groupId, defaultOn] of Object.entries(defaults)) { + const group = byId.get(groupId); + expect(group?.kind, `${workflowId}:${groupId}`).toBe("optional-group"); + expect(group?.config?.defaultOn, `${workflowId}:${groupId}`).toBe(defaultOn); + } + const nodeOrder = workflow.ir.nodes.map((node) => node.id); + const executionBoundary = nodeOrder.includes("execute") ? nodeOrder.indexOf("execute") : nodeOrder.indexOf("steps"); + expect(executionBoundary, workflowId).toBeGreaterThanOrEqual(0); + expect(nodeOrder.indexOf("plan-review"), workflowId).toBeLessThan(executionBoundary); + expect(nodeOrder.indexOf("browser-verification"), workflowId).toBeGreaterThan(executionBoundary); + expect(nodeOrder.indexOf("code-review"), workflowId).toBeGreaterThan(nodeOrder.indexOf("browser-verification")); + } + }); + it("does not expose lowercase Code review step names in built-in workflow nodes", () => { for (const workflow of BUILTIN_WORKFLOWS) { for (const node of workflow.ir.nodes) { @@ -446,10 +479,9 @@ describe("built-in workflows", () => { expect(design).toBeDefined(); expect(design!.kind).toBe("workflow"); expect(() => parseWorkflowIr(design!.ir)).not.toThrow(); - expect(() => compileWorkflowToSteps(design!.ir)).not.toThrow(); const authoredNodeIds = design!.ir.nodes.filter((node) => node.id !== "start" && node.id !== "end").map((node) => node.id); - expect(authoredNodeIds).toEqual(["execute", "design-review", "review", "merge"]); + expect(authoredNodeIds).toEqual(["plan-review", "execute", "browser-verification", "code-review", "design-review", "review", "merge"]); const execute = design!.ir.nodes.find((node) => node.id === "execute"); expect(execute?.config?.seam).toBe("execute"); @@ -605,17 +637,13 @@ describe("built-in workflows", () => { } }); - it("compound-engineering compiles exactly one ce-code-review step and no generic review seam", () => { + it("compound-engineering exposes ce-code-review as the optional Code Review group and no generic review seam", () => { const ce = getBuiltinWorkflow("builtin:compound-engineering")!; - const steps = compileWorkflowToSteps(ce.ir); - // plan + execute (ce-work) + code-review (pre-merge) + commit-pr + - // resolve-feedback + document (post-merge) — merge seams and graph-native - // optional-groups are skipped by the legacy step compiler. - expect(steps.length).toBeGreaterThanOrEqual(6); - expect(steps.some((s) => s.name === "Plan")).toBe(true); - expect(steps.filter((s) => s.skillName === "compound-engineering:ce-code-review")).toHaveLength(1); - expect(steps.some((s) => s.name === "CE Doc Review")).toBe(false); - expect(steps.some((s) => s.name === "Review" && !s.skillName)).toBe(false); + const codeReview = ce.ir.nodes.find((node) => node.id === "code-review"); + const template = codeReview?.config?.template as { nodes?: Array<{ id: string; config?: Record }> } | undefined; + expect(codeReview?.kind).toBe("optional-group"); + expect(template?.nodes?.filter((node) => node.config?.skillName === "compound-engineering:ce-code-review")).toHaveLength(1); + expect(ce.ir.nodes.some((node) => node.config?.seam === "review")).toBe(false); }); it("compound-engineering runs ce-work for the execute step in coding mode", () => { @@ -625,11 +653,7 @@ describe("built-in workflows", () => { const executeNode = ce.ir.nodes.find((n) => n.id === "execute"); expect(executeNode?.config?.executor).toBe("skill"); expect(executeNode?.config?.skillName).toBe("compound-engineering:ce-work"); - // The compiled step runs in coding mode so write/spawn tools are available. - const steps = compileWorkflowToSteps(ce.ir); - const execute = steps.find((s) => s.name === "Execute"); - expect(execute).toBeDefined(); - expect(execute!.toolMode).toBe("coding"); + expect(executeNode?.config?.toolMode).toBe("coding"); }); it("compound-engineering skill-node prompts name their /ce- slash commands", () => { @@ -638,7 +662,6 @@ describe("built-in workflows", () => { const expectedPrompts = new Map([ ["plan", "/ce-plan"], ["execute", "/ce-work"], - ["code-review", "/ce-code-review"], ["commit-pr", "/ce-commit-push-pr"], ["resolve-feedback", "/ce-resolve-pr-feedback"], ["document", "/ce-compound"], @@ -649,6 +672,8 @@ describe("built-in workflows", () => { } const docReviewTemplate = byId("ce-doc-review")?.config?.template as { nodes?: Array<{ config?: Record }> } | undefined; expect(String(docReviewTemplate?.nodes?.[0]?.config?.prompt ?? "")).toContain("/ce-doc-review"); + const codeReviewTemplate = byId("code-review")?.config?.template as { nodes?: Array<{ config?: Record }> } | undefined; + expect(String(codeReviewTemplate?.nodes?.[0]?.config?.prompt ?? "")).toContain("/ce-code-review"); expect(String(byId("merge")?.config?.prompt ?? "")).not.toContain("/ce-"); }); @@ -678,7 +703,9 @@ describe("built-in workflows", () => { expect(authoredNodeIds).toEqual([ "plan", "ce-doc-review", + "plan-review", "execute", + "browser-verification", "code-review", "commit-pr", "resolve-feedback", @@ -703,11 +730,19 @@ describe("built-in workflows", () => { }); const codeReview = byId("code-review"); - expect(codeReview?.kind).toBe("gate"); + expect(codeReview?.kind).toBe("optional-group"); expect(codeReview?.config?.name).toBe("Code Review"); - expect(codeReview?.config?.skillName).toBe("compound-engineering:ce-code-review"); - expect(codeReview?.config?.gateMode).toBe("gate"); - expect(codeReview?.config?.toolMode).toBe("coding"); + expect(codeReview?.config?.defaultOn).toBe(true); + const codeReviewTemplate = codeReview?.config?.template as { nodes?: Array<{ id: string; kind: string; config?: Record }> } | undefined; + expect(codeReviewTemplate?.nodes?.[0]).toMatchObject({ + id: CODE_REVIEW_STEP_NODE_ID, + kind: "gate", + config: { + skillName: "compound-engineering:ce-code-review", + gateMode: "gate", + toolMode: "coding", + }, + }); const layout = ce.layout ?? {}; expect(Object.keys(layout).sort()).toEqual(ce.ir.nodes.map((node) => node.id).sort()); @@ -715,8 +750,10 @@ describe("built-in workflows", () => { expect(layout[ce.ir.nodes[i].id].x - layout[ce.ir.nodes[i - 1].id].x).toBe(170); } expect(ce.ir.edges.some((edge) => edge.from === "plan" && edge.to === "ce-doc-review")).toBe(true); - expect(ce.ir.edges.some((edge) => edge.from === "ce-doc-review" && edge.to === "execute")).toBe(true); - expect(ce.ir.edges.some((edge) => edge.from === "execute" && edge.to === "code-review")).toBe(true); + expect(ce.ir.edges.some((edge) => edge.from === "ce-doc-review" && edge.to === "plan-review")).toBe(true); + expect(ce.ir.edges.some((edge) => edge.from === "plan-review" && edge.to === "execute")).toBe(true); + expect(ce.ir.edges.some((edge) => edge.from === "execute" && edge.to === "browser-verification")).toBe(true); + expect(ce.ir.edges.some((edge) => edge.from === "browser-verification" && edge.to === "code-review")).toBe(true); expect(ce.ir.edges.some((edge) => edge.from === "code-review" && edge.to === "commit-pr")).toBe(true); }); @@ -738,20 +775,12 @@ describe("built-in workflows", () => { // U4: fan-out steps (plan, code-review) need coding so fn_spawn_agent is // available for persona fan-out; document needs coding to WRITE docs/solutions. expect(byId("plan")?.config?.toolMode).toBe("coding"); - expect(byId("code-review")?.config?.toolMode).toBe("coding"); expect(byId("document")?.config?.toolMode).toBe("coding"); - // U1: the compiler carries each node's skillName onto the materialized step so - // the step session can actually LOAD the skill (not just name it in prompt text). - const steps = compileWorkflowToSteps(ce.ir); - const plan = steps.find((s) => s.name === "Plan"); - expect(plan?.skillName).toBe("compound-engineering:ce-plan"); - expect(plan?.toolMode).toBe("coding"); - const codeReviewSteps = steps.filter((s) => s.skillName === "compound-engineering:ce-code-review"); - expect(codeReviewSteps).toHaveLength(1); - expect(codeReviewSteps[0].gateMode).toBe("gate"); - expect(codeReviewSteps[0].toolMode).toBe("coding"); - const document = steps.find((s) => s.skillName === "compound-engineering:ce-compound"); - expect(document?.toolMode).toBe("coding"); + const codeReview = byId("code-review"); + const template = codeReview?.config?.template as { nodes?: Array<{ config?: Record }> } | undefined; + expect(template?.nodes?.[0]?.config?.skillName).toBe("compound-engineering:ce-code-review"); + expect(template?.nodes?.[0]?.config?.gateMode).toBe("gate"); + expect(template?.nodes?.[0]?.config?.toolMode).toBe("coding"); }); describe("store integration", () => { @@ -855,7 +884,7 @@ describe("built-in workflows", () => { // skipped the gate). const expectedGroups: Record = { "builtin:coding": ["plan-review", "code-review"], - "builtin:legacy-coding": ["code-review"], + "builtin:legacy-coding": ["plan-review", "code-review"], "builtin:marketing": [], "builtin:stepwise-coding": ["plan-review", "code-review"], }; diff --git a/packages/core/src/__tests__/workflow-optional-steps.test.ts b/packages/core/src/__tests__/workflow-optional-steps.test.ts index 652e4dbc7d..f6d50e0a3f 100644 --- a/packages/core/src/__tests__/workflow-optional-steps.test.ts +++ b/packages/core/src/__tests__/workflow-optional-steps.test.ts @@ -110,10 +110,17 @@ describe("resolveWorkflowOptionalSteps (optional-group nodes)", () => { }); it("resolves the built-in coding/stepwise optional-groups in execution order", () => { - // Legacy coding carries two optional-group toggles on the pre-merge path: - // `browser-verification` (default OFF) then `code-review` (default ON — runs by default - // but is toggleable off per task). - const legacyExpected = [ + // Legacy/default/stepwise engineering workflows expose the same three toggles + // in execution order: plan review before execution, then browser verification + // and code review after implementation. + const engineeringExpected = [ + { + templateId: "plan-review", + name: "Plan Review", + description: "", + phase: "pre-merge" as const, + defaultOn: true, + }, { templateId: "browser-verification", name: "Browser Verification", @@ -129,19 +136,9 @@ describe("resolveWorkflowOptionalSteps (optional-group nodes)", () => { defaultOn: true, }, ]; - const stepwiseExpected = [ - { - templateId: "plan-review", - name: "Plan Review", - description: "", - phase: "pre-merge" as const, - defaultOn: true, - }, - ...legacyExpected, - ]; - expect(resolveWorkflowOptionalSteps(BUILTIN_CODING_WORKFLOW_IR)).toEqual(legacyExpected); - expect(resolveWorkflowOptionalSteps(BUILTIN_STEPWISE_CODING_WORKFLOW_IR)).toEqual(stepwiseExpected); - expect(resolveWorkflowOptionalSteps(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)).toEqual(stepwiseExpected); + expect(resolveWorkflowOptionalSteps(BUILTIN_CODING_WORKFLOW_IR)).toEqual(engineeringExpected); + expect(resolveWorkflowOptionalSteps(BUILTIN_STEPWISE_CODING_WORKFLOW_IR)).toEqual(engineeringExpected); + expect(resolveWorkflowOptionalSteps(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)).toEqual(engineeringExpected); }); it("orders Plan Review before execution even when the node is appended after Code Review", () => { @@ -169,7 +166,7 @@ describe("resolveWorkflowOptionalSteps (optional-group nodes)", () => { it("seeds default-on optional groups but not browser-verification for the built-ins", () => { // resolveDefaultOnOptionalGroupIds drives which groups a new task gets enabled by // default: review groups are on, browser-verification is off. - expect(resolveDefaultOnOptionalGroupIds(BUILTIN_CODING_WORKFLOW_IR)).toEqual(["code-review"]); + expect(resolveDefaultOnOptionalGroupIds(BUILTIN_CODING_WORKFLOW_IR)).toEqual(["plan-review", "code-review"]); expect(resolveDefaultOnOptionalGroupIds(BUILTIN_STEPWISE_CODING_WORKFLOW_IR)).toEqual(["plan-review", "code-review"]); expect(resolveDefaultOnOptionalGroupIds(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)).toEqual(["plan-review", "code-review"]); }); diff --git a/packages/core/src/builtin-browser-verification-group.ts b/packages/core/src/builtin-browser-verification-group.ts index a4979c1e36..f5eb3f3911 100644 --- a/packages/core/src/builtin-browser-verification-group.ts +++ b/packages/core/src/builtin-browser-verification-group.ts @@ -84,14 +84,17 @@ Note: Refs (@e1, @e2) are invalidated after page navigation. Re-snapshot after c * Mirrors `stepTemplateToNode(browser-verification)`: a single `prompt` node whose * config carries the inlined prompt + `toolMode: "coding"` + `gateMode: "advisory"`. */ -export function browserVerificationOptionalGroupNode(column: string): WorkflowIrNode { +export function browserVerificationOptionalGroupNode( + column: string, + options: { defaultOn?: boolean } = {}, +): WorkflowIrNode { return { id: BROWSER_VERIFICATION_GROUP_ID, kind: "optional-group", column, config: { name: BROWSER_VERIFICATION_NAME, - defaultOn: false, + defaultOn: options.defaultOn ?? false, template: { nodes: [ { diff --git a/packages/core/src/builtin-code-review-group.ts b/packages/core/src/builtin-code-review-group.ts index c099a0e629..7d0554c408 100644 --- a/packages/core/src/builtin-code-review-group.ts +++ b/packages/core/src/builtin-code-review-group.ts @@ -73,7 +73,10 @@ Be specific: cite \`file:line\` for every finding and explain the concrete failu * Mirrors `stepTemplateToNode(code-review)`: a single `prompt` node whose config carries * the inlined prompt + `toolMode: "readonly"` + `gateMode: "advisory"`. */ -export function codeReviewOptionalGroupNode(column: string): WorkflowIrNode { +export function codeReviewOptionalGroupNode( + column: string, + options: { defaultOn?: boolean } = {}, +): WorkflowIrNode { return { id: CODE_REVIEW_GROUP_ID, kind: "optional-group", @@ -82,7 +85,7 @@ export function codeReviewOptionalGroupNode(column: string): WorkflowIrNode { name: CODE_REVIEW_NAME, // Default-ON: runs for every coding task by default, but operators can toggle it // off per task (remove `code-review` from enabledWorkflowSteps). - defaultOn: true, + defaultOn: options.defaultOn ?? true, template: { nodes: [ { diff --git a/packages/core/src/builtin-coding-workflow-ir.ts b/packages/core/src/builtin-coding-workflow-ir.ts index 741189fc0c..4800c6554e 100644 --- a/packages/core/src/builtin-coding-workflow-ir.ts +++ b/packages/core/src/builtin-coding-workflow-ir.ts @@ -4,6 +4,7 @@ import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js"; import { builtinPromptConfig } from "./builtin-workflow-prompts.js"; import { browserVerificationOptionalGroupNode } from "./builtin-browser-verification-group.js"; import { codeReviewOptionalGroupNode } from "./builtin-code-review-group.js"; +import { planReviewOptionalGroupNode } from "./builtin-plan-review-group.js"; /** * The built-in default workflow as a v2 IR. Its six columns have ids that are @@ -68,6 +69,7 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = { column: "triage", config: builtinPromptConfig("planning", "Plan / specify"), }, + planReviewOptionalGroupNode("in-progress"), { id: "execute", kind: "prompt", @@ -104,7 +106,8 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = { ], edges: [ { from: "start", to: "planning" }, - { from: "planning", to: "execute", condition: "success" }, + { from: "planning", to: "plan-review", condition: "success" }, + { from: "plan-review", to: "execute", condition: "success" }, // execute → browser-verification (optional-group) → review. When the group is // disabled it passes through with outcome=success and routes straight to review. { from: "execute", to: "browser-verification", condition: "success" }, @@ -126,6 +129,7 @@ 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: "execute", to: "end", condition: "failure" }, { from: "browser-verification", to: "end", condition: "failure" }, { from: "code-review", 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 42db64554f..e19e21b06d 100644 --- a/packages/core/src/builtin-plan-review-group.ts +++ b/packages/core/src/builtin-plan-review-group.ts @@ -39,14 +39,17 @@ Be specific: cite the plan section or file path for every finding and explain th {"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}`; /** Build the `plan-review` optional-group node placed between planning and execution. */ -export function planReviewOptionalGroupNode(column: string): WorkflowIrNode { +export function planReviewOptionalGroupNode( + column: string, + options: { defaultOn?: boolean } = {}, +): WorkflowIrNode { return { id: PLAN_REVIEW_GROUP_ID, kind: "optional-group", column, config: { name: PLAN_REVIEW_NAME, - defaultOn: true, + defaultOn: options.defaultOn ?? true, template: { nodes: [ { diff --git a/packages/core/src/builtin-workflows.ts b/packages/core/src/builtin-workflows.ts index 4cc0f08285..8af2b92fb5 100644 --- a/packages/core/src/builtin-workflows.ts +++ b/packages/core/src/builtin-workflows.ts @@ -6,6 +6,9 @@ import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-w import { BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR } from "./builtin-stepwise-final-review-coding-workflow-ir.js"; import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js"; import { builtinPromptConfig } from "./builtin-workflow-prompts.js"; +import { browserVerificationOptionalGroupNode } from "./builtin-browser-verification-group.js"; +import { codeReviewOptionalGroupNode } from "./builtin-code-review-group.js"; +import { planReviewOptionalGroupNode } from "./builtin-plan-review-group.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"; @@ -35,6 +38,39 @@ export function defaultEnabledBuiltinWorkflowIds(): string[] { ).map((workflow) => workflow.id); } +function ceCodeReviewOptionalGroupNode(column: string): WorkflowIrNode { + return { + id: "code-review", + kind: "optional-group", + column, + config: { + /* + * FNXC:Workflows 2026-06-29-10:24: + * Compound Engineering uses the same optional Code Review toggle as other engineering built-ins, but the inner reviewer remains the CE skill. Keep the stable `code-review` group id so per-task toggles work uniformly while preserving the CE review implementation. + */ + name: "Code Review", + defaultOn: true, + template: { + nodes: [ + { + id: "code-review-step", + kind: "gate", + config: { + name: "Code Review", + executor: "skill", + skillName: "compound-engineering:ce-code-review", + gateMode: "gate", + toolMode: "coding", + prompt: "Run /ce-code-review to perform a structured code review of the changes. Block merge on P0/P1 findings.", + }, + }, + ], + edges: [], + }, + }, + }; +} + export function isBuiltinWorkflowEnabled(id: string, enabledIds?: readonly string[]): boolean { if (!isBuiltinWorkflowId(id)) return true; if (!enabledIds) return true; @@ -50,6 +86,11 @@ interface BuiltinSpec { description: string; /** Ordered node specs between start and end; seams use {seam}. */ nodes: Array<{ id: string; kind: WorkflowIr["nodes"][number]["kind"]; config?: Record }>; + engineeringOptionalGroups?: { + planReviewDefaultOn?: boolean; + codeReviewDefaultOn?: boolean; + browserVerificationDefaultOn?: boolean; + }; } function defaultColumnForLinearNode(node: WorkflowIrNode): string { @@ -75,9 +116,12 @@ function canonicalBuiltinWorkflowColumns(): WorkflowIrColumn[] { /** Build a linear IR (start → nodes… → end) with simple x-spaced layout. */ function linear(spec: BuiltinSpec): WorkflowDefinition { + const specNodes = spec.engineeringOptionalGroups + ? withEngineeringOptionalGroups(spec.nodes, spec.engineeringOptionalGroups) + : spec.nodes; const nodes: WorkflowIr["nodes"] = [ { id: "start", kind: "start" }, - ...spec.nodes, + ...specNodes, { id: "end", kind: "end" }, ]; const edges: WorkflowIr["edges"] = []; @@ -85,8 +129,8 @@ function linear(spec: BuiltinSpec): WorkflowDefinition { edges.push({ from: nodes[i].id, to: nodes[i + 1].id, condition: "success" }); } // Seam nodes also fail straight to end (mirrors the legacy pipeline). - for (const node of spec.nodes) { - if (typeof node.config?.seam === "string") { + for (const node of specNodes) { + if (typeof node.config?.seam === "string" || node.kind === "optional-group") { edges.push({ from: node.id, to: "end", condition: "failure" }); } } @@ -129,6 +173,26 @@ function linear(spec: BuiltinSpec): WorkflowDefinition { }; } +function withEngineeringOptionalGroups( + nodes: BuiltinSpec["nodes"], + options: NonNullable, +): BuiltinSpec["nodes"] { + const executeIndex = nodes.findIndex((node) => node.id === "execute"); + if (executeIndex < 0) return nodes; + /* + * FNXC:WorkflowBuiltins 2026-06-29-10:17: + * Engineering built-ins must expose the same operator toggles: Plan Review before execution, then Browser Verification and Code Review after implementation. Quick Fix keeps all three default-off; other engineering built-ins keep plan/code review default-on and browser verification default-off. + */ + return [ + ...nodes.slice(0, executeIndex), + planReviewOptionalGroupNode("in-progress", { defaultOn: options.planReviewDefaultOn ?? true }), + nodes[executeIndex], + browserVerificationOptionalGroupNode("in-progress", { defaultOn: options.browserVerificationDefaultOn ?? false }), + codeReviewOptionalGroupNode("in-progress", { defaultOn: options.codeReviewDefaultOn ?? true }), + ...nodes.slice(executeIndex + 1), + ]; +} + /** * Read-only built-in workflow templates. Selectable like any workflow; they * cannot be edited or deleted. In compile mode (flag off) only the custom @@ -188,6 +252,11 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ id: "builtin:quick-fix", name: "Quick fix (built-in)", description: "Implement and merge with no review step — for trivial, low-risk changes.", + engineeringOptionalGroups: { + planReviewDefaultOn: false, + codeReviewDefaultOn: false, + browserVerificationDefaultOn: false, + }, nodes: [ { id: "execute", kind: "prompt", config: builtinPromptConfig("execute", "Execute") }, { id: "merge", kind: "prompt", config: builtinPromptConfig("merge", "Merge boundary") }, @@ -197,6 +266,7 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ id: "builtin:review-heavy", name: "Review-heavy (built-in)", description: "Adds an extra security pass before merge, on top of the standard review.", + engineeringOptionalGroups: {}, nodes: [ { id: "execute", kind: "prompt", config: builtinPromptConfig("execute", "Execute") }, { id: "review", kind: "prompt", config: builtinPromptConfig("review", "Review") }, @@ -287,6 +357,7 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ }, }, }, + planReviewOptionalGroupNode("in-progress"), { id: "execute", kind: "prompt", @@ -301,29 +372,8 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ prompt: "Run /ce-work to execute the plan for this task, following existing patterns and maintaining quality throughout.", }, }, - { - id: "code-review", - kind: "gate", - config: { - /* - * FNXC:Workflows 2026-06-25-00:00: - * FN-7045 requires every built-in code-review step display name to be title-case "Code Review" so compound-engineering matches WORKFLOW_STEP_TEMPLATES and the optional Code Review group. - */ - name: "Code Review", - executor: "skill", - skillName: "compound-engineering:ce-code-review", - gateMode: "gate", - /* - * FNXC:Workflows 2026-06-21-00:00: - * FN-6891 requires the compound-engineering Review stage to invoke compound-engineering:ce-code-review directly. The prior generic reviewer seam was removed so CE review runs through the CE skill and still blocks merge as a gate. - */ - // Coding mode so ce-code-review can fan out to its reviewer-persona - // subagents via fn_spawn_agent. As a gate step it still emits the - // verdict JSON (KTD-6); it is not meant to write the tree (Risk-1). - toolMode: "coding", - prompt: "Run /ce-code-review to perform a structured code review of the changes. Block merge on P0/P1 findings.", - }, - }, + browserVerificationOptionalGroupNode("in-progress"), + ceCodeReviewOptionalGroupNode("in-progress"), { id: "commit-pr", kind: "prompt", @@ -418,6 +468,7 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ id: "builtin:design", name: "Design (built-in)", description: "Implement, then run a design/UX review gate before the standard review and merge — for UI-heavy work.", + engineeringOptionalGroups: {}, nodes: [ { id: "execute",