diff --git a/.changeset/fn-6168-triage-workflow-routing.md b/.changeset/fn-6168-triage-workflow-routing.md new file mode 100644 index 0000000000..a5c2003d3c --- /dev/null +++ b/.changeset/fn-6168-triage-workflow-routing.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": minor +--- + +Expose workflow discovery and selection during triage planning, including workflow routing metadata for child task creation. diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index b266d6ab9e..0f2c2f75b5 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -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. diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 8e94476efd..ccbdfd44a5 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -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("/.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).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 diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 8ff648e6b4..5857053272 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -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,