From 62aba2c0959595f3712421d81043fe777e07018d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 3 Jun 2026 13:54:57 -0700 Subject: [PATCH] feat(core): built-in workflow templates incl. compound engineering Ship read-only built-in workflows surfaced in the workflow list and selectable like any workflow: 'Coding' (the existing execute->review->merge pipeline as a graph), 'Quick fix' (no review), 'Review-heavy' (extra security gate), and 'Compound engineering' (plan -> implement -> review -> code-review gate -> merge -> document, invoking ce-plan/ce-code-review/ce-compound skills). Built-ins lead the list, resolve by id for selection, and reject edit/delete. --- .../src/__tests__/builtin-workflows.test.ts | 61 ++++++++ packages/core/src/builtin-workflows.ts | 148 ++++++++++++++++++ packages/core/src/index.ts | 6 + packages/core/src/store.ts | 8 +- 4 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/__tests__/builtin-workflows.test.ts create mode 100644 packages/core/src/builtin-workflows.ts diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts new file mode 100644 index 0000000000..b6e54b247e --- /dev/null +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; + +import { BUILTIN_WORKFLOWS, getBuiltinWorkflow, isBuiltinWorkflowId } from "../builtin-workflows.js"; +import { compileWorkflowToSteps } from "../workflow-compiler.js"; +import { parseWorkflowIr } from "../workflow-ir.js"; +import { createTaskStoreTestHarness } from "./store-test-helpers.js"; + +describe("built-in workflows", () => { + it("every built-in has a valid IR and compiles 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(); + expect(() => compileWorkflowToSteps(wf.ir)).not.toThrow(); + } + }); + + it("includes a coding and a compound-engineering workflow", () => { + expect(getBuiltinWorkflow("builtin:coding")).toBeDefined(); + expect(getBuiltinWorkflow("builtin:compound-engineering")).toBeDefined(); + }); + + it("compound-engineering compiles its skill nodes to steps", () => { + const ce = getBuiltinWorkflow("builtin:compound-engineering")!; + const steps = compileWorkflowToSteps(ce.ir); + // plan + code-review (pre-merge) + document (post-merge) — seams are skipped. + expect(steps.length).toBeGreaterThanOrEqual(3); + expect(steps.some((s) => s.name === "Plan")).toBe(true); + }); + + describe("store integration", () => { + const harness = createTaskStoreTestHarness(); + let store: ReturnType; + beforeEach(async () => { + await harness.beforeEach(); + store = harness.store(); + }); + afterEach(async () => { + await harness.afterEach(); + }); + + it("lists built-ins ahead of user workflows and resolves them by id", async () => { + const list = await store.listWorkflowDefinitions(); + expect(list[0].id.startsWith("builtin:")).toBe(true); + expect(await store.getWorkflowDefinition("builtin:coding")).toBeDefined(); + }); + + it("rejects editing or deleting a built-in", async () => { + await expect( + store.updateWorkflowDefinition("builtin:coding", { name: "x" }), + ).rejects.toThrow(/cannot be edited/i); + await expect(store.deleteWorkflowDefinition("builtin:coding")).rejects.toThrow(/cannot be deleted/i); + }); + + it("a task can select a built-in workflow", async () => { + const task = await store.createTask({ description: "T", enabledWorkflowSteps: [] }); + await store.selectTaskWorkflow(task.id, "builtin:compound-engineering"); + expect(store.getTaskWorkflowSelection(task.id)?.workflowId).toBe("builtin:compound-engineering"); + }); + }); +}); diff --git a/packages/core/src/builtin-workflows.ts b/packages/core/src/builtin-workflows.ts new file mode 100644 index 0000000000..b8ecafc400 --- /dev/null +++ b/packages/core/src/builtin-workflows.ts @@ -0,0 +1,148 @@ +import type { WorkflowDefinition } from "./workflow-definition-types.js"; +import type { WorkflowIr } from "./workflow-ir-types.js"; +import { parseWorkflowIr } from "./workflow-ir.js"; + +/** Prefix marking a workflow as a read-only built-in template. */ +export const BUILTIN_WORKFLOW_ID_PREFIX = "builtin:"; + +export function isBuiltinWorkflowId(id: string): boolean { + return id.startsWith(BUILTIN_WORKFLOW_ID_PREFIX); +} + +// Stable timestamp so built-ins round-trip deterministically. +const BUILTIN_TS = "2026-01-01T00:00:00.000Z"; + +interface BuiltinSpec { + id: string; + name: string; + description: string; + /** Ordered node specs between start and end; seams use {seam}. */ + nodes: Array<{ id: string; kind: WorkflowIr["nodes"][number]["kind"]; config?: Record }>; +} + +/** Build a linear IR (start → nodes… → end) with simple x-spaced layout. */ +function linear(spec: BuiltinSpec): WorkflowDefinition { + const nodes: WorkflowIr["nodes"] = [ + { id: "start", kind: "start" }, + ...spec.nodes, + { id: "end", kind: "end" }, + ]; + const edges: WorkflowIr["edges"] = []; + for (let i = 0; i < nodes.length - 1; i += 1) { + 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") { + edges.push({ from: node.id, to: "end", condition: "failure" }); + } + } + const layout: Record = {}; + nodes.forEach((node, i) => { + layout[node.id] = { x: 60 + i * 170, y: 160 }; + }); + const ir = parseWorkflowIr({ version: "v1", name: spec.name, nodes, edges }); + return { + id: spec.id, + name: spec.name, + description: spec.description, + ir, + layout, + createdAt: BUILTIN_TS, + updatedAt: BUILTIN_TS, + }; +} + +/** + * Read-only built-in workflow templates. Selectable like any workflow; they + * cannot be edited or deleted. In compile mode (flag off) only the custom + * prompt/script/gate nodes become WorkflowSteps; the execute/review/merge + * seams are honored only by the graph interpreter (flag on). + */ +export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ + linear({ + id: "builtin:coding", + name: "Coding (built-in)", + description: "The standard coding pipeline: implement, review, then merge. Equivalent to the default behavior.", + nodes: [ + { id: "execute", kind: "prompt", config: { seam: "execute" } }, + { id: "review", kind: "prompt", config: { seam: "review" } }, + { id: "merge", kind: "prompt", config: { seam: "merge" } }, + ], + }), + linear({ + id: "builtin:quick-fix", + name: "Quick fix (built-in)", + description: "Implement and merge with no review step — for trivial, low-risk changes.", + nodes: [ + { id: "execute", kind: "prompt", config: { seam: "execute" } }, + { id: "merge", kind: "prompt", config: { seam: "merge" } }, + ], + }), + linear({ + id: "builtin:review-heavy", + name: "Review-heavy (built-in)", + description: "Adds an extra security pass before merge, on top of the standard review.", + nodes: [ + { id: "execute", kind: "prompt", config: { seam: "execute" } }, + { id: "review", kind: "prompt", config: { seam: "review" } }, + { + id: "security", + kind: "gate", + config: { + name: "Security review", + gateMode: "gate", + prompt: "Review the diff for security issues: injection, auth/authorization gaps, secret handling, unsafe deserialization. Block on any exploitable finding.", + }, + }, + { id: "merge", kind: "prompt", config: { seam: "merge" } }, + ], + }), + linear({ + id: "builtin:compound-engineering", + name: "Compound engineering (built-in)", + description: "Plan → implement → review → document, invoking the compound-engineering skills at each stage.", + nodes: [ + { + id: "plan", + kind: "prompt", + config: { + name: "Plan", + executor: "skill", + skillName: "compound-engineering:ce-plan", + prompt: "Produce a short implementation plan for this task before any code is written.", + }, + }, + { id: "execute", kind: "prompt", config: { seam: "execute" } }, + { id: "review", kind: "prompt", config: { seam: "review" } }, + { + id: "code-review", + kind: "gate", + config: { + name: "Code review", + executor: "skill", + skillName: "compound-engineering:ce-code-review", + gateMode: "gate", + prompt: "Run a structured code review of the changes. Block merge on P0/P1 findings.", + }, + }, + { id: "merge", kind: "prompt", config: { seam: "merge" } }, + { + id: "document", + kind: "prompt", + config: { + name: "Document learnings", + executor: "skill", + skillName: "compound-engineering:ce-compound", + prompt: "Capture any reusable learnings from this task into docs/solutions.", + }, + }, + ], + }), +]; + +const BUILTIN_BY_ID = new Map(BUILTIN_WORKFLOWS.map((wf) => [wf.id, wf])); + +export function getBuiltinWorkflow(id: string): WorkflowDefinition | undefined { + return BUILTIN_BY_ID.get(id); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d94b28b0fb..4cf090ccf3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -65,6 +65,12 @@ export { validateLinearity, WorkflowCompileError, } from "./workflow-compiler.js"; +export { + BUILTIN_WORKFLOWS, + BUILTIN_WORKFLOW_ID_PREFIX, + getBuiltinWorkflow, + isBuiltinWorkflowId, +} from "./builtin-workflows.js"; // ── Engine wiring (set by @fusion/engine at module load) ──────────── export { diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index afb9918de6..28429ca504 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -15,6 +15,7 @@ import type { WorkflowNodeLayout, } from "./workflow-definition-types.js"; import { compileWorkflowToSteps } from "./workflow-compiler.js"; +import { BUILTIN_WORKFLOWS, getBuiltinWorkflow, isBuiltinWorkflowId } from "./builtin-workflows.js"; /** Tags WorkflowStep rows materialized by compiling a workflow so they can be * filtered out of the user-facing step manager and cleaned up on re-selection. */ @@ -11029,7 +11030,8 @@ ${stepsSection}`; createdAt: string; updatedAt: string; }>; - this.workflowDefinitionsCache = rows.map((row) => this.toWorkflowDefinition(row)); + // Built-in templates lead the list and cannot be edited/deleted. + this.workflowDefinitionsCache = [...BUILTIN_WORKFLOWS, ...rows.map((row) => this.toWorkflowDefinition(row))]; return this.workflowDefinitionsCache; } @@ -11037,6 +11039,8 @@ ${stepsSection}`; async getWorkflowDefinition( id: string, ): Promise { + const builtin = getBuiltinWorkflow(id); + if (builtin) return builtin; const row = this.db.prepare("SELECT * FROM workflows WHERE id = ?").get(id) as | { id: string; @@ -11056,6 +11060,7 @@ ${stepsSection}`; id: string, updates: WorkflowDefinitionUpdate, ): Promise { + if (isBuiltinWorkflowId(id)) throw new Error("Built-in workflows cannot be edited"); return this.withConfigLock(async () => { const existing = await this.getWorkflowDefinition(id); if (!existing) throw new Error(`Workflow '${id}' not found`); @@ -11095,6 +11100,7 @@ ${stepsSection}`; * materialized step rows, and the project default. Throws when the id does * not exist. */ async deleteWorkflowDefinition(id: string): Promise { + if (isBuiltinWorkflowId(id)) throw new Error("Built-in workflows cannot be deleted"); const deleted = this.db.prepare("DELETE FROM workflows WHERE id = ?").run(id) as { changes?: number }; if ((deleted.changes || 0) === 0) { throw new Error(`Workflow '${id}' not found`);