From bcc77c9608439e6e324e0974524bff131a9978ab Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Wed, 19 Aug 2026 13:41:36 +0000 Subject: [PATCH] FN-035: require automatic task recommendations Require task recommendations to be explicitly enabled and consistently surfaced across execution and settings. - Add the automatic task recommendations setting, persistence, validation, and localized UI copy. - Gate executor recommendation prompts and completion-tool behavior on the setting. - Update dashboard/API coverage, documentation, and regression tests. Files changed: .changeset/fn-035-required-task-recommendations.md | 7 +++ docs/dashboard-guide.md | 3 +- docs/settings-reference.md | 2 + .../postgres/settings-persistence.pg.test.ts | 18 +++++++ .../core/src/__tests__/settings-parity.test.ts | 3 ++ packages/core/src/config/settings-schema.ts | 2 + packages/core/src/task-store/settings-ops.ts | 16 ++++-- packages/core/src/types/settings/settings-scope.ts | 8 +++ .../app/__tests__/settings-save-split.test.ts | 16 ++++++ .../__tests__/SettingsModal.general.test.tsx | 36 +++++++++++++ .../app/components/settings/section-keys.ts | 1 + .../settings/sections/GeneralSection.search.ts | 10 ++++ .../settings/sections/GeneralSection.tsx | 15 +++++- .../settings-default-descriptions.test.tsx | 2 + .../register-settings-memory-worktrunk.test.ts | 28 ++++++++++ .../src/routes/register-settings-memory-routes.ts | 5 ++ ...cutor-prompt-completion-recommendations.test.ts | 31 +++++++++++ .../executor-task-recommendations.test.ts | 61 ++++++++++++++++++++-- .../engine/src/executor/create-task-done-tool.ts | 18 +++++-- packages/engine/src/executor/system-prompt.ts | 20 +++++-- packages/i18n/locales/en/app.json | 3 ++ packages/i18n/locales/es/app.json | 3 ++ packages/i18n/locales/fr/app.json | 3 ++ packages/i18n/locales/ko/app.json | 3 ++ packages/i18n/locales/pt-BR/app.json | 5 +- packages/i18n/locales/zh-CN/app.json | 3 ++ packages/i18n/locales/zh-TW/app.json | 3 ++ packages/i18n/src/resources.d.ts | 3 ++ 28 files changed, 307 insertions(+), 21 deletions(-) Fusion-Task-Id: FN-035 Fusion-Task-Lineage: 336d4d99-3105-43df-b82c-ca413b9c5892 Co-authored-by: Fusion --- .../fn-035-required-task-recommendations.md | 7 +++ docs/dashboard-guide.md | 3 +- docs/settings-reference.md | 2 + .../postgres/settings-persistence.pg.test.ts | 18 ++++++ .../src/__tests__/settings-parity.test.ts | 3 + packages/core/src/config/settings-schema.ts | 2 + packages/core/src/task-store/settings-ops.ts | 16 +++-- .../core/src/types/settings/settings-scope.ts | 8 +++ .../app/__tests__/settings-save-split.test.ts | 16 +++++ .../__tests__/SettingsModal.general.test.tsx | 36 +++++++++++ .../app/components/settings/section-keys.ts | 1 + .../sections/GeneralSection.search.ts | 10 +++ .../settings/sections/GeneralSection.tsx | 15 ++++- .../settings-default-descriptions.test.tsx | 2 + ...register-settings-memory-worktrunk.test.ts | 28 +++++++++ .../routes/register-settings-memory-routes.ts | 5 ++ ...-prompt-completion-recommendations.test.ts | 31 ++++++++++ .../executor-task-recommendations.test.ts | 61 +++++++++++++++++-- .../src/executor/create-task-done-tool.ts | 18 +++++- packages/engine/src/executor/system-prompt.ts | 20 ++++-- packages/i18n/locales/en/app.json | 3 + packages/i18n/locales/es/app.json | 3 + packages/i18n/locales/fr/app.json | 3 + packages/i18n/locales/ko/app.json | 3 + packages/i18n/locales/pt-BR/app.json | 5 +- packages/i18n/locales/zh-CN/app.json | 3 + packages/i18n/locales/zh-TW/app.json | 3 + packages/i18n/src/resources.d.ts | 3 + 28 files changed, 307 insertions(+), 21 deletions(-) create mode 100644 .changeset/fn-035-required-task-recommendations.md diff --git a/.changeset/fn-035-required-task-recommendations.md b/.changeset/fn-035-required-task-recommendations.md new file mode 100644 index 0000000000..17860c502e --- /dev/null +++ b/.changeset/fn-035-required-task-recommendations.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Require quality-first task recommendation evaluation at completion. +category: feature +dev: Adds the project setting `requireTaskRecommendations`; positive caps require an explicit recommendation array while relevance permits fewer or none. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index b0f8b23b4f..02989bc5dd 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1569,7 +1569,8 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou - The top-level **Chat** tab appears first for active task details and is the default landing tab for non-`done` tasks. It uses the project Direct Chat default model and thinking level, and exposes reusable model/thinking selectors without impersonating a Direct Chat agent. Opening the tab is lookup-only: Fusion creates the task-scoped Chat session only after you send a composer message, starter prompt, or question answer. Once a user message exists, the resumable planner chat can appear in the global Chat list; interacted chats are kept when the task reaches `done` and removed when the task is archived. Each send includes bounded server-built task context so the planner can answer current status, progress, recent activity, dependency, and task definition questions. It shows starter prompts for common planning questions, can render structured planner questions, and converts only explicit operator steering intent through the scoped steering tool. The composer stays pinned while the transcript, loading, error, starter, history, and streaming states scroll internally; on mobile/narrow task detail, the default focused Chat layout hides nonessential title/metadata/tab/action rows until you collapse it from the in-view expand control. - The **Activity → Live**, **Feed**, and **Raw Logs** segments remain immediately after **Chat** and share an expand/collapse control that lets the active Activity segment fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed. - The **Summary** tab appears for `done` tasks and remains their default landing tab. It shows the recorded completion summary, the **Merge Details** card (merge status, commit, PR, timestamp, and message), changed-file/merge stats when available, completed steps, workflow results, retry counts, and a token usage & cost section broken down by model from the already-loaded task detail; unpriced models show cost as unavailable rather than `$0`. -- The **Recommendations** tab appears on a completed task only when at least one recommendation was captured. At accepted completion, executors evaluate optional, non-blocking out-of-scope findings and submit task-ready recommendations; an explicit `[]` means none qualified, not that filler should be invented. The project cap bounds captured results, and `maxRecommendationsPerTask: 0` disables capture and therefore removes the tab entirely. A non-empty set sends one mailbox notice per distinct recommendation-id set only after completion is accepted; interrupted or rolled-back handoffs and linking an already-captured recommendation to a created task send nothing. Delivery is asynchronous and best-effort, so it never delays task completion; **Settings → General → Recommendation mailbox notices** can disable only this notice, not capture. Recommendations are distinct from immediately created/delegated tasks, which remain appropriate for required dependency coordination, explicit task requirements, or operator-directed filing. An empty result surfaces as no tab; otherwise each row shows a task-ready title, category, and description. **Create task** uses the normal guarded intake policy (including duplicate checks), so a duplicate conflict creates no child and leaves the recommendation available to retry; successful repeated clicks reuse the same linked triage task. The same recommendations also appear project-wide in **Insights → Task Recommendations**, where row pagination and an explicit **Load more** control keep the aggregate bounded without hiding later suggestions. + +- The **Recommendations** tab appears on a completed task only when at least one recommendation was captured. At accepted completion, executors evaluate optional, non-blocking out-of-scope findings and submit task-ready recommendations; an explicit `[]` means none qualified, not that filler should be invented. The project cap bounds captured results, and `maxRecommendationsPerTask: 0` disables capture and therefore removes the tab entirely. Enable **Settings → General → Require automatic task recommendations** to make every positive-cap successful completion explicitly submit an array at the accepted completion checkpoint; this is automatic executor capture, not a background generator or a retroactive backfill. The executor aims toward the configured maximum from concrete source-task/worktree evidence, but a shorter list or `[]` is correct when grounded candidates run out. Duplicate, restated, speculative, filler, or scope-drifting suggestions are never valid, and relevance always outranks count. A non-empty set sends one mailbox notice per distinct recommendation-id set only after completion is accepted; interrupted or rolled-back handoffs and linking an already-captured recommendation to a created task send nothing. Delivery is asynchronous and best-effort, so it never delays task completion; **Settings → General → Recommendation mailbox notices** can disable only this notice, not capture. Recommendations are distinct from immediately created/delegated tasks, which remain appropriate for required dependency coordination, explicit task requirements, or operator-directed filing. Fusion does not automatically create follow-up tasks from recommendations and does not generate them for already-completed tasks; operators use each row's **Create task** action when appropriate. An empty result surfaces as no tab; otherwise each row shows a task-ready title, category, and description. **Create task** uses the normal guarded intake policy (including duplicate checks), so a duplicate conflict creates no child and leaves the recommendation available to retry; successful repeated clicks reuse the same linked triage task. The same recommendations also appear project-wide in **Insights → Task Recommendations**, where row pagination and an explicit **Load more** control keep the aggregate bounded without hiding later suggestions. - The **Cost** tab is available for tasks in every column and sits immediately after **Comments → Terminal** in the tab strip. It shows the read-time derived per-model cost breakdown (input, output, cached, cache-write, total tokens, derived USD) and a task total; no token usage shows an explicit empty state, while unpriced or zero-usage rows use `—` instead of a guessed `$0`. - The **Models** tab exposes inline **Thinking Level** selectors for **Executor Model**, **Reviewer Model**, and **Planning Model**. Executor saves the shared task thinking level, while Reviewer and Planning save independent per-lane overrides; leaving either lane on **Default** inherits the shared task thinking level and then the configured workflow/project defaults. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index d404c379b5..5b59238404 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -482,6 +482,8 @@ Security-sensitive file-browser escape hatches are project-only. `allowAbsoluteF | `enginePaused` | `boolean` | `false` | Soft pause: stop dispatching new work while letting active sessions finish. While paused (including shared pause windows with `globalPause`), stuck-task polling/timers are suspended so paused wall-clock time does not count against `taskStuckTimeoutMs`. Clearing pause state resumes runtime scheduling and gives tracked active sessions a fresh stuck-task grace window before normal detection resumes; when `autoMerge` is enabled, eligible `in-review` tasks are re-swept into the auto-merge queue (paused/blocked/failed review tasks remain skipped). | | `maxConcurrent` | `number` | `2` | Max concurrency for top-level working agents per project across planning, execution, and review/merge. Nested helper agents remain parent-internal and may temporarily exceed this displayed count. Editable from Settings, Command Center, and Engine Control. | | `maxRecommendationsPerTask` | `number` | `3` | Project-scoped maximum accepted completion recommendations per task. Integers **0–20** only; `0` disables recommendation capture and `1–20` bounds task-ready out-of-scope suggestions. | + +| `requireTaskRecommendations` | `boolean` | `false` | Project-only opt-in. When `true` and `maxRecommendationsPerTask` is positive, every accepted successful completion must explicitly submit a recommendation array. The executor aims toward the cap using grounded, distinct, task-ready findings, but fewer recommendations or `[]` is correct when relevance does not support more; filler, duplicates, restatements, speculation, and scope drift are never valid. Cap `0` remains authoritative and disables capture, so this toggle cannot request a payload. Reset clears the project override back to `false`. | | `recommendationMailboxNoticeEnabled` | `boolean` | `true` | Send a best-effort, non-blocking mailbox notice after accepted completion captures a non-empty recommendation set. Disabling it suppresses only the notice; recommendation capture and storage are unchanged. | | `maxConcurrentVerifications` | `number` | `1` | Max concurrent verification subprocesses (`fn_run_verification`, merge test/build commands) process-wide. Caps stacked monorepo typecheck/build so concurrent tasks do not peg host CPU. Range **1–8** (clamped at runtime and in Settings). Editable from Settings → Scheduling. Each project engine registers its cap; the effective process limit is the **minimum** of registered project caps. | | `maxTriageConcurrent` | `number` | `2` | Legacy persisted value; ignored. Planning shares `maxConcurrent` and no Max Triage control is displayed. | diff --git a/packages/core/src/__tests__/postgres/settings-persistence.pg.test.ts b/packages/core/src/__tests__/postgres/settings-persistence.pg.test.ts index dd9e60d8b7..4c0cba7f0c 100644 --- a/packages/core/src/__tests__/postgres/settings-persistence.pg.test.ts +++ b/packages/core/src/__tests__/postgres/settings-persistence.pg.test.ts @@ -124,6 +124,24 @@ pgTest("VAL-CROSS-004: Settings persistence (PostgreSQL)", () => { expect(settings.taskPrefix).toBe("ACTIVE"); }); + it("keeps recommendation policy project-scoped and validates it atomically", async () => { + const store = h.store(); + expect((await store.getSettings()).requireTaskRecommendations).toBe(false); + + await store.updateSettings({ requireTaskRecommendations: true }); + expect((await store.getSettings()).requireTaskRecommendations).toBe(true); + + // JSON/API callers can evade the GlobalSettings TypeScript shape; global persistence must not. + await store.updateGlobalSettings({ requireTaskRecommendations: false } as never); + expect((await store.getSettings()).requireTaskRecommendations).toBe(true); + + await expect(store.updateSettings({ requireTaskRecommendations: "yes" } as never)).rejects.toThrow("requireTaskRecommendations must be a boolean"); + expect((await store.getSettings()).requireTaskRecommendations).toBe(true); + + await store.updateSettings({ requireTaskRecommendations: null }); + expect((await store.getSettings()).requireTaskRecommendations).toBe(false); + }); + it("keeps the recommendation cap project-scoped when an untyped global patch includes it", async () => { const store = h.store(); await store.updateSettings({ maxRecommendationsPerTask: 7 }); diff --git a/packages/core/src/__tests__/settings-parity.test.ts b/packages/core/src/__tests__/settings-parity.test.ts index ab32288f29..538d636600 100644 --- a/packages/core/src/__tests__/settings-parity.test.ts +++ b/packages/core/src/__tests__/settings-parity.test.ts @@ -63,6 +63,9 @@ describe("settings key parity", () => { expect(isProjectSettingsKey("maxConcurrent")).toBe(true); expect(isProjectSettingsKey("maxRecommendationsPerTask")).toBe(true); expect(isGlobalSettingsKey("maxRecommendationsPerTask")).toBe(false); + expect(isProjectSettingsKey("requireTaskRecommendations")).toBe(true); + expect(isGlobalSettingsKey("requireTaskRecommendations")).toBe(false); + expect(DEFAULT_PROJECT_SETTINGS.requireTaskRecommendations).toBe(false); expect(isProjectSettingsKey("chatMessageLayout")).toBe(true); expect(isGlobalSettingsKey("chatMessageLayout")).toBe(false); expect(PROJECT_SETTINGS_KEYS).toContain("chatMessageLayout"); diff --git a/packages/core/src/config/settings-schema.ts b/packages/core/src/config/settings-schema.ts index 4c01683325..10091d1ef9 100644 --- a/packages/core/src/config/settings-schema.ts +++ b/packages/core/src/config/settings-schema.ts @@ -425,6 +425,8 @@ export const DEFAULT_GLOBAL_SETTINGS = { export const DEFAULT_PROJECT_SETTINGS = { // FNXC:TaskRecommendations 2026-08-08-05:02: completion follows-ups stay bounded by default; 0 disables writing them. maxRecommendationsPerTask: 3, + // FNXC:TaskRecommendations 2026-08-19-13:05: explicit recommendation evaluation is opt-in and only applies while the positive cap enables capture; relevance always outranks count. + requireTaskRecommendations: false, // FNXC:TaskRecommendations 2026-08-13-03:56: surface completed-task proposals by default; operators can suppress the notice without suppressing capture. recommendationMailboxNoticeEnabled: true, globalPause: false, diff --git a/packages/core/src/task-store/settings-ops.ts b/packages/core/src/task-store/settings-ops.ts index 1da144a243..97c7884eed 100644 --- a/packages/core/src/task-store/settings-ops.ts +++ b/packages/core/src/task-store/settings-ops.ts @@ -33,16 +33,20 @@ import { */ /** * FNXC:TaskRecommendations 2026-08-08-05:02: - * Reject an invalid project cap atomically rather than coercing an executor's - * completion policy. Zero deliberately disables recommendations; 1..20 bounds - * retained operator-visible suggestions. + * Reject invalid recommendation policy atomically rather than coercing an + * executor's completion contract. Zero deliberately disables recommendations; + * 1..20 bounds retained operator-visible suggestions, and the requirement is a + * project-only boolean that never permits irrelevant filler. */ function assertValidRecommendationSettingsPatch(patch: Record): void { const value = patch.maxRecommendationsPerTask; - if (value === undefined || value === null) return; - if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 20) { + if (value !== undefined && value !== null && (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 20)) { throw new Error("maxRecommendationsPerTask must be an integer between 0 and 20"); } + const requireRecommendations = patch.requireTaskRecommendations; + if (requireRecommendations !== undefined && requireRecommendations !== null && typeof requireRecommendations !== "boolean") { + throw new Error("requireTaskRecommendations must be a boolean"); + } } /* @@ -349,6 +353,8 @@ export async function updateGlobalSettingsImpl(store: TaskStore, patch: Partial< boundary instead of allowing one project's completion cap to leak into every project. */ delete (globalPatch as Record).maxRecommendationsPerTask; + // FNXC:TaskRecommendations 2026-08-19-13:05: The completion requirement is project policy; discard untyped global patches so it cannot leak across projects. + delete (globalPatch as Record).requireTaskRecommendations; // Handle deep merge + targeted null clear semantics for remoteAccess const incomingRemoteAccess = (globalPatch as Record)["remoteAccess"]; diff --git a/packages/core/src/types/settings/settings-scope.ts b/packages/core/src/types/settings/settings-scope.ts index 49f912aa3d..2e7f0be27e 100644 --- a/packages/core/src/types/settings/settings-scope.ts +++ b/packages/core/src/types/settings/settings-scope.ts @@ -1039,6 +1039,14 @@ export interface ProjectSettings { * explicit opt-out; executor validation enforces the 0..20 integer boundary. */ maxRecommendationsPerTask?: number; + /** + * FNXC:TaskRecommendations 2026-08-19-13:05: + * Default-off project policy requiring an explicit completion recommendation + * evaluation when the cap is positive. The executor targets the cap for + * relevant, task-ready findings, but a shorter list or [] is correct when + * grounded candidates do not qualify; this setting never authorizes filler. + */ + requireTaskRecommendations?: boolean; /** * FNXC:TaskRecommendations 2026-08-13-03:56: * The operator requested an on/off switch for recommendation mailbox notices. This controls diff --git a/packages/dashboard/app/__tests__/settings-save-split.test.ts b/packages/dashboard/app/__tests__/settings-save-split.test.ts index 2b21097f89..8c95636cdf 100644 --- a/packages/dashboard/app/__tests__/settings-save-split.test.ts +++ b/packages/dashboard/app/__tests__/settings-save-split.test.ts @@ -34,6 +34,8 @@ describe("scope anchors", () => { expect(isProjectSettingsKey("gitlabAuthToken")).toBe(true); expect(isGlobalSettingsKey("gitlabAuthTokenType")).toBe(true); expect(isProjectSettingsKey("gitlabAuthTokenType")).toBe(true); + expect(isProjectSettingsKey("requireTaskRecommendations")).toBe(true); + expect(isGlobalSettingsKey("requireTaskRecommendations")).toBe(false); }); it("every MODEL_LANE_KEYS entry is a project settings key", () => { @@ -77,6 +79,20 @@ describe("resolveScopedMcpSettings", () => { }); }); +describe("required recommendation policy ownership", () => { + it("routes the changed toggle to the project patch only", () => { + const result = splitSettingsSave({ + payload: { requireTaskRecommendations: true }, + initialValues: { requireTaskRecommendations: false } as never, + initialScopedValues: { global: {}, project: { requireTaskRecommendations: false } } as never, + activeSection: "general", + }); + + expect(result.projectPatch).toEqual({ requireTaskRecommendations: true }); + expect(result.globalPatch).toEqual({}); + }); +}); + describe("agent clarification notification ownership", () => { it("persists the Notifications setting through the section save split", () => { const result = splitSettingsSave({ diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx index bb54dfa1a9..72d3ec40fa 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx @@ -276,6 +276,41 @@ describe("SettingsModal", () => { expect(mockUpdateSettings).toHaveBeenCalled(); expect(mockUpdateSettings.mock.calls.at(-1)?.[0]).toMatchObject({ recommendationMailboxNoticeEnabled: false }); }); + + it.each(["mobile", "desktop"] as const)("shows exactly one default-off required recommendation toggle on %s", async (mode) => { + viewportMode = mode; + renderModal({ initialSection: "general" }); + await waitForSettingsModalReady(); + + const toggle = screen.getByRole("checkbox", { name: "Require automatic task recommendations" }); + expect(screen.getAllByRole("checkbox", { name: "Require automatic task recommendations" })).toHaveLength(1); + expect(toggle).not.toBeChecked(); + expect(screen.getByText(/cap 0 disables capture regardless/i)).toBeInTheDocument(); + + vi.useFakeTimers(); + fireEvent.click(toggle); + await flushSettingsAutoSave(); + vi.useRealTimers(); + expect(mockUpdateSettings.mock.calls.at(-1)?.[0]).toMatchObject({ requireTaskRecommendations: true }); + }); + + it("renders enabled required recommendations and preserves one usable control when capture is disabled", async () => { + mockFetchSettings.mockResolvedValueOnce({ + ...defaultSettings, + requireTaskRecommendations: true, + maxRecommendationsPerTask: 0, + }); + mockFetchSettingsByScope.mockResolvedValueOnce({ + global: defaultSettings, + project: { requireTaskRecommendations: true, maxRecommendationsPerTask: 0 }, + }); + renderModal({ initialSection: "general" }); + await waitForSettingsModalReady(); + + expect(screen.getByRole("checkbox", { name: "Require automatic task recommendations" })).toBeChecked(); + expect(screen.getAllByRole("checkbox", { name: "Require automatic task recommendations" })).toHaveLength(1); + expect(screen.getByText(/cap 0 disables capture regardless/i)).toBeInTheDocument(); + }); // Keep Advanced off by default so disclosure default/persist tests stay truthful. installSettingsModalEnv({ advancedSettings: false }); @@ -1821,6 +1856,7 @@ describe("SettingsModal", () => { expect(payload.autoMerge).toBeNull(); expect(payload.maxConcurrent).toBeNull(); expect(payload.maxRecommendationsPerTask).toBeNull(); + expect(payload.requireTaskRecommendations).toBeNull(); expect(payload.recommendationMailboxNoticeEnabled).toBeNull(); // Global-only key must never appear in a project-scope reset payload. expect(payload).not.toHaveProperty("themeMode"); diff --git a/packages/dashboard/app/components/settings/section-keys.ts b/packages/dashboard/app/components/settings/section-keys.ts index 9d8f303fa8..269f858f44 100644 --- a/packages/dashboard/app/components/settings/section-keys.ts +++ b/packages/dashboard/app/components/settings/section-keys.ts @@ -83,6 +83,7 @@ export const PROJECT_SECTION_KEYS: Readonly> = "sessionAdvisorEnabledByDefault", "mailAutoCleanupDays", "maxRecommendationsPerTask", + "requireTaskRecommendations", "recommendationMailboxNoticeEnabled", "mobileNavPrimaryItems", "operationalLogRetentionDays", diff --git a/packages/dashboard/app/components/settings/sections/GeneralSection.search.ts b/packages/dashboard/app/components/settings/sections/GeneralSection.search.ts index c8e0a9341f..91c7f96a84 100644 --- a/packages/dashboard/app/components/settings/sections/GeneralSection.search.ts +++ b/packages/dashboard/app/components/settings/sections/GeneralSection.search.ts @@ -38,6 +38,16 @@ export const generalSearchEntries: SettingsSearchEntry[] = [ helpFallback: "Default: 3. Set 0 to disable recommendations; choose a whole number from 1 to 20 to cap each completed task.", keywords: ["suggestions", "follow-up", "cap", "limit"], }, + { + /* FNXC:TaskRecommendations 2026-08-19-13:05: Keep the required completion toggle searchable through the same descriptor index as its shared GeneralSection row. */ + sectionId: "general", + key: "requireTaskRecommendations", + labelKey: "settings.general.requireTaskRecommendations", + labelFallback: "Require automatic task recommendations", + helpKey: "settings.general.requireTaskRecommendationsHelp", + helpFallback: "Default: disabled. When enabled, successful completion must explicitly evaluate grounded follow-ups. The executor aims toward the configured maximum, but fewer or [] are correct when relevance does not support more; cap 0 disables capture regardless of this setting.", + keywords: ["recommendations", "automatic", "completion", "required", "quality"], + }, { /* FNXC:SettingsSearch 2026-08-16-05:07: diff --git a/packages/dashboard/app/components/settings/sections/GeneralSection.tsx b/packages/dashboard/app/components/settings/sections/GeneralSection.tsx index ec5b326d82..f3c3ce16ec 100644 --- a/packages/dashboard/app/components/settings/sections/GeneralSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GeneralSection.tsx @@ -201,6 +201,17 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError value={form.maxRecommendationsPerTask ?? 3} onChange={(value) => setForm((current) => ({ ...current, maxRecommendationsPerTask: value ?? 3 }))} /> + {/* FNXC:TaskRecommendations 2026-08-19-13:05: A single shared project toggle makes completion evaluation mandatory only for positive caps; relevance wins over filling the configured maximum, and cap 0 remains authoritative. */} + setForm((current) => ({ ...current, requireTaskRecommendations: value === true }))} + /> {/* FNXC:TaskRecommendations 2026-08-13-03:56: The operator asked to be notified in the mailbox when a completed task produces @@ -259,11 +270,11 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError {t("settings.general.disabledFusionWorkflowsAreHiddenFromWorkflow", "Disabled Fusion workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve. Default: all built-in workflows enabled (unset).")}
- At least one built-in workflow must remain enabled. + {t("settings.general.builtinWorkflowAtLeastOneEnabled", "At least one built-in workflow must remain enabled.")} {builtinWorkflows.map((workflow) => { const checked = enabledBuiltinWorkflowIds.has(workflow.id); const isLastEnabled = checked && enabledBuiltinWorkflowCount <= 1; - return (