FN-6168: expose workflow routing in triage

Add workflow discovery, selection, and investigation-task routing guidance to triage.

- document workflow routing guidance in standard and fast triage prompts
- add workflow list/select tools to triage sessions for task specification
- pass workflow_id and noCommitsExpected through fn_task_create for child tasks
- cover prompt/tool availability and child-task routing in triage tests
- add a changeset and workflow docs for triage workflow selection behavior

Files changed:
 .changeset/fn-6168-triage-workflow-routing.md |  5 ++
 docs/workflow-steps.md                        |  2 +
 packages/engine/src/__tests__/triage.test.ts  | 93 +++++++++++++++++++++++++++
 packages/engine/src/triage.ts                 | 27 ++++++++
 4 files changed, 127 insertions(+)

Fusion-Task-Id: FN-6168

Fusion-Task-Lineage: acfcebd5-bb9c-42ba-90a0-e9bcb90216b3
This commit is contained in:
gsxdsm
2026-06-09 22:34:34 -07:00
parent 4d7c51d526
commit 1b7e52ea99
4 changed files with 127 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Expose workflow discovery and selection during triage planning, including workflow routing metadata for child task creation.

View File

@@ -40,6 +40,8 @@ The default built-in catalog entry `builtin:coding` is backed by the canonical `
`builtin:stepwise-coding` is a separate graph variant backed by `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`; it keeps the same lifecycle columns/traits while modeling per-step parse/execute/review/rework as authored graph structure.
During triage/planning sessions, agents can call `fn_workflow_list` to discover available built-in and custom workflows and read their descriptions before routing work. They can call `fn_workflow_select` to select a workflow for the task being specified, or pass `workflow_id` when creating child tasks with `fn_task_create`; decision-only or investigation tasks can also set `noCommitsExpected` / `**No commits expected:** true` when no code changes are expected.
#### Runtime invariant criterion
Workflow-driven coding runs must preserve observable task transitions and reliability invariants: file-scope guards including `FileScopeViolationError`, squash/merge contract, recovery expectations, `autoMerge:false` terminal-until-merged, and `moveTask(in-progress→todo)` hard-cancel semantics.

View File

@@ -737,6 +737,18 @@ describe("fast-mode triage", () => {
expect(FAST_TRIAGE_SYSTEM_PROMPT).not.toContain("Frontend UX Criteria");
});
it("documents workflow routing in standard and fast prompts", () => {
for (const prompt of [TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) {
expect(prompt).toContain("## Workflow Routing");
expect(prompt).toContain("fn_workflow_list");
expect(prompt).toContain("fn_workflow_select");
expect(prompt).toContain("workflow_id");
expect(prompt).toContain("**No commits expected:** true");
expect(prompt).toContain("builtin:quick-fix");
expect(prompt).toContain("builtin:coding");
}
});
it("includes task-artifact location guidance for forensic/reconciliation tasks", () => {
expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("Task Artifact Location");
expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("<rootDir>/.fusion/tasks/{TARGET_ID}/");
@@ -1206,6 +1218,32 @@ describe("TriageProcessor", () => {
expect(processor).toBeInstanceOf(TriageProcessor);
});
it("includes workflow discovery and selection tools in the full triage toolset", async () => {
const task = createTriageTask({ id: "FN-WORKFLOW-TOOLS" });
const detailedTask = { ...mockTaskDetail, id: task.id, attachments: [], comments: [] };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(detailedTask);
let capturedTools: any[] = [];
mockCreateFnAgent.mockImplementationOnce(async (opts: any) => {
capturedTools = opts.customTools;
return {
session: {
state: {},
sessionManager: { getLeafId: vi.fn().mockReturnValue(null) },
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
navigateTree: vi.fn(),
},
};
});
await processor.specifyTask(task);
const toolNames = capturedTools.map((tool) => tool.name);
expect(toolNames).toContain("fn_workflow_list");
expect(toolNames).toContain("fn_workflow_select");
});
it("can be started and stopped", () => {
processor.start();
processor.stop();
@@ -2051,6 +2089,61 @@ describe("taskCreate tool model inheritance", () => {
}));
});
it("fn_task_create passes workflow_id and noCommitsExpected through to child tasks", async () => {
const parentTask: Task = {
id: "FN-410",
description: "Parent task",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
const createdSubtask: Task = {
...parentTask,
id: "FN-411",
description: "Decision child task",
workflowId: "builtin:quick-fix",
noCommitsExpected: true,
};
const store = createMockStore({
getTask: vi.fn().mockResolvedValue(parentTask),
createTask: vi.fn().mockResolvedValue(createdSubtask),
});
const processor = new TriageProcessor(store, "/test/root");
const createdSubtasksRef = { current: [] };
const tools = (processor as any).createTriageTools({
parentTaskId: "FN-410",
allowTaskCreate: true,
createdSubtasksRef,
});
const taskCreateTool = tools.find((t: any) => t.name === "fn_task_create");
const result = await taskCreateTool.execute("call-1", {
description: "Investigate and report the routing decision",
workflow_id: "builtin:quick-fix",
noCommitsExpected: true,
});
expect(result.content[0].text).toContain("Created child task FN-411");
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
description: "Investigate and report the routing decision",
workflowId: "builtin:quick-fix",
noCommitsExpected: true,
}), expect.objectContaining({
settings: expect.objectContaining({
maxConcurrent: 2,
maxWorktrees: 4,
}),
}));
expect(createdSubtasksRef.current).toContain("FN-411");
});
it("fn_task_create rejects a dependency on the parent task being split", async () => {
// Regression: triage used to accept any id in `dependencies`. If the AI
// named the parent, the parent got deleted after the split and the child

View File

@@ -75,6 +75,8 @@ import {
createWebFetchTool,
createTaskDocumentReadTool,
createTaskDocumentWriteTool,
createWorkflowListTool,
createWorkflowSelectTool,
} from "./agent-tools.js";
import {
getResearchGuidanceForSurface,
@@ -343,6 +345,14 @@ commands, use those EXACT commands in the testing/verification steps and anywher
the spec references running tests or builds. Do NOT guess or infer commands from
package.json when explicit commands are provided.
## Workflow Routing
- Call \`fn_workflow_list\` to discover available workflows before selecting a routing path, and read each workflow description as the routing signal.
- For investigation, audit, research, or decision-only tasks that produce no code changes, set \`**No commits expected:** true\` in the PROMPT.md header when the no-commits criteria above are met, then select an appropriate lightweight workflow.
- For decision-only tasks (Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report), prefer \`builtin:quick-fix\` or a custom investigation workflow when one is available.
- For standard coding tasks, \`builtin:coding\` is the default and is usually appropriate.
- Use \`fn_workflow_select\` to set the workflow on the current task, or pass \`workflow_id\` to \`fn_task_create\` when creating subtasks.
- Match the task nature to the workflow description; descriptions are authoritative for routing decisions.
## Spec Review
After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent quality review.
@@ -582,6 +592,9 @@ Anti-heuristics (bias to false-negative when ambiguous):
## Project commands
When the user prompt includes explicit test/build commands, use those exact commands in the generated spec.
## Workflow Routing
Call \`fn_workflow_list\` and use workflow descriptions as the routing signal. For investigation/audit/research or decision-only tasks that meet the no-commits criteria above, include \`**No commits expected:** true\` in the PROMPT.md header and prefer \`builtin:quick-fix\` or a custom investigation workflow; standard coding tasks can stay on the default \`builtin:coding\`. Use \`fn_workflow_select\` for the current task or pass \`workflow_id\` to \`fn_task_create\` for subtasks.
## Task Artifact Location for Forensic / Reconciliation Tasks
For audit/forensic/historical reconciliation tasks that target a different task ID, explicitly state in generated PROMPT.md context/scope that authoritative artifacts and DB state are at project root, not the worktree.
@@ -1183,6 +1196,8 @@ export class TriageProcessor {
}),
createTaskDocumentWriteTool(this.store, task.id),
createTaskDocumentReadTool(this.store, task.id),
createWorkflowListTool(this.store),
createWorkflowSelectTool(this.store, task.id),
...(isResearchToolSurfaceEnabled(settings)
? createResearchTools({
store: this.store,
@@ -1875,6 +1890,16 @@ export class TriageProcessor {
description: "Task priority (low, normal, high, urgent)",
}),
),
workflow_id: Type.Optional(
Type.String({
description: "Workflow ID to assign (e.g. 'builtin:coding', 'builtin:quick-fix'). Use fn_workflow_list to discover valid IDs.",
}),
),
noCommitsExpected: Type.Optional(
Type.Boolean({
description: "Set true for investigation/audit/decision tasks that produce no code changes.",
}),
),
});
const taskList: ToolDefinition = {
@@ -2083,6 +2108,8 @@ export class TriageProcessor {
dependencies: validDeps,
column: "triage",
priority: params.priority,
workflowId: params.workflow_id,
noCommitsExpected: params.noCommitsExpected,
// Inherit parent's model settings if available
modelProvider: parentTask?.modelProvider,
modelId: parentTask?.modelId,