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:
61
packages/core/src/__tests__/builtin-workflows.test.ts
Normal file
61
packages/core/src/__tests__/builtin-workflows.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
148
packages/core/src/builtin-workflows.ts
Normal file
148
packages/core/src/builtin-workflows.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user