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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user