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.
This commit is contained in:
Fusion Agent
2026-08-24 05:59:49 +00:00
parent 5990ebb752
commit b818eb20ad
6 changed files with 314 additions and 0 deletions

View File

@@ -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.

View File

@@ -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<string>();
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<string, unknown> }> };
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" },
]));
});
});

View File

@@ -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";

View File

@@ -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<string, unknown> }> } | 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);

View File

@@ -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",

View File

@@ -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<string>();
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);
});
});