feat(FN-6879): document workflow nodes in editor detail pane

Add a per-node Help section to the workflow editor's node detail pane:
description, configuration, inputs, outputs, and edges for every node
kind. Keys off the effective kind (preserved IR kind) so graph-only
policy nodes — auto-merge gate, branch-group member integration /
promotion, PR and recovery nodes — get specific help instead of reading
as a generic merge/gate/hold, and are flagged "Engine-managed".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-21 17:25:12 -07:00
parent 1f23a2e622
commit 9c6b4dd3cd
6 changed files with 535 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Workflow editor: add a Help section to the node detail pane. Every node now documents what it does, how to configure it, and its inputs/outputs/edges — including the engine-managed merge-lifecycle nodes (auto-merge gate, branch-group member integration, branch-group promotion, PR and recovery nodes), which are surfaced read-only with an "Engine-managed" badge.

View File

@@ -957,6 +957,80 @@ React Flow ships white default controls and mini-map chrome, but the workflow ed
color: var(--ws-warning);
}
/* ── Per-node Help (FNXC:WorkflowEditor 2026-06-21-10:00) ───────────
* Collapsible <details> teaching what the selected node does, how to
* configure it, and its inputs/outputs/edges. Sits under the heading,
* collapsed by default so it never pushes config fields below the fold. */
.wf-inspector-help {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-secondary);
}
.wf-inspector-help-summary {
display: flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
font-size: 0.78rem;
color: var(--text);
cursor: pointer;
list-style: none;
user-select: none;
}
.wf-inspector-help-summary::-webkit-details-marker {
display: none;
}
.wf-inspector-help-summary:hover {
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
}
/* Engine-managed badge for graph-only policy nodes (read-only lifecycle). */
.wf-inspector-help-badge {
margin-left: auto;
padding: 1px var(--space-xs);
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-dim);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.wf-inspector-help-body {
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding: 0 var(--space-sm) var(--space-sm);
font-size: 0.76rem;
color: var(--text-muted);
}
.wf-inspector-help-summary-text {
margin: 0;
color: var(--text);
}
.wf-inspector-help-dl {
display: grid;
grid-template-columns: max-content 1fr;
gap: 2px var(--space-sm);
margin: 0;
}
.wf-inspector-help-dl dt {
font-weight: 600;
color: var(--text-dim);
}
.wf-inspector-help-dl dd {
margin: 0;
color: var(--text-muted);
}
.wf-field--checkbox {
flex-direction: row;
align-items: center;

View File

@@ -53,6 +53,7 @@ import { isMobileViewport, useViewportMode } from "../hooks/useViewportMode";
import { workflowNodeTypes, type WorkflowFlowNodeData, type WorkflowEditorNodeKind } from "./nodes/WorkflowNodeTypes";
import { WorkflowEditorCatalogContext } from "./nodes/WorkflowEditorCatalogContext";
import { bareSkillName, type NodeSummaryCatalogs } from "./nodes/node-summary";
import { nodeHelpForData } from "./nodes/node-help";
import {
irToFlow,
flowToIr,
@@ -1968,6 +1969,8 @@ function InnerEditor({
* The structural start node needs an inspector because its entry column is editable and persisted in the workflow IR. Keep end structural-only until it has a meaningful editable property.
*/
const selectedNodeHasInspector = selectedNode !== null && selectedNode.data.kind !== "end";
// FNXC:WorkflowEditor 2026-06-21-10:00: Help content for the inspector, keyed by the node's effective kind (preserved IR kind when a graph-only policy node collapsed onto a generic merge/gate/hold shape).
const selectedNodeHelp = selectedNode !== null ? nodeHelpForData(selectedNode.data) : null;
const selectedEdge = edges.find((e) => e.id === selectedEdgeId) ?? null;
const mobileNodeDetailStage = isMobileMode && selectedNodeHasInspector && !inspectorCollapsed;
const mobileEdgeDetailStage = isMobileMode && selectedEdge !== null;
@@ -3309,7 +3312,8 @@ function InnerEditor({
!(compactLayoutEnabled && !isMobileMode) && (
<aside className="wf-editor-inspector" data-testid="wf-node-inspector">
<div className="wf-inspector-heading">
<h3>{t("workflowNodes.nodeInspector", "Node")}</h3>
{/* FNXC:WorkflowEditor 2026-06-21-10:00: Heading shows the node-kind title (from the help registry) so the pane names what is selected, falling back to the generic "Node" label. */}
<h3>{selectedNodeHelp?.title ?? t("workflowNodes.nodeInspector", "Node")}</h3>
{isMobileMode && (
<button
type="button"
@@ -3329,6 +3333,37 @@ function InnerEditor({
</button>
)}
</div>
{/* FNXC:WorkflowEditor 2026-06-21-10:00: Per-node Help — what the node does, how to configure it, and its inputs/outputs/edges. Collapsed by default so it never pushes config fields below the fold; remembered open/closed within the session is intentionally not persisted (cheap to reopen). Engine-managed graph-only nodes (merge gate, branch-group integration/promotion, PR/recovery nodes) get an "Engine-managed" badge since they are read-only. */}
{selectedNodeHelp && (
<details className="wf-inspector-help" data-testid="wf-node-help">
<summary className="wf-inspector-help-summary">
<HelpCircle size={13} aria-hidden />
<span>{t("workflowNodes.helpTitle", "What does this node do?")}</span>
{selectedNodeHelp.graphOnly && (
<span className="wf-inspector-help-badge" data-testid="wf-node-help-engine-managed">
{t("workflowNodes.helpEngineManaged", "Engine-managed")}
</span>
)}
</summary>
<div className="wf-inspector-help-body">
<p className="wf-inspector-help-summary-text">{selectedNodeHelp.summary}</p>
<dl className="wf-inspector-help-dl">
{selectedNodeHelp.configure && (
<>
<dt>{t("workflowNodes.helpConfigure", "Configure")}</dt>
<dd>{selectedNodeHelp.configure}</dd>
</>
)}
<dt>{t("workflowNodes.helpInputs", "Inputs")}</dt>
<dd>{selectedNodeHelp.inputs}</dd>
<dt>{t("workflowNodes.helpOutputs", "Outputs")}</dt>
<dd>{selectedNodeHelp.outputs}</dd>
<dt>{t("workflowNodes.helpEdges", "Edges")}</dt>
<dd>{selectedNodeHelp.edges}</dd>
</dl>
</div>
</details>
)}
{isBuiltin && (
<p className="wf-inspector-note wf-inspector-note--info">
{t("workflowNodes.readOnlyDuplicateToEdit", "Read-only built-in — duplicate the workflow to edit nodes.")}

View File

@@ -794,6 +794,25 @@ describe("WorkflowNodeEditor", () => {
expect(within(inspector).queryByLabelText("Name")).not.toBeInTheDocument();
});
// FNXC:WorkflowEditor 2026-06-21-10:00: Every node's detail pane carries a Help section describing what it does and its inputs/outputs/edges.
it("renders a Help section in the node detail pane", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
await screen.findByText("Save");
fireEvent.click(await screen.findByTestId("wf-node-start"));
const inspector = await screen.findByTestId("wf-node-inspector");
const help = within(inspector).getByTestId("wf-node-help");
expect(help).toHaveTextContent("What does this node do?");
expect(help).toHaveTextContent("Inputs");
expect(help).toHaveTextContent("Outputs");
expect(help).toHaveTextContent("Edges");
// Editor (non-policy) nodes are not flagged engine-managed.
expect(within(inspector).queryByTestId("wf-node-help-engine-managed")).not.toBeInTheDocument();
});
it("keeps built-in start node entry-column controls read-only", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef()]);

View File

@@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import { effectiveNodeKind, nodeHelpFor, nodeHelpForData } from "../node-help";
import type { WorkflowFlowNodeData } from "../WorkflowNodeTypes";
/** All editor kinds plus the graph-only IR kinds the help registry must cover.
* Kept inline (not imported from core) so a missing entry fails loudly here. */
const EDITOR_KINDS = [
"start",
"end",
"prompt",
"script",
"gate",
"merge",
"hold",
"split",
"join",
"foreach",
"loop",
"step-review",
"parse-steps",
"code",
"notify",
] as const;
const GRAPH_ONLY_KINDS = [
"merge-gate",
"merge-attempt",
"manual-merge-hold",
"retry-backoff",
"recovery-router",
"branch-group-member-integration",
"branch-group-promotion",
"pr-create",
"pr-respond",
"pr-merge",
] as const;
describe("nodeHelpFor", () => {
it("returns help for every editor node kind", () => {
for (const kind of EDITOR_KINDS) {
const help = nodeHelpFor(kind);
expect(help, `missing help for editor kind ${kind}`).not.toBeNull();
// Every node documents what it does and its I/O + edges.
expect(help!.title).toBeTruthy();
expect(help!.summary).toBeTruthy();
expect(help!.inputs).toBeTruthy();
expect(help!.outputs).toBeTruthy();
expect(help!.edges).toBeTruthy();
}
});
it("returns help for every graph-only policy node kind, flagged engine-managed", () => {
for (const kind of GRAPH_ONLY_KINDS) {
const help = nodeHelpFor(kind);
expect(help, `missing help for graph-only kind ${kind}`).not.toBeNull();
expect(help!.graphOnly).toBe(true);
}
});
it("editor kinds are not flagged engine-managed", () => {
for (const kind of EDITOR_KINDS) {
expect(nodeHelpFor(kind)!.graphOnly).toBeFalsy();
}
});
it("returns null for an unknown kind", () => {
expect(nodeHelpFor("not-a-kind")).toBeNull();
});
it("describes branch-group promotion's single-managed-PR idempotency", () => {
const help = nodeHelpFor("branch-group-promotion")!;
expect(help.summary).toMatch(/single managed PR/i);
expect(help.summary).toMatch(/never creates a second PR/i);
expect(help.edges).toMatch(/merge attempt/i);
});
it("distinguishes member integration (off-switch exempt) from promotion (gated)", () => {
expect(nodeHelpFor("branch-group-member-integration")!.summary).toMatch(/even when global auto-merge is off/i);
expect(nodeHelpFor("branch-group-promotion")!.summary).toMatch(/[Gg]ated by group\/global auto-merge/);
});
it("merge gate documents its auto-on / auto-off routing", () => {
const help = nodeHelpFor("merge-gate")!;
expect(help.edges).toMatch(/auto-on/);
expect(help.edges).toMatch(/auto-off/);
});
});
describe("effectiveNodeKind / nodeHelpForData", () => {
function data(kind: WorkflowFlowNodeData["kind"], irKind?: string): WorkflowFlowNodeData {
return { kind, label: kind, ...(irKind ? { irKind } : {}) };
}
it("prefers the preserved IR kind over the collapsed editor kind", () => {
// A branch-group-promotion node renders as a generic "merge" shape but
// preserves its IR kind so the help stays specific.
const d = data("merge", "branch-group-promotion");
expect(effectiveNodeKind(d)).toBe("branch-group-promotion");
expect(nodeHelpForData(d)!.title).toBe("Branch group · promotion");
});
it("falls back to the editor kind when no IR kind is preserved", () => {
const d = data("merge");
expect(effectiveNodeKind(d)).toBe("merge");
expect(nodeHelpForData(d)!.title).toBe("Merge boundary");
});
});

View File

@@ -0,0 +1,294 @@
import type { WorkflowEditorNodeKind, WorkflowFlowNodeData } from "./WorkflowNodeTypes";
/*
FNXC:WorkflowEditor 2026-06-21-10:00:
The node detail pane must teach, not just edit. Every workflow node — including the engine-managed graph-only policy nodes (merge gate, branch-group member integration / promotion, PR nodes, recovery/retry) — needs an in-editor Help section describing what it does, how to configure it, and its inputs/outputs/edges. This was prompted by a user unable to tell what "branch-group-member-integration", "branch-group-promotion", and "merge gate" meant in the editor.
Help is keyed by the node's EFFECTIVE kind: the preserved original IR kind (`data.irKind`) when present, else the editor kind (`data.kind`). Graph-only IR kinds collapse to merge/gate/hold editor shapes via GRAPH_ONLY_EDITOR_KIND, so without the preserved kind the branch-group/PR/merge nodes would all read as a generic "merge"/"gate".
Per-node body text is English reference documentation (analogous to node-summary's raw, untranslated config values); only the repeated structural section labels are routed through i18n by the inspector. Keep this content in sync when node config fields or edge routing change.
*/
/** A node's effective kind for help lookup: the preserved original IR kind when
* the editor collapsed a graph-only policy node onto a generic shape, else the
* editor kind. Mirrors workflow-flow-mapping's `preservedIrKind`. */
export function effectiveNodeKind(data: WorkflowFlowNodeData): string {
return typeof data.irKind === "string" ? data.irKind : data.kind;
}
export interface NodeHelp {
/** Human title for the node kind (the inspector heading reuses this). */
title: string;
/** One- to two-sentence description of what the node does. */
summary: string;
/** How to configure it. Omitted for structural nodes with no config. */
configure?: string;
/** What arrives at the node (incoming edges / available context). */
inputs: string;
/** What the node produces / passes downstream. */
outputs: string;
/** Outgoing edges and the conditions/outcomes that route them. */
edges: string;
/** Engine-managed policy node: surfaced read-only, not hand-authored. The
* inspector shows an "Engine-managed" badge for these. */
graphOnly?: boolean;
}
/** Help content keyed by effective node kind. Covers every editor kind plus the
* graph-only IR kinds (merge lifecycle, branch groups, PR mode, recovery). */
const NODE_HELP: Record<string, NodeHelp> = {
// ── Editor (user-authored) kinds ──────────────────────────────────────────
start: {
title: "Start",
summary: "Marks where a task enters the workflow. Every workflow has exactly one start node.",
configure:
"Set the Entry column to choose which board column a task lands in when it enters (v2 workflows). Leave on Auto to use the first column.",
inputs: "None — this is the entry point.",
outputs: "Hands the task to the first downstream node.",
edges: "One outgoing edge (success). No incoming edges.",
},
end: {
title: "End",
summary: "A terminal state. A task that reaches an end node is finished on that path.",
inputs: "One or more incoming edges.",
outputs: "None — the task stops here.",
edges: "Incoming edges only; no outgoing edges.",
},
prompt: {
title: "Prompt (agent step)",
summary:
"Runs a unit of work against the task — an AI model, a named agent, a skill, or a CLI command. The workhorse node for executing, planning, and reviewing.",
configure:
"Write the Prompt, then pick an Executor (model, agent, skill, CLI, or CLI-agent) and its options (model, agent, skill, or command). Optionally set Gate mode (advisory vs blocking), Max retries, Auto-approve, or Wait for user input.",
inputs: "The task plus any prior step output and context.",
outputs: "The step's result, passed downstream; may record a gate verdict.",
edges: "success / failure outgoing edges. As a blocking gate it can stop the task on failure.",
},
script: {
title: "Script",
summary: "Runs a named project script (defined in project settings) as a workflow step.",
configure:
"Set Script name to a script from project settings. Set Gate mode to choose whether a non-zero exit blocks the task. The node prompt is passed to the script via FUSION_NODE_PROMPT.",
inputs: "The task; the node prompt via FUSION_NODE_PROMPT.",
outputs: "The script's exit status and output.",
edges: "success / failure.",
},
gate: {
title: "Gate",
summary:
"A decision checkpoint that evaluates a prompt and routes the task by its verdict, optionally blocking progress.",
configure:
"Write the gate Prompt. Set Gate mode to Advisory (records a verdict but never blocks) or Gate (blocks the task on failure).",
inputs: "The task plus prior context.",
outputs: "A pass/fail (or outcome) verdict.",
edges: "success / failure; a blocking gate holds the task on failure.",
},
merge: {
title: "Merge boundary",
summary:
"A marker separating pre-merge from post-merge steps. Steps before it run before the branch merges; steps after run after.",
configure: "No fields to set — placement is what matters. Position it where the merge happens in your pipeline.",
inputs: "The task after upstream steps complete.",
outputs: "Passes the task to post-merge steps.",
edges: "One outgoing edge (success).",
},
hold: {
title: "Hold",
summary:
"Pauses the task until a release condition is met — a manual promote, a timer, downstream capacity, a dependency, or an external event.",
configure:
"Pick a Release condition: Manual promote, Timer, Downstream capacity, Dependency complete, or External event.",
inputs: "The task arriving from upstream.",
outputs: "Releases the task downstream once the condition is satisfied.",
edges: "One outgoing edge (success), taken once released.",
},
split: {
title: "Split (parallel branch)",
summary:
"Fans the task out into multiple branches that run concurrently. Pair with a Join downstream to recombine them.",
configure: "No fields to set — connect multiple outgoing edges; each becomes a parallel branch.",
inputs: "A single task path.",
outputs: "Multiple concurrent branches.",
edges: "Multiple outgoing edges, one per branch. Recombine with a Join.",
},
join: {
title: "Join",
summary: "Waits for parallel branches (from a Split) and recombines them according to a join policy.",
configure:
"Set Join mode: All branches, Any branch, or Quorum (n) with a count. Set On branch failure to Collect (wait for all) or Fail-fast (cancel siblings).",
inputs: "Multiple parallel branches.",
outputs: "A single resumed path once the join policy is satisfied.",
edges: "One outgoing edge (success), taken when the join condition is met.",
},
foreach: {
title: "For-each",
summary:
"Runs a template of steps once per item (e.g. per parsed step), sequentially or in parallel. Renders as a group you drop step nodes into.",
configure:
"Set Mode (sequential/parallel), Isolation (shared or per-step worktree), Concurrency (parallel only), and Max rework cycles (the bound on rework loop-backs). Drop a step-execute node inside.",
inputs: "A collection of items (e.g. parsed steps) plus the task.",
outputs: "Aggregated per-item results.",
edges:
"success once all iterations finish. Internal rework edges loop back within a step instance, bounded by Max rework cycles.",
},
loop: {
title: "Loop",
summary:
"Repeats a template of steps until an exit condition is met or a cap is hit. Renders as a group you drop loop steps into.",
configure:
"Set the Exit condition (output contains / output matches regex) and its value or pattern, an optional Watch node id, Max iterations, and Timeout (ms).",
inputs: "The task plus the loop body steps.",
outputs: "The final iteration's result.",
edges: "One outgoing edge (success) on exit. Exits on condition match, max iterations, or timeout.",
},
"step-review": {
title: "Step review",
summary:
"An AI review gate that emits a verdict (approve / revise / rethink / unavailable) used to route the task — typically back for rework or forward on approval.",
configure:
"Set Review type (plan or code) and an optional Review model. Route each outgoing edge by verdict; mark a loop-back edge as Rework.",
inputs: "The artifact or step output to review.",
outputs: "A verdict: approve, revise, rethink, or unavailable.",
edges:
"Verdict edges (outcome:approve / revise / rethink / unavailable). A rework edge loops back, bounded by Max rework cycles.",
},
"parse-steps": {
title: "Parse steps",
summary:
"Parses a task artifact (e.g. PROMPT.md) into discrete steps a downstream for-each can iterate over.",
configure: "Pick the Artifact to parse (e.g. PROMPT.md) and the Parser (e.g. step-headings, plus any plugin parsers).",
inputs: "A task artifact or document.",
outputs: "A list of parsed steps for a downstream for-each.",
edges: "success / failure.",
},
code: {
title: "Code",
summary:
"Runs a sandboxed TypeScript snippet as a workflow step — for lightweight transforms, routing, or computed values.",
configure: "Write the TypeScript Source and an optional Timeout (ms). Syntax is validated at save.",
inputs: "Task context available to the snippet.",
outputs: "The snippet's return value.",
edges: "success / failure.",
},
notify: {
title: "Notify",
summary:
"Emits a notification event (and optional title/message) without changing the task's path — for pings on state changes.",
configure:
"Pick an Event type (or a Custom event) and optional Title/Message. Templates may use {{taskTitle}}, {{taskId}}, {{workflowName}}, and {{context:key}}.",
inputs: "The task at this point in the flow.",
outputs: "A notification event; the task continues unchanged.",
edges: "One outgoing edge (success); the node is pass-through.",
},
// ── Graph-only (engine-managed) IR kinds ──────────────────────────────────
"merge-gate": {
title: "Auto-merge gate",
summary:
"Checks whether the task is ready to auto-merge: a live PR/merge entity exists, auto-merge is opted in, and the entity is merge-ready (approved, checks green, mergeable clean).",
configure: "Engine-managed checkpoint — not hand-edited. Governed by the project and task auto-merge settings.",
inputs: "An approved task with its PR/merge entity.",
outputs: "An auto-on / auto-off decision.",
edges:
"outcome:auto-on → branch-group member integration; auto-off → parks at the manual merge hold for a human.",
graphOnly: true,
},
"merge-attempt": {
title: "Merge attempt",
summary:
"Performs the actual merge of the task's branch toward the integration/default branch (squash by project default), with conflict and post-merge audit handling.",
configure: "Engine-managed — not hand-edited. Follows the project's merge strategy and audit settings.",
inputs: "A promotion-ready branch.",
outputs: "A merged branch, or a conflict requiring manual resolution.",
edges: "success → end; conflict/failure → manual merge hold.",
graphOnly: true,
},
"manual-merge-hold": {
title: "Manual merge hold",
summary:
"Parks the task in review for a human to merge when auto-merge is off or a step needs manual resolution. While auto-merge is off, in-review is terminal until a person merges.",
configure: "Engine-managed park state — not hand-edited.",
inputs: "A task blocked from auto-merge, or one with a merge conflict.",
outputs: "A human-resolved merge that resumes the flow.",
edges: "On manual resolution, loops back into integration/merge (rework).",
graphOnly: true,
},
"retry-backoff": {
title: "Retry backoff",
summary: "Waits a backoff interval before retrying a failed step, bounded by a retry budget.",
configure: "Engine-managed — not hand-edited.",
inputs: "A failed step eligible for retry.",
outputs: "A delayed retry of the step.",
edges: "Loops back to the step until the retry budget is exhausted.",
graphOnly: true,
},
"recovery-router": {
title: "Recovery router",
summary:
"A self-healing decision point that routes a stuck or interrupted task onto the right recovery path (retry, rebound, or escalate).",
configure: "Engine-managed — not hand-edited.",
inputs: "A task in an anomalous or interrupted state.",
outputs: "A recovery-route decision.",
edges: "Branches to retry, rebound, or manual paths by recovery outcome.",
graphOnly: true,
},
"branch-group-member-integration": {
title: "Branch group · member integration",
summary:
"For a task in a shared branch group, integrates this member's work onto the group's shared branch. A soft pre-integration step that runs even when global auto-merge is off (it only assembles the group branch).",
configure: "Engine-managed — not hand-edited. Active only for shared-branch-group members.",
inputs: "An approved group-member task and the group's shared branch.",
outputs: "The member's work landed on the shared branch.",
edges: "success → branch group promotion; manual-required → manual merge hold.",
graphOnly: true,
},
"branch-group-promotion": {
title: "Branch group · promotion",
summary:
"Once all members have landed on the shared branch, carries the complete group forward — merging the group branch toward the integration branch and creating-or-reusing the group's single managed PR. Idempotent: re-running never creates a second PR. Gated by group/global auto-merge.",
configure: "Engine-managed — not hand-edited. Runs once the group is complete and auto-merge is eligible.",
inputs: "A complete shared branch group (all members landed).",
outputs: "The group promoted toward the integration branch, plus its single managed PR.",
edges: "success → merge attempt; manual-required → manual merge hold.",
graphOnly: true,
},
"pr-create": {
title: "PR create",
summary: "Creates (or reuses) the pull request for the task in pull-request merge mode.",
configure: "Engine-managed — not hand-edited. Active in pull-request merge mode.",
inputs: "A task branch ready for review.",
outputs: "An open PR entity (created or reused).",
edges: "success → the PR review/merge path.",
graphOnly: true,
},
"pr-respond": {
title: "PR respond",
summary:
"Responds to PR review feedback — addressing comments and pushing follow-up commits — during the PR review cycle.",
configure: "Engine-managed — not hand-edited.",
inputs: "PR review comments and threads.",
outputs: "Replies and follow-up commits on the PR.",
edges: "Loops within the PR review cycle until feedback is resolved.",
graphOnly: true,
},
"pr-merge": {
title: "PR merge",
summary: "Merges the pull request once it is approved and all checks pass, in pull-request mode.",
configure: "Engine-managed — not hand-edited. Governed by auto-merge readiness.",
inputs: "An approved, green PR.",
outputs: "A merged PR.",
edges: "success → end; blocked → manual merge hold.",
graphOnly: true,
},
};
/** Resolve help for a node by its effective kind, or null when none is known
* (callers skip rendering the Help section). */
export function nodeHelpFor(kind: WorkflowEditorNodeKind | string): NodeHelp | null {
return NODE_HELP[kind] ?? null;
}
/** Resolve help for a flow node, honoring the preserved IR kind. */
export function nodeHelpForData(data: WorkflowFlowNodeData): NodeHelp | null {
return nodeHelpFor(effectiveNodeKind(data));
}