Files
fusion/packages/core/src/builtin-workflows.ts
gsxdsm 3f313567dd feat(pr): built-in PR workflow template, end-to-end (U9)
Ships BUILTIN_PR_WORKFLOW_IR (builtin:pr-workflow) wiring the lifecycle
end to end: start -> pr-create -> await-review hold -> (changes-requested)
pr-respond -[rework]-> await-review -> (approved) auto-merge gate ->
pr-merge -> end, with conflict/failed holds and manual force-merge/close
edges. Completes U6's validator side: validateV2 permits a top-level
kind:rework edge whose loop-head opts in via config.reworkRegion (non-
rework cycles still rejected). Additive (default builtin:coding untouched);
behind workflowGraphExecutor. Legacy PrCommentHandler/PrMonitor retirement
deferred (they remain the flag-off path's PR handling) — recorded in the
plan. Fast e2e test; 41 core IR/builtin tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:42:36 -07:00

211 lines
8.2 KiB
TypeScript

import { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js";
import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js";
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.",
},
},
],
}),
// The stepwise coding workflow (KTD-9) — step inversion as authored graph
// structure (parse-steps → foreach{ step-execute → step-review } → review →
// merge). Authored directly as a v2 IR (the `linear` helper only builds simple
// pipelines); it is read-only like every built-in. Requires the
// `workflowGraphExecutor` flag at run time (foreach/step-review/parse-steps are
// interpreter-only node kinds, KTD-8); under the flag-off compile path its
// step-inversion nodes are skipped, the same posture as the other seam nodes.
{
id: "builtin:stepwise-coding",
name: "Stepwise coding (built-in)",
description:
"Per-step plan, execute, and review modeled as graph structure: each planned step runs and is reviewed (approve / revise / rethink) before the next, with bounded rework. Requires the workflow graph executor.",
ir: BUILTIN_STEPWISE_CODING_WORKFLOW_IR,
layout: {
start: { x: 60, y: 160 },
plan: { x: 230, y: 160 },
parse: { x: 400, y: 160 },
steps: { x: 570, y: 160 },
"rework-hold": { x: 570, y: 320 },
review: { x: 740, y: 160 },
merge: { x: 910, y: 160 },
end: { x: 1080, y: 160 },
},
createdAt: BUILTIN_TS,
updatedAt: BUILTIN_TS,
},
// The PR workflow (U9) — the unified PR-entity lifecycle wired end to end as
// first-class graph nodes/edges: pr-create → await-review (hold) → pr-respond
// (bounded rework loop) → auto-merge gate → pr-merge → end, with the await
// states modeled as hold columns the U4 reconcile advances via external-event
// releases. Authored directly as a v2 IR (the `linear` helper only builds
// simple pipelines); read-only like every built-in. Requires the
// `workflowGraphExecutor` flag at run time (pr-* node kinds, holds, and the
// top-level rework loop are interpreter-only).
//
// ADDITIVE: this is a NEW built-in alongside the unchanged default
// `builtin:coding`. Full retirement of the legacy comment/monitor PR path is
// deferred until the graph executor is the default (see the plan's "Deferred to
// follow-up work").
{
id: "builtin:pr-workflow",
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.",
ir: BUILTIN_PR_WORKFLOW_IR,
layout: {
start: { x: 60, y: 160 },
"pr-create": { x: 230, y: 160 },
failed: { x: 230, y: 320 },
"await-review": { x: 400, y: 160 },
"pr-respond": { x: 400, y: 320 },
"await-review-hold": { x: 570, y: 320 },
gate: { x: 570, y: 160 },
"await-rebase": { x: 740, y: 320 },
"pr-merge": { x: 740, y: 160 },
end: { x: 910, y: 160 },
},
createdAt: BUILTIN_TS,
updatedAt: BUILTIN_TS,
},
];
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);
}