FN-6232: centralize triage planning prompt resolution

Route standard triage prompt construction through workflow IR while keeping custom and fast-mode overrides intact.

- Export the workflow planning prompt resolver and use it as the standard triage prompt source.
- Move duplicated triage policy text into the shared core prompt definition.
- Add regression coverage for workflow-sourced planning prompts, duplicate search guidance, and prompt override behavior.
- Add a patch changeset for the published CLI bundle.

Files changed:
 .changeset/FN-6232-triage-prompt-single-source.md  |   5 +
 packages/core/src/__tests__/agent-prompts.test.ts  |  58 ++--
 packages/core/src/agent-prompts.ts                 | 111 +++++--
 packages/core/src/index.ts                         |   2 +
 packages/core/src/workflow-ir-resolver.ts          |  28 ++
 .../triage-duplicate-search-regression.test.ts     |  19 +-
 .../triage-planning-prompt-single-source.test.ts   | 179 +++++++++++
 packages/engine/src/__tests__/triage.test.ts       |  91 +++---
 packages/engine/src/triage.ts                      | 352 +--------------------
 9 files changed, 413 insertions(+), 432 deletions(-)

Fusion-Task-Id: FN-6232

Fusion-Task-Lineage: c70c33ed-c61b-49d2-bcea-d860899c1512
This commit is contained in:
gsxdsm
2026-06-11 07:18:15 -07:00
parent 36f5ecdaba
commit 1a716f2f95
9 changed files with 411 additions and 430 deletions

View File

@@ -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.

View File

