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.
This commit is contained in:
gsxdsm
2026-06-03 13:54:57 -07:00
parent a98d14c252
commit 62aba2c095
4 changed files with 222 additions and 1 deletions

View File

@@ -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<typeof harness.store>;
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");
});
});
});

View File

@@ -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<string, unknown> }>;
}
/** 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<string, { x: number; y: number }> = {};
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);
}

View File

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

View File

@@ -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<WorkflowDefinition | undefined> {
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<WorkflowDefinition> {
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<void> {
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`);