fix(FN-7224): normalize built-in workflow review toggles
Expose Plan Review, Code Review, and Browser Verification optional groups across engineering built-ins, with Quick Fix defaulting all three off and Compound Engineering keeping CE code review inside the shared optional toggle. Fusion-Task-Id: FN-7224
This commit is contained in:
7
.changeset/fn-7224-builtin-workflow-options.md
Normal file
7
.changeset/fn-7224-builtin-workflow-options.md
Normal file
@@ -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.
|
||||
@@ -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<string, Record<string, boolean>> = {
|
||||
"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<string, unknown> }> } | 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<string, unknown> }> } | undefined;
|
||||
expect(String(docReviewTemplate?.nodes?.[0]?.config?.prompt ?? "")).toContain("/ce-doc-review");
|
||||
const codeReviewTemplate = byId("code-review")?.config?.template as { nodes?: Array<{ config?: Record<string, unknown> }> } | 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<string, unknown> }> } | 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<string, unknown> }> } | 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<string, string[]> = {
|
||||
"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"],
|
||||
};
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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<string, unknown> }>;
|
||||
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["engineeringOptionalGroups"]>,
|
||||
): 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",
|
||||
|
||||
Reference in New Issue
Block a user