diff --git a/.changeset/FN-7491-triage-splitting-setting.md b/.changeset/FN-7491-triage-splitting-setting.md new file mode 100644 index 0000000000..a8a2494275 --- /dev/null +++ b/.changeset/FN-7491-triage-splitting-setting.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Add a workflow setting to disable automatic large-task triage splitting. +category: feature +dev: Adds triageProactiveSubtaskSplittingEnabled while preserving explicit breakIntoSubtasks requests. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index edb8968729..bc9326faca 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -327,12 +327,16 @@ These groups moved out of project settings and into workflow settings (built-in The built-in workflows also declare triage/spec policy settings that were **not** moved from project settings. They are workflow-native declarations: they never lived in `DEFAULT_PROJECT_SETTINGS`, are not `MOVED_SETTINGS_KEYS`, and resolve only through the workflow effective-settings path. | Setting | Default | Purpose | |---|---:|---| +| `triageProactiveSubtaskSplittingEnabled` | `true` | Enables automatic large-task splitting guidance for oversized M/L tasks. Set to `false` to keep tasks whole unless `breakIntoSubtasks: true` is explicitly requested. | | `triageSizeSmallMaxHours` | `2` | Size S upper hour boundary (`S (<2h)`). | | `triageSizeMediumMaxHours` | `4` | Size M upper hour boundary (`M (2-4h)`). | | `triageSizeLargeMaxHours` | `8` | Size L upper hour boundary; XL starts at `8h+`. | @@ -350,6 +354,8 @@ The built-in workflows also declare triage/spec policy settings that were **not* | `planReviewMaxRevisions` | unset | Workflow-native Plan Review/spec revision cap. Unset/empty means unbounded automatic replans; a non-negative integer caps attempts; `0` disables automatic Plan Review revision. | | `codeReviewMaxRevisions` | unset | Workflow-native Code Review remediation cap. Unset/empty means unbounded automatic code-fix passes; a non-negative integer caps attempts; `0` disables automatic Code Review remediation. | +When `triageProactiveSubtaskSplittingEnabled` is `true` (the default), triage may proactively replace a large task with 2-5 child tasks when the size, step-count, package breadth, file-scope, or remediation-batch signals justify the coordination overhead. When it is `false`, those automatic oversized-task signals are advisory only for writing a realistic single-task spec; triage must not split solely because the task is large. The per-task `breakIntoSubtasks: true` flag is separate and remains mandatory: if a user explicitly asks for subtask breakdown, triage still evaluates and creates child tasks when the work is meaningfully decomposable. + In the dashboard Settings modal, Project Models exposes Plan/Triage, Executor, Reviewer, and declared fallback dropdown controls for the default workflow. The modal's primary **Save** action persists pending default-workflow model lane diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index ee1dccaaad..23eda071f6 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -829,9 +829,13 @@ engine resolves *effective settings* per task as `stored value ?? declaration default`, dropping any stored value that no longer validates against the current declaration (drop-on-orphan) and falling back to the default. -The **step-execution**, **review/approval**, and **per-phase model-lane** knobs that -used to be project settings are now workflow settings declared by `builtin:coding` -with their former defaults. See +The **step-execution**, **review/approval**, **per-phase model-lane**, and +**triage/spec policy** knobs are workflow settings declared by `builtin:coding`. +Triage policy includes `triageProactiveSubtaskSplittingEnabled` (default `true`), +which controls automatic large-task splitting guidance for oversized M/L work. +Set it to `false` in a workflow's Values tab when triage should keep large tasks +whole unless the task explicitly has `breakIntoSubtasks: true`; explicit subtask +requests still follow the mandatory split flow. See [Settings Reference → Workflow Settings](./settings-reference.md#workflow-settings) for the full moved-key catalog, the editor walkthrough, and the export/sync posture. diff --git a/packages/core/src/__tests__/agent-prompts.test.ts b/packages/core/src/__tests__/agent-prompts.test.ts index 716a2af683..4764479d5d 100644 --- a/packages/core/src/__tests__/agent-prompts.test.ts +++ b/packages/core/src/__tests__/agent-prompts.test.ts @@ -306,18 +306,25 @@ describe("resolveAgentPrompt", () => { 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"); - expect(corePrompt).toContain("would reach {{triageSubtaskAdditiveStepSignal}} or more"); - expect(corePrompt).toContain("{{triageSubtaskFileScopeThreshold}} or more entries"); - expect(corePrompt).toContain("at or above {{triageSubtaskRemediationBatchThreshold}} items"); + expect(corePrompt).toContain("{{triageProactiveSubtaskSplittingEnabled}}"); + expect(corePrompt).toContain("Explicit user-requested `breakIntoSubtasks: true` remains governed"); const renderedPrompt = renderTriagePolicyPlaceholders(corePrompt, {}); + expect(renderedPrompt).toContain("**Broad-scope decomposition signals:**"); expect(renderedPrompt).toContain("step count would reach 9 or more"); expect(renderedPrompt).toContain("would reach 12 or more"); expect(renderedPrompt).toContain("20 or more entries"); expect(renderedPrompt).toContain("at or above 30 items"); + expect(renderedPrompt).toContain("Even when `breakIntoSubtasks` is not set to `true`, apply these thresholds proactively"); expect(renderedPrompt).not.toContain("{{"); + + const disabledPrompt = renderTriagePolicyPlaceholders(corePrompt, { + triageProactiveSubtaskSplittingEnabled: false, + } as never); + expect(disabledPrompt).toContain("Proactive oversized-task splitting is DISABLED"); + expect(disabledPrompt).toContain("Only create child tasks when `breakIntoSubtasks: true` is explicitly present"); + expect(disabledPrompt).not.toContain("Even when `breakIntoSubtasks` is not set to `true`, apply these thresholds proactively"); + expect(disabledPrompt).not.toContain("{{"); }); it("resolves custom seam prompts and ignores IRs without matching prompts", () => { 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 cb302decdd..a84d5c58d3 100644 --- a/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts +++ b/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts @@ -9,6 +9,7 @@ import { import { MOVED_SETTINGS_KEYS } from "../moved-settings.js"; const expectedDefaults: Record = { + triageProactiveSubtaskSplittingEnabled: { type: "boolean", default: true }, triageSizeSmallMaxHours: { type: "number", default: 2 }, triageSizeMediumMaxHours: { type: "number", default: 4 }, triageSizeLargeMaxHours: { type: "number", default: 8 }, @@ -98,4 +99,26 @@ describe("workflow-native built-in workflow settings", () => { expect(rendered).not.toContain("{{"); expect(() => renderTriagePolicyPlaceholders("{{unknownTriageToken}}", {})).toThrow(/Unresolved triage policy placeholder/); }); + + it("renders proactive splitting policy as enabled by default", () => { + const rendered = renderTriagePolicyPlaceholders("{{triageProactiveSubtaskSplittingEnabled}}", {}); + + expect(rendered).toContain("For tasks you assess as Size M or L, consider whether splitting"); + expect(rendered).toContain("Even when `breakIntoSubtasks` is not set to `true`, apply these thresholds proactively"); + expect(rendered).toContain("MORE THAN 7 implementation steps"); + expect(rendered).not.toContain("Proactive oversized-task splitting is DISABLED"); + expect(rendered).not.toContain("{{"); + }); + + it("renders disabled proactive policy without weakening explicit subtask requests", () => { + const rendered = renderTriagePolicyPlaceholders("{{triageProactiveSubtaskSplittingEnabled}}", { + triageProactiveSubtaskSplittingEnabled: false, + } as never); + + expect(rendered).toContain("Proactive oversized-task splitting is DISABLED"); + expect(rendered).toContain("Do NOT split solely because the task is Size M/L"); + expect(rendered).toContain("Only create child tasks when `breakIntoSubtasks: true` is explicitly present"); + expect(rendered).not.toContain("Even when `breakIntoSubtasks` is not set to `true`, apply these thresholds proactively"); + expect(rendered).not.toContain("{{"); + }); }); diff --git a/packages/core/src/agent-prompts.ts b/packages/core/src/agent-prompts.ts index fd7106a201..32b2079b55 100644 --- a/packages/core/src/agent-prompts.ts +++ b/packages/core/src/agent-prompts.ts @@ -464,28 +464,11 @@ When the task includes \`breakIntoSubtasks: true\`, first decide whether it shou - If not splitting: proceed with a normal PROMPT.md specification. ## Proactive Subtask Breakdown for M/L Tasks -For tasks you assess as Size M or L, 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 {{triageSubtaskStepThreshold}} implementation steps -- The task affects MORE THAN {{triageSubtaskPackageThreshold}} different packages/modules with distinct concerns (a typed field change that naturally touches core types + store + UI + tests is NOT 4 distinct concerns — it's one coherent change) -- Any single step would take more than 1-2 hours to complete -- The task has multiple 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 {{triageSubtaskLargeStepSignal}} or more. -- Plans whose implementation-step count would reach {{triageSubtaskAdditiveStepSignal}} or more (additive signal — counts even when the surrounding step-count threshold above has not yet fired). -- Tasks whose declared \`## File Scope\` would list {{triageSubtaskFileScopeThreshold}} or more entries. -- Descriptions that quantify large remediation batches (for example "47 failing tests", "30+ broken files") at or above {{triageSubtaskRemediationBatchThreshold}} 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. + +{{triageProactiveSubtaskSplittingEnabled}} ## Triage tools You have these extra tools during triage: diff --git a/packages/core/src/builtin-workflow-settings.ts b/packages/core/src/builtin-workflow-settings.ts index 4646ea0681..d48f2d1b63 100644 --- a/packages/core/src/builtin-workflow-settings.ts +++ b/packages/core/src/builtin-workflow-settings.ts @@ -262,6 +262,18 @@ export const BUILTIN_MOVED_WORKFLOW_SETTINGS: WorkflowSettingDefinition[] = [ ]; export const BUILTIN_TRIAGE_POLICY_SETTINGS: WorkflowSettingDefinition[] = [ + { + id: "triageProactiveSubtaskSplittingEnabled", + name: "Triage proactive subtask splitting", + type: "boolean", + default: true, + /* + * FNXC:TriagePolicy 2026-07-04-00:00: + * Operators need a workflow/project policy switch that disables automatic large-task splitting without weakening explicit `breakIntoSubtasks: true` requests. Keep the default enabled to preserve existing triage behavior for workflows that have no stored override. + */ + description: + "Enable automatic large-task splitting guidance during triage. Turn off to split only when breakIntoSubtasks is explicitly requested.", + }, { id: "triageSizeSmallMaxHours", name: "Triage size S max hours", @@ -431,6 +443,38 @@ function formatTriagePolicyValue(id: string, value: unknown): string { const verbs = Array.isArray(value) ? value : TRIAGE_POLICY_DEFAULTS.get(id); return (Array.isArray(verbs) ? verbs : []).map((verb) => String(verb)).join(", "); } + if (id === "triageProactiveSubtaskSplittingEnabled") { + const enabled = value !== false; + if (!enabled) { + return `Proactive oversized-task splitting is DISABLED for this workflow/project. + +- Do NOT split solely because the task is Size M/L, has many planned implementation steps, touches many files/packages, or otherwise looks oversized. +- Only create child tasks when \`breakIntoSubtasks: true\` is explicitly present; in that case, follow the mandatory \`## Triage subtask breakdown\` flow above exactly. +- When proactive splitting is disabled and \`breakIntoSubtasks: true\` is absent, write a normal PROMPT.md for the original task even if it is large; document realistic scope, risks, and quality gates instead of replacing it with child tasks.`; + } + return `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 {{triageSubtaskStepThreshold}} implementation steps +- The task affects MORE THAN {{triageSubtaskPackageThreshold}} different packages/modules with distinct concerns (a typed field change that naturally touches core types + store + UI + tests is NOT 4 distinct concerns — it's one coherent change) +- Any single step would take more than 1-2 hours to complete +- The task has multiple 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 {{triageSubtaskLargeStepSignal}} or more. +- Plans whose implementation-step count would reach {{triageSubtaskAdditiveStepSignal}} or more (additive signal — counts even when the surrounding step-count threshold above has not yet fired). +- Tasks whose declared \`## File Scope\` would list {{triageSubtaskFileScopeThreshold}} or more entries. +- Descriptions that quantify large remediation batches (for example "47 failing tests", "30+ broken files") at or above {{triageSubtaskRemediationBatchThreshold}} 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.`; + } return String(value ?? TRIAGE_POLICY_DEFAULTS.get(id) ?? ""); } diff --git a/packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx index ef1cbdc204..cb689af9c7 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx @@ -216,6 +216,59 @@ describe("WorkflowSettingsPanel — Values tab", () => { expect(screen.getByText(/Leave empty for unbounded automatic Code Review remediation/i)).toBeInTheDocument(); }); + it("renders and saves the automatic large-task splitting workflow toggle once", async () => { + const triageToggle: WorkflowSettingDefinition[] = [ + { + id: "triageProactiveSubtaskSplittingEnabled", + name: "Triage proactive subtask splitting", + type: "boolean", + default: true, + }, + ]; + mockFetchValues.mockResolvedValueOnce( + payload({ effective: { triageProactiveSubtaskSplittingEnabled: true } }), + ); + mockUpdateValues + .mockResolvedValueOnce( + payload({ + stored: { triageProactiveSubtaskSplittingEnabled: false }, + effective: { triageProactiveSubtaskSplittingEnabled: false }, + }), + ) + .mockResolvedValueOnce( + payload({ effective: { triageProactiveSubtaskSplittingEnabled: true } }), + ); + + render(); + + const controls = await screen.findAllByLabelText("Automatic large-task splitting"); + expect(controls).toHaveLength(1); + const toggle = controls[0] as HTMLInputElement; + expect(toggle.checked).toBe(true); + expect(screen.getByText(/Default enabled/i)).toBeInTheDocument(); + expect(screen.getByText(/breakIntoSubtasks: true/i)).toBeInTheDocument(); + expect(screen.queryByTestId("wf-settings-customized-triageProactiveSubtaskSplittingEnabled")).not.toBeInTheDocument(); + + fireEvent.click(toggle); + fireEvent.click(screen.getByTestId("wf-settings-save-values")); + await waitFor(() => expect(mockUpdateValues).toHaveBeenCalledWith( + "wf-1", + { triageProactiveSubtaskSplittingEnabled: false }, + "proj-1", + )); + expect(screen.getByTestId("wf-settings-customized-triageProactiveSubtaskSplittingEnabled")).toBeInTheDocument(); + + const row = screen.getByTestId("wf-settings-value-triageProactiveSubtaskSplittingEnabled"); + const clearButton = within(row).getByRole("button", { name: "Reset to default" }); + fireEvent.click(clearButton); + fireEvent.click(screen.getByTestId("wf-settings-save-values")); + await waitFor(() => expect(mockUpdateValues).toHaveBeenLastCalledWith( + "wf-1", + { triageProactiveSubtaskSplittingEnabled: null }, + "proj-1", + )); + }); + it("batches three field edits into exactly ONE patch on Save values", async () => { mockFetchValues.mockResolvedValue(payload({ effective: { "timeout-ms": 1000, "new-sessions": false } })); render(); diff --git a/packages/dashboard/app/components/workflow-setting-display.ts b/packages/dashboard/app/components/workflow-setting-display.ts index c62c014f24..719b62f91e 100644 --- a/packages/dashboard/app/components/workflow-setting-display.ts +++ b/packages/dashboard/app/components/workflow-setting-display.ts @@ -101,6 +101,16 @@ const DISPLAY: Record = { */ description: "Leave empty for unbounded automatic Code Review remediation; set 0 to disable automatic revision.", }, + triageProactiveSubtaskSplittingEnabled: { + group: "steps", + label: "Automatic large-task splitting", + /* + * FNXC:TriagePolicy 2026-07-04-00:00: + * Workflow Settings is the canonical operator surface for this workflow/project policy. The copy must make the default enabled state clear and preserve trust that explicit `breakIntoSubtasks: true` requests still split even when automatic large-task splitting is off. + */ + description: + "Default enabled. When off, triage keeps oversized tasks whole unless breakIntoSubtasks: true is explicitly requested.", + }, workflowStepTimeoutMs: { group: "steps", label: "Step timeout", diff --git a/packages/engine/src/__tests__/triage-threshold-settings.test.ts b/packages/engine/src/__tests__/triage-threshold-settings.test.ts index 43e4dfd098..944c46bf6f 100644 --- a/packages/engine/src/__tests__/triage-threshold-settings.test.ts +++ b/packages/engine/src/__tests__/triage-threshold-settings.test.ts @@ -75,6 +75,33 @@ describe("triage threshold workflow settings", () => { } }); + it("uses stored disabled proactive splitting policy while retaining explicit split instructions", async () => { + const rootDir = makeTempDir("fn-7491-triage-root-"); + const globalDir = makeTempDir("fn-7491-triage-global-"); + const store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); + await store.init(); + try { + const projectId = store.getWorkflowSettingsProjectId(); + await store.updateWorkflowSettingValues("builtin:coding", projectId, { + triageProactiveSubtaskSplittingEnabled: false, + }); + + const effective = await resolveEffectiveSettingsById(store, "builtin:coding", projectId); + expect(effective.triageProactiveSubtaskSplittingEnabled).toBe(false); + + const rendered = renderTriagePolicyPlaceholders(builtinPlanningPrompt(), effective); + expect(rendered).toContain("## Triage subtask breakdown"); + expect(rendered).toContain("When the task includes `breakIntoSubtasks: true`, first decide whether it should be split"); + expect(rendered).toContain("Proactive oversized-task splitting is DISABLED"); + expect(rendered).toContain("Do NOT split solely because the task is Size M/L"); + expect(rendered).toContain("Only create child tasks when `breakIntoSubtasks: true` is explicitly present"); + expect(rendered).not.toContain("Even when `breakIntoSubtasks` is not set to `true`, apply these thresholds proactively"); + expect(rendered).not.toContain("{{"); + } finally { + store.close(); + } + }); + it("keeps migrated threshold numbers out of the triage prompt assembly code path", async () => { const source = await readFile(new URL("../triage.ts", import.meta.url), "utf8"); const promptAssembly = source.slice( @@ -84,6 +111,7 @@ describe("triage threshold workflow settings", () => { expect(promptAssembly).toContain("renderTriagePolicyPlaceholders"); expect(promptAssembly).not.toMatch(/\b(?:7|9|12|20|30)\b/); + expect(promptAssembly).not.toMatch(/triageProactiveSubtaskSplittingEnabled\s*[:=]\s*(?:true|false)/); expect(promptAssembly).not.toMatch(/builtin:quick-fix|builtin:coding/); expect(promptAssembly).not.toMatch(/Decide|Evaluate|Verify|Confirm|Audit|Review whether|Investigate and report/); }); diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 5531cb2a41..30aea32746 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -701,10 +701,11 @@ describe("canonical triage policy prompt", () => { expect(TRIAGE_POLICY_PROMPT).toContain( "## Proactive Subtask Breakdown for M/L Tasks", ); - expect(TRIAGE_POLICY_PROMPT).toContain( + expect(TRIAGE_POLICY_PROMPT).toContain("{{triageProactiveSubtaskSplittingEnabled}}"); + expect(RENDERED_TRIAGE_POLICY_PROMPT).toContain( "Even when `breakIntoSubtasks` is not set to `true`", ); - expect(TRIAGE_POLICY_PROMPT).toContain( + expect(RENDERED_TRIAGE_POLICY_PROMPT).toContain( "Size S tasks should NOT be split", ); }); @@ -714,13 +715,13 @@ describe("canonical triage policy prompt", () => { expect(RENDERED_TRIAGE_POLICY_PROMPT).toContain( "MORE THAN 3 different packages/modules", ); - expect(TRIAGE_POLICY_PROMPT).toContain("MORE THAN {{triageSubtaskStepThreshold}} implementation steps"); + expect(RENDERED_TRIAGE_POLICY_PROMPT).not.toContain("{{triageSubtaskStepThreshold}}"); }); it("biases toward keeping tasks whole and acknowledges coordination overhead", () => { - expect(TRIAGE_POLICY_PROMPT).toContain("Default to keeping the task whole"); - expect(TRIAGE_POLICY_PROMPT).toContain("Coordination overhead"); - expect(TRIAGE_POLICY_PROMPT).toContain( + expect(RENDERED_TRIAGE_POLICY_PROMPT).toContain("Default to keeping the task whole"); + expect(RENDERED_TRIAGE_POLICY_PROMPT).toContain("Coordination overhead"); + expect(RENDERED_TRIAGE_POLICY_PROMPT).toContain( "7-10 focused steps within a coherent scope is fine as one unit", ); }); @@ -901,6 +902,54 @@ describe("fast-mode triage", () => { expect(capturedSystemPrompt).toContain("## Review Level"); }); + it("renders disabled proactive splitting while preserving explicit breakIntoSubtasks prompts", async () => { + const task = createTriageTask({ id: "FN-FAST-003", executionMode: "standard", breakIntoSubtasks: true }); + const rootDir = await createTriageFixtureRoot("fn-7491-triage-"); + const detail = { ...mockTaskDetail, id: task.id, breakIntoSubtasks: true, attachments: [], comments: [] }; + const store = createMockStore({ + getTask: vi.fn().mockResolvedValue(detail), + getSettings: vi.fn().mockResolvedValue({ + maxConcurrent: 2, + maxWorktrees: 4, + pollIntervalMs: 10000, + groupOverlappingFiles: false, + autoMerge: true, + triageProactiveSubtaskSplittingEnabled: false, + } as Settings), + }); + + let capturedSystemPrompt = ""; + const { promptWithFallback } = await import("../pi.js"); + (promptWithFallback as ReturnType).mockImplementationOnce(async (_session: unknown, prompt: string) => { + await mkdir(join(rootDir, ".fusion", "tasks", "FN-FAST-003"), { recursive: true }).catch(() => undefined); + await writeFile(join(rootDir, ".fusion", "tasks", "FN-FAST-003", "PROMPT.md"), "# Task: FN-FAST-003 - Split\n\n## Mission\n\nDone.", { flag: "w" }).catch(() => undefined); + expect(prompt).toContain("## Subtask Breakdown Requested"); + expect(prompt).toContain("The user has requested that this task be broken into smaller subtasks"); + expect(prompt).not.toContain("## Subtask Consideration"); + }); + mockCreateFnAgent.mockImplementationOnce(async (opts: any) => { + capturedSystemPrompt = opts.systemPrompt; + return { + session: { + state: {}, + sessionManager: { getLeafId: vi.fn().mockReturnValue(null) }, + prompt: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn(), + navigateTree: vi.fn(), + }, + }; + }); + + const processor = new TriageProcessor(store, rootDir); + await processor.specifyTask(task); + + expect(capturedSystemPrompt).toContain("Proactive oversized-task splitting is DISABLED"); + expect(capturedSystemPrompt).toContain("Only create child tasks when `breakIntoSubtasks: true` is explicitly present"); + expect(capturedSystemPrompt).not.toContain("Even when `breakIntoSubtasks` is not set to `true`, apply these thresholds proactively"); + expect(promptWithFallback).toHaveBeenCalled(); + await cleanupTriageFixtureRoot(rootDir); + }); + it("includes triage plugin contributions when provided", async () => { const task = createTriageTask({ id: "FN-FAST-PLUGIN-001", executionMode: "standard" }); const store = createMockStore({