feat(pr): pr-create/pr-respond/pr-merge node kinds + handlers (U3)

Adds the three first-class PR workflow node kinds and their handlers via
createPrNodeHandlers(deps), registered in createDefaultNodeHandlers
(fail-closed pr-nodes-unwired when absent). GitHub ops are injected as
callbacks (PrNodeGithubOps) at all three CLI sites (daemon/serve/dashboard)
so the engine never imports the dashboard client (FN-3049). pr-create
routes open/failed as outcomes; pr-merge passes expectedHeadOid and never
writes 'merged' (reconcile corroborates); pr-respond delegates to an
injected respond callback (U5 fills the body). 10 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-05 19:55:31 -07:00
parent b871046ad2
commit 4e16145aab
17 changed files with 750 additions and 13 deletions

View File

@@ -2,6 +2,7 @@ import { WorkflowIrError, getStepParser } from "@fusion/core";
import type { TaskDetail, TaskStep, WorkflowIrNode } from "@fusion/core";
import type { WorkflowNodeHandler, WorkflowNodeResult } from "./workflow-graph-executor.js";
import { createPrNodeHandlers, type PrNodeDeps } from "./pr-nodes.js";
export type WorkflowSeamName = "planning" | "execute" | "review" | "merge" | "schedule" | "step-execute";
@@ -517,6 +518,8 @@ export interface DefaultNodeHandlerDeps {
parseSteps?: ParseStepsHandlerDeps;
/** code node runner (U14). When absent, a code node fails cleanly. */
runCode?: CodeNodeRunner;
/** PR node deps (U3). When absent, the three pr-* kinds fail cleanly. */
prNodes?: PrNodeDeps;
}
export function createDefaultNodeHandlers(
@@ -524,7 +527,15 @@ export function createDefaultNodeHandlers(
runCustomNode?: WorkflowCustomNodeRunner,
deps?: DefaultNodeHandlerDeps,
): Record<
"prompt" | "script" | "gate" | "step-review" | "parse-steps" | "code",
| "prompt"
| "script"
| "gate"
| "step-review"
| "parse-steps"
| "code"
| "pr-create"
| "pr-respond"
| "pr-merge",
WorkflowNodeHandler
> {
const promptLike = createPromptLikeHandler(seams, runCustomNode);
@@ -533,6 +544,16 @@ export function createDefaultNodeHandlers(
const parseSteps: WorkflowNodeHandler = deps?.parseSteps
? createParseStepsHandler(deps.parseSteps)
: async () => ({ outcome: "failure", value: "parse-steps-unwired" });
// PR nodes without deps fail closed (mirrors parse-steps): a pr-* node reached
// without GitHub wiring must NOT silently succeed — it would route an
// unverified PR side effect forward.
const prNodes: Record<"pr-create" | "pr-respond" | "pr-merge", WorkflowNodeHandler> = deps?.prNodes
? createPrNodeHandlers(deps.prNodes)
: {
"pr-create": async () => ({ outcome: "failure", value: "pr-nodes-unwired" }),
"pr-respond": async () => ({ outcome: "failure", value: "pr-nodes-unwired" }),
"pr-merge": async () => ({ outcome: "failure", value: "pr-nodes-unwired" }),
};
return {
prompt: promptLike,
script: promptLike,
@@ -540,6 +561,7 @@ export function createDefaultNodeHandlers(
"step-review": createStepReviewHandler(seams),
"parse-steps": parseSteps,
code: createCodeNodeHandler(deps?.runCode),
...prNodes,
};
}