FN-6006: name built-in workflow seam nodes
Ensure built-in workflow seams render readable labels instead of "Not configured". - add explicit names to built-in execute, review, merge, planning, and step-execute seam nodes - surface seam-specific summaries in dashboard node summaries, with a fallback for unknown seam values - extend core and dashboard tests to cover seam naming invariants and rendered summaries Files changed: .../__tests__/builtin-coding-workflow-ir.test.ts | 7 +++++ .../core/src/__tests__/builtin-workflows.test.ts | 24 +++++++++++++++ packages/core/src/builtin-coding-workflow-ir.ts | 6 ++-- .../src/builtin-stepwise-coding-workflow-ir.ts | 8 ++--- packages/core/src/builtin-workflows.ts | 22 +++++++------- .../nodes/__tests__/node-summary.test.ts | 35 ++++++++++++++++++++++ .../dashboard/app/components/nodes/node-summary.ts | 17 +++++++++++ 7 files changed, 101 insertions(+), 18 deletions(-) Fusion-Task-Id: FN-6006 Fusion-Task-Lineage: 198fc500-ac8f-45c9-b6b5-7aa15285d1df
This commit is contained in:
@@ -58,4 +58,11 @@ describe("builtin coding workflow ir", () => {
|
||||
expect(byId.get("review")?.column).toBe("in-review");
|
||||
expect(byId.get("merge")?.column).toBe("in-review");
|
||||
});
|
||||
|
||||
it("assigns descriptive names to execute/review/merge seam nodes", () => {
|
||||
const byId = new Map(BUILTIN_CODING_WORKFLOW_IR.nodes.map((n) => [n.id, n]));
|
||||
expect(byId.get("execute")?.config?.name).toBe("Execute");
|
||||
expect(byId.get("review")?.config?.name).toBe("Review");
|
||||
expect(byId.get("merge")?.config?.name).toBe("Merge boundary");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,6 +91,30 @@ describe("built-in workflows", () => {
|
||||
expect(getBuiltinWorkflow("builtin:compound-engineering")).toBeDefined();
|
||||
});
|
||||
|
||||
it("all seam nodes carry a descriptive name", () => {
|
||||
for (const workflow of BUILTIN_WORKFLOWS) {
|
||||
const visitNodes = (nodes: Array<{ config?: unknown; id: string }>) => {
|
||||
for (const node of nodes) {
|
||||
const config = node.config as { seam?: unknown; name?: unknown } | undefined;
|
||||
if (typeof config?.seam === "string") {
|
||||
expect(typeof config.name).toBe("string");
|
||||
expect(String(config.name).trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
visitNodes(workflow.ir.nodes);
|
||||
if (workflow.ir.version === "v2") {
|
||||
for (const node of workflow.ir.nodes) {
|
||||
if (node.kind !== "foreach") continue;
|
||||
const template = (node.config as { template?: { nodes?: Array<{ config?: unknown; id: string }> } } | undefined)
|
||||
?.template;
|
||||
if (template?.nodes) visitNodes(template.nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("compound-engineering compiles its skill nodes to steps", () => {
|
||||
const ce = getBuiltinWorkflow("builtin:compound-engineering")!;
|
||||
const steps = compileWorkflowToSteps(ce.ir);
|
||||
|
||||
@@ -46,9 +46,9 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = {
|
||||
],
|
||||
nodes: [
|
||||
{ id: "start", kind: "start", column: "triage" },
|
||||
{ id: "execute", kind: "prompt", column: "in-progress", config: { seam: "execute" } },
|
||||
{ id: "review", kind: "prompt", column: "in-review", config: { seam: "review" } },
|
||||
{ id: "merge", kind: "prompt", column: "in-review", config: { seam: "merge" } },
|
||||
{ id: "execute", kind: "prompt", column: "in-progress", config: { seam: "execute", name: "Execute" } },
|
||||
{ id: "review", kind: "prompt", column: "in-review", config: { seam: "review", name: "Review" } },
|
||||
{ id: "merge", kind: "prompt", column: "in-review", config: { seam: "merge", name: "Merge boundary" } },
|
||||
{ id: "end", kind: "end", column: "done" },
|
||||
],
|
||||
edges: [
|
||||
|
||||
@@ -63,7 +63,7 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = {
|
||||
nodes: [
|
||||
{ id: "start", kind: "start", column: "triage" },
|
||||
// Planning seam: produces PROMPT.md (the declared step-source artifact).
|
||||
{ id: "plan", kind: "prompt", column: "in-progress", config: { seam: "planning" } },
|
||||
{ id: "plan", kind: "prompt", column: "in-progress", config: { seam: "planning", name: "Plan" } },
|
||||
// KTD-12: parse the planned PROMPT.md into the task step list. This node must
|
||||
// dominate the foreach (validator-enforced).
|
||||
{
|
||||
@@ -86,7 +86,7 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = {
|
||||
template: {
|
||||
nodes: [
|
||||
// KTD-2: run exactly this step inside the task's session/worktree.
|
||||
{ id: "step-execute", kind: "prompt", config: { seam: "step-execute" } },
|
||||
{ id: "step-execute", kind: "prompt", config: { seam: "step-execute", name: "Step execute" } },
|
||||
// KTD-4: per-step code review; verdicts become outcome edges.
|
||||
{ id: "step-review", kind: "step-review", config: { type: "code" } },
|
||||
// Template exit (the single sink the validator requires): a config-less
|
||||
@@ -121,8 +121,8 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = {
|
||||
},
|
||||
// KTD-5: rework exhaustion escalates to a manual hold (a human releases it).
|
||||
{ id: "rework-hold", kind: "hold", column: "in-progress", config: { release: "manual" } },
|
||||
{ id: "review", kind: "prompt", column: "in-review", config: { seam: "review" } },
|
||||
{ id: "merge", kind: "prompt", column: "in-review", config: { seam: "merge" } },
|
||||
{ id: "review", kind: "prompt", column: "in-review", config: { seam: "review", name: "Review" } },
|
||||
{ id: "merge", kind: "prompt", column: "in-review", config: { seam: "merge", name: "Merge boundary" } },
|
||||
{ id: "end", kind: "end", column: "done" },
|
||||
],
|
||||
edges: [
|
||||
|
||||
@@ -78,9 +78,9 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
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" } },
|
||||
{ id: "execute", kind: "prompt", config: { seam: "execute", name: "Execute" } },
|
||||
{ id: "review", kind: "prompt", config: { seam: "review", name: "Review" } },
|
||||
{ id: "merge", kind: "prompt", config: { seam: "merge", name: "Merge boundary" } },
|
||||
],
|
||||
}),
|
||||
linear({
|
||||
@@ -88,8 +88,8 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
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" } },
|
||||
{ id: "execute", kind: "prompt", config: { seam: "execute", name: "Execute" } },
|
||||
{ id: "merge", kind: "prompt", config: { seam: "merge", name: "Merge boundary" } },
|
||||
],
|
||||
}),
|
||||
linear({
|
||||
@@ -97,8 +97,8 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
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: "execute", kind: "prompt", config: { seam: "execute", name: "Execute" } },
|
||||
{ id: "review", kind: "prompt", config: { seam: "review", name: "Review" } },
|
||||
{
|
||||
id: "security",
|
||||
kind: "gate",
|
||||
@@ -108,7 +108,7 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
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" } },
|
||||
{ id: "merge", kind: "prompt", config: { seam: "merge", name: "Merge boundary" } },
|
||||
],
|
||||
}),
|
||||
linear({
|
||||
@@ -126,8 +126,8 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
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: "execute", kind: "prompt", config: { seam: "execute", name: "Execute" } },
|
||||
{ id: "review", kind: "prompt", config: { seam: "review", name: "Review" } },
|
||||
{
|
||||
id: "code-review",
|
||||
kind: "gate",
|
||||
@@ -139,7 +139,7 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
prompt: "Run a structured code review of the changes. Block merge on P0/P1 findings.",
|
||||
},
|
||||
},
|
||||
{ id: "merge", kind: "prompt", config: { seam: "merge" } },
|
||||
{ id: "merge", kind: "prompt", config: { seam: "merge", name: "Merge boundary" } },
|
||||
{
|
||||
id: "document",
|
||||
kind: "prompt",
|
||||
|
||||
@@ -68,6 +68,41 @@ describe("nodeConfigSummary", () => {
|
||||
expect(summary).toBe("deploy");
|
||||
});
|
||||
|
||||
it("prompt with seam=execute → Execute (engine)", () => {
|
||||
expect(nodeConfigSummary(node("prompt", { seam: "execute" }))).toBe("Execute (engine)");
|
||||
});
|
||||
|
||||
it("prompt with seam=review → Review (engine)", () => {
|
||||
expect(nodeConfigSummary(node("prompt", { seam: "review" }))).toBe("Review (engine)");
|
||||
});
|
||||
|
||||
it("prompt with seam=merge → Merge boundary", () => {
|
||||
expect(nodeConfigSummary(node("prompt", { seam: "merge" }))).toBe("Merge boundary");
|
||||
});
|
||||
|
||||
it("prompt with seam=planning → Plan (engine)", () => {
|
||||
expect(nodeConfigSummary(node("prompt", { seam: "planning" }))).toBe("Plan (engine)");
|
||||
});
|
||||
|
||||
it("prompt with seam=step-execute → Step execute (engine)", () => {
|
||||
expect(nodeConfigSummary(node("prompt", { seam: "step-execute" }))).toBe("Step execute (engine)");
|
||||
});
|
||||
|
||||
it("prompt with unknown seam → Seam: <value>", () => {
|
||||
expect(nodeConfigSummary(node("prompt", { seam: "custom-seam" }))).toBe("Seam: custom-seam");
|
||||
});
|
||||
|
||||
it("seam node ignores executor/model config — seam takes priority", () => {
|
||||
const summary = nodeConfigSummary(
|
||||
node("prompt", {
|
||||
seam: "execute",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4",
|
||||
}),
|
||||
);
|
||||
expect(summary).toBe("Execute (engine)");
|
||||
});
|
||||
|
||||
it("prompt with awaitInput → waits for user input", () => {
|
||||
const summary = nodeConfigSummary(node("prompt", { awaitInput: true }));
|
||||
expect(summary).toBe("Waits for user input");
|
||||
|
||||
@@ -89,6 +89,23 @@ export function nodeConfigSummary(
|
||||
|
||||
switch (kind) {
|
||||
case "prompt": {
|
||||
const seam = str(config.seam);
|
||||
if (seam) {
|
||||
switch (seam) {
|
||||
case "execute":
|
||||
return t("workflowNodes.summarySeamExecute", "Execute (engine)");
|
||||
case "review":
|
||||
return t("workflowNodes.summarySeamReview", "Review (engine)");
|
||||
case "merge":
|
||||
return t("workflowNodes.summarySeamMerge", "Merge boundary");
|
||||
case "planning":
|
||||
return t("workflowNodes.summarySeamPlanning", "Plan (engine)");
|
||||
case "step-execute":
|
||||
return t("workflowNodes.summarySeamStepExecute", "Step execute (engine)");
|
||||
default:
|
||||
return t("workflowNodes.summarySeamUnknown", "Seam: {{seam}}", { seam });
|
||||
}
|
||||
}
|
||||
const executor = str(config.executor) || "model";
|
||||
if (executor === "agent") {
|
||||
const agentId = str(config.agentId);
|
||||
|
||||
Reference in New Issue
Block a user