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:
5
.changeset/fn-6168-triage-workflow-routing.md
Normal file
5
.changeset/fn-6168-triage-workflow-routing.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Expose workflow discovery and selection during triage planning, including workflow routing metadata for child task creation.
|
||||||
@@ -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.
|
`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
|
#### 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.
|
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.
|
||||||
|
|||||||
@@ -737,6 +737,18 @@ describe("fast-mode triage", () => {
|
|||||||
expect(FAST_TRIAGE_SYSTEM_PROMPT).not.toContain("Frontend UX Criteria");
|
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", () => {
|
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("Task Artifact Location");
|
||||||
expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("<rootDir>/.fusion/tasks/{TARGET_ID}/");
|
expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("<rootDir>/.fusion/tasks/{TARGET_ID}/");
|
||||||
@@ -1206,6 +1218,32 @@ describe("TriageProcessor", () => {
|
|||||||
expect(processor).toBeInstanceOf(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", () => {
|
it("can be started and stopped", () => {
|
||||||
processor.start();
|
processor.start();
|
||||||
processor.stop();
|
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 () => {
|
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
|
// 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
|
// named the parent, the parent got deleted after the split and the child
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ import {
|
|||||||
createWebFetchTool,
|
createWebFetchTool,
|
||||||
createTaskDocumentReadTool,
|
createTaskDocumentReadTool,
|
||||||
createTaskDocumentWriteTool,
|
createTaskDocumentWriteTool,
|
||||||
|
createWorkflowListTool,
|
||||||
|
createWorkflowSelectTool,
|
||||||
} from "./agent-tools.js";
|
} from "./agent-tools.js";
|
||||||
import {
|
import {
|
||||||
getResearchGuidanceForSurface,
|
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
|
the spec references running tests or builds. Do NOT guess or infer commands from
|
||||||
package.json when explicit commands are provided.
|
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
|
## Spec Review
|
||||||
|
|
||||||
After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent quality 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
|
## Project commands
|
||||||
When the user prompt includes explicit test/build commands, use those exact commands in the generated spec.
|
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
|
## 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.
|
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),
|
createTaskDocumentWriteTool(this.store, task.id),
|
||||||
createTaskDocumentReadTool(this.store, task.id),
|
createTaskDocumentReadTool(this.store, task.id),
|
||||||
|
createWorkflowListTool(this.store),
|
||||||
|
createWorkflowSelectTool(this.store, task.id),
|
||||||
...(isResearchToolSurfaceEnabled(settings)
|
...(isResearchToolSurfaceEnabled(settings)
|
||||||
? createResearchTools({
|
? createResearchTools({
|
||||||
store: this.store,
|
store: this.store,
|
||||||
@@ -1875,6 +1890,16 @@ export class TriageProcessor {
|
|||||||
description: "Task priority (low, normal, high, urgent)",
|
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 = {
|
const taskList: ToolDefinition = {
|
||||||
@@ -2083,6 +2108,8 @@ export class TriageProcessor {
|
|||||||
dependencies: validDeps,
|
dependencies: validDeps,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
priority: params.priority,
|
priority: params.priority,
|
||||||
|
workflowId: params.workflow_id,
|
||||||
|
noCommitsExpected: params.noCommitsExpected,
|
||||||
// Inherit parent's model settings if available
|
// Inherit parent's model settings if available
|
||||||
modelProvider: parentTask?.modelProvider,
|
modelProvider: parentTask?.modelProvider,
|
||||||
modelId: parentTask?.modelId,
|
modelId: parentTask?.modelId,
|
||||||
|
|||||||
Reference in New Issue
Block a user