From fbc2c37d4010ef1340f747e4cfeb48f7459cf4f0 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 9 Jun 2026 15:19:26 -0700 Subject: [PATCH] FN-6137: convert PR lifecycle built-in to fragment Convert the built-in PR lifecycle into a reusable fragment instead of a selectable task workflow. - mark builtin:pr-workflow as a fragment and exclude fragments from default-enabled built-ins - hide built-in fragments from the General settings workflow toggles - update core and dashboard tests to assert fragment behavior and reject task selection of the PR lifecycle fragment - add a changeset for the published CLI package Files changed: .changeset/pr-lifecycle-fragment.md | 5 +++++ .../core/src/__tests__/builtin-workflows.test.ts | 24 ++++++++++++++++------ packages/core/src/builtin-workflows.ts | 7 ++++--- packages/core/src/store.ts | 2 +- .../__tests__/WorkflowNodeEditor.test.tsx | 3 ++- .../settings/sections/GeneralSection.tsx | 4 +++- 6 files changed, 33 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-6137 Fusion-Task-Lineage: d959ba3b-2e50-4c9b-a1fa-161776939403 --- .changeset/pr-lifecycle-fragment.md | 5 ++++ .../src/__tests__/builtin-workflows.test.ts | 24 ++++++++++++++----- packages/core/src/builtin-workflows.ts | 7 +++--- packages/core/src/store.ts | 2 +- .../__tests__/WorkflowNodeEditor.test.tsx | 3 ++- .../settings/sections/GeneralSection.tsx | 4 +++- 6 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 .changeset/pr-lifecycle-fragment.md diff --git a/.changeset/pr-lifecycle-fragment.md b/.changeset/pr-lifecycle-fragment.md new file mode 100644 index 0000000000..4f5aa37bf6 --- /dev/null +++ b/.changeset/pr-lifecycle-fragment.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": minor +--- + +Convert the built-in PR lifecycle from a selectable task workflow into a reusable workflow-editor fragment template. diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index 35d861f9e9..dd19a80ab2 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -16,17 +16,16 @@ import { createTaskStoreTestHarness } from "./store-test-helpers.js"; const EXECUTE_NODE_MAX_RETRIES = 2; describe("built-in workflows", () => { - // Graph-only built-ins (step inversion, KTD-9) model branching/foreach/rework - // structure the linear compiler cannot lower to a step list — they run only - // under the workflow graph executor. They still must parse as valid IR. - const GRAPH_ONLY_BUILTIN_IDS = new Set(["builtin:stepwise-coding", "builtin:pr-workflow"]); + // Non-compiler built-ins model graph-only node kinds or reusable fragments the + // linear compiler cannot lower to a step list. They still must parse as valid IR. + const NON_COMPILABLE_BUILTIN_IDS = new Set(["builtin:stepwise-coding", "builtin:pr-workflow"]); it("every built-in has a valid IR; linear built-ins compile without error", () => { expect(BUILTIN_WORKFLOWS.length).toBeGreaterThanOrEqual(4); for (const wf of BUILTIN_WORKFLOWS) { expect(isBuiltinWorkflowId(wf.id)).toBe(true); expect(() => parseWorkflowIr(wf.ir)).not.toThrow(); - if (!GRAPH_ONLY_BUILTIN_IDS.has(wf.id)) { + if (!NON_COMPILABLE_BUILTIN_IDS.has(wf.id)) { expect(() => compileWorkflowToSteps(wf.ir)).not.toThrow(); } } @@ -51,6 +50,8 @@ describe("built-in workflows", () => { it("includes the PR lifecycle built-in wiring the PR nodes end to end (U9)", () => { const pr = getBuiltinWorkflow("builtin:pr-workflow"); expect(pr).toBeDefined(); + expect(pr!.kind).toBe("fragment"); + expect(BUILTIN_WORKFLOWS.some((workflow) => workflow.id === "builtin:pr-workflow")).toBe(true); const ir = parseWorkflowIr(pr!.ir); if (ir.version !== "v2") throw new Error("expected v2"); @@ -143,7 +144,11 @@ describe("built-in workflows", () => { expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); - expect(defaultEnabledBuiltinWorkflowIds()).toEqual(BUILTIN_WORKFLOWS.map((workflow) => workflow.id)); + expect(defaultEnabledBuiltinWorkflowIds()).toEqual( + BUILTIN_WORKFLOWS.filter((workflow) => workflow.kind !== "fragment").map((workflow) => workflow.id), + ); + expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:pr-workflow"); + expect(getBuiltinWorkflow("builtin:pr-workflow")!.kind).toBe("fragment"); expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([ "builtin:coding", "builtin:quick-fix", @@ -295,5 +300,12 @@ describe("built-in workflows", () => { await store.selectTaskWorkflow(task.id, "builtin:compound-engineering"); expect(store.getTaskWorkflowSelection(task.id)?.workflowId).toBe("builtin:compound-engineering"); }); + + it("rejects selecting the PR lifecycle fragment for a task", async () => { + const task = await store.createTask({ description: "T", enabledWorkflowSteps: [] }); + await expect(store.selectTaskWorkflow(task.id, "builtin:pr-workflow")).rejects.toThrow( + "is a fragment and cannot be selected for a task", + ); + }); }); }); diff --git a/packages/core/src/builtin-workflows.ts b/packages/core/src/builtin-workflows.ts index 6cb3da9eb4..428ab65a1a 100644 --- a/packages/core/src/builtin-workflows.ts +++ b/packages/core/src/builtin-workflows.ts @@ -15,7 +15,7 @@ export function isBuiltinWorkflowId(id: string): boolean { } export function defaultEnabledBuiltinWorkflowIds(): string[] { - return BUILTIN_WORKFLOWS.map((workflow) => workflow.id); + return BUILTIN_WORKFLOWS.filter((workflow) => workflow.kind !== "fragment").map((workflow) => workflow.id); } export function isBuiltinWorkflowEnabled(id: string, enabledIds?: readonly string[]): boolean { @@ -69,7 +69,8 @@ function linear(spec: BuiltinSpec): WorkflowDefinition { id: spec.id, name: spec.name, description: spec.description, - // Built-ins are always selectable workflows, never fragments (KTD-1). + // Linear built-ins remain selectable workflows; catalog entries authored + // directly below may opt into fragment kind when they are palette templates. kind: "workflow", ir, layout, @@ -215,7 +216,7 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ name: "PR lifecycle (built-in)", description: "The unified PR lifecycle as graph nodes: create the PR, await review, respond to changes (bounded rework loop), gate on auto-merge, then merge — with GitHub reconciliation advancing the await holds. Requires the workflow graph executor.", - kind: "workflow", + kind: "fragment", ir: BUILTIN_PR_WORKFLOW_IR, layout: { start: { x: 60, y: 160 }, diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 5d6dcf8b0d..d9bf454cdf 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -13299,7 +13299,7 @@ ${stepsSection}`; /** Read (and cache) the full merged workflow-definition set, oldest first. * Built-in templates lead the list and cannot be edited/deleted; built-ins - * are always kind "workflow". */ + * may be selectable workflows or reusable fragments. */ private async readAllWorkflowDefinitions(): Promise { if (this.workflowDefinitionsCache) return this.workflowDefinitionsCache; const rows = this.db.prepare("SELECT * FROM workflows ORDER BY createdAt ASC").all() as Array<{ diff --git a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx index 2d26e30670..b13945decc 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx @@ -169,7 +169,7 @@ function builtinDef(): WorkflowDefinition { function builtinPrDef(): WorkflowDefinition { return { id: "builtin:pr-workflow", - kind: "workflow", + kind: "fragment", name: "PR lifecycle (built-in)", description: "Ships with Fusion", ir: BUILTIN_PR_WORKFLOW_IR, @@ -271,6 +271,7 @@ describe("workflow-flow-mapping", () => { expect(BUILTIN_WORKFLOWS.map((workflow) => workflow.id).sort()).toEqual( expect.arrayContaining(["builtin:coding", "builtin:stepwise-coding", "builtin:pr-workflow"]), ); + expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:pr-workflow")?.kind).toBe("fragment"); for (const workflow of BUILTIN_WORKFLOWS) { edgeRenderableAssertion(workflow); diff --git a/packages/dashboard/app/components/settings/sections/GeneralSection.tsx b/packages/dashboard/app/components/settings/sections/GeneralSection.tsx index 4566844430..88ab6325c2 100644 --- a/packages/dashboard/app/components/settings/sections/GeneralSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GeneralSection.tsx @@ -48,7 +48,9 @@ export function GeneralSection({ fetchWorkflows(projectId, { includeDisabledBuiltins: true }) .then((workflows) => { if (!cancelled) { - setBuiltinWorkflows(workflows.filter((workflow) => workflow.id.startsWith("builtin:"))); + setBuiltinWorkflows( + workflows.filter((workflow) => workflow.id.startsWith("builtin:") && workflow.kind !== "fragment"), + ); } }) .catch(() => {