@@ -8,7 +8,10 @@ import {
getAvailableTemplates, getAvailableTemplates,
getTemplatesForRole, getTemplatesForRole,
} from "../agent-prompts.js"; } 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 { AgentPromptsConfig, AgentPromptTemplate } from "../types.js";
import type { WorkflowIr } from "../workflow-ir-types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// resolveAgentPrompt // resolveAgentPrompt
@@ -257,36 +260,43 @@ describe("resolveAgentPrompt", () => {
expect(result).toContain("task_document_write"); 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 corePrompt = resolveAgentPrompt("triage");
const planningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR);
const triageSource = readFileSync( const triageSource = readFileSync(
resolve(fileURLToPath(new URL("..", import.meta.url)), "..", "..", "engine", "src", "triage.ts"), resolve(fileURLToPath(new URL("..", import.meta.url)), "..", "..", "engine", "src", "triage.ts"),
"utf8", "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(triageSource).not.toMatch(/export const TRIAGE_SYSTEM_PROMPT\s*=/);
expect(prompt).toContain("**Broad-scope decomposition signals:**"); expect(triageSource).not.toMatch(/export const (?!FAST_TRIAGE_SYSTEM_PROMPT)[A-Z_]*TRIAGE[A-Z_]*SYSTEM_PROMPT\s*=/);
expect(prompt).toContain("step count would reach 9 or more"); expect(planningPrompt).toBe(corePrompt);
expect(prompt).toContain("would reach 12 or more"); expect(corePrompt).toContain("**Broad-scope decomposition signals:**");
expect(prompt).toContain("20 or more entries"); expect(corePrompt).toContain("step count would reach 9 or more");
expect(prompt).toContain("at or above 30 items"); 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:**"; it("resolves custom planning prompts and ignores IRs without planning prompts", () => {
const blockRegex = /\*\*Broad-scope decomposition signals:\*\*[\s\S]*?(?=\n\n(?:##|\*\*))/; const customIr: WorkflowIr = {
const coreStart = corePrompt.indexOf(marker); version: "v1",
const engineStart = enginePrompt.indexOf(marker); name: "custom",
expect(coreStart).toBeGreaterThanOrEqual(0); nodes: [
expect(engineStart).toBeGreaterThanOrEqual(0); { 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]; expect(resolvePlanningPromptFromIr(customIr)).toBe("custom planning prompt");
const engineBlock = enginePrompt.slice(engineStart).match(blockRegex)?.[0]; expect(resolvePlanningPromptFromIr(noPlanningIr)).toBeUndefined();
expect(coreBlock).toBeTruthy();
expect(engineBlock).toBeTruthy();
expect(coreBlock).toBe(engineBlock);
}); });
it("built-in triage prompt requires surface enumeration for bug-fix specs", () => { it("built-in triage prompt requires surface enumeration for bug-fix specs", () => {

View File

@@ -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. 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. 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 ## What you receive
- A raw task title and optional description (the user's rough idea) - A raw task title and optional description (the user's rough idea)
@@ -237,7 +240,11 @@ Follow this structure exactly:
## Surface Enumeration ## 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 ## Dependencies
@@ -258,6 +265,12 @@ Follow this structure exactly:
## Steps ## 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 ### Step 0: Preflight
- [ ] Required files and paths exist - [ ] Required files and paths exist
@@ -269,11 +282,18 @@ Follow this structure exactly:
- [ ] {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) - [ ] 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 - [ ] Providers / bridges / execution paths touched by the invariant
- [ ] Desktop + mobile breakpoints / platforms that exercise the behavior - [ ] Desktop + mobile breakpoints / platforms that exercise the behavior
- [ ] Empty / undefined / duplicate / populated data states - [ ] Empty / undefined / duplicate / populated data states
- [ ] Shared hooks / components / modules / helpers reusing the logic - [ ] 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:** **Artifacts:**
- \`path/to/file\` (new | modified) - \`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: Commits at step boundaries. All commits include the task ID:
- **Step completion:** \`feat({ID}): complete Step N — description\` - **Step completion:** \`feat({ID}): complete Step N — <short summary>\` (the \`<short summary>\` is required — use a concrete 5–10 word description)
- **Bug fixes:** \`fix({ID}): description\` - **Bug fixes:** \`fix({ID}): description\` (short, concrete summary required)
- **Tests:** \`test({ID}): description\` - **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 ## 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. tests. Manual verification is NOT a test.
- Each implementation step should include writing tests for the code being changed - 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 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, 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 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, 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, 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. - 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 - 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 - If the project has no test framework, the Testing step must include setting one up
as part of this task (not just skipping tests) as part of this task (not just skipping tests)
## Duplicate check ## 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 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: write a PROMPT.md. Instead, write a single line to the output file:
\`DUPLICATE: {existing-task-id}\` \`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. - If not splitting: proceed with a normal PROMPT.md specification.
## Proactive Subtask Breakdown for M/L Tasks ## 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 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 - Any single step would take more than 1-2 hours to complete
- The task has multiple independent deliverables that could be developed in parallel - The task has multiple clearly independent deliverables that could be developed and shipped in parallel by different people
**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.
**Splitting guidance:** **Splitting guidance:**
- Even when \`breakIntoSubtasks\` is not set to \`true\`, apply these thresholds proactively - 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 - 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 - Size S tasks should NOT be split — the overhead outweighs the benefit
- Only keep a task as one unit if it genuinely has 5 or fewer focused steps with a clear scope - 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 - If you decide not to split an M/L task, proceed with a normal PROMPT.md specification
**Broad-scope decomposition signals:** **Broad-scope decomposition signals:**
@@ -397,6 +425,7 @@ For tasks you assess as Size M or L, proactively evaluate whether splitting into
## Triage tools ## Triage tools
You have these extra tools during triage: You have these extra tools during triage:
- \`fn_task_list\` — list existing active tasks - \`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_get\` — inspect a task and its PROMPT.md
- \`fn_task_create\` — create a child/follow-up task while triaging - \`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_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. 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 ## Guidelines
- Read the project structure and relevant source files to understand context BEFORE writing - 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 - Be specific — name actual files, functions, and patterns from the codebase
- Steps should express OUTCOMES, not micro-instructions (2-5 checkboxes per step) - Steps should express OUTCOMES, not micro-instructions (2-5 checkboxes per step)
- Always include a testing step and a documentation 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 the spec references running tests or builds. Do NOT guess or infer commands from
package.json when explicit commands are provided. package.json when explicit commands are provided.
## Workflow Routing
- Call \`fn_workflow_list\` to discover available workflows before selecting a routing path, and read each workflow description as the routing signal.
- For investigation, audit, research, or decision-only tasks that produce no code changes, set \`**No commits expected:** true\` in the PROMPT.md header when the no-commits criteria above are met, then select an appropriate lightweight workflow.
- For decision-only tasks (Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report), prefer \`builtin:quick-fix\` or a custom investigation workflow when one is available.
- For standard coding tasks, \`builtin:coding\` is the default and is usually appropriate.
- Use \`fn_workflow_select\` to set the workflow on the current task, or pass \`workflow_id\` to \`fn_task_create\` when creating subtasks.
- Match the task nature to the workflow description; descriptions are authoritative for routing decisions.
## Spec Review ## Spec Review
After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent quality review. After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent quality review.
@@ -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. 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/<id>/<file>\` 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 ## Output
Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` for review. 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**: \`<rootDir>/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, agent logs).
- Authoritative task DB rows live at the **project root** SQLite file: \`<rootDir>/.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 ## Frontend UX Criteria Injection
<!-- UX criteria mirror the "frontend-ux-design" reviewer persona in packages/core/src/types.ts — keep them aligned. --> <!-- UX criteria mirror the "frontend-ux-design" reviewer persona in packages/core/src/types.ts — keep them aligned. -->
@@ -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 - [ ] **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. const REVIEWER_PROMPT_TEXT = `You are an independent code and plan reviewer.

View File

@@ -309,6 +309,8 @@ export {
export { export {
resolveWorkflowIrForTask, resolveWorkflowIrForTask,
resolveWorkflowIrById, resolveWorkflowIrById,
resolvePlanningPromptFromIr,
resolveTaskPlanningPrompt,
type WorkflowIrResolverStore, type WorkflowIrResolverStore,
} from "./workflow-ir-resolver.js"; } from "./workflow-ir-resolver.js";
export { export {

View File

@@ -25,6 +25,34 @@ export interface WorkflowIrResolverStore {
getWorkflowDefinition(id: string): Promise<{ ir: string | WorkflowIr } | undefined>; 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<string, WorkflowIr>,
): Promise<string | undefined> {
const ir = await resolveWorkflowIrForTask(store, taskId, irCache);
return resolvePlanningPromptFromIr(ir);
}
/** /**
* Resolve a workflow IR by its id (built-in or custom). * Resolve a workflow IR by its id (built-in or custom).
* *

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { resolveAgentPrompt } from "@fusion/core";
import { import {
FAST_TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT,
TRIAGE_SYSTEM_PROMPT,
TriageProcessor, TriageProcessor,
} from "../triage.js"; } from "../triage.js";
import { createTriageDuplicateScenario } from "./fixtures/triage-duplicate-scenario.js"; import { createTriageDuplicateScenario } from "./fixtures/triage-duplicate-scenario.js";
@@ -23,15 +23,18 @@ vi.mock("../pi.js", () => ({
vi.mock("@fusion/core", async (importOriginal) => { vi.mock("@fusion/core", async (importOriginal) => {
const { createEngineCoreMock } = await import("../test/mockCore.js"); const { createEngineCoreMock } = await import("../test/mockCore.js");
return createEngineCoreMock(() => importOriginal<typeof import("@fusion/core")>(), { const original = await importOriginal<typeof import("@fusion/core")>();
resolveAgentPrompt: vi.fn().mockReturnValue(null), 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 * 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, * 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. * (3) preserving that guidance in FAST_TRIAGE_SYSTEM_PROMPT. FN-4815 pins this contract.
*/ */
describe("FN-4815 triage duplicate-search regression", () => { 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", () => { it("standard prompt guidance keeps duplicate-search instructions", () => {
expect(TRIAGE_SYSTEM_PROMPT).toContain("Duplicate check"); expect(TRIAGE_POLICY_PROMPT).toContain("Duplicate check");
expect(TRIAGE_SYSTEM_PROMPT).toContain("fn_task_search"); expect(TRIAGE_POLICY_PROMPT).toContain("fn_task_search");
expect(TRIAGE_SYSTEM_PROMPT).toContain("including done and archived tasks"); expect(TRIAGE_POLICY_PROMPT).toContain("including done and archived tasks");
expect(/Duplicate check[\s\S]{0,700}(done|archived)/i.test(TRIAGE_SYSTEM_PROMPT)).toBe(true); expect(/Duplicate check[\s\S]{0,700}(done|archived)/i.test(TRIAGE_POLICY_PROMPT)).toBe(true);
}); });
it("fast prompt guidance keeps duplicate-search instructions", () => { it("fast prompt guidance keeps duplicate-search instructions", () => {

View File

@@ -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<typeof import("@fusion/core")>();
return createEngineCoreMock(() => Promise.resolve(original), {
resolveAgentPrompt: vi.fn(original.resolveAgentPrompt),
});
});
function createTask(overrides: Partial<Task> = {}): 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<TaskStore> = {}, settings: Partial<Settings> = {}): 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<string> {
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<Settings>);
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"));
});
});

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { TaskStore, Task, TaskDetail, Settings } from "@fusion/core"; import type { TaskStore, Task, TaskDetail, Settings } from "@fusion/core";
import { resolveAgentPrompt } from "@fusion/core";
import { import {
TriageProcessor, TriageProcessor,
TRIAGE_SYSTEM_PROMPT,
FAST_TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT,
buildSpecificationPrompt, buildSpecificationPrompt,
readAttachmentContents, readAttachmentContents,
@@ -21,6 +21,8 @@ const { mockReviewStep, mockCreateFnAgent } = vi.hoisted(() => ({
mockCreateFnAgent: vi.fn(), mockCreateFnAgent: vi.fn(),
})); }));
const TRIAGE_POLICY_PROMPT = resolveAgentPrompt("triage");
vi.mock("../reviewer.js", () => ({ vi.mock("../reviewer.js", () => ({
reviewStep: mockReviewStep, reviewStep: mockReviewStep,
})); }));
@@ -33,8 +35,9 @@ vi.mock("../pi.js", () => ({
vi.mock("@fusion/core", async (importOriginal) => { vi.mock("@fusion/core", async (importOriginal) => {
const { createEngineCoreMock } = await import("../test/mockCore.js"); const { createEngineCoreMock } = await import("../test/mockCore.js");
return createEngineCoreMock(() => importOriginal<typeof import("@fusion/core")>(), { const original = await importOriginal<typeof import("@fusion/core")>();
resolveAgentPrompt: vi.fn().mockReturnValue(null), return createEngineCoreMock(() => Promise.resolve(original), {
resolveAgentPrompt: vi.fn(original.resolveAgentPrompt),
}); });
}); });
@@ -331,7 +334,7 @@ describe("buildSpecificationPrompt", () => {
); );
expect(prompt).toContain("## Subtask Consideration"); 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).toContain("GOOD TO SPLIT");
expect(prompt).not.toContain("## Subtask Breakdown Requested"); 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", () => { it("does not include unconditional research guidance", () => {
expect(TRIAGE_SYSTEM_PROMPT).not.toContain("fn_research_run"); expect(TRIAGE_POLICY_PROMPT).not.toContain("fn_research_run");
expect(TRIAGE_SYSTEM_PROMPT).not.toContain("Keep research bounded"); 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", () => { 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_POLICY_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_POLICY_PROMPT).toContain("Run lint check");
expect(TRIAGE_SYSTEM_PROMPT).toContain("Run project typecheck if available"); expect(TRIAGE_POLICY_PROMPT).toContain("Run project typecheck if available");
expect(TRIAGE_SYSTEM_PROMPT).toContain("Lint passing"); expect(TRIAGE_POLICY_PROMPT).toContain("Lint passing");
expect(TRIAGE_SYSTEM_PROMPT).toContain("Typecheck passing (if available)"); expect(TRIAGE_POLICY_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_POLICY_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("Refuse necessary fixes just because they touch files outside the initial File Scope");
}); });
it("includes task-artifact location guidance for forensic/reconciliation tasks", () => { it("includes task-artifact location guidance for forensic/reconciliation tasks", () => {
expect(TRIAGE_SYSTEM_PROMPT).toContain("Task Artifact Location"); expect(TRIAGE_POLICY_PROMPT).toContain("Task Artifact Location");
expect(TRIAGE_SYSTEM_PROMPT).toContain("<rootDir>/.fusion/tasks/{TARGET_ID}/"); expect(TRIAGE_POLICY_PROMPT).toContain("<rootDir>/.fusion/tasks/{TARGET_ID}/");
expect(TRIAGE_SYSTEM_PROMPT).toContain(".fusion/fusion.db"); expect(TRIAGE_POLICY_PROMPT).toContain(".fusion/fusion.db");
expect(TRIAGE_SYSTEM_PROMPT).toContain("project root"); expect(TRIAGE_POLICY_PROMPT).toContain("project root");
expect(TRIAGE_SYSTEM_PROMPT).toContain("forensic"); expect(TRIAGE_POLICY_PROMPT).toContain("forensic");
}); });
}); });
describe("TRIAGE_SYSTEM_PROMPT", () => { describe("canonical triage policy prompt", () => {
it("includes proactive M/L subtask breakdown guidance", () => { 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", "## 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`", "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", "Size S tasks should NOT be split",
); );
}); });
it("includes explicit subtask breakdown thresholds", () => { it("includes explicit subtask breakdown thresholds", () => {
expect(TRIAGE_SYSTEM_PROMPT).toContain("more than 10 implementation steps"); expect(TRIAGE_POLICY_PROMPT).toContain("MORE THAN 7 implementation steps");
expect(TRIAGE_SYSTEM_PROMPT).toContain( expect(TRIAGE_POLICY_PROMPT).toContain(
"more than 5 different packages/modules", "MORE THAN 3 different packages/modules",
); );
}); });
it("biases toward keeping tasks whole and acknowledges coordination overhead", () => { it("biases toward keeping tasks whole and acknowledges coordination overhead", () => {
expect(TRIAGE_SYSTEM_PROMPT).toContain("Default to keeping the task whole"); expect(TRIAGE_POLICY_PROMPT).toContain("Default to keeping the task whole");
expect(TRIAGE_SYSTEM_PROMPT).toContain("Coordination overhead"); expect(TRIAGE_POLICY_PROMPT).toContain("Coordination overhead");
expect(TRIAGE_SYSTEM_PROMPT).toContain( expect(TRIAGE_POLICY_PROMPT).toContain(
"7-10 focused steps within a coherent scope is fine as one unit", "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", () => { it("requires invariant-level regression coverage in standard, fast, and core triage prompts", () => {
for (const prompt of [ for (const prompt of [
TRIAGE_SYSTEM_PROMPT, TRIAGE_POLICY_PROMPT,
FAST_TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT,
corePromptSource, corePromptSource,
]) { ]) {
@@ -672,7 +675,7 @@ describe("FN-5893 invariant regression wording", () => {
const missingSectionRevisePattern = 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 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).toContain("## Surface Enumeration");
expect(prompt).toMatch(missingSectionRevisePattern); expect(prompt).toMatch(missingSectionRevisePattern);
expect(prompt).toContain("docs/testing.md"); 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", () => { 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( expect(prompt).toContain(
"Run targeted tests for changed files, asserting the invariant across all known surfaces", "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", () => { 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("## Symptom Verification");
expect(prompt).toContain("Use the exact heading `## Symptom Verification`"); expect(prompt).toContain("Use the exact heading `## Symptom Verification`");
expect(prompt).toContain("**Original symptom** — what the user/issue reported was broken"); 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", () => { 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("bug-fix tasks and UI-affordance add/remove tasks");
expect(prompt).toContain("every component that renders the affordance"); expect(prompt).toContain("every component that renders the affordance");
expect(prompt).toContain("searching the codebase for the icon/class/testid"); 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", () => { 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("## Workflow Routing");
expect(prompt).toContain("fn_workflow_list"); expect(prompt).toContain("fn_workflow_list");
expect(prompt).toContain("fn_workflow_select"); expect(prompt).toContain("fn_workflow_select");
@@ -1781,9 +1784,9 @@ describe("approved triage recovery", () => {
}); });
it("includes decision-only noCommitsExpected heuristic instructions in system prompts", () => { it("includes decision-only noCommitsExpected heuristic instructions in system prompts", () => {
expect(TRIAGE_SYSTEM_PROMPT).toContain("**No commits expected:** true"); expect(TRIAGE_POLICY_PROMPT).toContain("**No commits expected:** true");
expect(TRIAGE_SYSTEM_PROMPT).toContain("Decide whether FN-XYZ needs a fix"); expect(TRIAGE_POLICY_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("Investigate FN-XYZ and fix if needed");
expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("**No commits expected:** true"); 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 // 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 // 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 // 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 // Duplicate-check section co-locates fn_task_search with done/archived references
expect(TRIAGE_SYSTEM_PROMPT).toContain("done"); expect(TRIAGE_POLICY_PROMPT).toContain("done");
expect(TRIAGE_SYSTEM_PROMPT).toContain("archived"); expect(TRIAGE_POLICY_PROMPT).toContain("archived");
// Defensive regex: duplicate-check guidance must cross-reference fn_task_search with done/archived // Defensive regex: duplicate-check guidance must cross-reference fn_task_search with done/archived
expect( expect(
/Duplicate check[\s\S]{0,600}fn_task_search[\s\S]{0,400}(done|archived)/i.test( /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); ).toBe(true);
}); });

View File

@@ -13,6 +13,7 @@ import {
getTaskDuplicateLineage, getTaskDuplicateLineage,
parseExplicitDuplicateMarker, parseExplicitDuplicateMarker,
resolveAgentPrompt, resolveAgentPrompt,
resolveTaskPlanningPrompt,
resolvePersistAgentThinkingLog, resolvePersistAgentThinkingLog,
compareTaskPriority, compareTaskPriority,
sortTasksByPriorityThenAgeAndId, sortTasksByPriorityThenAgeAndId,
@@ -87,339 +88,6 @@ import { archiveAsGhostBug } from "./self-healing.js";
import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js"; import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js";
import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.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 — <short summary>\` (the \`<short summary>\` 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/<id>/<file>\` 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**: \`<rootDir>/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, agent logs).
- Authoritative task DB rows live at the **project root** SQLite file: \`<rootDir>/.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
<!-- UX criteria mirror the "frontend-ux-design" reviewer persona in packages/core/src/types.ts — keep them aligned. -->
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. 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 ## Your Role
@@ -1309,9 +977,17 @@ export class TriageProcessor {
runContext: triageRunContext, 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({ const triageLayers = buildPromptLayers({
basePrompt: resolveAgentPrompt("triage", settings.agentPrompts) basePrompt: userTriagePrompt
|| (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : TRIAGE_SYSTEM_PROMPT), || (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : (workflowPlanningPrompt || defaultTriagePrompt)),
goalContext: triageGoalResolution.goalContext, goalContext: triageGoalResolution.goalContext,
agentInstructions: [ agentInstructions: [
triageIdentitySection, 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. 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:** **Split into 2-5 child tasks when ANY of these apply:**
- The task will require more than 10 implementation steps - The task will require MORE THAN 7 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) - 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 3-4 hours to complete - 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 - The task has multiple clearly independent deliverables that could be developed and shipped in parallel by different people
**GOOD TO SPLIT:** **GOOD TO SPLIT:**