From b818eb20ad3a6303df9b123be66f4e477e36a263 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Mon, 24 Aug 2026 05:59:49 +0000 Subject: [PATCH] feat(FN-WF): add the Coding (Ideas) V2 workflow with review-column gates Selectable built-in `builtin:coding-ideas-v2`. It clones the Coding (Ideas) IR without mutating it, so the manual `ideas` intake (`autoTriage: false`) and the whole board shape are unchanged, and moves testing and documentation out of the planner's implementation checklist into visible review-column gates: in-progress : steps = implementation only in-review : verification -> documentation-delivery -> code-review -> completion-summary -> merge-gate -> merge Ordering is load-bearing, not cosmetic. `execute-workflow-graph.ts` refuses any write-capable node once a Code Review APPROVE exists, so that a passed review seals the tree and nothing unreviewed reaches main. `verification-step` and `documentation-delivery-step` are both write-capable and therefore run BEFORE the review; `completion-summary` is `toolMode: "readonly"` and runs after it, so the card blurb describes the state that was actually approved. Both remediation loops re-enter at `verification`, never at `code-review`: a REVISE replays verification AND documentation-delivery, so the docs and changeset are regenerated to include what the review demanded before it re-reads them. Documentation stays both current and reviewed. The planner is switched to the `planning-implementation-only` seam so it stops emitting "Testing & Verification" and "Documentation & Delivery" steps, which would otherwise duplicate the gates under identical names. Adds a ratchet running the production `workflowNodeRequiresWorktree` classifier over the success chain: it reports zero offenders here and correctly flags `documentation-delivery` on builtin:review-gated-coding, whose post-review ordering deadlocks every task once its review approves. --- .changeset/coding-ideas-v2-workflow.md | 7 ++ .../builtin-coding-ideas-v2-workflow.test.ts | 102 ++++++++++++++++++ packages/core/src/index.ts | 1 + .../builtin-coding-ideas-v2-workflow-ir.ts | 83 ++++++++++++++ .../core/src/workflows/builtin-workflows.ts | 43 ++++++++ .../coding-ideas-v2-review-seal.test.ts | 78 ++++++++++++++ 6 files changed, 314 insertions(+) create mode 100644 .changeset/coding-ideas-v2-workflow.md create mode 100644 packages/core/src/__tests__/builtin-coding-ideas-v2-workflow.test.ts create mode 100644 packages/core/src/workflows/builtin-coding-ideas-v2-workflow-ir.ts create mode 100644 packages/engine/src/__tests__/coding-ideas-v2-review-seal.test.ts diff --git a/.changeset/coding-ideas-v2-workflow.md b/.changeset/coding-ideas-v2-workflow.md new file mode 100644 index 0000000000..e7d2dfe688 --- /dev/null +++ b/.changeset/coding-ideas-v2-workflow.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add the Coding (Ideas) V2 workflow, with verification and documentation as visible review steps. +category: feature +dev: New selectable built-in `builtin:coding-ideas-v2` clones `BUILTIN_CODING_IDEAS_WORKFLOW_IR` without mutating it, keeps the manual `ideas` intake (`autoTriage: false`), and moves Testing/Verification and Documentation & Delivery out of the planner's implementation checklist into `in-review` gates: `steps → verification → documentation-delivery → code-review → completion-summary → merge-gate`. Both write-capable gates precede Code Review because `execute-workflow-graph.ts` refuses write-capable nodes once an APPROVE exists (`workspace-review-seal-required`); the readonly `completion-summary` runs after it. Remediation edges re-enter at `verification` so a REVISE replays documentation before re-review. `packages/engine/src/__tests__/coding-ideas-v2-review-seal.test.ts` runs the production `workflowNodeRequiresWorktree` classifier over the graph as a ratchet against re-introducing the ordering defect. diff --git a/packages/core/src/__tests__/builtin-coding-ideas-v2-workflow.test.ts b/packages/core/src/__tests__/builtin-coding-ideas-v2-workflow.test.ts new file mode 100644 index 0000000000..cb785a6beb --- /dev/null +++ b/packages/core/src/__tests__/builtin-coding-ideas-v2-workflow.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import { resolveRequiredPreMergeStepIds } from "../merge/required-pre-merge-steps.js"; +import { BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR } from "../workflows/builtin-coding-ideas-v2-workflow-ir.js"; +import { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "../workflows/builtin-coding-ideas-workflow-ir.js"; +import { getBuiltinWorkflow } from "../workflows/builtin-workflows.js"; +import { parseWorkflowIr, serializeWorkflowIr } from "../workflows/workflow-ir.js"; +import { resolveWorkflowOptionalSteps } from "../workflows/workflow-optional-steps.js"; + +/** The single-success-edge walk an executing task actually follows from a node. */ +function successChainFrom(start: string): string[] { + const chain: string[] = []; + const seen = new Set(); + let current: string | undefined = start; + while (current && !seen.has(current)) { + seen.add(current); + const edge = BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.edges + .find((candidate) => candidate.from === current && candidate.condition === "success"); + if (!edge) break; + chain.push(edge.to); + current = edge.to; + } + return chain; +} + +describe("builtin:coding-ideas-v2", () => { + it("is a selectable validated workflow that keeps the Ideas intake untouched", () => { + const workflow = getBuiltinWorkflow("builtin:coding-ideas-v2"); + expect(workflow?.name).toBe("Coding (Ideas) V2"); + expect(parseWorkflowIr(serializeWorkflowIr(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR))) + .toEqual(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR); + + // The whole point of the Ideas board: cards park in a manual intake and the + // engine must not plan them until an operator promotes them. + expect(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.columns) + .toEqual(BUILTIN_CODING_IDEAS_WORKFLOW_IR.columns); + const intake = BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.columns.find((column) => column.id === "ideas"); + expect(intake?.traits).toEqual([{ trait: "intake", config: { autoTriage: false } }]); + }); + + it("runs verify -> document -> review -> summarize -> merge in review", () => { + expect(successChainFrom("steps")).toEqual([ + "verification", + "documentation-delivery", + "code-review", + "completion-summary", + "merge-gate", + ]); + + for (const nodeId of ["verification", "documentation-delivery", "code-review", "completion-summary"]) { + expect(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.nodes.find((node) => node.id === nodeId)?.column) + .toBe("in-review"); + } + + // Coding (Ideas) carries no post-merge-verification node, so V2 inherits none either. + expect(resolveWorkflowOptionalSteps(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR).map((step) => step.templateId)) + .toEqual(["plan-review", "verification", "documentation-delivery", "code-review"]); + expect(resolveRequiredPreMergeStepIds(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR, undefined)) + .toEqual(new Set(["plan-review", "verification", "documentation-delivery", "code-review"])); + }); + + /* + FNXC:CodingIdeasV2Workflow 2026-08-24-05:35: + Both remediation loops must re-enter at `verification`, never at `code-review`. A REVISE has to + replay documentation-delivery so the docs and changeset are regenerated to include what the review + demanded; re-entering at the review would merge documentation describing a superseded tree. + */ + it("replays documentation on rework by re-entering upstream of it", () => { + expect(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.edges).toEqual(expect.arrayContaining([ + { from: "verification", to: "verification-remediation", condition: "failure" }, + { from: "code-review", to: "code-review-remediation", condition: "failure" }, + { from: "verification-remediation", to: "verification", condition: "success", kind: "rework" }, + { from: "code-review-remediation", to: "verification", condition: "success", kind: "rework" }, + ])); + // Re-entering at `verification` only replays the docs because the doc node sits downstream of it. + expect(successChainFrom("verification")).toContain("documentation-delivery"); + + for (const remediationId of ["verification-remediation", "code-review-remediation"]) { + expect(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.nodes.find((node) => node.id === remediationId)?.column) + .toBe("in-progress"); + } + }); + + it("stops the planner emitting the gates as duplicate implementation steps", () => { + const ir = BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR; + const plan = ir.nodes.find((node) => node.id === "plan"); + const parse = ir.nodes.find((node) => node.id === "parse"); + const planReview = ir.nodes.find((node) => node.id === "plan-review"); + const planReviewTemplate = planReview?.config.template as { nodes?: Array<{ config?: Record }> }; + + expect(plan?.config?.seam).toBe("planning-implementation-only"); + expect(parse?.config).toMatchObject({ implementationOnlySteps: true, preserveRemediationSteps: true }); + expect(planReviewTemplate.nodes?.[0]?.config).toMatchObject({ requireImplementationOnlySteps: true }); + }); + + it("never mutates the inherited Coding (Ideas) graph", () => { + expect(BUILTIN_CODING_IDEAS_WORKFLOW_IR.nodes.some((node) => node.id === "verification")).toBe(false); + expect(BUILTIN_CODING_IDEAS_WORKFLOW_IR.nodes.some((node) => node.id === "documentation-delivery")).toBe(false); + expect(BUILTIN_CODING_IDEAS_WORKFLOW_IR.edges).toEqual(expect.arrayContaining([ + { from: "steps", to: "completion-summary", condition: "success" }, + ])); + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6838fea00e..abb86ead82 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -340,6 +340,7 @@ export type { } from "./agents/column-agent-resolver.js"; export { BUILTIN_CODING_WORKFLOW_IR } from "./workflows/builtin-coding-workflow-ir.js"; export { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./workflows/builtin-coding-ideas-workflow-ir.js"; +export { BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR } from "./workflows/builtin-coding-ideas-v2-workflow-ir.js"; export { PLAN_REVIEW_GROUP_ID } from "./workflows/builtin-plan-review-group.js"; export { BUILTIN_MARKETING_WORKFLOW_IR } from "./workflows/builtin-marketing-workflow-ir.js"; export { evaluateForeachMergeProof } from "./workflow-merge-proof.js"; diff --git a/packages/core/src/workflows/builtin-coding-ideas-v2-workflow-ir.ts b/packages/core/src/workflows/builtin-coding-ideas-v2-workflow-ir.ts new file mode 100644 index 0000000000..f0ffbd2089 --- /dev/null +++ b/packages/core/src/workflows/builtin-coding-ideas-v2-workflow-ir.ts @@ -0,0 +1,83 @@ +import type { WorkflowIr } from "./workflow-ir-types.js"; +import { parseWorkflowIr } from "./workflow-ir.js"; +import { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./builtin-coding-ideas-workflow-ir.js"; +import { verificationOptionalGroupNode } from "./builtin-verification-gate-group.js"; +import { documentationDeliveryOptionalGroupNode } from "./builtin-documentation-delivery-group.js"; +import { verificationRemediationNode } from "./builtin-workflow-remediation-nodes.js"; +import { builtinPromptConfig } from "./builtin-workflow-prompts.js"; + +const clone = (ir: WorkflowIr): WorkflowIr => JSON.parse(JSON.stringify(ir)) as WorkflowIr; + +/* +FNXC:CodingIdeasV2Workflow 2026-08-24-05:35: +Operator intent: keep the Coding (Ideas) board exactly as it is (manual "Ideas" intake, autoTriage +false), but stop hiding testing and documentation inside the implementation checklist. They become +VISIBLE review-column gates, and the merge is the last thing that happens after delivery. + +in-progress : steps = implementation only +in-review : verification -> documentation-delivery -> code-review -> completion-summary -> merge + +Ordering is NOT cosmetic. `execute-workflow-graph.ts` refuses any write-capable node once a Code +Review APPROVE exists (`workspace-review-seal-required`): a passed review seals the tree so nothing +unreviewed can reach main. `verification-step` (its name matches the write-capable classifier) and +`documentation-delivery-step` (`toolMode: "coding"`) are both write-capable, so both MUST precede +`code-review`. builtin:review-gated-coding places them after it and therefore deadlocks on every +task the moment the review approves — that defect is the reason this ordering is explicit here. + +`completion-summary` is deliberately AFTER `code-review`: it is `toolMode: "readonly"`, so the seal +does not apply, and writing it last lets the card blurb describe the state that was actually +approved. It stays best-effort with a success-only edge — a summary failure must never wedge a task. +*/ +const RAW_BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR: WorkflowIr = (() => { + const ir = clone(BUILTIN_CODING_IDEAS_WORKFLOW_IR); + ir.name = "builtin-coding-ideas-v2"; + + /* + FNXC:CodingIdeasV2Workflow 2026-08-24-05:35: + The planner must stop emitting "Testing & Verification" and "Documentation & Delivery" steps: they + are gates now, and leaving them in PROMPT.md would run the same work twice under the same names. + `planning-implementation-only` is the seam that carries that instruction. + */ + const plan = ir.nodes.find((node) => node.id === "plan"); + if (plan) plan.config = { ...plan.config, ...builtinPromptConfig("planning-implementation-only", "Plan") }; + const planReview = ir.nodes.find((node) => node.id === "plan-review"); + const planTemplate = planReview?.config?.template as { nodes?: Array<{ config?: Record }> } | undefined; + if (planTemplate?.nodes?.[0]?.config) planTemplate.nodes[0].config.requireImplementationOnlySteps = true; + const parse = ir.nodes.find((node) => node.id === "parse"); + if (parse) parse.config = { ...parse.config, implementationOnlySteps: true, preserveRemediationSteps: true }; + + const codeReviewIndex = ir.nodes.findIndex((node) => node.id === "code-review"); + if (codeReviewIndex < 0) throw new Error("coding-ideas-v2 requires the inherited code-review gate"); + ir.nodes.splice(codeReviewIndex, 0, verificationOptionalGroupNode("in-review"), documentationDeliveryOptionalGroupNode("in-review")); + ir.nodes.push(verificationRemediationNode()); + + ir.edges = ir.edges.filter((edge) => !( + (edge.from === "steps" && edge.to === "completion-summary") + || (edge.from === "completion-summary" && edge.to === "code-review") + || (edge.from === "code-review" && edge.to === "merge-gate") + || (edge.from === "code-review-remediation" && edge.to === "code-review") + )); + + /* + FNXC:CodingIdeasV2Workflow 2026-08-24-05:35: + Both remediation loops re-enter at `verification`, never directly at `code-review`. That is what + keeps the documentation honest: a REVISE sends the fix back to in-progress, then the walk replays + verification AND documentation-delivery, so the docs and changeset are regenerated to include what + the review demanded before it re-reads them. Re-entering at `code-review` would leave the docs + describing a tree that no longer exists. `verification` is the rework-region head (`reworkRegion: + true`, `maxReworkCycles: 3`), which is what makes these edges legal. + */ + ir.edges.push( + { from: "steps", to: "verification", condition: "success" }, + { from: "verification", to: "documentation-delivery", condition: "success" }, + { from: "documentation-delivery", to: "code-review", condition: "success" }, + { from: "code-review", to: "completion-summary", condition: "success" }, + { from: "completion-summary", to: "merge-gate", condition: "success" }, + { from: "verification", to: "verification-remediation", condition: "failure" }, + { from: "verification-remediation", to: "verification", condition: "success", kind: "rework" }, + { from: "code-review-remediation", to: "verification", condition: "success", kind: "rework" }, + ); + return ir; +})(); + +export const BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR = parseWorkflowIr(RAW_BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR); diff --git a/packages/core/src/workflows/builtin-workflows.ts b/packages/core/src/workflows/builtin-workflows.ts index cbaaae2ad4..19b2c05308 100644 --- a/packages/core/src/workflows/builtin-workflows.ts +++ b/packages/core/src/workflows/builtin-workflows.ts @@ -1,5 +1,6 @@ import { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js"; import { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./builtin-coding-ideas-workflow-ir.js"; +import { BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR } from "./builtin-coding-ideas-v2-workflow-ir.js"; import { BUILTIN_BRAINSTORMING_WORKFLOW_IR } from "./builtin-brainstorming-workflow-ir.js"; import { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "./builtin-lead-generation-workflow-ir.js"; import { BUILTIN_MARKETING_WORKFLOW_IR } from "./builtin-marketing-workflow-ir.js"; @@ -601,6 +602,48 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ createdAt: BUILTIN_TS, updatedAt: BUILTIN_TS, }, + /* + * FNXC:CodingIdeasV2Workflow 2026-08-24-05:35: + * Same Ideas board as builtin:coding-ideas, but testing and documentation stop being hidden + * checklist items inside the implementation steps and become visible in-review gates, with the + * merge last. Every write-capable gate runs BEFORE Code Review because a passed review seals the + * tree (`workspace-review-seal-required`); the readonly completion summary runs after it. + */ + { + id: "builtin:coding-ideas-v2", + name: "Coding (Ideas) V2", + description: + "Capture-first coding pipeline with visible review-column gates: park ideas in a manual intake, plan, implement per step, then verify, document, review, summarize, and merge.", + kind: "workflow", + ir: BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR, + layout: { + start: { x: 60, y: 160 }, + plan: { x: 230, y: 160 }, + "plan-review": { x: 400, y: 160 }, + "plan-replan": { x: 400, y: 320 }, + "plan-review-no-op": { x: 570, y: 320 }, + parse: { x: 570, y: 160 }, + steps: { x: 740, y: 160 }, + "review-pending-handoff": { x: 740, y: 320 }, + verification: { x: 910, y: 160 }, + "verification-remediation": { x: 910, y: 320 }, + "documentation-delivery": { x: 1080, y: 160 }, + "code-review": { x: 1250, y: 160 }, + "code-review-remediation": { x: 1250, y: 320 }, + "completion-summary": { x: 1420, y: 160 }, + "merge-gate": { x: 1590, y: 160 }, + "branch-group-member-integration": { x: 1760, y: 80 }, + "branch-group-promotion": { x: 1930, y: 80 }, + "merge-attempt": { x: 2100, y: 160 }, + "merge-retry": { x: 2270, y: 80 }, + "recovery-router": { x: 2270, y: 240 }, + "merge-manual-hold": { x: 1760, y: 240 }, + "post-merge-verification": { x: 2440, y: 160 }, + end: { x: 2610, y: 160 }, + }, + createdAt: BUILTIN_TS, + updatedAt: BUILTIN_TS, + }, { id: "builtin:legacy-coding", name: "Legacy coding", diff --git a/packages/engine/src/__tests__/coding-ideas-v2-review-seal.test.ts b/packages/engine/src/__tests__/coding-ideas-v2-review-seal.test.ts new file mode 100644 index 0000000000..cde8e2716e --- /dev/null +++ b/packages/engine/src/__tests__/coding-ideas-v2-review-seal.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import { BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR, type WorkflowIr, type WorkflowIrNode } from "@fusion/core"; +import { workflowNodeRequiresWorktree } from "../workflows/workflow-node-execution-needs.js"; + +/* +FNXC:CodingIdeasV2Workflow 2026-08-24-05:35: +The ratchet FN-175 did not have. `execute-workflow-graph.ts` refuses any write-capable node once a +Code Review APPROVE exists (`workspace-review-seal-required`), because a passed review seals the +tree so nothing unreviewed reaches main. builtin:review-gated-coding routes code-review straight +into two write-capable nodes, so every task on it deadlocks the moment the review approves — and +nothing caught it, because its only coverage asserted graph shape by hand rather than running the +production classifier over the graph. + +This test runs the REAL classifier over the real success chain. It fails if anyone ever moves a +write-capable gate after the review again. +*/ + +/** The executed node for a gate: an optional-group runs its template's inner node. */ +function executableNodes(node: WorkflowIrNode): Array<{ node: WorkflowIrNode; optionalGroupId?: string }> { + const template = node.config?.template as { nodes?: WorkflowIrNode[] } | undefined; + if (node.kind === "optional-group" && Array.isArray(template?.nodes)) { + return template.nodes.map((inner) => ({ node: inner, optionalGroupId: node.id })); + } + return [{ node }]; +} + +function isWriteCapable(node: WorkflowIrNode): boolean { + return executableNodes(node).some(({ node: executed, optionalGroupId }) => + workflowNodeRequiresWorktree(executed, { optionalGroupId }) || executed.kind === "code"); +} + +function successChainFrom(ir: WorkflowIr, start: string): WorkflowIrNode[] { + const chain: WorkflowIrNode[] = []; + const seen = new Set(); + let current: string | undefined = start; + while (current && !seen.has(current)) { + seen.add(current); + const edge = ir.edges.find((candidate) => candidate.from === current && candidate.condition === "success"); + if (!edge) break; + const next = ir.nodes.find((node) => node.id === edge.to); + if (next) chain.push(next); + current = edge.to; + } + return chain; +} + +describe("builtin:coding-ideas-v2 review seal", () => { + const ir = BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR as WorkflowIr; + + it("has no write-capable node after Code Review", () => { + const after = successChainFrom(ir, "code-review"); + // Guard the guard: an empty chain would make this assertion vacuously true. + expect(after.map((node) => node.id)).toContain("completion-summary"); + + const offenders = after.filter(isWriteCapable).map((node) => node.id); + expect(offenders).toEqual([]); + }); + + it("keeps both write-capable gates strictly before Code Review", () => { + // If these ever stop being write-capable the ordering constraint is moot and this + // whole ratchet is measuring nothing — so assert the premise, not just the outcome. + for (const gateId of ["verification", "documentation-delivery"]) { + const gate = ir.nodes.find((node) => node.id === gateId); + expect(gate, `${gateId} is missing`).toBeDefined(); + expect(isWriteCapable(gate!), `${gateId} is expected to be write-capable`).toBe(true); + } + + const chain = successChainFrom(ir, "steps").map((node) => node.id); + expect(chain.indexOf("verification")).toBeLessThan(chain.indexOf("code-review")); + expect(chain.indexOf("documentation-delivery")).toBeLessThan(chain.indexOf("code-review")); + }); + + it("keeps the completion summary readonly so it may run after the seal", () => { + const summary = ir.nodes.find((node) => node.id === "completion-summary"); + expect(summary?.config?.toolMode).toBe("readonly"); + expect(isWriteCapable(summary!)).toBe(false); + }); +});