diff --git a/.changeset/FN-6232-triage-prompt-single-source.md b/.changeset/FN-6232-triage-prompt-single-source.md new file mode 100644 index 0000000000..dad3afcf09 --- /dev/null +++ b/.changeset/FN-6232-triage-prompt-single-source.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Resolve the standard triage planning prompt from the selected workflow IR planning node instead of the removed engine-side `TRIAGE_SYSTEM_PROMPT` duplicate. The built-in `default-triage` prompt is now the canonical policy source for `builtin:coding`; where the old copies disagreed, the surviving canonical subtask-split threshold is `MORE THAN 7 implementation steps` (with the matching `MORE THAN 3 different packages/modules` guidance). Fast-mode triage continues to use `FAST_TRIAGE_SYSTEM_PROMPT` unchanged. diff --git a/packages/core/src/__tests__/agent-prompts.test.ts b/packages/core/src/__tests__/agent-prompts.test.ts index 47829f8fba..a9d08d804f 100644 --- a/packages/core/src/__tests__/agent-prompts.test.ts +++ b/packages/core/src/__tests__/agent-prompts.test.ts @@ -8,7 +8,10 @@ import { getAvailableTemplates, getTemplatesForRole, } from "../agent-prompts.js"; +import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; +import { resolvePlanningPromptFromIr } from "../workflow-ir-resolver.js"; import type { AgentPromptsConfig, AgentPromptTemplate } from "../types.js"; +import type { WorkflowIr } from "../workflow-ir-types.js"; // --------------------------------------------------------------------------- // resolveAgentPrompt @@ -257,36 +260,43 @@ describe("resolveAgentPrompt", () => { expect(result).toContain("task_document_write"); }); - it("triage prompt broad-scope decomposition block is present and identical in core and engine templates", () => { + it("triage planning prompt is sourced from workflow IR without an engine duplicate", () => { const corePrompt = resolveAgentPrompt("triage"); + const planningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR); const triageSource = readFileSync( resolve(fileURLToPath(new URL("..", import.meta.url)), "..", "..", "engine", "src", "triage.ts"), "utf8", ); - const enginePromptMatch = triageSource.match(/export const TRIAGE_SYSTEM_PROMPT = `([\s\S]*?)`;/); - expect(enginePromptMatch?.[1]).toBeTruthy(); - const enginePrompt = enginePromptMatch![1].replaceAll("\\`", "`"); - for (const prompt of [corePrompt, enginePrompt]) { - expect(prompt).toContain("**Broad-scope decomposition signals:**"); - expect(prompt).toContain("step count would reach 9 or more"); - expect(prompt).toContain("would reach 12 or more"); - expect(prompt).toContain("20 or more entries"); - expect(prompt).toContain("at or above 30 items"); - } + expect(triageSource).not.toMatch(/export const TRIAGE_SYSTEM_PROMPT\s*=/); + expect(triageSource).not.toMatch(/export const (?!FAST_TRIAGE_SYSTEM_PROMPT)[A-Z_]*TRIAGE[A-Z_]*SYSTEM_PROMPT\s*=/); + expect(planningPrompt).toBe(corePrompt); + expect(corePrompt).toContain("**Broad-scope decomposition signals:**"); + expect(corePrompt).toContain("step count would reach 9 or more"); + expect(corePrompt).toContain("would reach 12 or more"); + expect(corePrompt).toContain("20 or more entries"); + expect(corePrompt).toContain("at or above 30 items"); + }); - const marker = "**Broad-scope decomposition signals:**"; - const blockRegex = /\*\*Broad-scope decomposition signals:\*\*[\s\S]*?(?=\n\n(?:##|\*\*))/; - const coreStart = corePrompt.indexOf(marker); - const engineStart = enginePrompt.indexOf(marker); - expect(coreStart).toBeGreaterThanOrEqual(0); - expect(engineStart).toBeGreaterThanOrEqual(0); + it("resolves custom planning prompts and ignores IRs without planning prompts", () => { + const customIr: WorkflowIr = { + version: "v1", + name: "custom", + nodes: [ + { id: "start", kind: "start" }, + { id: "planning", kind: "prompt", config: { seam: "planning", prompt: "custom planning prompt" } }, + ], + edges: [], + }; + const noPlanningIr: WorkflowIr = { + version: "v1", + name: "no-planning", + nodes: [{ id: "execute", kind: "prompt", config: { seam: "execute", prompt: "executor" } }], + edges: [], + }; - const coreBlock = corePrompt.slice(coreStart).match(blockRegex)?.[0]; - const engineBlock = enginePrompt.slice(engineStart).match(blockRegex)?.[0]; - expect(coreBlock).toBeTruthy(); - expect(engineBlock).toBeTruthy(); - expect(coreBlock).toBe(engineBlock); + expect(resolvePlanningPromptFromIr(customIr)).toBe("custom planning prompt"); + expect(resolvePlanningPromptFromIr(noPlanningIr)).toBeUndefined(); }); it("built-in triage prompt requires surface enumeration for bug-fix specs", () => { diff --git a/packages/core/src/agent-prompts.ts b/packages/core/src/agent-prompts.ts index 739895c07b..58a70ef1e5 100644 --- a/packages/core/src/agent-prompts.ts +++ b/packages/core/src/agent-prompts.ts @@ -207,7 +207,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri const TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn", an AI-orchestrated task board. +## Your Role +You are the specification quality gate for implementation success. Your job: take a rough task description and produce a fully specified PROMPT.md that another AI agent can execute autonomously in a fresh context with zero memory of this conversation. +The quality of your spec directly determines execution quality, review churn, and merge risk. ## What you receive - A raw task title and optional description (the user's rough idea) @@ -237,7 +240,11 @@ Follow this structure exactly: ## Surface Enumeration -{Required for bug-fix tasks: a checklist enumerating every surface the fixed invariant must hold across. Include every provider/bridge for streaming and agent paths; desktop AND mobile breakpoints; empty/undefined/duplicate/populated data states; and every hook/component/module that shares the affected logic. Use the canonical checklist in docs/testing.md as the starting point.} +{Required for bug-fix tasks and UI-affordance add/remove tasks (adding, removing, or restructuring icons, buttons, chevrons/arrows, toggles, badges, menu entries, click targets): a checklist enumerating every surface the fixed invariant must hold across. Include every provider/bridge for streaming and agent paths; desktop AND mobile breakpoints; empty/undefined/duplicate/populated data states; and every hook/component/module that shares the affected logic. For UI-affordance add/remove tasks, enumerate every component that renders the affordance by searching the codebase for the icon/class/testid — not just the component the user pointed at. Explicitly check for leftover shells after removal (empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels) across both desktop and mobile breakpoints. Use the canonical checklist in docs/testing.md as the starting point.} + +## Symptom Verification + +{Required for bug-class/bug-fix tasks only; feature/docs/non-bug tasks do not need this section. Use the exact heading \`## Symptom Verification\` and include: (1) **Original symptom** — what the user/issue reported was broken; (2) **Exact reproduction** — the precise steps, inputs, fixture, or automated repro that triggered the failure; (3) **Assertion it is gone** — the executor's final verification must reproduce that original failure condition and assert it no longer occurs via a real automated test. Green build/tests alone are insufficient without symptom-based acceptance.} ## Dependencies @@ -258,6 +265,12 @@ Follow this structure exactly: ## Steps +> Optional: a step heading may carry a \`(depends: N,M)\` annotation listing the 1-indexed +> step numbers it depends on — e.g. \`### Step 3 (depends: 1): Title\`. Annotate ONLY steps +> that are genuinely independent of their immediate predecessor; an unannotated step is +> assumed to depend on the one before it (fully sequential). Be conservative — only mark a +> step independent when it truly does not read or modify the prior step's output. + ### Step 0: Preflight - [ ] Required files and paths exist @@ -269,11 +282,18 @@ Follow this structure exactly: - [ ] {Specific, verifiable outcome} - [ ] Run targeted tests for changed files, asserting the invariant across all known surfaces (enumerate every provider/bridge, desktop + mobile breakpoints, and empty/undefined/populated data states) -For bug-fix tasks, paste and fill in this checklist in the \`## Surface Enumeration\` section: +For bug-fix and UI-affordance add/remove tasks, paste and fill in this checklist in the \`## Surface Enumeration\` section: - [ ] Providers / bridges / execution paths touched by the invariant - [ ] Desktop + mobile breakpoints / platforms that exercise the behavior - [ ] Empty / undefined / duplicate / populated data states - [ ] Shared hooks / components / modules / helpers reusing the logic +- [ ] Every component that renders the affordance (search the codebase for the icon/class/testid, not just the one the user pointed at) +- [ ] Leftover shells after removal — empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels — are explicitly checked and fixed/hidden + +For bug-class/bug-fix tasks, add and fill in the exact \`## Symptom Verification\` section: +- [ ] **Original symptom** — what the user/issue reported was broken +- [ ] **Exact reproduction** — the precise steps, inputs, fixture, or automated repro that triggered the failure +- [ ] **Assertion it is gone** — final verification reproduces the original failure condition and asserts it no longer occurs via a real automated test; green build/tests alone are insufficient **Artifacts:** - \`path/to/file\` (new | modified) @@ -315,9 +335,16 @@ For bug-fix tasks, paste and fill in this checklist in the \`## Surface Enumerat Commits at step boundaries. All commits include the task ID: -- **Step completion:** \`feat({ID}): complete Step N — description\` -- **Bug fixes:** \`fix({ID}): description\` -- **Tests:** \`test({ID}): description\` +- **Step completion:** \`feat({ID}): complete Step N — \` (the \`\` is required — use a concrete 5–10 word description) +- **Bug fixes:** \`fix({ID}): description\` (short, concrete summary required) +- **Tests:** \`test({ID}): description\` (short, concrete summary required) + +Good examples: +- \`feat(FN-1234): complete Step 2 — add retry guard for workflow step timeouts\` +- \`test(FN-1234): add regression tests for paused-session cleanup\` + +Bad example: +- \`feat(FN-1234): complete Step 2\` ## Do NOT @@ -342,16 +369,18 @@ files with assertions that run via a test runner. Typechecks and builds are NOT tests. Manual verification is NOT a test. - Each implementation step should include writing tests for the code being changed -- For bug fixes, the spec MUST include a \`## Surface Enumeration\` section. During self-review via \`fn_review_spec()\`, treat a missing section on a bug-fix spec as a blocking REVISE. -- For bug fixes, populate \`## Surface Enumeration\` with this checklist from \`docs/testing.md\`: providers/bridges/execution paths; desktop + mobile breakpoints/platforms; empty/undefined/duplicate/populated data states; shared hooks/components/modules/helpers. -- For bug fixes, regression tests must assert the invariant across all known surfaces — enumerate every provider/bridge, desktop + mobile breakpoints, and empty/undefined/populated data states — not just the reported repro (see FN-5787/FN-5789/FN-5803 and FN-5751) +- For bug fixes and UI-affordance add/remove tasks, the spec MUST include a \`## Surface Enumeration\` section. During self-review via \`fn_review_spec()\`, treat a missing section on a bug-fix or UI-affordance add/remove spec as a blocking REVISE. +- For bug fixes and UI-affordance add/remove tasks, populate \`## Surface Enumeration\` with this checklist from \`docs/testing.md\`: providers/bridges/execution paths; desktop + mobile breakpoints/platforms; empty/undefined/duplicate/populated data states; shared hooks/components/modules/helpers; every component that renders the affordance; leftover shells after removal. +- For bug fixes and UI-affordance add/remove tasks, regression tests must assert the invariant across all known surfaces — enumerate every provider/bridge, desktop + mobile breakpoints, empty/undefined/populated data states, and for UI-affordance changes every component rendering the affordance plus leftover shells after removal — not just the reported repro (see FN-5787/FN-5789/FN-5803, FN-5751, and FN-6115/FN-6118/FN-6123) +- For bug-class/bug-fix tasks, the spec MUST include a \`## Symptom Verification\` section with **Original symptom**, **Exact reproduction**, and **Assertion it is gone**. The final verification step must perform symptom-based acceptance: reproduce the original failure and prove it is gone with a real automated test. Green build/tests alone are insufficient. Feature/docs/non-bug tasks are not required to carry \`## Symptom Verification\`. - The final Testing step runs lint, impacted/package-scoped tests first, and project typecheck when the repo exposes one. Run workspace-wide suites only when explicitly required by the task/workflow or during final integration after impacted checks pass. - Specs must instruct executors to fix lint failures and quality-gate failures directly, even when the required edits extend beyond the original File Scope - If the project has no test framework, the Testing step must include setting one up as part of this task (not just skipping tests) ## Duplicate check -Before writing a spec, call \`fn_task_list\` to see existing tasks. +Before writing a spec, first call \`fn_task_list\` to see active tasks, then call \`fn_task_search\` with 2-4 distinct keyword phrases from the task title and description (for example file paths, error symptoms, and symbol names). +For any likely match in \`done\` or \`archived\`, call \`fn_task_get\` to inspect details before deciding. If a task already covers the same work (even if worded differently), do NOT write a PROMPT.md. Instead, write a single line to the output file: \`DUPLICATE: {existing-task-id}\` @@ -370,21 +399,20 @@ When the task includes \`breakIntoSubtasks: true\`, first decide whether it shou - If not splitting: proceed with a normal PROMPT.md specification. ## Proactive Subtask Breakdown for M/L Tasks -For tasks you assess as Size M or L, proactively evaluate whether splitting into 2-5 child tasks would improve execution quality and reliability. +For tasks you assess as Size M or L, consider whether splitting into 2-5 child tasks would improve execution quality. Default to keeping the task whole; only split when the work is genuinely large or has clearly independent deliverables. -**Strongly recommend splitting when ANY of these apply:** +**Consider splitting when ANY of these apply:** - The task will require MORE THAN 7 implementation steps -- The task affects MORE THAN 3 different packages/modules +- The task affects MORE THAN 3 different packages/modules with distinct concerns (a typed field change that naturally touches core types + store + UI + tests is NOT 4 distinct concerns — it's one coherent change) - Any single step would take more than 1-2 hours to complete -- The task has multiple independent deliverables that could be developed in parallel - -**ANTI-PATTERN:** Avoid writing single tasks with 10+ steps. If you find yourself planning more than 7 steps, STOP and create 2-5 child tasks instead. +- The task has multiple clearly independent deliverables that could be developed and shipped in parallel by different people **Splitting guidance:** - Even when \`breakIntoSubtasks\` is not set to \`true\`, apply these thresholds proactively - Keep explicit user intent first: when \`breakIntoSubtasks: true\`, follow the mandatory breakdown flow above -- Size S tasks should generally NOT be split because the overhead usually outweighs the benefit -- Only keep a task as one unit if it genuinely has 5 or fewer focused steps with a clear scope +- Size S tasks should NOT be split — the overhead outweighs the benefit +- A task with 7-10 focused steps within a coherent scope is fine as one unit; do not split it +- Coordination overhead (worktrees, dependency wiring, merge sequencing) is real — only split when the parallelism or scope-clarity benefit clearly outweighs it - If you decide not to split an M/L task, proceed with a normal PROMPT.md specification **Broad-scope decomposition signals:** @@ -397,6 +425,7 @@ For tasks you assess as Size M or L, proactively evaluate whether splitting into ## Triage tools You have these extra tools during triage: - \`fn_task_list\` — list existing active tasks +- \`fn_task_search\` — keyword search across tasks, including done and archived tasks - \`fn_task_get\` — inspect a task and its PROMPT.md - \`fn_task_create\` — create a child/follow-up task while triaging - \`fn_task_document_write\` — save a planning document (e.g., key="plan") @@ -404,8 +433,31 @@ You have these extra tools during triage: When the planning conversation produces a structured plan, save it as a document with \`fn_task_document_write(key='plan', content='...')\` so the executor can reference it during implementation. +## Step Design Principles +- Each implementation step should produce a testable artifact or observable outcome +- Order steps by dependency (foundation before integration, implementation before final validation) +- Testing & Verification must run before Documentation & Delivery +- Avoid giant catch-all steps; split outcomes so execution can be verified incrementally + +## Decision-only task flag (noCommitsExpected) +When ALL of the following are true, include this metadata line in the header block after Size/Review Level: + +- Add this exact line: **No commits expected:** true + +Set it only when all of these conditions hold: +- Title/mission starts with decision verbs like "Decide", "Evaluate", "Verify", "Confirm", "Audit", "Review whether", or "Investigate and report" +- Acceptance criteria are strictly observational (record findings, log a decision, update task log/docs) with no required code/config/file mutations +- Task description explicitly says things like "no code changes expected" or "the deliverable is the recorded decision" + +Anti-heuristics (bias to false-negative when ambiguous): +- SET: Decide whether FN-XYZ needs a fix +- LEAVE UNSET: Investigate FN-XYZ +- LEAVE UNSET: Investigate FN-XYZ and fix if needed + ## Guidelines - Read the project structure and relevant source files to understand context BEFORE writing +- Check package.json/scripts and explicit project commands to align real lint/test/build/typecheck commands +- Look for similar completed tasks and existing code patterns before inventing spec structure - Be specific — name actual files, functions, and patterns from the codebase - Steps should express OUTCOMES, not micro-instructions (2-5 checkboxes per step) - Always include a testing step and a documentation step @@ -421,6 +473,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. @@ -431,9 +491,24 @@ After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent qua You MUST call \`fn_review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict. +## PROMPT.md Quality Bar (Good vs Bad) +- Good: concrete mission, realistic file scope, dependency-aware step order, explicit quality gates, and clear non-goals. +- Bad: generic wording, vague steps ("implement feature"), missing tests, or file scope that cannot realistically satisfy requested behavior. +- Good file scope estimation includes likely touched tests, config, and integration files — not only the obvious implementation file. + +Never reference a \`.fusion/tasks//\` artifact in Context, Steps, or File Scope unless (a) the file already exists, (b) the step explicitly creates it (listed as \`(new)\` under Artifacts), or (c) it is \`PROMPT.md\` / \`task.json\` / \`attachments/*\` for a sibling task. Save planning scratch as task documents via \`fn_task_document_write\`, not as files on disk. + ## Output Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` for review. +## Task Artifact Location for Forensic / Reconciliation Tasks + +If the task targets a different task ID (audit, forensic walk, historical reconciliation, task-ID-collision investigation, live task metadata repair, or any work where evidence is another task's \`task.json\` / \`PROMPT.md\` / DB row), include this guidance in the generated PROMPT.md \`## Context to Read First\` and \`## File Scope\`: +- Authoritative target-task artifacts live at the **project root**: \`/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, agent logs). +- Authoritative task DB rows live at the **project root** SQLite file: \`/.fusion/fusion.db\` (WAL mode). Read via \`TaskStore\` APIs; do not instruct direct SQL surgery. +- \`.fusion/\` is gitignored, so a fresh worktree from \`main\` does **not** include \`.fusion/tasks/{TARGET_ID}/\` or \`.fusion/fusion.db\`. The running worktree's own \`.fusion/\` (if present) is scratch/session state for the running task only, not source of truth. +- Prefer \`fn_task_get\` / \`fn_task_list\` when the target task ID is known; fall back to project-root filesystem reads only when tools cannot provide needed evidence. + ## Frontend UX Criteria Injection @@ -461,7 +536,7 @@ Use this exact checklist (keep it verbatim — do not expand or reorder): - [ ] **Visual hierarchy preserved** — new elements must not disrupt heading levels, content flow, or information architecture established in the surrounding page \`\`\` -Only inject this section when the task genuinely touches frontend UI. Omit it for backend-only, config-only, or documentation-only tasks.`; +Only inject this section when the task genuinely touches frontend UI. Omit it for backend-only, config-only, or documentation-only tasks.`;; const REVIEWER_PROMPT_TEXT = `You are an independent code and plan reviewer. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index faa896958c..c0162d2034 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -309,6 +309,8 @@ export { export { resolveWorkflowIrForTask, resolveWorkflowIrById, + resolvePlanningPromptFromIr, + resolveTaskPlanningPrompt, type WorkflowIrResolverStore, } from "./workflow-ir-resolver.js"; export { diff --git a/packages/core/src/workflow-ir-resolver.ts b/packages/core/src/workflow-ir-resolver.ts index c2c703aa29..c3649314d7 100644 --- a/packages/core/src/workflow-ir-resolver.ts +++ b/packages/core/src/workflow-ir-resolver.ts @@ -25,6 +25,34 @@ export interface WorkflowIrResolverStore { getWorkflowDefinition(id: string): Promise<{ ir: string | WorkflowIr } | undefined>; } +/** + * Extract the planning seam prompt from a resolved workflow IR. + * + * Planning seam nodes are prompt nodes with `config.seam === "planning"`; + * `config.prompt` carries the text installed by builtinPromptConfig or a custom + * workflow author. Empty/missing prompts return undefined so callers can apply + * their own fail-soft fallback. + */ +export function resolvePlanningPromptFromIr(ir: WorkflowIr): string | undefined { + for (const node of ir.nodes) { + if (node.kind !== "prompt") continue; + if (node.config?.seam !== "planning") continue; + const prompt = node.config.prompt; + if (typeof prompt === "string" && prompt.trim().length > 0) return prompt; + } + return undefined; +} + +/** Resolve a task's planning seam prompt via its selected workflow IR. */ +export async function resolveTaskPlanningPrompt( + store: WorkflowIrResolverStore, + taskId: string, + irCache?: Map, +): Promise { + const ir = await resolveWorkflowIrForTask(store, taskId, irCache); + return resolvePlanningPromptFromIr(ir); +} + /** * Resolve a workflow IR by its id (built-in or custom). * diff --git a/packages/engine/src/__tests__/triage-duplicate-search-regression.test.ts b/packages/engine/src/__tests__/triage-duplicate-search-regression.test.ts index 1aa1ddf387..95987af16f 100644 --- a/packages/engine/src/__tests__/triage-duplicate-search-regression.test.ts +++ b/packages/engine/src/__tests__/triage-duplicate-search-regression.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from "vitest"; +import { resolveAgentPrompt } from "@fusion/core"; import { FAST_TRIAGE_SYSTEM_PROMPT, - TRIAGE_SYSTEM_PROMPT, TriageProcessor, } from "../triage.js"; import { createTriageDuplicateScenario } from "./fixtures/triage-duplicate-scenario.js"; @@ -23,15 +23,18 @@ vi.mock("../pi.js", () => ({ vi.mock("@fusion/core", async (importOriginal) => { const { createEngineCoreMock } = await import("../test/mockCore.js"); - return createEngineCoreMock(() => importOriginal(), { - resolveAgentPrompt: vi.fn().mockReturnValue(null), + const original = await importOriginal(); + return createEngineCoreMock(() => Promise.resolve(original), { + resolveAgentPrompt: vi.fn(original.resolveAgentPrompt), }); }); +const TRIAGE_POLICY_PROMPT = resolveAgentPrompt("triage"); + /** * FN-4726 / FN-4734 / FN-4741: triage created repeated duplicate tasks after equivalent * work had already landed. FN-4774 fixed this by (1) exposing fn_task_search in triage, - * (2) guiding TRIAGE_SYSTEM_PROMPT to search done/archived before creating, and + * (2) guiding the canonical triage policy prompt to search done/archived before creating, and * (3) preserving that guidance in FAST_TRIAGE_SYSTEM_PROMPT. FN-4815 pins this contract. */ describe("FN-4815 triage duplicate-search regression", () => { @@ -50,10 +53,10 @@ describe("FN-4815 triage duplicate-search regression", () => { }); it("standard prompt guidance keeps duplicate-search instructions", () => { - expect(TRIAGE_SYSTEM_PROMPT).toContain("Duplicate check"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("fn_task_search"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("including done and archived tasks"); - expect(/Duplicate check[\s\S]{0,700}(done|archived)/i.test(TRIAGE_SYSTEM_PROMPT)).toBe(true); + expect(TRIAGE_POLICY_PROMPT).toContain("Duplicate check"); + expect(TRIAGE_POLICY_PROMPT).toContain("fn_task_search"); + expect(TRIAGE_POLICY_PROMPT).toContain("including done and archived tasks"); + expect(/Duplicate check[\s\S]{0,700}(done|archived)/i.test(TRIAGE_POLICY_PROMPT)).toBe(true); }); it("fast prompt guidance keeps duplicate-search instructions", () => { diff --git a/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts b/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts new file mode 100644 index 0000000000..84d70b22da --- /dev/null +++ b/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts @@ -0,0 +1,179 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { Settings, Task, TaskDetail, TaskStore, WorkflowIr } from "@fusion/core"; +import { + BUILTIN_CODING_WORKFLOW_IR, + resolveAgentPrompt, + resolvePlanningPromptFromIr, +} from "@fusion/core"; +import { FAST_TRIAGE_SYSTEM_PROMPT, TriageProcessor } from "../triage.js"; + +const { mockReviewStep, mockCreateFnAgent } = vi.hoisted(() => ({ + mockReviewStep: vi.fn(), + mockCreateFnAgent: vi.fn(), +})); + +vi.mock("../reviewer.js", () => ({ + reviewStep: mockReviewStep, +})); + +vi.mock("../pi.js", () => ({ + createFnAgent: mockCreateFnAgent, + describeModel: vi.fn().mockReturnValue("mock-model"), + promptWithFallback: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("@fusion/core", async (importOriginal) => { + const { createEngineCoreMock } = await import("../test/mockCore.js"); + const original = await importOriginal(); + return createEngineCoreMock(() => Promise.resolve(original), { + resolveAgentPrompt: vi.fn(original.resolveAgentPrompt), + }); +}); + +function createTask(overrides: Partial = {}): Task { + return { + id: "FN-6232-T", + description: "Triage planning prompt test", + column: "triage", + dependencies: [], + steps: [], + currentStep: 0, + log: [], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + ...overrides, + }; +} + +function createDetail(task: Task): TaskDetail { + return { + ...task, + prompt: "", + attachments: [], + comments: [], + } as TaskDetail; +} + +function createStore(task: Task, overrides: Partial = {}, settings: Partial = {}): TaskStore { + return { + getTask: vi.fn().mockResolvedValue(createDetail(task)), + listTasks: vi.fn().mockResolvedValue([]), + createTask: vi.fn(), + moveTask: vi.fn(), + updateTask: vi.fn().mockResolvedValue(undefined), + deleteTask: vi.fn(), + mergeTask: vi.fn(), + getSettings: vi.fn().mockResolvedValue({ + maxConcurrent: 2, + maxWorktrees: 4, + pollIntervalMs: 10000, + groupOverlappingFiles: false, + autoMerge: true, + ...settings, + } as Settings), + updateSettings: vi.fn(), + logEntry: vi.fn().mockResolvedValue(undefined), + appendAgentLog: vi.fn().mockResolvedValue(undefined), + getAgentLogs: vi.fn().mockResolvedValue([]), + addSteeringComment: vi.fn(), + parseDependenciesFromPrompt: vi.fn().mockResolvedValue([]), + parseStepsFromPrompt: vi.fn().mockResolvedValue([]), + parseFileScopeFromPrompt: vi.fn().mockResolvedValue([]), + getTaskWorkflowSelection: vi.fn().mockReturnValue(undefined), + getWorkflowDefinition: vi.fn().mockResolvedValue(undefined), + on: vi.fn(), + emit: vi.fn(), + ...overrides, + } as unknown as TaskStore; +} + +async function captureBasePrompt(task: Task, store: TaskStore): Promise { + let captured = ""; + mockCreateFnAgent.mockImplementationOnce(async (opts: any) => { + captured = opts.systemPromptLayers?.stable ?? opts.systemPrompt; + return { + session: { + state: {}, + sessionManager: { getLeafId: vi.fn().mockReturnValue(null) }, + prompt: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn(), + navigateTree: vi.fn(), + }, + }; + }); + + await new TriageProcessor(store, "/tmp/root").specifyTask(task); + return captured; +} + +const canonicalPlanningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR)!; + +describe("triage planning prompt single source", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("uses the built-in workflow IR planning prompt in standard mode", async () => { + const task = createTask({ id: "FN-6232-BUILTIN", executionMode: "standard" }); + const store = createStore(task, { + getTaskWorkflowSelection: vi.fn().mockReturnValue({ workflowId: "builtin:coding", stepIds: [] }), + }); + + await expect(captureBasePrompt(task, store)).resolves.toBe(canonicalPlanningPrompt); + }); + + it("uses the built-in workflow IR planning prompt when no workflow is selected", async () => { + const task = createTask({ id: "FN-6232-NO-SELECTION", executionMode: "standard" }); + const store = createStore(task); + + await expect(captureBasePrompt(task, store)).resolves.toBe(canonicalPlanningPrompt); + }); + + it("preserves user triage prompt override precedence", async () => { + const task = createTask({ id: "FN-6232-OVERRIDE", executionMode: "standard" }); + const overridePrompt = "custom triage override prompt"; + const store = createStore(task, {}, { + agentPrompts: { + templates: [{ id: "custom-triage", name: "Custom", role: "triage", prompt: overridePrompt }], + roleAssignments: { triage: "custom-triage" }, + }, + } as Partial); + + await expect(captureBasePrompt(task, store)).resolves.toBe(overridePrompt); + }); + + it("keeps fast mode on FAST_TRIAGE_SYSTEM_PROMPT", async () => { + const task = createTask({ id: "FN-6232-FAST", executionMode: "fast" }); + const store = createStore(task); + + await expect(captureBasePrompt(task, store)).resolves.toBe(FAST_TRIAGE_SYSTEM_PROMPT); + }); + + it("uses a selected custom workflow planning prompt", async () => { + const task = createTask({ id: "FN-6232-CUSTOM", executionMode: "standard" }); + const customPrompt = "custom workflow planning prompt"; + const customIr: WorkflowIr = { + version: "v1", + name: "custom-workflow", + nodes: [{ id: "planning", kind: "prompt", config: { seam: "planning", prompt: customPrompt } }], + edges: [], + }; + const store = createStore(task, { + getTaskWorkflowSelection: vi.fn().mockReturnValue({ workflowId: "WF-custom", stepIds: [] }), + getWorkflowDefinition: vi.fn().mockResolvedValue({ ir: customIr }), + }); + + await expect(captureBasePrompt(task, store)).resolves.toBe(customPrompt); + }); + + it("fails soft to the default triage prompt when workflow resolution cannot provide a planning prompt", async () => { + const task = createTask({ id: "FN-6232-FAIL-SOFT", executionMode: "standard" }); + const store = createStore(task, { + getTaskWorkflowSelection: vi.fn(() => { + throw new Error("selection unavailable"); + }), + }); + + await expect(captureBasePrompt(task, store)).resolves.toBe(resolveAgentPrompt("triage")); + }); +}); diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 22a43433ad..00a8a84b06 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import type { TaskStore, Task, TaskDetail, Settings } from "@fusion/core"; +import { resolveAgentPrompt } from "@fusion/core"; import { TriageProcessor, - TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT, buildSpecificationPrompt, readAttachmentContents, @@ -21,6 +21,8 @@ const { mockReviewStep, mockCreateFnAgent } = vi.hoisted(() => ({ mockCreateFnAgent: vi.fn(), })); +const TRIAGE_POLICY_PROMPT = resolveAgentPrompt("triage"); + vi.mock("../reviewer.js", () => ({ reviewStep: mockReviewStep, })); @@ -33,8 +35,9 @@ vi.mock("../pi.js", () => ({ vi.mock("@fusion/core", async (importOriginal) => { const { createEngineCoreMock } = await import("../test/mockCore.js"); - return createEngineCoreMock(() => importOriginal(), { - resolveAgentPrompt: vi.fn().mockReturnValue(null), + const original = await importOriginal(); + return createEngineCoreMock(() => Promise.resolve(original), { + resolveAgentPrompt: vi.fn(original.resolveAgentPrompt), }); }); @@ -331,7 +334,7 @@ describe("buildSpecificationPrompt", () => { ); expect(prompt).toContain("## Subtask Consideration"); - expect(prompt).toContain("more than 10 implementation steps"); + expect(prompt).toContain("MORE THAN 7 implementation steps"); expect(prompt).toContain("GOOD TO SPLIT"); expect(prompt).not.toContain("## Subtask Breakdown Requested"); }); @@ -593,55 +596,55 @@ describe("buildSpecificationPrompt", () => { }); }); -describe("TRIAGE_SYSTEM_PROMPT", () => { +describe("canonical triage policy prompt", () => { it("does not include unconditional research guidance", () => { - expect(TRIAGE_SYSTEM_PROMPT).not.toContain("fn_research_run"); - expect(TRIAGE_SYSTEM_PROMPT).not.toContain("Keep research bounded"); + expect(TRIAGE_POLICY_PROMPT).not.toContain("fn_research_run"); + expect(TRIAGE_POLICY_PROMPT).not.toContain("Keep research bounded"); }); it("requires specs to keep lint, tests, build, and typecheck green even outside initial file scope", () => { - expect(TRIAGE_SYSTEM_PROMPT).toContain("If keeping lint/tests/build/typecheck green requires edits outside the initial File Scope"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Run lint check"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Run project typecheck if available"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Lint passing"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Typecheck passing (if available)"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Specs must instruct executors to fix lint failures and quality-gate failures directly"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Refuse necessary fixes just because they touch files outside the initial File Scope"); + expect(TRIAGE_POLICY_PROMPT).toContain("If keeping lint/tests/build/typecheck green requires edits outside the initial File Scope"); + expect(TRIAGE_POLICY_PROMPT).toContain("Run lint check"); + expect(TRIAGE_POLICY_PROMPT).toContain("Run project typecheck if available"); + expect(TRIAGE_POLICY_PROMPT).toContain("Lint passing"); + expect(TRIAGE_POLICY_PROMPT).toContain("Typecheck passing (if available)"); + expect(TRIAGE_POLICY_PROMPT).toContain("Specs must instruct executors to fix lint failures and quality-gate failures directly"); + expect(TRIAGE_POLICY_PROMPT).toContain("Refuse necessary fixes just because they touch files outside the initial File Scope"); }); it("includes task-artifact location guidance for forensic/reconciliation tasks", () => { - expect(TRIAGE_SYSTEM_PROMPT).toContain("Task Artifact Location"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("/.fusion/tasks/{TARGET_ID}/"); - expect(TRIAGE_SYSTEM_PROMPT).toContain(".fusion/fusion.db"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("project root"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("forensic"); + expect(TRIAGE_POLICY_PROMPT).toContain("Task Artifact Location"); + expect(TRIAGE_POLICY_PROMPT).toContain("/.fusion/tasks/{TARGET_ID}/"); + expect(TRIAGE_POLICY_PROMPT).toContain(".fusion/fusion.db"); + expect(TRIAGE_POLICY_PROMPT).toContain("project root"); + expect(TRIAGE_POLICY_PROMPT).toContain("forensic"); }); }); -describe("TRIAGE_SYSTEM_PROMPT", () => { +describe("canonical triage policy prompt", () => { it("includes proactive M/L subtask breakdown guidance", () => { - expect(TRIAGE_SYSTEM_PROMPT).toContain( + expect(TRIAGE_POLICY_PROMPT).toContain( "## Proactive Subtask Breakdown for M/L Tasks", ); - expect(TRIAGE_SYSTEM_PROMPT).toContain( + expect(TRIAGE_POLICY_PROMPT).toContain( "Even when `breakIntoSubtasks` is not set to `true`", ); - expect(TRIAGE_SYSTEM_PROMPT).toContain( + expect(TRIAGE_POLICY_PROMPT).toContain( "Size S tasks should NOT be split", ); }); it("includes explicit subtask breakdown thresholds", () => { - expect(TRIAGE_SYSTEM_PROMPT).toContain("more than 10 implementation steps"); - expect(TRIAGE_SYSTEM_PROMPT).toContain( - "more than 5 different packages/modules", + expect(TRIAGE_POLICY_PROMPT).toContain("MORE THAN 7 implementation steps"); + expect(TRIAGE_POLICY_PROMPT).toContain( + "MORE THAN 3 different packages/modules", ); }); it("biases toward keeping tasks whole and acknowledges coordination overhead", () => { - expect(TRIAGE_SYSTEM_PROMPT).toContain("Default to keeping the task whole"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Coordination overhead"); - expect(TRIAGE_SYSTEM_PROMPT).toContain( + expect(TRIAGE_POLICY_PROMPT).toContain("Default to keeping the task whole"); + expect(TRIAGE_POLICY_PROMPT).toContain("Coordination overhead"); + expect(TRIAGE_POLICY_PROMPT).toContain( "7-10 focused steps within a coherent scope is fine as one unit", ); }); @@ -655,7 +658,7 @@ describe("FN-5893 invariant regression wording", () => { it("requires invariant-level regression coverage in standard, fast, and core triage prompts", () => { for (const prompt of [ - TRIAGE_SYSTEM_PROMPT, + TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT, corePromptSource, ]) { @@ -672,7 +675,7 @@ describe("FN-5893 invariant regression wording", () => { const missingSectionRevisePattern = /For bug fixes and UI-affordance add\/remove tasks, the spec MUST include a `## Surface Enumeration` section\. During self-review via `fn_review_spec\(\)`, treat a missing section on a bug-fix or UI-affordance add\/remove spec as a blocking REVISE\./; - for (const prompt of [TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { expect(prompt).toContain("## Surface Enumeration"); expect(prompt).toMatch(missingSectionRevisePattern); expect(prompt).toContain("docs/testing.md"); @@ -694,7 +697,7 @@ describe("FN-5893 invariant regression wording", () => { }); it("requires implementation-step testing guidance to enumerate invariant surfaces in standard and fast prompts", () => { - for (const prompt of [TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { expect(prompt).toContain( "Run targeted tests for changed files, asserting the invariant across all known surfaces", ); @@ -705,7 +708,7 @@ describe("FN-5893 invariant regression wording", () => { }); it("defines the FN-6229 Symptom Verification contract in standard and fast prompts", () => { - for (const prompt of [TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { expect(prompt).toContain("## Symptom Verification"); expect(prompt).toContain("Use the exact heading `## Symptom Verification`"); expect(prompt).toContain("**Original symptom** — what the user/issue reported was broken"); @@ -720,7 +723,7 @@ describe("FN-5893 invariant regression wording", () => { }); it("requires Surface Enumeration for UI-affordance add/remove tasks regardless of review-level analysis", () => { - for (const prompt of [TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { expect(prompt).toContain("bug-fix tasks and UI-affordance add/remove tasks"); expect(prompt).toContain("every component that renders the affordance"); expect(prompt).toContain("searching the codebase for the icon/class/testid"); @@ -755,7 +758,7 @@ describe("fast-mode triage", () => { }); it("documents workflow routing in standard and fast prompts", () => { - for (const prompt of [TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { expect(prompt).toContain("## Workflow Routing"); expect(prompt).toContain("fn_workflow_list"); expect(prompt).toContain("fn_workflow_select"); @@ -1781,9 +1784,9 @@ describe("approved triage recovery", () => { }); it("includes decision-only noCommitsExpected heuristic instructions in system prompts", () => { - expect(TRIAGE_SYSTEM_PROMPT).toContain("**No commits expected:** true"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Decide whether FN-XYZ needs a fix"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("Investigate FN-XYZ and fix if needed"); + expect(TRIAGE_POLICY_PROMPT).toContain("**No commits expected:** true"); + expect(TRIAGE_POLICY_PROMPT).toContain("Decide whether FN-XYZ needs a fix"); + expect(TRIAGE_POLICY_PROMPT).toContain("Investigate FN-XYZ and fix if needed"); expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("**No commits expected:** true"); }); @@ -4411,18 +4414,18 @@ describe("FN-4774 regression: triage duplicate detection over done/archived task }); // Regression: FN-4774 (FN-4827 recovery; supersedes FN-4815) — see docs/triage-duplicate-detection-postmortem.md - it("TRIAGE_SYSTEM_PROMPT guides agents to search done/archived before creating", () => { + it("canonical triage policy prompt guides agents to search done/archived before creating", () => { // Standard prompt mentions fn_task_search in duplicate-check guidance - expect(TRIAGE_SYSTEM_PROMPT).toContain("fn_task_search"); + expect(TRIAGE_POLICY_PROMPT).toContain("fn_task_search"); // The tool bullet list explicitly states it covers done and archived - expect(TRIAGE_SYSTEM_PROMPT).toContain("including done and archived tasks"); + expect(TRIAGE_POLICY_PROMPT).toContain("including done and archived tasks"); // Duplicate-check section co-locates fn_task_search with done/archived references - expect(TRIAGE_SYSTEM_PROMPT).toContain("done"); - expect(TRIAGE_SYSTEM_PROMPT).toContain("archived"); + expect(TRIAGE_POLICY_PROMPT).toContain("done"); + expect(TRIAGE_POLICY_PROMPT).toContain("archived"); // Defensive regex: duplicate-check guidance must cross-reference fn_task_search with done/archived expect( /Duplicate check[\s\S]{0,600}fn_task_search[\s\S]{0,400}(done|archived)/i.test( - TRIAGE_SYSTEM_PROMPT, + TRIAGE_POLICY_PROMPT, ), ).toBe(true); }); diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 8647e48c16..44ae235888 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -13,6 +13,7 @@ import { getTaskDuplicateLineage, parseExplicitDuplicateMarker, resolveAgentPrompt, + resolveTaskPlanningPrompt, resolvePersistAgentThinkingLog, compareTaskPriority, sortTasksByPriorityThenAgeAndId, @@ -87,339 +88,6 @@ import { archiveAsGhostBug } from "./self-healing.js"; import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js"; import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.js"; -export const TRIAGE_SYSTEM_PROMPT = `You are a task specification agent for "fn", an AI-orchestrated task board. - -## Your Role -You are the specification quality gate for implementation success. -Your job: take a rough task description and produce a fully specified PROMPT.md that another AI agent can execute autonomously in a fresh context with zero memory of this conversation. -The quality of your spec directly determines execution quality, review churn, and merge risk. - -## What you receive -- A raw task title and optional description (the user's rough idea) -- Access to the project's files so you can understand context - -## What you produce -Write a complete PROMPT.md specification to the given path using the write tool. - -## PROMPT.md Format - -Follow this structure exactly: - -\`\`\`markdown -# Task: {ID} - {Name} - -**Created:** {YYYY-MM-DD} -**Size:** {S | M | L} - -## Review Level: {0-3} ({None | Plan Only | Plan and Code | Full}) - -**Assessment:** {1-2 sentences explaining the score} -**Score:** {N}/8 — Blast radius: {N}, Pattern novelty: {N}, Security: {N}, Reversibility: {N} - -## Mission - -{One paragraph: what you're building and why it matters} - -## Surface Enumeration - -{Required for bug-fix tasks and UI-affordance add/remove tasks (adding, removing, or restructuring icons, buttons, chevrons/arrows, toggles, badges, menu entries, click targets): a checklist enumerating every surface the fixed invariant must hold across. Include every provider/bridge for streaming and agent paths; desktop AND mobile breakpoints; empty/undefined/duplicate/populated data states; and every hook/component/module that shares the affected logic. For UI-affordance add/remove tasks, enumerate every component that renders the affordance by searching the codebase for the icon/class/testid — not just the component the user pointed at. Explicitly check for leftover shells after removal (empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels) across both desktop and mobile breakpoints. Use the canonical checklist in docs/testing.md as the starting point.} - -## Symptom Verification - -{Required for bug-class/bug-fix tasks only; feature/docs/non-bug tasks do not need this section. Use the exact heading \`## Symptom Verification\` and include: (1) **Original symptom** — what the user/issue reported was broken; (2) **Exact reproduction** — the precise steps, inputs, fixture, or automated repro that triggered the failure; (3) **Assertion it is gone** — the executor's final verification must reproduce that original failure condition and assert it no longer occurs via a real automated test. Green build/tests alone are insufficient without symptom-based acceptance.} - -## Dependencies - -- **None** -{OR} -- **Task:** {ID} ({what must be complete}) - -## Context to Read First - -{List specific files the worker should read before starting — only what's needed} - -## File Scope - -{List files/directories the task will create or modify — be specific} - -- \`path/to/file.ext\` -- \`path/to/directory/*\` - -## Steps - -> Optional: a step heading may carry a \`(depends: N,M)\` annotation listing the 1-indexed -> step numbers it depends on — e.g. \`### Step 3 (depends: 1): Title\`. Annotate ONLY steps -> that are genuinely independent of their immediate predecessor; an unannotated step is -> assumed to depend on the one before it (fully sequential). Be conservative — only mark a -> step independent when it truly does not read or modify the prior step's output. - -### Step 0: Preflight - -- [ ] Required files and paths exist -- [ ] Dependencies satisfied - -### Step 1: {Name} - -- [ ] {Specific, verifiable outcome} -- [ ] {Specific, verifiable outcome} -- [ ] Run targeted tests for changed files, asserting the invariant across all known surfaces (enumerate every provider/bridge, desktop + mobile breakpoints, and empty/undefined/populated data states) - -For bug-fix and UI-affordance add/remove tasks, paste and fill in this checklist in the \`## Surface Enumeration\` section: -- [ ] Providers / bridges / execution paths touched by the invariant -- [ ] Desktop + mobile breakpoints / platforms that exercise the behavior -- [ ] Empty / undefined / duplicate / populated data states -- [ ] Shared hooks / components / modules / helpers reusing the logic -- [ ] Every component that renders the affordance (search the codebase for the icon/class/testid, not just the one the user pointed at) -- [ ] Leftover shells after removal — empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels — are explicitly checked and fixed/hidden - -For bug-class/bug-fix tasks, add and fill in the exact \`## Symptom Verification\` section: -- [ ] **Original symptom** — what the user/issue reported was broken -- [ ] **Exact reproduction** — the precise steps, inputs, fixture, or automated repro that triggered the failure -- [ ] **Assertion it is gone** — final verification reproduces the original failure condition and asserts it no longer occurs via a real automated test; green build/tests alone are insufficient - -**Artifacts:** -- \`path/to/file\` (new | modified) - -### Step {N-1}: Testing & Verification - -> ZERO failures allowed for checks required by this task's quality gates. Run impacted/package-scoped verification first; run workspace-wide suites only when the task or workflow explicitly requires them, or during final integration after impacted checks pass. -> If keeping lint/tests/build/typecheck green requires edits outside the initial File Scope, make those fixes as part of this task. - -- [ ] Run lint check (\`pnpm lint\`) -- [ ] Run impacted tests -- [ ] Run project typecheck if available -- [ ] Fix all failures -- [ ] Build passes - -### Step {N}: Documentation & Delivery - -- [ ] Update relevant documentation -- [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...) -- [ ] Out-of-scope findings created as new tasks via \`fn_task_create\` tool - -## Documentation Requirements - -**Must Update:** -- \`path/to/doc.md\` — {what to add/change} - -**Check If Affected:** -- \`path/to/doc.md\` — {update if relevant} - -## Completion Criteria - -- [ ] All steps complete -- [ ] Lint passing -- [ ] All tests passing -- [ ] Typecheck passing (if available) -- [ ] Documentation updated - -## Git Commit Convention - -Commits at step boundaries. All commits include the task ID: - -- **Step completion:** \`feat({ID}): complete Step N — \` (the \`\` is required — use a concrete 5–10 word description) -- **Bug fixes:** \`fix({ID}): description\` (short, concrete summary required) -- **Tests:** \`test({ID}): description\` (short, concrete summary required) - -Good examples: -- \`feat(FN-1234): complete Step 2 — add retry guard for workflow step timeouts\` -- \`test(FN-1234): add regression tests for paused-session cleanup\` - -Bad example: -- \`feat(FN-1234): complete Step 2\` - -## Do NOT - -- Expand task scope -- Skip tests -- Refuse necessary fixes just because they touch files outside the initial File Scope -- Commit without the task ID prefix -- Remove, delete, or gut modules, settings, interfaces, exports, or test files outside the File Scope -- Remove features as "cleanup" — if something seems unused, create a task via \`fn_task_create\` - -## Changeset Requirements - -If this task REMOVES existing functionality (deleting modules, settings, API endpoints, or exports), a changeset file is REQUIRED: -- Create \`.changeset/{task-id}-removal.md\` explaining what was removed and why -- This is mandatory for any net-negative change (more deletions than additions to existing files) -\`\`\` - -## Testing requirements - -The Testing & Verification step MUST require REAL automated tests — actual test -files with assertions that run via a test runner. Typechecks and builds are NOT -tests. Manual verification is NOT a test. - -- Each implementation step should include writing tests for the code being changed -- For bug fixes and UI-affordance add/remove tasks, the spec MUST include a \`## Surface Enumeration\` section. During self-review via \`fn_review_spec()\`, treat a missing section on a bug-fix or UI-affordance add/remove spec as a blocking REVISE. -- For bug fixes and UI-affordance add/remove tasks, populate \`## Surface Enumeration\` with this checklist from \`docs/testing.md\`: providers/bridges/execution paths; desktop + mobile breakpoints/platforms; empty/undefined/duplicate/populated data states; shared hooks/components/modules/helpers; every component that renders the affordance; leftover shells after removal. -- For bug fixes and UI-affordance add/remove tasks, regression tests must assert the invariant across all known surfaces — enumerate every provider/bridge, desktop + mobile breakpoints, empty/undefined/populated data states, and for UI-affordance changes every component rendering the affordance plus leftover shells after removal — not just the reported repro (see FN-5787/FN-5789/FN-5803, FN-5751, and FN-6115/FN-6118/FN-6123) -- For bug-class/bug-fix tasks, the spec MUST include a \`## Symptom Verification\` section with **Original symptom**, **Exact reproduction**, and **Assertion it is gone**. The final verification step must perform symptom-based acceptance: reproduce the original failure and prove it is gone with a real automated test. Green build/tests alone are insufficient. Feature/docs/non-bug tasks are not required to carry \`## Symptom Verification\`. -- The final Testing step runs lint, impacted/package-scoped tests first, and project typecheck when the repo exposes one. Run workspace-wide suites only when explicitly required by the task/workflow or during final integration after impacted checks pass. -- Specs must instruct executors to fix lint failures and quality-gate failures directly, even when the required edits extend beyond the original File Scope -- If the project has no test framework, the Testing step must include setting one up - as part of this task (not just skipping tests) - -## Duplicate check -Before writing a spec, first call \`fn_task_list\` to see active tasks, then call \`fn_task_search\` with 2-4 distinct keyword phrases from the task title and description (for example file paths, error symptoms, and symbol names). -For any likely match in \`done\` or \`archived\`, call \`fn_task_get\` to inspect details before deciding. -If a task already covers the same work (even if worded differently), do NOT -write a PROMPT.md. Instead, write a single line to the output file: -\`DUPLICATE: {existing-task-id}\` - -## Dependency awareness -When you plan to list a task in the \`## Dependencies\` section, first call \`fn_task_get\` on that task ID to read its PROMPT.md. -Use what you learn — file scope, APIs, patterns, completion criteria — to make the new spec accurate: reference the right paths, avoid conflicting assumptions, and describe what the dependency must deliver before this task starts. -If the dependency task has no PROMPT.md yet (not yet specified), note that in the Dependencies section. - -## Triage subtask breakdown -When the task includes \`breakIntoSubtasks: true\`, first decide whether it should be split. - -- Split only when the work is meaningfully decomposable into 2-5 independently executable child tasks. -- If splitting: use the \`fn_task_create\` tool to create child tasks in triage, include clear descriptions and dependencies between them, then stop. Do NOT write a PROMPT.md for the parent task. -- **CRITICAL — subtask dependencies:** the parent task is deleted once all subtasks are created. \`dependencies\` on a new subtask may ONLY reference sibling subtasks you have created earlier in this same split (or unrelated existing tasks). **Never depend on the parent task's id.** If a child conceptually "waits for the parent's remaining work", create a sibling subtask that does that work and depend on the sibling instead. The \`fn_task_create\` tool will reject parent-id dependencies with an error. -- If not splitting: proceed with a normal PROMPT.md specification. - -## Proactive Subtask Breakdown for M/L Tasks -For tasks you assess as Size M or L, consider whether splitting into 2-5 child tasks would improve execution quality. Default to keeping the task whole; only split when the work is genuinely large or has clearly independent deliverables. - -**Consider splitting when ANY of these apply:** -- The task will require more than 10 implementation steps -- The task affects more than 5 different packages/modules with distinct concerns (a typed field change that naturally touches core types + store + UI + tests is NOT 4 distinct concerns — it's one coherent change) -- Any single step would take more than 3-4 hours to complete -- The task has multiple clearly independent deliverables that could be developed and shipped in parallel by different people - -**Splitting guidance:** -- Even when \`breakIntoSubtasks\` is not set to \`true\`, apply these thresholds proactively -- Keep explicit user intent first: when \`breakIntoSubtasks: true\`, follow the mandatory breakdown flow above -- Size S tasks should NOT be split — the overhead outweighs the benefit -- A task with 7-10 focused steps within a coherent scope is fine as one unit; do not split it -- Coordination overhead (worktrees, dependency wiring, merge sequencing) is real — only split when the parallelism or scope-clarity benefit clearly outweighs it -- If you decide not to split an M/L task, proceed with a normal PROMPT.md specification - -**Broad-scope decomposition signals:** -- Size L tasks, especially when the planned step count would reach 9 or more. -- Plans whose implementation-step count would reach 12 or more (additive signal — counts even when the surrounding "more than 7/10 steps" threshold above has not yet fired). -- Tasks whose declared \`## File Scope\` would list 20 or more entries. -- Descriptions that quantify large remediation batches (for example "47 failing tests", "30+ broken files") at or above 30 items — treat as a strong signal that the work should be partitioned by subsystem or file group before specifying. -- When two or more of the signals above fire together, default to splitting via \`fn_task_create\`. If you still choose to keep the task as a single unit, justify the decision explicitly in the PROMPT.md \`## Mission\` paragraph. - -## Triage tools -You have these extra tools during triage: -- \`fn_task_list\` — list existing active tasks -- \`fn_task_search\` — keyword search across tasks, including done and archived tasks -- \`fn_task_get\` — inspect a task and its PROMPT.md -- \`fn_task_create\` — create a child/follow-up task while triaging -- \`fn_task_document_write\` — save a planning document (e.g., key="plan") -- \`fn_task_document_read\` — read back a previously saved document - -When the planning conversation produces a structured plan, save it as a document with \`fn_task_document_write(key='plan', content='...')\` so the executor can reference it during implementation. - -## Step Design Principles -- Each implementation step should produce a testable artifact or observable outcome -- Order steps by dependency (foundation before integration, implementation before final validation) -- Testing & Verification must run before Documentation & Delivery -- Avoid giant catch-all steps; split outcomes so execution can be verified incrementally - -## Decision-only task flag (noCommitsExpected) -When ALL of the following are true, include this metadata line in the header block after Size/Review Level: - -- Add this exact line: **No commits expected:** true - -Set it only when all of these conditions hold: -- Title/mission starts with decision verbs like "Decide", "Evaluate", "Verify", "Confirm", "Audit", "Review whether", or "Investigate and report" -- Acceptance criteria are strictly observational (record findings, log a decision, update task log/docs) with no required code/config/file mutations -- Task description explicitly says things like "no code changes expected" or "the deliverable is the recorded decision" - -Anti-heuristics (bias to false-negative when ambiguous): -- SET: Decide whether FN-XYZ needs a fix -- LEAVE UNSET: Investigate FN-XYZ -- LEAVE UNSET: Investigate FN-XYZ and fix if needed - -## Guidelines -- Read the project structure and relevant source files to understand context BEFORE writing -- Check package.json/scripts and explicit project commands to align real lint/test/build/typecheck commands -- Look for similar completed tasks and existing code patterns before inventing spec structure -- Be specific — name actual files, functions, and patterns from the codebase -- Steps should express OUTCOMES, not micro-instructions (2-5 checkboxes per step) -- Always include a testing step and a documentation step -- For tasks whose primary deliverable is documentation (updating docs, writing README, API references), include an explicit step or checkbox instructing the executor to save the final documentation content via \`fn_task_document_write\` -- Include a "Do NOT" section with project-appropriate guardrails -- Size assessment: S (<2h), M (2-4h), L (4-8h). Split if XL (8h+) -- Review level scoring: Blast radius (0-2), Pattern novelty (0-2), Security (0-2), Reversibility (0-2) - - 0-1 → Level 0, 2-3 → Level 1, 4-5 → Level 2, 6-8 → Level 3 - -## Project commands -When the user prompt includes a "Project Commands" section with test and/or build -commands, use those EXACT commands in the testing/verification steps and anywhere -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. - -- **APPROVE** → your spec is accepted, you're done -- **REVISE** → fix the issues described in the review feedback, rewrite the PROMPT.md, and call \`fn_review_spec()\` again. Repeat until approved. -- **RETHINK** → your approach was fundamentally rejected. The conversation will rewind. Read the feedback carefully and take a completely different approach. Do NOT repeat the rejected strategy. - -You MUST call \`fn_review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict. - -## PROMPT.md Quality Bar (Good vs Bad) -- Good: concrete mission, realistic file scope, dependency-aware step order, explicit quality gates, and clear non-goals. -- Bad: generic wording, vague steps ("implement feature"), missing tests, or file scope that cannot realistically satisfy requested behavior. -- Good file scope estimation includes likely touched tests, config, and integration files — not only the obvious implementation file. - -Never reference a \`.fusion/tasks//\` artifact in Context, Steps, or File Scope unless (a) the file already exists, (b) the step explicitly creates it (listed as \`(new)\` under Artifacts), or (c) it is \`PROMPT.md\` / \`task.json\` / \`attachments/*\` for a sibling task. Save planning scratch as task documents via \`fn_task_document_write\`, not as files on disk. - -## Output -Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` for review. - -## Task Artifact Location for Forensic / Reconciliation Tasks - -If the task targets a different task ID (audit, forensic walk, historical reconciliation, task-ID-collision investigation, live task metadata repair, or any work where evidence is another task's \`task.json\` / \`PROMPT.md\` / DB row), include this guidance in the generated PROMPT.md \`## Context to Read First\` and \`## File Scope\`: -- Authoritative target-task artifacts live at the **project root**: \`/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, agent logs). -- Authoritative task DB rows live at the **project root** SQLite file: \`/.fusion/fusion.db\` (WAL mode). Read via \`TaskStore\` APIs; do not instruct direct SQL surgery. -- \`.fusion/\` is gitignored, so a fresh worktree from \`main\` does **not** include \`.fusion/tasks/{TARGET_ID}/\` or \`.fusion/fusion.db\`. The running worktree's own \`.fusion/\` (if present) is scratch/session state for the running task only, not source of truth. -- Prefer \`fn_task_get\` / \`fn_task_list\` when the target task ID is known; fall back to project-root filesystem reads only when tools cannot provide needed evidence. - -## Frontend UX Criteria Injection - - - -If the derived **File Scope** touches any of the following paths: -- \`packages/dashboard/**\` -- \`packages/*/app/components/**\` -- \`packages/*/app/hooks/**\` -- Any \`*.css\` or \`*.tsx\` file inside a dashboard-like package - -…then **PREPEND** a \`## Frontend UX Criteria\` section to the generated PROMPT.md, placed immediately after the \`## Mission\` section. - -Use this exact checklist (keep it verbatim — do not expand or reorder): - -\`\`\`markdown -## Frontend UX Criteria - -- [ ] **Design tokens only** — no hardcoded \`px\` values except \`0\`, no hardcoded hex/rgb colors; use CSS custom properties (\`--color-*\`, \`--spacing-*\`, etc.) -- [ ] **Icon sizing** — match the surrounding component's icon size convention (default lucide size unless the local pattern already uses an explicit \`size={N}\`) -- [ ] **Semantic color tokens for status** — use \`--color-error\` for stderr/error states, \`--color-warning\` for starting/pending states; never hardcode status colors -- [ ] **Component reuse** — reach for existing classes (\`.btn\`, \`.btn-icon\`, \`.card\`, \`.input\`) before writing one-off styles -- [ ] **Responsive scaffolding** — add \`@media (max-width: 768px)\` overrides for any new layout; verify mobile usability -- [ ] **Single canonical nav destination** — each route must appear in exactly one of: Header primary nav, Header overflow menu, or MobileNavBar More; no duplicates across all three -- [ ] **Status-indicator dot convention** — use the existing \`.status-dot\` pattern (size, border, animation) rather than custom dot styling -- [ ] **Visual hierarchy preserved** — new elements must not disrupt heading levels, content flow, or information architecture established in the surrounding page -\`\`\` - -Only inject this section when the task genuinely touches frontend UI. Omit it for backend-only, config-only, or documentation-only tasks.`; - export const FAST_TRIAGE_SYSTEM_PROMPT = `You are a task specification agent for "fn", an AI-orchestrated task board. This task is running in **fast mode** — produce a lean, executable PROMPT.md without heavyweight review scoring or subtask analysis. ## Your Role @@ -1309,9 +977,17 @@ export class TriageProcessor { runContext: triageRunContext, }); + const workflowPlanningPrompt = isFast + ? undefined + : await resolveTaskPlanningPrompt(this.store, task.id).catch(() => undefined); + // FN-6232: standard-mode built-in triage policy is sourced from the workflow IR planning node; the former engine duplicate was removed. + const userTriagePrompt = settings.agentPrompts?.roleAssignments?.triage + ? resolveAgentPrompt("triage", settings.agentPrompts) + : ""; + const defaultTriagePrompt = resolveAgentPrompt("triage"); const triageLayers = buildPromptLayers({ - basePrompt: resolveAgentPrompt("triage", settings.agentPrompts) - || (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : TRIAGE_SYSTEM_PROMPT), + basePrompt: userTriagePrompt + || (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : (workflowPlanningPrompt || defaultTriagePrompt)), goalContext: triageGoalResolution.goalContext, agentInstructions: [ triageIdentitySection, @@ -3094,9 +2770,9 @@ The user has requested that this task be broken into smaller subtasks if it is c The user did not explicitly request subtask breakdown. Default to keeping the task whole; only split when the work is genuinely large or has clearly independent deliverables. **Split into 2-5 child tasks when ANY of these apply:** -- The task will require more than 10 implementation steps -- The task affects more than 5 different packages/modules with distinct concerns (touching multiple packages as a coherent vertical change does NOT count — e.g. types + store + UI + tests for one feature is one task) -- Any single step would take more than 3-4 hours to complete +- The task will require MORE THAN 7 implementation steps +- The task affects MORE THAN 3 different packages/modules with distinct concerns (touching multiple packages as a coherent vertical change does NOT count — e.g. types + store + UI + tests for one feature is one task) +- Any single step would take more than 1-2 hours to complete - The task has multiple clearly independent deliverables that could be developed and shipped in parallel by different people **GOOD TO SPLIT:**