diff --git a/.changeset/fn-6934-workflow-movement-policy.md b/.changeset/fn-6934-workflow-movement-policy.md new file mode 100644 index 0000000000..7f4abf694e --- /dev/null +++ b/.changeset/fn-6934-workflow-movement-policy.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Tighten agent workflow-routing prompt policy so triage and executor agents must not move a task's workflow unless the user explicitly requested it or the agent created that task. Executor prompts now include an explicit `fn_workflow_select` guardrail while preserving workflow selection for tasks agents create. diff --git a/docs/agents.md b/docs/agents.md index c015ea6e77..94b99a1937 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -4,6 +4,11 @@ Fusion uses multiple agent roles for planning, execution, review, and merge workflows. + + ## CLI session actions The dashboard's CLI session banner uses authenticated `POST /api/cli-sessions/:id/*` routes for task-bound CLI sessions. `POST /api/cli-sessions/:id/relaunch` is project-scoped, rejects sessions that do not have a `taskId`, records a relaunch intent, and lets the engine listener clear resume linkage before moving the owning task back to `todo` for a fresh executor launch. This route backs the `resume-exhausted` banner's **Relaunch fresh** action; when a session summary has no `cliSessionId`, the client does not call the route. @@ -27,6 +32,7 @@ fn chat [message…] [--once] [--non-interactive] [--poll-ms ] - Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, milestone/slice interview, agent-onboarding interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them. - In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command. - Dashboard chat and planning sessions with a scoped task store expose `fn_task_document_write` and `fn_task_document_read`; because neither lane has an ambient task, both tools require an explicit `task_id`. +- Agent workflow-routing tools follow an intent boundary: agents may select or change a task workflow only when the user explicitly requested that workflow or when the agent created the task. Executors must not call `fn_workflow_select` to reroute the task they are executing unless the task instructions or a user steering comment explicitly asks for the workflow change. - Executor, heartbeat, and dashboard chat sessions expose artifact registry tools: `fn_artifact_register` publishes document/image/video/audio/other artifacts with inline `content` or a `uri`, `fn_artifact_list` discovers artifacts across agents/tasks with filters, and `fn_artifact_view` reads metadata plus inline content or URI references. Each successful registration sends a best-effort `system` → dashboard user inbox notification with artifact metadata; notification failures are logged but do not fail the registration. Planning sessions intentionally exclude artifact tools until they can thread the existing `MessageStore` dependency. ### Flags diff --git a/docs/custom-workflow-reliability-acceptance-map.md b/docs/custom-workflow-reliability-acceptance-map.md index 150f09be71..72ae74c28e 100644 --- a/docs/custom-workflow-reliability-acceptance-map.md +++ b/docs/custom-workflow-reliability-acceptance-map.md @@ -6,6 +6,9 @@ FNXC:CustomWorkflowReliability 2026-06-17-05:41: Goal G-MPW67VQR-0001-97S3 needs an end-to-end reliability acceptance map for the custom workflow system so authoring, selection, execution, recovery, and restart behavior can be verified by measurable criteria instead of ad hoc spot checks. This artifact distinguishes MVP/blocking requirements from nice-to-have enhancements and keeps implementation out of scope: confirmed gaps become focused follow-up tasks rather than product-code changes in this documentation task. + +FNXC:WorkflowRouting 2026-06-22-12:00: +Workflow selection acceptance must distinguish operator intent and task creator ownership from executor opportunism. Agents can assign workflows when the user asked or when creating the task; executors cannot reroute the task under execution unless instructed. --> ## Purpose @@ -49,9 +52,9 @@ Use this document to write engineering tasks, QA plans, and release checks. It i ### 3. Select a workflow for a task, board, or mission-derived feature task -- **Actor / need:** An operator or triage agent needs to route work through the intended workflow at task creation or before execution, including tasks that originate from mission features. +- **Actor / need:** An operator or task-creating agent needs to route work through the intended workflow at task creation or before execution, including tasks that originate from mission features. - **Trigger:** Use the dashboard task/board workflow selector, task detail **Workflow** tab, `fn_workflow_select`, `workflow_id` on `fn_task_create` / delegation tools, or mission feature triage/linking surfaces such as `fn_feature_link_task` where the created/linked task carries a workflow selection. -- **Expected happy path + lifecycle transitions + feedback:** Unselected tasks resolve to `builtin:coding`; explicitly selected workflows persist on the task before scheduler pickup; newly created tasks enter the normal planning/todo path for their selected workflow; mission goal provenance remains derived through the mission/feature hierarchy rather than copied onto the task row. The UI shows the selected workflow and offers **Edit workflow** in the task workflow context. +- **Expected happy path + lifecycle transitions + feedback:** Unselected tasks resolve to `builtin:coding`; explicitly selected workflows persist on the task before scheduler pickup; agents select/change workflows only when the user explicitly requested the workflow or when they created the task; executors do not reroute the task under execution unless instructed by the user; newly created tasks enter the normal planning/todo path for their selected workflow; mission goal provenance remains derived through the mission/feature hierarchy rather than copied onto the task row. The UI shows the selected workflow and offers **Edit workflow** in the task workflow context. - **Failure / recovery expectation:** A missing or corrupt explicit custom workflow fails closed as a workflow-resolution failure instead of silently falling back to `builtin:coding`. Invalid workflow IDs supplied through tools reject with a clear validation error. Mission links must preserve their own linked-task guards; deleting mission hierarchy cannot silently drop live linked tasks. - **Measurable success signal:** The task record/tool output shows the selected workflow ID; task detail shows the workflow context; runtime starts with the selected workflow; workflow-resolution failures park the task with an explicit error rather than executing the wrong workflow. - **Priority:** MVP/blocking for per-task selection and fail-closed resolution; nice-to-have for first-class mission-feature workflow defaults if not already supported by a triage entry point. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 8f9b51a214..566cf9b237 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -253,6 +253,11 @@ These groups moved out of project settings and into workflow settings (built-in ### Workflow-native triage policy settings + + The built-in workflows also declare triage/spec policy settings that were **not** moved from project settings. They are workflow-native declarations: they never lived in `DEFAULT_PROJECT_SETTINGS`, are not `MOVED_SETTINGS_KEYS`, and resolve only through the workflow effective-settings path. | Setting | Default | Purpose | @@ -267,8 +272,8 @@ The built-in workflows also declare triage/spec policy settings that were **not* | `triageSubtaskFileScopeThreshold` | `20` | File Scope entry count that signals broad work. | | `triageSubtaskRemediationBatchThreshold` | `30` | Large remediation batch threshold. | | `triageNoCommitsDecisionVerbs` | all seven built-ins | Decision-only verbs: Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report. | -| `triageDecisionOnlyWorkflowId` | `builtin:quick-fix` | Preferred workflow for decision-only/no-commit tasks. | -| `triageDefaultWorkflowId` | `builtin:coding` | Default workflow for standard coding tasks. | +| `triageDecisionOnlyWorkflowId` | `builtin:quick-fix` | Preferred workflow for decision-only/no-commit tasks when the user explicitly requests that routing or the agent is creating the task. | +| `triageDefaultWorkflowId` | `builtin:coding` | Default workflow for standard coding tasks and for existing tasks without an explicit user-requested or creator-owned workflow selection. | | `leanPlanning` | `false` | Workflow-native fast-mode policy: select the lean `planning-fast` prompt variant instead of the full triage spec prompt. | | `autoApproveSpec` | `false` | Workflow-native fast-mode policy: auto-approve generated specs and skip the independent spec reviewer. | diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index e669b8c530..369eecc21d 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -16,6 +16,9 @@ The built-in catalog now includes a business lead-generation workflow with custo FNXC:WorkflowRouting 2026-06-21-04:25: Triage and planning agents must preserve the project default workflow unless the user explicitly requests a different workflow. No-commit markers describe expected artifact behavior only; they no longer imply automatic Quick fix workflow selection. +FNXC:WorkflowRouting 2026-06-22-12:00: +Agents may select or change a workflow only when the user explicitly requested that workflow or the agent created that specific task. Executor agents must not reroute the task they are executing unless the user asked, but they may set workflows on follow-up tasks they create. + FNXC:Docs 2026-06-21-12:00: FN-6906 makes non-coding built-in prompts artifact-oriented: marketing drafts, lead enrichment/outreach, and design previews are persisted with fn_task_document_write, while fn_artifact_register remains conditional until the artifact tool is available. --> @@ -30,7 +33,9 @@ Operators can select workflows in the dashboard wherever the task or board workf - `fn_workflow_select` — assign a workflow to the current or named task. - `workflow_id` on `fn_task_create` / delegation tools — create a task with a workflow already selected. -Decision-only or investigation tasks can also declare `noCommitsExpected` / `**No commits expected:** true`; that marker does not change workflow selection by itself. Tasks without an explicit workflow request stay on the project default (`builtin:coding`). +Agent-initiated workflow assignment is intentionally narrow: an agent may select or change a task's workflow only when the user explicitly requested that workflow, or when the agent created that task itself (for example by passing `workflow_id` to `fn_task_create` / delegation tools). Executors should not call `fn_workflow_select` to reroute the task they are currently executing unless that task's instructions or a user steering comment explicitly asks for the workflow change. + +Decision-only or investigation tasks can also declare `noCommitsExpected` / `**No commits expected:** true`; that marker does not change workflow selection by itself. Tasks without an explicit workflow request or creator-owned workflow selection stay on the project default (`builtin:coding`). ### Built-in workflow catalog @@ -106,7 +111,7 @@ The default built-in catalog entry `builtin:coding` is backed by the canonical ` `builtin:marketing` is a non-coding content workflow with marketing-specific columns (`ideation`, `backlog`, `drafting`, `editorial-review`, `published`, `archived`) and prompt seams for content brief, draft, and editorial review. Its draft stage saves the primary content deliverable as a task document for human review, while the workflow uses the same lifecycle traits (`intake`, `hold`, `wip`, `merge-blocker`, `human-review`, `complete`, `archived`) and the same merge-gate/branch-group/merge-attempt primitive region as coding workflows, so scheduler, capacity, review blocking, and merge orchestration behavior remain standard. -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. The built-in triage thresholds, decision-only verb list, and default routing IDs are workflow-native typed settings resolved from the selected workflow. +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` only when the user explicitly requested a workflow or when selecting a workflow for a task they created, and they can 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. The built-in triage thresholds, decision-only verb list, and default routing IDs are workflow-native typed settings resolved from the selected workflow. #### Runtime invariant criterion diff --git a/packages/core/src/__tests__/agent-prompts.test.ts b/packages/core/src/__tests__/agent-prompts.test.ts index 93464d08fa..8837b7b0e2 100644 --- a/packages/core/src/__tests__/agent-prompts.test.ts +++ b/packages/core/src/__tests__/agent-prompts.test.ts @@ -141,6 +141,21 @@ describe("resolveAgentPrompt", () => { expect(result).not.toContain("Resolve ALL lint failures and test failures"); }); + it("executor prompt variants block workflow moves unless asked or created", () => { + const defaultExecutor = resolveAgentPrompt("executor"); + const seniorEngineer = resolveAgentPrompt("executor", { + roleAssignments: { + executor: "senior-engineer", + }, + }); + + for (const result of [defaultExecutor, seniorEngineer]) { + expect(result).toContain("Do not call `fn_workflow_select` to change the workflow of the task you are executing"); + expect(result).toContain("The only exception is when the user explicitly requested a specific workflow for this task"); + expect(result).toContain("You may still set the workflow on tasks you create via `fn_task_create` or `fn_delegate_task`"); + } + }); + it("senior-engineer prompt limits fixes to impacted failures and follow-ups unrelated broad-suite failures", () => { const config: AgentPromptsConfig = { roleAssignments: { diff --git a/packages/core/src/agent-prompts.ts b/packages/core/src/agent-prompts.ts index d35ca70333..64f20c0291 100644 --- a/packages/core/src/agent-prompts.ts +++ b/packages/core/src/agent-prompts.ts @@ -139,6 +139,11 @@ You are running in an **isolated git worktree**. This means: If you attempt to write to a path outside the worktree, the file tools will reject the operation with an error explaining the boundary. ## Guardrails + +- Do not call \`fn_workflow_select\` to change the workflow of the task you are executing; you did not create that task, the user or triage did. The only exception is when the user explicitly requested a specific workflow for this task in a steering comment, task instruction, or similar direct instruction. You may still set the workflow on tasks you create via \`fn_task_create\` or \`fn_delegate_task\`, because you are the creator of those new tasks. - **NEVER kill processes on port 4040.** Port 4040 is the production dashboard. Do not run \`kill\`, \`pkill\`, \`killall\`, or \`lsof -ti:4040 | xargs kill\` against it. If you need to start a test server, use \`--port 0\` for a random free port. If port 4040 is occupied, pick a different port — do NOT kill the occupant. - Treat the File Scope in PROMPT.md as the expected starting scope, not a hard boundary when quality gates fail - Read "Context to Read First" files before starting @@ -406,11 +411,11 @@ If an executor later proves an ordinary implementation task is already satisfied When the user prompt includes explicit test/build commands, use those exact commands in the generated spec. ## Workflow Routing -Keep the project default workflow (\`builtin:coding\`) unless the user explicitly requested a specific workflow for this task or subtask. Do NOT call \`fn_workflow_select\` or pass \`workflow_id\` to \`fn_task_create\` just because a task looks like investigation, audit, research, coordination, decision-only work, or coding work. If the user explicitly asks for a workflow, call \`fn_workflow_list\` to discover valid IDs, then use \`fn_workflow_select\` for the current task or pass \`workflow_id\` to \`fn_task_create\` for the requested subtask. For investigation/audit/research, operational routing/coordination, or decision-only tasks that meet the no-commits criteria above, still include \`**No commits expected:** true\` in the PROMPT.md header when appropriate; that header marker does not change the workflow. +Keep the project default workflow (\`builtin:coding\`) unless the user explicitly requested a specific workflow for this task or subtask, or you created that task yourself. Do NOT call \`fn_workflow_select\` or pass \`workflow_id\` to \`fn_task_create\` just because a task looks like investigation, audit, research, coordination, decision-only work, or coding work. If the user explicitly asks for a workflow, call \`fn_workflow_list\` to discover valid IDs, then use \`fn_workflow_select\` for the current task or pass \`workflow_id\` to \`fn_task_create\` for the requested subtask. When you create a task via \`fn_task_create\` or \`fn_delegate_task\`, you may select that created task's workflow with \`workflow_id\` at create time or \`fn_workflow_select\` afterward; do not move a task you did not create unless the user asked. For investigation/audit/research, operational routing/coordination, or decision-only tasks that meet the no-commits criteria above, still include \`**No commits expected:** true\` in the PROMPT.md header when appropriate; that header marker does not change the workflow. ## Task Artifact Location for Forensic / Reconciliation Tasks @@ -703,12 +708,13 @@ the spec references running tests or builds. Do NOT guess or infer commands from package.json when explicit commands are provided. ## Workflow Routing -- Keep the project default workflow (\`{{triageDefaultWorkflowId}}\`) unless the user explicitly requested a specific workflow for this task or subtask. +- Keep the project default workflow (\`{{triageDefaultWorkflowId}}\`) unless the user explicitly requested a specific workflow for this task or subtask, or you created that task yourself. - Do NOT call \`fn_workflow_select\` or pass \`workflow_id\` to \`fn_task_create\` just because a task looks like investigation, audit, research, operational routing/coordination, decision-only work, or standard coding work. +- When you create a task via \`fn_task_create\` or \`fn_delegate_task\`, you may select that created task's workflow with \`workflow_id\` at create time or \`fn_workflow_select\` afterward; do not move a task you did not create unless the user asked. - For decision-only tasks ({{triageNoCommitsDecisionVerbs}}) or other no-code tasks, set \`**No commits expected:** true\` in the PROMPT.md header when the no-commits criteria above are met; this is a header marker only and does not select \`{{triageDecisionOnlyWorkflowId}}\` or any custom investigation workflow by itself. - If the user explicitly asks for a workflow, call \`fn_workflow_list\` to discover valid IDs, then use \`fn_workflow_select\` to set the workflow on the current task or pass \`workflow_id\` to \`fn_task_create\` when creating a requested subtask. @@ -1060,6 +1066,11 @@ You are running in an **isolated git worktree**. This means: If you attempt to write to a path outside the worktree, the file tools will reject the operation with an error explaining the boundary. ## Guardrails + +- Do not call \`fn_workflow_select\` to change the workflow of the task you are executing; you did not create that task, the user or triage did. The only exception is when the user explicitly requested a specific workflow for this task in a steering comment, task instruction, or similar direct instruction. You may still set the workflow on tasks you create via \`fn_task_create\` or \`fn_delegate_task\`, because you are the creator of those new tasks. - **NEVER kill processes on port 4040.** Port 4040 is the production dashboard. Do not run \`kill\`, \`pkill\`, \`killall\`, or \`lsof -ti:4040 | xargs kill\` against it. If you need to start a test server, use \`--port 0\` for a random free port. If port 4040 is occupied, pick a different port — do NOT kill the occupant. - Treat the File Scope in PROMPT.md as the expected starting scope, not a hard boundary when quality gates fail - Read "Context to Read First" files before starting diff --git a/packages/engine/src/__tests__/executor-review-verdicts.test.ts b/packages/engine/src/__tests__/executor-review-verdicts.test.ts index addfe9a000..e3bed21b5a 100644 --- a/packages/engine/src/__tests__/executor-review-verdicts.test.ts +++ b/packages/engine/src/__tests__/executor-review-verdicts.test.ts @@ -720,6 +720,9 @@ describe("Code review verdict enforcement - fn_task_update blocking", () => { expect(capturedSystemPrompt).toContain("Do NOT run the full/workspace-wide test suite as your normal verification path"); expect(capturedSystemPrompt).toContain("A full/workspace-wide run is allowed ONLY when the task or workflow explicitly requires it"); expect(capturedSystemPrompt).toContain("allowFullSuite: true"); + expect(capturedSystemPrompt).toContain("Do not call `fn_workflow_select` to change the workflow of the task you are executing"); + expect(capturedSystemPrompt).toContain("The only exception is when the user explicitly requested a specific workflow for this task"); + expect(capturedSystemPrompt).toContain("You may still set the workflow on tasks you create via `fn_task_create` or `fn_delegate_task`"); }); // Note: The EXECUTOR_SYSTEM_PROMPT constant is tested indirectly via the buildExecutionPrompt test. diff --git a/packages/engine/src/__tests__/triage-threshold-settings.test.ts b/packages/engine/src/__tests__/triage-threshold-settings.test.ts index 6b012898fa..43e4dfd098 100644 --- a/packages/engine/src/__tests__/triage-threshold-settings.test.ts +++ b/packages/engine/src/__tests__/triage-threshold-settings.test.ts @@ -45,6 +45,9 @@ describe("triage threshold workflow settings", () => { expect(rendered).toContain("Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report"); expect(rendered).toContain("Keep the project default workflow (`builtin:coding`)"); expect(rendered).toContain("unless the user explicitly requested a specific workflow"); + expect(rendered).toContain("or you created that task yourself"); + expect(rendered).toContain("When you create a task via `fn_task_create`"); + expect(rendered).toContain("do not move a task you did not create unless the user asked"); expect(rendered).toContain("Do NOT call `fn_workflow_select` or pass `workflow_id`"); expect(rendered).toContain("set `**No commits expected:** true` in the PROMPT.md header"); expect(rendered).not.toContain("prefer `builtin:quick-fix`"); diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 9b2838a8a3..56731468fd 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -808,7 +808,7 @@ describe("fast-mode triage", () => { }); it("documents explicit-request-only workflow routing in standard and fast prompts", () => { - const required = ["## Workflow Routing", "Keep the project default workflow", "unless the user explicitly requested a specific workflow", "Do NOT call `fn_workflow_select` or pass `workflow_id`", "If the user explicitly", "fn_workflow_list", "fn_workflow_select", "workflow_id", "**No commits expected:** true", "builtin:coding"]; + const required = ["## Workflow Routing", "Keep the project default workflow", "unless the user explicitly requested a specific workflow", "or you created that task yourself", "When you create a task via `fn_task_create`", "do not move a task you did not create unless the user asked", "Do NOT call `fn_workflow_select` or pass `workflow_id`", "If the user explicitly", "fn_workflow_list", "fn_workflow_select", "workflow_id", "**No commits expected:** true", "builtin:coding"]; const forbidden = ["use workflow descriptions as the routing signal", "select an appropriate lightweight workflow", "prefer `builtin:quick-fix` or a custom investigation workflow", "Match the task nature to the workflow description", "descriptions are authoritative for routing decisions"]; for (const prompt of [RENDERED_TRIAGE_POLICY_PROMPT, FAST_PLANNING_PROMPT]) { for (const text of required) expect(prompt).toContain(text); diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 250dde27ce..9d474584a8 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -1297,6 +1297,11 @@ You are running in an **isolated git worktree**. This means: If you attempt to write to a path outside the worktree, the file tools will reject the operation with an error explaining the boundary. ## Guardrails + +- Do not call \`fn_workflow_select\` to change the workflow of the task you are executing; you did not create that task, the user or triage did. The only exception is when the user explicitly requested a specific workflow for this task in a steering comment, task instruction, or similar direct instruction. You may still set the workflow on tasks you create via \`fn_task_create\` or \`fn_delegate_task\`, because you are the creator of those new tasks. - **NEVER kill processes on port 4040.** Port 4040 is the production dashboard. Do not run \`kill\`, \`pkill\`, \`killall\`, or \`lsof -ti:4040 | xargs kill\` against it. If you need to start a test server, use \`--port 0\` for a random free port. If port 4040 is occupied, pick a different port — do NOT kill the occupant. - Treat the File Scope in PROMPT.md as the expected starting scope, not a hard boundary when quality gates fail - Read "Context to Read First" files before starting