diff --git a/.changeset/FN-6236-fast-mode-workflow-variant.md b/.changeset/FN-6236-fast-mode-workflow-variant.md new file mode 100644 index 0000000000..09849f932f --- /dev/null +++ b/.changeset/FN-6236-fast-mode-workflow-variant.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +Fast-mode triage is now expressed as workflow-declared policy: the lean prompt lives in the built-in `default-triage-fast` agent prompt and `planning-fast` seam, while `leanPlanning` and `autoApproveSpec` are workflow-native settings for prompt selection and spec-review auto-approval. + +The internal `FAST_TRIAGE_SYSTEM_PROMPT` engine constant was removed. Existing `executionMode: "fast"` tasks remain byte-equivalent through a single legacy execution-mode-to-resolved-policy bridge. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index ca24be258b..b06c501e29 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -254,6 +254,8 @@ The built-in workflows also declare triage/spec policy settings that were **not* | `triageNoCommitsDecisionVerbs` | all seven built-ins | Decision-only verbs: Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report. | | `triageDecisionOnlyWorkflowId` | `builtin:quick-fix` | Preferred workflow for decision-only/no-commit tasks. | | `triageDefaultWorkflowId` | `builtin:coding` | Default workflow for standard coding tasks. | +| `leanPlanning` | `false` | Workflow-native fast-mode policy: select the lean `planning-fast` prompt variant instead of the full triage spec prompt. | +| `autoApproveSpec` | `false` | Workflow-native fast-mode policy: auto-approve generated specs and skip the independent spec reviewer. | In the dashboard Settings modal, Project Models now exposes Plan/Triage, Executor, and Reviewer dropdown controls for the default workflow. The modal's primary diff --git a/packages/core/src/__tests__/agent-prompts.test.ts b/packages/core/src/__tests__/agent-prompts.test.ts index 002cae9f4f..93464d08fa 100644 --- a/packages/core/src/__tests__/agent-prompts.test.ts +++ b/packages/core/src/__tests__/agent-prompts.test.ts @@ -9,6 +9,7 @@ import { getTemplatesForRole, } from "../agent-prompts.js"; import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; +import { BUILTIN_SEAM_PROMPTS, builtinSeamPrompt } from "../builtin-workflow-prompts.js"; import { renderTriagePolicyPlaceholders } from "../builtin-workflow-settings.js"; import { resolvePlanningPromptFromIr, resolveSeamPromptFromIr } from "../workflow-ir-resolver.js"; import type { AgentPromptsConfig, AgentPromptTemplate } from "../types.js"; @@ -261,6 +262,17 @@ describe("resolveAgentPrompt", () => { expect(result).toContain("task_document_write"); }); + it("fast triage prompt is sourced from built-in workflow seam data", () => { + const fastTemplate = BUILTIN_AGENT_PROMPTS.find((prompt) => prompt.id === "default-triage-fast"); + + expect(fastTemplate).toBeDefined(); + expect(fastTemplate?.role).toBe("triage"); + expect(BUILTIN_SEAM_PROMPTS["planning-fast"]).toBe(fastTemplate?.prompt); + expect(builtinSeamPrompt("planning-fast")).toBe(fastTemplate?.prompt); + expect(builtinSeamPrompt("planning-fast")).toContain("This task is running in **fast mode**"); + expect(builtinSeamPrompt("planning-fast")).not.toContain("## Review Level"); + }); + it("triage planning prompt is sourced from workflow IR without an engine duplicate", () => { const corePrompt = resolveAgentPrompt("triage"); const planningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR); @@ -269,8 +281,8 @@ describe("resolveAgentPrompt", () => { "utf8", ); - 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(triageSource).not.toContain(["FAST", "TRIAGE", "SYSTEM", "PROMPT"].join("_")); + expect(triageSource).not.toMatch(/export const [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 {{triageSubtaskLargeStepSignal}} or more"); diff --git a/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts b/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts index 4c35579cb5..3a45df98e7 100644 --- a/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts +++ b/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts @@ -5,6 +5,7 @@ import { BUILTIN_WORKFLOW_SETTINGS, renderTriagePolicyPlaceholders, } from "../builtin-workflow-settings.js"; +import { MOVED_SETTINGS_KEYS } from "../moved-settings.js"; const expectedDefaults: Record = { triageSizeSmallMaxHours: { type: "number", default: 2 }, @@ -22,6 +23,8 @@ const expectedDefaults: Record = { }, triageDecisionOnlyWorkflowId: { type: "enum", default: "builtin:quick-fix" }, triageDefaultWorkflowId: { type: "enum", default: "builtin:coding" }, + leanPlanning: { type: "boolean", default: false }, + autoApproveSpec: { type: "boolean", default: false }, }; describe("workflow-native triage policy settings", () => { @@ -29,6 +32,7 @@ describe("workflow-native triage policy settings", () => { const triageById = new Map(BUILTIN_TRIAGE_POLICY_SETTINGS.map((setting) => [setting.id, setting])); const fullIds = new Set(BUILTIN_WORKFLOW_SETTINGS.map((setting) => setting.id)); const movedIds = new Set(BUILTIN_MOVED_WORKFLOW_SETTINGS.map((setting) => setting.id)); + const movedKeyIds = new Set(MOVED_SETTINGS_KEYS); expect(BUILTIN_TRIAGE_POLICY_SETTINGS).toHaveLength(Object.keys(expectedDefaults).length); for (const [id, expected] of Object.entries(expectedDefaults)) { @@ -38,6 +42,7 @@ describe("workflow-native triage policy settings", () => { expect(setting?.default).toStrictEqual(expected.default); expect(fullIds.has(id), `${id} should be in the full built-in catalog`).toBe(true); expect(movedIds.has(id), `${id} should not be in the moved-key catalog`).toBe(false); + expect(movedKeyIds.has(id), `${id} should not be in MOVED_SETTINGS_KEYS`).toBe(false); } }); diff --git a/packages/core/src/agent-prompts.ts b/packages/core/src/agent-prompts.ts index 178684dc61..cd12bc7d5e 100644 --- a/packages/core/src/agent-prompts.ts +++ b/packages/core/src/agent-prompts.ts @@ -204,6 +204,219 @@ The tool prevents your session from being killed by the inactivity watchdog duri - If you need to run \`pnpm install\` (e.g. you added a new package), use \`fn_run_verification\` with \`scope: "workspace"\` and \`timeoutSec: 600\`. - If a verification command times out, do NOT blindly retry — investigate. Check for hung subprocesses, infinite test loops, or tests waiting on missing dependencies. Use \`node_modules/.modules.yaml\` presence to confirm bootstrap.`; +const FAST_TRIAGE_PROMPT_TEXT = `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 +You are a fast-path spec writer. Keep output lean but executable, with enough precision that an executor can run immediately. + +Your job: turn a rough task description into a focused PROMPT.md another agent can execute autonomously. + +## 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} + +## Mission + +{One paragraph: what to build 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 first}) + +## Context to Read First + +{List the minimal, specific files needed for implementation} + +## File Scope + +{List exact files/directories expected to change} + +- \`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: {Implementation step 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 +- [ ] Build passes + +### Step {N}: Documentation & Delivery + +- [ ] Update relevant documentation +- [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...) +- [ ] Create out-of-scope follow-up tasks via \`fn_task_create\` when needed + +## 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 +- Require real automated tests with assertions that run in the project's test runner +- Typecheck/build/manual checks are not tests and cannot replace tests +- 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\`. +- Include targeted tests in implementation steps and full quality-gate runs in final verification + +## Duplicate check +Before writing a spec, call \`fn_task_list\` to find existing 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 an existing task already covers the same work, do NOT write a PROMPT.md. Instead write exactly: +\`DUPLICATE: {existing-task-id}\` + +## Dependency awareness +When adding a dependency in \`## Dependencies\`, first call \`fn_task_get\` for that task and read its PROMPT.md. +Use that context to align file paths, APIs, assumptions, and completion expectations. If the dependency has no PROMPT.md yet, note that explicitly. + +## Decision-only task flag (noCommitsExpected) +When ALL of the following are true, include this metadata line in the header block after Size: + +- 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 relevant source files before writing the spec +- Be specific: reference concrete files, modules, and commands from this repo +- Keep steps outcome-focused with 2–4 checkboxes per step +- Keep file scope realistic: include tests and integration touchpoints likely required for green quality gates +- Always include Testing & Verification and Documentation & Delivery steps +- Keep fast-mode scope lean and executable; do not add heavyweight review scoring or subtask-analysis sections + +## Project commands +When the user prompt includes explicit test/build commands, use those exact commands in the generated spec. + +## Workflow Routing +Call \`fn_workflow_list\` and use workflow descriptions as the routing signal. For investigation/audit/research or decision-only tasks that meet the no-commits criteria above, include \`**No commits expected:** true\` in the PROMPT.md header and prefer \`builtin:quick-fix\` or a custom investigation workflow; standard coding tasks can stay on the default \`builtin:coding\`. Use \`fn_workflow_select\` for the current task or pass \`workflow_id\` to \`fn_task_create\` for subtasks. + +## Task Artifact Location for Forensic / Reconciliation Tasks + +For audit/forensic/historical reconciliation tasks that target a different task ID, explicitly state in generated PROMPT.md context/scope that authoritative artifacts and DB state are at project root, not the worktree. +- Target-task files live at \`/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, logs). +- Task DB truth lives at \`/.fusion/fusion.db\` (SQLite/WAL) and should be accessed via \`TaskStore\`/task tools, not direct SQL edits. +- \`.fusion/\` is gitignored: fresh worktrees from \`main\` do not contain other tasks' \`.fusion/tasks/{TARGET_ID}/\` or \`.fusion/fusion.db\`; worktree-local \`.fusion/\` is running-task scratch/session state only. + +## Spec Review + +After writing the PROMPT.md, call \`fn_review_spec()\` to confirm the spec. + +Fast-mode specs are auto-approved — the review tool will return APPROVE immediately without spawning an independent reviewer. You do NOT need to wait for or iterate on review feedback. + +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()\` to confirm.`; + const TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn", an AI-orchestrated task board. ## Your Role @@ -1125,6 +1338,14 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [ prompt: `${TRIAGE_PROMPT_TEXT}\n\n${TRIAGE_HEARTBEAT_GUIDANCE}`, builtIn: true, }, + { + id: "default-triage-fast", + name: "Default Triage (Fast)", + description: "Lean fast-path task specification agent producing executable PROMPT.md files without heavyweight review scoring.", + role: "triage", + prompt: FAST_TRIAGE_PROMPT_TEXT, + builtIn: true, + }, { id: "default-reviewer", name: "Default Reviewer", diff --git a/packages/core/src/builtin-workflow-prompts.ts b/packages/core/src/builtin-workflow-prompts.ts index 4d7ff77090..7d142a4ce3 100644 --- a/packages/core/src/builtin-workflow-prompts.ts +++ b/packages/core/src/builtin-workflow-prompts.ts @@ -1,19 +1,25 @@ import { BUILTIN_AGENT_PROMPTS } from "./agent-prompts.js"; const DEFAULT_EXECUTOR_PROMPT = BUILTIN_AGENT_PROMPTS.find((prompt) => prompt.role === "executor")?.prompt ?? ""; -const DEFAULT_TRIAGE_PROMPT = BUILTIN_AGENT_PROMPTS.find((prompt) => prompt.role === "triage")?.prompt ?? ""; +const DEFAULT_TRIAGE_PROMPT = BUILTIN_AGENT_PROMPTS.find((prompt) => prompt.id === "default-triage")?.prompt ?? ""; +const DEFAULT_TRIAGE_FAST_PROMPT = BUILTIN_AGENT_PROMPTS.find((prompt) => prompt.id === "default-triage-fast")?.prompt ?? ""; const DEFAULT_REVIEWER_PROMPT = BUILTIN_AGENT_PROMPTS.find((prompt) => prompt.role === "reviewer")?.prompt ?? ""; const DEFAULT_MERGER_PROMPT = BUILTIN_AGENT_PROMPTS.find((prompt) => prompt.role === "merger")?.prompt ?? ""; -const BUILTIN_SEAM_PROMPTS: Record = { +export const BUILTIN_SEAM_PROMPTS: Record = { execute: DEFAULT_EXECUTOR_PROMPT, planning: DEFAULT_TRIAGE_PROMPT, + "planning-fast": DEFAULT_TRIAGE_FAST_PROMPT, "step-execute": DEFAULT_EXECUTOR_PROMPT, "workflow-step": DEFAULT_REVIEWER_PROMPT, review: DEFAULT_REVIEWER_PROMPT, merge: DEFAULT_MERGER_PROMPT, }; -export function builtinPromptConfig(seam: string, name: string): Record { - return { seam, name, prompt: BUILTIN_SEAM_PROMPTS[seam] ?? "" }; +export function builtinSeamPrompt(seam: string): string { + return BUILTIN_SEAM_PROMPTS[seam] ?? ""; +} + +export function builtinPromptConfig(seam: string, name: string): Record { + return { seam, name, prompt: builtinSeamPrompt(seam) }; } diff --git a/packages/core/src/builtin-workflow-settings.ts b/packages/core/src/builtin-workflow-settings.ts index 3074c2595b..3faded76a1 100644 --- a/packages/core/src/builtin-workflow-settings.ts +++ b/packages/core/src/builtin-workflow-settings.ts @@ -13,7 +13,9 @@ import type { WorkflowSettingDefinition } from "./workflow-ir-types.js"; * hard-move migration, must never be added to `MOVED_SETTINGS_KEYS`, and must * not appear in project/global settings schemas. Canonical values are inherited * from the post-FN-6232 planning prompt: subtask step threshold `7` (not the - * older engine copy) and packages/modules threshold `3`. + * older engine copy) and packages/modules threshold `3`. Fast-mode policy is + * workflow-native here too: `leanPlanning` selects the lean planning variant, + * and `autoApproveSpec` skips the independent spec reviewer. */ /** @@ -349,6 +351,20 @@ export const BUILTIN_TRIAGE_POLICY_SETTINGS: WorkflowSettingDefinition[] = [ ], description: "Default workflow id for standard coding tasks.", }, + { + id: "leanPlanning", + name: "Lean planning", + type: "boolean", + default: false, + description: "Use the lean fast-path planning prompt variant instead of the full triage spec prompt.", + }, + { + id: "autoApproveSpec", + name: "Auto-approve spec", + type: "boolean", + default: false, + description: "Auto-approve the generated PROMPT.md and skip the independent spec reviewer.", + }, ]; export const BUILTIN_WORKFLOW_SETTINGS: WorkflowSettingDefinition[] = [ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index baa152807f..1515e727c7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -112,6 +112,11 @@ export { BUILTIN_TRIAGE_POLICY_SETTINGS, renderTriagePolicyPlaceholders, } from "./builtin-workflow-settings.js"; +export { + BUILTIN_SEAM_PROMPTS, + builtinPromptConfig, + builtinSeamPrompt, +} from "./builtin-workflow-prompts.js"; export { MOVED_SETTINGS_KEYS, SETTINGS_MIGRATION_VERSION, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 966f0003f7..cf5e9b3121 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -4151,6 +4151,10 @@ export interface Settings extends GlobalSettings, ProjectSettings { /** Whether PR authentication is currently available (read-only, set by server). * True when authenticated gh CLI access is available or token fallback exists. */ prAuthAvailable?: boolean; + /** Use the lean fast-path planning prompt variant instead of the full triage spec prompt. */ + leanPlanning?: boolean; + /** Auto-approve generated specs and skip the independent spec reviewer. */ + autoApproveSpec?: boolean; /** Index signature for dynamic settings access */ [key: string]: unknown; } 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 95987af16f..7e7c39f2a2 100644 --- a/packages/engine/src/__tests__/triage-duplicate-search-regression.test.ts +++ b/packages/engine/src/__tests__/triage-duplicate-search-regression.test.ts @@ -1,9 +1,6 @@ import { describe, it, expect, vi } from "vitest"; -import { resolveAgentPrompt } from "@fusion/core"; -import { - FAST_TRIAGE_SYSTEM_PROMPT, - TriageProcessor, -} from "../triage.js"; +import { builtinSeamPrompt, resolveAgentPrompt } from "@fusion/core"; +import { TriageProcessor } from "../triage.js"; import { createTriageDuplicateScenario } from "./fixtures/triage-duplicate-scenario.js"; const { mockReviewStep, mockCreateFnAgent } = vi.hoisted(() => ({ @@ -30,12 +27,13 @@ vi.mock("@fusion/core", async (importOriginal) => { }); const TRIAGE_POLICY_PROMPT = resolveAgentPrompt("triage"); +const FAST_PLANNING_PROMPT = builtinSeamPrompt("planning-fast"); /** * 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 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_PLANNING_PROMPT. FN-4815 pins this contract. */ describe("FN-4815 triage duplicate-search regression", () => { it("toolset contract: createTriageTools includes fn_task_search", () => { @@ -60,10 +58,10 @@ describe("FN-4815 triage duplicate-search regression", () => { }); it("fast prompt guidance keeps duplicate-search instructions", () => { - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("Duplicate check"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("fn_task_search"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("For any likely match in `done` or `archived`"); - expect(/Duplicate check[\s\S]{0,700}(done|archived)/i.test(FAST_TRIAGE_SYSTEM_PROMPT)).toBe(true); + expect(FAST_PLANNING_PROMPT).toContain("Duplicate check"); + expect(FAST_PLANNING_PROMPT).toContain("fn_task_search"); + expect(FAST_PLANNING_PROMPT).toContain("For any likely match in `done` or `archived`"); + expect(/Duplicate check[\s\S]{0,700}(done|archived)/i.test(FAST_PLANNING_PROMPT)).toBe(true); }); it("end-to-end duplicate discovery via fixture shows done match before create", async () => { diff --git a/packages/engine/src/__tests__/triage-fast-mode-workflow-variant.test.ts b/packages/engine/src/__tests__/triage-fast-mode-workflow-variant.test.ts new file mode 100644 index 0000000000..181071e9ab --- /dev/null +++ b/packages/engine/src/__tests__/triage-fast-mode-workflow-variant.test.ts @@ -0,0 +1,219 @@ +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { Settings, Task, TaskDetail, TaskStore } from "@fusion/core"; +import { + BUILTIN_CODING_WORKFLOW_IR, + builtinSeamPrompt, + renderTriagePolicyPlaceholders, + resolvePlanningPromptFromIr, +} from "@fusion/core"; +import { TriageProcessor } from "../triage.js"; + +const { mockReviewStep, mockCreateFnAgent, mockPromptWithFallback } = vi.hoisted(() => ({ + mockReviewStep: vi.fn(), + mockCreateFnAgent: vi.fn(), + mockPromptWithFallback: vi.fn(), +})); + +vi.mock("../reviewer.js", () => ({ + reviewStep: mockReviewStep, +})); + +vi.mock("../pi.js", () => ({ + createFnAgent: mockCreateFnAgent, + describeModel: vi.fn().mockReturnValue("mock-model"), + promptWithFallback: mockPromptWithFallback, +})); + +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-6236-T", + description: "Fast workflow variant regression", + 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, settings: Partial = {}, overrides: 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({ workflowId: "builtin:coding", stepIds: [] }), + getWorkflowDefinition: vi.fn().mockResolvedValue(undefined), + getWorkflowSettingValues: vi.fn().mockResolvedValue({}), + getWorkflowSettingsProjectId: vi.fn().mockReturnValue("default"), + on: vi.fn(), + emit: vi.fn(), + ...overrides, + } as unknown as TaskStore; +} + +function mockSession(capture: { basePrompt?: string; customTools?: any[] } = {}) { + mockCreateFnAgent.mockImplementationOnce(async (opts: any) => { + capture.basePrompt = opts.systemPromptLayers?.stable ?? opts.systemPrompt; + capture.customTools = opts.customTools; + return { + session: { + state: {}, + sessionManager: { getLeafId: vi.fn().mockReturnValue(null) }, + prompt: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn(), + navigateTree: vi.fn(), + __customTools: opts.customTools, + }, + }; + }); +} + +async function captureBasePrompt(task: Task, store: TaskStore): Promise { + const capture: { basePrompt?: string } = {}; + mockSession(capture); + mockPromptWithFallback.mockResolvedValueOnce(undefined); + + await new TriageProcessor(store, "/tmp/root").specifyTask(task); + return capture.basePrompt ?? ""; +} + +async function runReviewSpec(task: Task, store: TaskStore, rootDir: string): Promise { + mockSession(); + mockPromptWithFallback.mockImplementationOnce(async (session: any) => { + const promptPath = join(rootDir, ".fusion", "tasks", task.id, "PROMPT.md"); + await mkdir(join(rootDir, ".fusion", "tasks", task.id), { recursive: true }); + await writeFile(promptPath, "# Task: FN-6236\n\n## Mission\n\nVerify fast policy.\n", "utf8"); + const reviewSpec = session.__customTools.find((tool: any) => tool.name === "fn_review_spec"); + await reviewSpec.execute(); + }); + + await new TriageProcessor(store, rootDir).specifyTask(task); +} + +const renderedFastPlanningPrompt = renderTriagePolicyPlaceholders(builtinSeamPrompt("planning-fast"), {}); +const renderedStandardPlanningPrompt = renderTriagePolicyPlaceholders( + resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR)!, + {}, +); + +describe("fast-mode workflow variant resolution", () => { + let tempRoots: string[] = []; + + beforeEach(() => { + vi.clearAllMocks(); + mockReviewStep.mockResolvedValue({ verdict: "APPROVE", summary: "ok", review: "" }); + }); + + afterEach(async () => { + await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true }))); + tempRoots = []; + }); + + it("resolves fast tasks to the lean planning-fast workflow prompt", async () => { + const task = createTask({ id: "FN-6236-FAST-PROMPT", executionMode: "fast" }); + const store = createStore(task); + + await expect(captureBasePrompt(task, store)).resolves.toBe(renderedFastPlanningPrompt); + }); + + it("resolves standard tasks to the standard workflow planning prompt", async () => { + const task = createTask({ id: "FN-6236-STANDARD-PROMPT", executionMode: "standard" }); + const store = createStore(task); + + const basePrompt = await captureBasePrompt(task, store); + + expect(basePrompt).toBe(renderedStandardPlanningPrompt); + expect(basePrompt).not.toBe(renderedFastPlanningPrompt); + }); + + it("auto-approves fast tasks without invoking the reviewer", async () => { + const rootDir = await mkdtemp(join(tmpdir(), "fusion-fn-6236-fast-")); + tempRoots.push(rootDir); + const task = createTask({ id: "FN-6236-FAST-REVIEW", executionMode: "fast" }); + const store = createStore(task); + + await runReviewSpec(task, store, rootDir); + + expect(mockReviewStep).not.toHaveBeenCalled(); + expect(store.logEntry).toHaveBeenCalledWith(task.id, "Spec review: APPROVE (auto-approve spec)"); + }); + + it("invokes the reviewer for standard tasks without autoApproveSpec", async () => { + const rootDir = await mkdtemp(join(tmpdir(), "fusion-fn-6236-standard-")); + tempRoots.push(rootDir); + const task = createTask({ id: "FN-6236-STANDARD-REVIEW", executionMode: "standard" }); + const store = createStore(task); + + await runReviewSpec(task, store, rootDir); + + expect(mockReviewStep).toHaveBeenCalledTimes(1); + }); + + it("auto-approves standard tasks when the workflow setting is enabled", async () => { + const rootDir = await mkdtemp(join(tmpdir(), "fusion-fn-6236-setting-")); + tempRoots.push(rootDir); + const task = createTask({ id: "FN-6236-SETTING-REVIEW", executionMode: "standard" }); + const store = createStore(task, { autoApproveSpec: true }); + + await runReviewSpec(task, store, rootDir); + + expect(mockReviewStep).not.toHaveBeenCalled(); + expect(store.logEntry).toHaveBeenCalledWith(task.id, "Spec review: APPROVE (auto-approve spec)"); + }); + + it("preserves user triage prompt override precedence over the fast variant", async () => { + const task = createTask({ id: "FN-6236-OVERRIDE", executionMode: "fast" }); + const overridePrompt = "custom fast 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); + }); +}); 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 index 02714d0305..1be76c1fd1 100644 --- a/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts +++ b/packages/engine/src/__tests__/triage-planning-prompt-single-source.test.ts @@ -2,11 +2,12 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import type { Settings, Task, TaskDetail, TaskStore, WorkflowIr } from "@fusion/core"; import { BUILTIN_CODING_WORKFLOW_IR, + builtinSeamPrompt, renderTriagePolicyPlaceholders, resolveAgentPrompt, resolvePlanningPromptFromIr, } from "@fusion/core"; -import { FAST_TRIAGE_SYSTEM_PROMPT, TriageProcessor } from "../triage.js"; +import { TriageProcessor } from "../triage.js"; const { mockReviewStep, mockCreateFnAgent } = vi.hoisted(() => ({ mockReviewStep: vi.fn(), @@ -110,6 +111,7 @@ async function captureBasePrompt(task: Task, store: TaskStore): Promise const canonicalPlanningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR)!; const renderedCanonicalPlanningPrompt = renderTriagePolicyPlaceholders(canonicalPlanningPrompt, {}); const renderedDefaultTriagePrompt = renderTriagePolicyPlaceholders(resolveAgentPrompt("triage"), {}); +const renderedFastPlanningPrompt = renderTriagePolicyPlaceholders(builtinSeamPrompt("planning-fast"), {}); describe("triage planning prompt single source", () => { beforeEach(() => { @@ -145,11 +147,11 @@ describe("triage planning prompt single source", () => { await expect(captureBasePrompt(task, store)).resolves.toBe(overridePrompt); }); - it("keeps fast mode on FAST_TRIAGE_SYSTEM_PROMPT", async () => { + it("keeps fast mode on the planning-fast workflow 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); + await expect(captureBasePrompt(task, store)).resolves.toBe(renderedFastPlanningPrompt); }); it("uses a selected custom workflow planning prompt", async () => { diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index ba72e7f06d..f0fb927f02 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -1,9 +1,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import type { TaskStore, Task, TaskDetail, Settings } from "@fusion/core"; -import { renderTriagePolicyPlaceholders, resolveAgentPrompt } from "@fusion/core"; +import { builtinSeamPrompt, renderTriagePolicyPlaceholders, resolveAgentPrompt } from "@fusion/core"; import { TriageProcessor, - FAST_TRIAGE_SYSTEM_PROMPT, buildSpecificationPrompt, readAttachmentContents, computeUserCommentFingerprint, @@ -22,6 +21,7 @@ const { mockReviewStep, mockCreateFnAgent } = vi.hoisted(() => ({ })); const TRIAGE_POLICY_PROMPT = resolveAgentPrompt("triage"); +const FAST_PLANNING_PROMPT = builtinSeamPrompt("planning-fast"); const RENDERED_TRIAGE_POLICY_PROMPT = renderTriagePolicyPlaceholders(TRIAGE_POLICY_PROMPT, {}); vi.mock("../reviewer.js", () => ({ @@ -661,7 +661,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_POLICY_PROMPT, - FAST_TRIAGE_SYSTEM_PROMPT, + FAST_PLANNING_PROMPT, corePromptSource, ]) { expect(prompt).toContain("invariant across all known surfaces"); @@ -677,7 +677,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_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_PLANNING_PROMPT]) { expect(prompt).toContain("## Surface Enumeration"); expect(prompt).toMatch(missingSectionRevisePattern); expect(prompt).toContain("docs/testing.md"); @@ -699,7 +699,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_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_PLANNING_PROMPT]) { expect(prompt).toContain( "Run targeted tests for changed files, asserting the invariant across all known surfaces", ); @@ -710,7 +710,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_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_PLANNING_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"); @@ -725,7 +725,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_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_PLANNING_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"); @@ -733,7 +733,7 @@ describe("FN-5893 invariant regression wording", () => { expect(prompt).toContain("empty buttons"); } - expect(FAST_TRIAGE_SYSTEM_PROMPT).not.toContain("## Review Level"); + expect(FAST_PLANNING_PROMPT).not.toContain("## Review Level"); }); it("pins the canonical docs checklist heading", () => { @@ -748,19 +748,19 @@ describe("FN-5893 invariant regression wording", () => { }); describe("fast-mode triage", () => { - it("exports a lean FAST_TRIAGE_SYSTEM_PROMPT", () => { - expect(typeof FAST_TRIAGE_SYSTEM_PROMPT).toBe("string"); - expect(FAST_TRIAGE_SYSTEM_PROMPT.length).toBeGreaterThan(0); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("This task is running in **fast mode**"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("fn_review_spec()"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).not.toContain("## Review Level"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).not.toContain("## Triage subtask breakdown"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).not.toContain("## Proactive Subtask Breakdown"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).not.toContain("Frontend UX Criteria"); + it("exports a lean FAST_PLANNING_PROMPT", () => { + expect(typeof FAST_PLANNING_PROMPT).toBe("string"); + expect(FAST_PLANNING_PROMPT.length).toBeGreaterThan(0); + expect(FAST_PLANNING_PROMPT).toContain("This task is running in **fast mode**"); + expect(FAST_PLANNING_PROMPT).toContain("fn_review_spec()"); + expect(FAST_PLANNING_PROMPT).not.toContain("## Review Level"); + expect(FAST_PLANNING_PROMPT).not.toContain("## Triage subtask breakdown"); + expect(FAST_PLANNING_PROMPT).not.toContain("## Proactive Subtask Breakdown"); + expect(FAST_PLANNING_PROMPT).not.toContain("Frontend UX Criteria"); }); it("documents workflow routing in standard and fast prompts", () => { - for (const prompt of [RENDERED_TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) { + for (const prompt of [RENDERED_TRIAGE_POLICY_PROMPT, FAST_PLANNING_PROMPT]) { expect(prompt).toContain("## Workflow Routing"); expect(prompt).toContain("fn_workflow_list"); expect(prompt).toContain("fn_workflow_select"); @@ -772,14 +772,14 @@ describe("fast-mode triage", () => { }); it("includes task-artifact location guidance for forensic/reconciliation tasks", () => { - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("Task Artifact Location"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("/.fusion/tasks/{TARGET_ID}/"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain(".fusion/fusion.db"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("project root"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("forensic"); + expect(FAST_PLANNING_PROMPT).toContain("Task Artifact Location"); + expect(FAST_PLANNING_PROMPT).toContain("/.fusion/tasks/{TARGET_ID}/"); + expect(FAST_PLANNING_PROMPT).toContain(".fusion/fusion.db"); + expect(FAST_PLANNING_PROMPT).toContain("project root"); + expect(FAST_PLANNING_PROMPT).toContain("forensic"); }); - it("selects FAST_TRIAGE_SYSTEM_PROMPT for fast tasks", async () => { + it("selects FAST_PLANNING_PROMPT for fast tasks", async () => { const task = createTriageTask({ id: "FN-FAST-001", executionMode: "fast" }); const store = createMockStore({ getTask: vi.fn().mockResolvedValue({ ...mockTaskDetail, id: task.id, attachments: [], comments: [] }), @@ -959,7 +959,7 @@ describe("fast-mode triage", () => { expect(mockReviewStep).not.toHaveBeenCalled(); expect(verdictRef.current).toBe("APPROVE"); expect(result.content[0]?.text).toBe("APPROVE"); - expect(store.logEntry).toHaveBeenCalledWith(taskId, "Spec review: APPROVE (auto, fast mode)"); + expect(store.logEntry).toHaveBeenCalledWith(taskId, "Spec review: APPROVE (auto-approve spec)"); } finally { await cleanupTriageFixtureRoot(rootDir); } @@ -1018,7 +1018,7 @@ describe("fast-mode triage", () => { expect(mockReviewStep).not.toHaveBeenCalled(); expect(store.moveTask).toHaveBeenCalledWith("FN-FAST-004", "todo"); - expect(store.logEntry).toHaveBeenCalledWith("FN-FAST-004", "Spec review: APPROVE (auto, fast mode)"); + expect(store.logEntry).toHaveBeenCalledWith("FN-FAST-004", "Spec review: APPROVE (auto-approve spec)"); } finally { await cleanupTriageFixtureRoot(rootDir); } @@ -1789,7 +1789,7 @@ describe("approved triage recovery", () => { 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"); + expect(FAST_PLANNING_PROMPT).toContain("**No commits expected:** true"); }); it("preserves imported GitHub issue titles during planning recovery", async () => { @@ -4433,16 +4433,16 @@ 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("FAST_TRIAGE_SYSTEM_PROMPT guides agents to search done/archived before creating", () => { + it("FAST_PLANNING_PROMPT guides agents to search done/archived before creating", () => { // Fast prompt mentions fn_task_search - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("fn_task_search"); + expect(FAST_PLANNING_PROMPT).toContain("fn_task_search"); // Duplicate-check section references done and archived - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("done"); - expect(FAST_TRIAGE_SYSTEM_PROMPT).toContain("archived"); + expect(FAST_PLANNING_PROMPT).toContain("done"); + expect(FAST_PLANNING_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( - FAST_TRIAGE_SYSTEM_PROMPT, + FAST_PLANNING_PROMPT, ), ).toBe(true); }); diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 7b22f00a14..34948329fb 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -13,8 +13,10 @@ import { getTaskDuplicateLineage, parseExplicitDuplicateMarker, resolveAgentPrompt, + builtinSeamPrompt, renderTriagePolicyPlaceholders, resolveTaskPlanningPrompt, + resolveTaskSeamPrompt, resolvePersistAgentThinkingLog, compareTaskPriority, sortTasksByPriorityThenAgeAndId, @@ -90,218 +92,6 @@ import { archiveAsGhostBug } from "./self-healing.js"; import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js"; import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.js"; -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 -You are a fast-path spec writer. Keep output lean but executable, with enough precision that an executor can run immediately. - -Your job: turn a rough task description into a focused PROMPT.md another agent can execute autonomously. - -## 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} - -## Mission - -{One paragraph: what to build 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 first}) - -## Context to Read First - -{List the minimal, specific files needed for implementation} - -## File Scope - -{List exact files/directories expected to change} - -- \`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: {Implementation step 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 -- [ ] Build passes - -### Step {N}: Documentation & Delivery - -- [ ] Update relevant documentation -- [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...) -- [ ] Create out-of-scope follow-up tasks via \`fn_task_create\` when needed - -## 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 -- Require real automated tests with assertions that run in the project's test runner -- Typecheck/build/manual checks are not tests and cannot replace tests -- 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\`. -- Include targeted tests in implementation steps and full quality-gate runs in final verification - -## Duplicate check -Before writing a spec, call \`fn_task_list\` to find existing 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 an existing task already covers the same work, do NOT write a PROMPT.md. Instead write exactly: -\`DUPLICATE: {existing-task-id}\` - -## Dependency awareness -When adding a dependency in \`## Dependencies\`, first call \`fn_task_get\` for that task and read its PROMPT.md. -Use that context to align file paths, APIs, assumptions, and completion expectations. If the dependency has no PROMPT.md yet, note that explicitly. - -## Decision-only task flag (noCommitsExpected) -When ALL of the following are true, include this metadata line in the header block after Size: - -- 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 relevant source files before writing the spec -- Be specific: reference concrete files, modules, and commands from this repo -- Keep steps outcome-focused with 2–4 checkboxes per step -- Keep file scope realistic: include tests and integration touchpoints likely required for green quality gates -- Always include Testing & Verification and Documentation & Delivery steps -- Keep fast-mode scope lean and executable; do not add heavyweight review scoring or subtask-analysis sections - -## Project commands -When the user prompt includes explicit test/build commands, use those exact commands in the generated spec. - -## Workflow Routing -Call \`fn_workflow_list\` and use workflow descriptions as the routing signal. For investigation/audit/research or decision-only tasks that meet the no-commits criteria above, include \`**No commits expected:** true\` in the PROMPT.md header and prefer \`builtin:quick-fix\` or a custom investigation workflow; standard coding tasks can stay on the default \`builtin:coding\`. Use \`fn_workflow_select\` for the current task or pass \`workflow_id\` to \`fn_task_create\` for subtasks. - -## Task Artifact Location for Forensic / Reconciliation Tasks - -For audit/forensic/historical reconciliation tasks that target a different task ID, explicitly state in generated PROMPT.md context/scope that authoritative artifacts and DB state are at project root, not the worktree. -- Target-task files live at \`/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, logs). -- Task DB truth lives at \`/.fusion/fusion.db\` (SQLite/WAL) and should be accessed via \`TaskStore\`/task tools, not direct SQL edits. -- \`.fusion/\` is gitignored: fresh worktrees from \`main\` do not contain other tasks' \`.fusion/tasks/{TARGET_ID}/\` or \`.fusion/fusion.db\`; worktree-local \`.fusion/\` is running-task scratch/session state only. - -## Spec Review - -After writing the PROMPT.md, call \`fn_review_spec()\` to confirm the spec. - -Fast-mode specs are auto-approved — the review tool will return APPROVE immediately without spawning an independent reviewer. You do NOT need to wait for or iterate on review feedback. - -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()\` to confirm.`; export interface TriageProcessorOptions { pollIntervalMs?: number; @@ -824,6 +614,10 @@ export class TriageProcessor { const settings = await mergeEffectiveSettings(this.store, task, await this.store.getSettings()); const promptPath = `.fusion/tasks/${task.id}/PROMPT.md`; const isFast = task.executionMode === "fast"; + // FN-6236: this is the only legacy executionMode="fast" bridge. Downstream + // triage policy reads resolved workflow flags instead of the raw string. + const leanPlanning = settings.leanPlanning === true || isFast; + const autoApproveSpec = settings.autoApproveSpec === true || isFast; const agentWork = async () => { // Set status only after the semaphore slot has been acquired, so @@ -925,7 +719,7 @@ export class TriageProcessor { specReviewVerdictRef, approvedCommentFingerprintRef, settings, - isFast, + autoApproveSpec, ), ]; @@ -957,7 +751,7 @@ export class TriageProcessor { planLog.warn(`${task.id}: failed to resolve triage agent instructions, continuing with defaults: ${msg}`); } } - planLog.log(`${task.id}: planning in ${isFast ? "fast" : "standard"} mode`); + planLog.log(`${task.id}: planning in ${leanPlanning ? "fast" : "standard"} mode`); const triageIdentitySection = assignedAgent ? `## Identity\n\nYou are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.title.trim()}` : ""} (agent ID: ${assignedAgent.id}, role: ${assignedAgent.role}).` : ""; @@ -979,16 +773,21 @@ export class TriageProcessor { runContext: triageRunContext, }); - const workflowPlanningPrompt = isFast + const workflowPlanningPrompt = leanPlanning ? undefined : await resolveTaskPlanningPrompt(this.store, task.id).catch(() => undefined); + const workflowFastPlanningPrompt = leanPlanning + ? await resolveTaskSeamPrompt(this.store, task.id, "planning-fast").catch(() => undefined) + : 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 resolvedBasePrompt = userTriagePrompt - || (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : (workflowPlanningPrompt || defaultTriagePrompt)); + || (leanPlanning + ? (workflowFastPlanningPrompt || builtinSeamPrompt("planning-fast") || defaultTriagePrompt) + : (workflowPlanningPrompt || defaultTriagePrompt)); // Apply the workflow-native triage policy renderer to both standard and // fast prompts. Fast mode currently has no policy placeholders, making // this a no-op there while still guaranteeing no dangling token leaks. @@ -1970,8 +1769,8 @@ export class TriageProcessor { approvedCommentFingerprintRef.current = currentUserComments.length > 0 ? computeUserCommentFingerprint(currentUserComments) : ""; - planLog.log(`${taskId}: spec review auto-approved (fast mode)`); - await store.logEntry(taskId, "Spec review: APPROVE (auto, fast mode)"); + planLog.log(`${taskId}: spec review auto-approved (auto-approve spec)`); + await store.logEntry(taskId, "Spec review: APPROVE (auto-approve spec)"); return { content: [{ type: "text" as const, text: "APPROVE" }], details: {} }; }