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 <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-035-required-task-recommendations.md
Normal file
7
.changeset/fn-035-required-task-recommendations.md
Normal file
@@ -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.
|
||||||
@@ -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 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 **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 **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.
|
<!-- FNXC:TaskRecommendations 2026-08-19-13:05: Task Detail docs distinguish optional capture from the project opt-in that requires an explicit quality-first evaluation without turning the cap into a quota. -->
|
||||||
|
- 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 **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`.
|
||||||
<!-- FNXC:Settings-ThinkingLevel 2026-07-13-00:27: The task-detail Models tab now persists validatorThinkingLevel and planningThinkingLevel separately so Reviewer and Planning lanes can choose reasoning effort without changing the Executor lane's task.thinkingLevel. -->
|
<!-- FNXC:Settings-ThinkingLevel 2026-07-13-00:27: The task-detail Models tab now persists validatorThinkingLevel and planningThinkingLevel separately so Reviewer and Planning lanes can choose reasoning effort without changing the Executor lane's task.thinkingLevel. -->
|
||||||
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -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). |
|
| `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. |
|
| `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. |
|
| `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. |
|
||||||
|
<!-- FNXC:TaskRecommendations 2026-08-19-13:05: Document the project-only required-evaluation switch, its cap-zero precedence, and quality-over-count contract beside the recommendation cap. -->
|
||||||
|
| `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. |
|
| `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. |
|
| `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. |
|
| `maxTriageConcurrent` | `number` | `2` | Legacy persisted value; ignored. Planning shares `maxConcurrent` and no Max Triage control is displayed. |
|
||||||
|
|||||||
@@ -124,6 +124,24 @@ pgTest("VAL-CROSS-004: Settings persistence (PostgreSQL)", () => {
|
|||||||
expect(settings.taskPrefix).toBe("ACTIVE");
|
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 () => {
|
it("keeps the recommendation cap project-scoped when an untyped global patch includes it", async () => {
|
||||||
const store = h.store();
|
const store = h.store();
|
||||||
await store.updateSettings({ maxRecommendationsPerTask: 7 });
|
await store.updateSettings({ maxRecommendationsPerTask: 7 });
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ describe("settings key parity", () => {
|
|||||||
expect(isProjectSettingsKey("maxConcurrent")).toBe(true);
|
expect(isProjectSettingsKey("maxConcurrent")).toBe(true);
|
||||||
expect(isProjectSettingsKey("maxRecommendationsPerTask")).toBe(true);
|
expect(isProjectSettingsKey("maxRecommendationsPerTask")).toBe(true);
|
||||||
expect(isGlobalSettingsKey("maxRecommendationsPerTask")).toBe(false);
|
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(isProjectSettingsKey("chatMessageLayout")).toBe(true);
|
||||||
expect(isGlobalSettingsKey("chatMessageLayout")).toBe(false);
|
expect(isGlobalSettingsKey("chatMessageLayout")).toBe(false);
|
||||||
expect(PROJECT_SETTINGS_KEYS).toContain("chatMessageLayout");
|
expect(PROJECT_SETTINGS_KEYS).toContain("chatMessageLayout");
|
||||||
|
|||||||
@@ -425,6 +425,8 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
|||||||
export const DEFAULT_PROJECT_SETTINGS = {
|
export const DEFAULT_PROJECT_SETTINGS = {
|
||||||
// FNXC:TaskRecommendations 2026-08-08-05:02: completion follows-ups stay bounded by default; 0 disables writing them.
|
// FNXC:TaskRecommendations 2026-08-08-05:02: completion follows-ups stay bounded by default; 0 disables writing them.
|
||||||
maxRecommendationsPerTask: 3,
|
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.
|
// FNXC:TaskRecommendations 2026-08-13-03:56: surface completed-task proposals by default; operators can suppress the notice without suppressing capture.
|
||||||
recommendationMailboxNoticeEnabled: true,
|
recommendationMailboxNoticeEnabled: true,
|
||||||
globalPause: false,
|
globalPause: false,
|
||||||
|
|||||||
@@ -33,16 +33,20 @@ import {
|
|||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* FNXC:TaskRecommendations 2026-08-08-05:02:
|
* FNXC:TaskRecommendations 2026-08-08-05:02:
|
||||||
* Reject an invalid project cap atomically rather than coercing an executor's
|
* Reject invalid recommendation policy atomically rather than coercing an
|
||||||
* completion policy. Zero deliberately disables recommendations; 1..20 bounds
|
* executor's completion contract. Zero deliberately disables recommendations;
|
||||||
* retained operator-visible suggestions.
|
* 1..20 bounds retained operator-visible suggestions, and the requirement is a
|
||||||
|
* project-only boolean that never permits irrelevant filler.
|
||||||
*/
|
*/
|
||||||
function assertValidRecommendationSettingsPatch(patch: Record<string, unknown>): void {
|
function assertValidRecommendationSettingsPatch(patch: Record<string, unknown>): void {
|
||||||
const value = patch.maxRecommendationsPerTask;
|
const value = patch.maxRecommendationsPerTask;
|
||||||
if (value === undefined || value === null) return;
|
if (value !== undefined && value !== null && (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 20)) {
|
||||||
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 20) {
|
|
||||||
throw new Error("maxRecommendationsPerTask must be an integer between 0 and 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.
|
boundary instead of allowing one project's completion cap to leak into every project.
|
||||||
*/
|
*/
|
||||||
delete (globalPatch as Record<string, unknown>).maxRecommendationsPerTask;
|
delete (globalPatch as Record<string, unknown>).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<string, unknown>).requireTaskRecommendations;
|
||||||
|
|
||||||
// Handle deep merge + targeted null clear semantics for remoteAccess
|
// Handle deep merge + targeted null clear semantics for remoteAccess
|
||||||
const incomingRemoteAccess = (globalPatch as Record<string, unknown>)["remoteAccess"];
|
const incomingRemoteAccess = (globalPatch as Record<string, unknown>)["remoteAccess"];
|
||||||
|
|||||||
@@ -1039,6 +1039,14 @@ export interface ProjectSettings {
|
|||||||
* explicit opt-out; executor validation enforces the 0..20 integer boundary.
|
* explicit opt-out; executor validation enforces the 0..20 integer boundary.
|
||||||
*/
|
*/
|
||||||
maxRecommendationsPerTask?: number;
|
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:
|
* FNXC:TaskRecommendations 2026-08-13-03:56:
|
||||||
* The operator requested an on/off switch for recommendation mailbox notices. This controls
|
* The operator requested an on/off switch for recommendation mailbox notices. This controls
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ describe("scope anchors", () => {
|
|||||||
expect(isProjectSettingsKey("gitlabAuthToken")).toBe(true);
|
expect(isProjectSettingsKey("gitlabAuthToken")).toBe(true);
|
||||||
expect(isGlobalSettingsKey("gitlabAuthTokenType")).toBe(true);
|
expect(isGlobalSettingsKey("gitlabAuthTokenType")).toBe(true);
|
||||||
expect(isProjectSettingsKey("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", () => {
|
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", () => {
|
describe("agent clarification notification ownership", () => {
|
||||||
it("persists the Notifications setting through the section save split", () => {
|
it("persists the Notifications setting through the section save split", () => {
|
||||||
const result = splitSettingsSave({
|
const result = splitSettingsSave({
|
||||||
|
|||||||
@@ -276,6 +276,41 @@ describe("SettingsModal", () => {
|
|||||||
expect(mockUpdateSettings).toHaveBeenCalled();
|
expect(mockUpdateSettings).toHaveBeenCalled();
|
||||||
expect(mockUpdateSettings.mock.calls.at(-1)?.[0]).toMatchObject({ recommendationMailboxNoticeEnabled: false });
|
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.
|
// Keep Advanced off by default so disclosure default/persist tests stay truthful.
|
||||||
installSettingsModalEnv({ advancedSettings: false });
|
installSettingsModalEnv({ advancedSettings: false });
|
||||||
|
|
||||||
@@ -1821,6 +1856,7 @@ describe("SettingsModal", () => {
|
|||||||
expect(payload.autoMerge).toBeNull();
|
expect(payload.autoMerge).toBeNull();
|
||||||
expect(payload.maxConcurrent).toBeNull();
|
expect(payload.maxConcurrent).toBeNull();
|
||||||
expect(payload.maxRecommendationsPerTask).toBeNull();
|
expect(payload.maxRecommendationsPerTask).toBeNull();
|
||||||
|
expect(payload.requireTaskRecommendations).toBeNull();
|
||||||
expect(payload.recommendationMailboxNoticeEnabled).toBeNull();
|
expect(payload.recommendationMailboxNoticeEnabled).toBeNull();
|
||||||
// Global-only key must never appear in a project-scope reset payload.
|
// Global-only key must never appear in a project-scope reset payload.
|
||||||
expect(payload).not.toHaveProperty("themeMode");
|
expect(payload).not.toHaveProperty("themeMode");
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export const PROJECT_SECTION_KEYS: Readonly<Record<string, readonly string[]>> =
|
|||||||
"sessionAdvisorEnabledByDefault",
|
"sessionAdvisorEnabledByDefault",
|
||||||
"mailAutoCleanupDays",
|
"mailAutoCleanupDays",
|
||||||
"maxRecommendationsPerTask",
|
"maxRecommendationsPerTask",
|
||||||
|
"requireTaskRecommendations",
|
||||||
"recommendationMailboxNoticeEnabled",
|
"recommendationMailboxNoticeEnabled",
|
||||||
"mobileNavPrimaryItems",
|
"mobileNavPrimaryItems",
|
||||||
"operationalLogRetentionDays",
|
"operationalLogRetentionDays",
|
||||||
|
|||||||
@@ -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.",
|
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"],
|
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:
|
FNXC:SettingsSearch 2026-08-16-05:07:
|
||||||
|
|||||||
@@ -201,6 +201,17 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
|
|||||||
value={form.maxRecommendationsPerTask ?? 3}
|
value={form.maxRecommendationsPerTask ?? 3}
|
||||||
onChange={(value) => setForm((current) => ({ ...current, maxRecommendationsPerTask: value ?? 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. */}
|
||||||
|
<SettingsToggleRow
|
||||||
|
descriptor={{
|
||||||
|
key: "requireTaskRecommendations",
|
||||||
|
label: t("settings.general.requireTaskRecommendations", "Require automatic task recommendations"),
|
||||||
|
help: t("settings.general.requireTaskRecommendationsHelp", "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."),
|
||||||
|
scope: "project",
|
||||||
|
}}
|
||||||
|
value={form.requireTaskRecommendations === true}
|
||||||
|
onChange={(value) => setForm((current) => ({ ...current, requireTaskRecommendations: value === true }))}
|
||||||
|
/>
|
||||||
{/*
|
{/*
|
||||||
FNXC:TaskRecommendations 2026-08-13-03:56:
|
FNXC:TaskRecommendations 2026-08-13-03:56:
|
||||||
The operator asked to be notified in the mailbox when a completed task produces
|
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
|
|||||||
<SettingsHelpTip settingKey="enabledBuiltinWorkflowIds">{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).")}</SettingsHelpTip>
|
<SettingsHelpTip settingKey="enabledBuiltinWorkflowIds">{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).")}</SettingsHelpTip>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
||||||
<span id="builtin-workflow-enablement-hint" className="sr-only">At least one built-in workflow must remain enabled.</span>
|
<span id="builtin-workflow-enablement-hint" className="sr-only">{t("settings.general.builtinWorkflowAtLeastOneEnabled", "At least one built-in workflow must remain enabled.")}</span>
|
||||||
{builtinWorkflows.map((workflow) => {
|
{builtinWorkflows.map((workflow) => {
|
||||||
const checked = enabledBuiltinWorkflowIds.has(workflow.id);
|
const checked = enabledBuiltinWorkflowIds.has(workflow.id);
|
||||||
const isLastEnabled = checked && enabledBuiltinWorkflowCount <= 1;
|
const isLastEnabled = checked && enabledBuiltinWorkflowCount <= 1;
|
||||||
return (<label key={workflow.id} htmlFor={`builtin-workflow-${workflow.id}`} className="checkbox-label" title={isLastEnabled ? "At least one built-in workflow must remain enabled" : undefined}>
|
return (<label key={workflow.id} htmlFor={`builtin-workflow-${workflow.id}`} className="checkbox-label" title={isLastEnabled ? t("settings.general.builtinWorkflowAtLeastOneEnabled", "At least one built-in workflow must remain enabled.") : undefined}>
|
||||||
<input id={`builtin-workflow-${workflow.id}`} type="checkbox" checked={checked} disabled={isLastEnabled} aria-describedby="builtin-workflow-enablement-hint" onChange={(e) => setBuiltinWorkflowEnabled(workflow.id, e.target.checked)}/>
|
<input id={`builtin-workflow-${workflow.id}`} type="checkbox" checked={checked} disabled={isLastEnabled} aria-describedby="builtin-workflow-enablement-hint" onChange={(e) => setBuiltinWorkflowEnabled(workflow.id, e.target.checked)}/>
|
||||||
<WorkflowIcon workflowId={workflow.id} decorative />
|
<WorkflowIcon workflowId={workflow.id} decorative />
|
||||||
<span>{workflow.name}</span>
|
<span>{workflow.name}</span>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
|
|||||||
updateChannel: "globalGeneral.releaseChannelHelp",
|
updateChannel: "globalGeneral.releaseChannelHelp",
|
||||||
autoUpdateAndRestart: "globalGeneral.autoUpdateAndRestartHelp",
|
autoUpdateAndRestart: "globalGeneral.autoUpdateAndRestartHelp",
|
||||||
// AppearanceSection
|
// AppearanceSection
|
||||||
|
chatMessageLayout: "appearance.chatMessageLayoutHelp",
|
||||||
openTasksInRightSidebar: "appearance.openTasksInRightSidebarHelp",
|
openTasksInRightSidebar: "appearance.openTasksInRightSidebarHelp",
|
||||||
openMobileTasksInPopup: "appearance.openMobileTasksInPopupHelp",
|
openMobileTasksInPopup: "appearance.openMobileTasksInPopupHelp",
|
||||||
taskPopupsBoardListOnly: "appearance.taskPopupsBoardListOnlyHelp",
|
taskPopupsBoardListOnly: "appearance.taskPopupsBoardListOnlyHelp",
|
||||||
@@ -295,6 +296,7 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
|
|||||||
showTaskChatsInCommonFeed: "general.showTaskChatsInCommonFeedHint",
|
showTaskChatsInCommonFeed: "general.showTaskChatsInCommonFeedHint",
|
||||||
taskPrefix: "general.prefixForNewTaskIDsEGKB",
|
taskPrefix: "general.prefixForNewTaskIDsEGKB",
|
||||||
maxRecommendationsPerTask: "general.maxRecommendationsPerTaskHelp",
|
maxRecommendationsPerTask: "general.maxRecommendationsPerTaskHelp",
|
||||||
|
requireTaskRecommendations: "general.requireTaskRecommendationsHelp",
|
||||||
recommendationMailboxNoticeEnabled: "general.recommendationMailboxNoticeEnabledHelp",
|
recommendationMailboxNoticeEnabled: "general.recommendationMailboxNoticeEnabledHelp",
|
||||||
workspaceMode: "general.workspaceModeHint",
|
workspaceMode: "general.workspaceModeHint",
|
||||||
defaultWorkflowId: "general.newTasksInheritThisCustomWorkflowsStepsOverridable",
|
defaultWorkflowId: "general.newTasksInheritThisCustomWorkflowsStepsOverridable",
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ function createApp(pluginRunner?: Record<string, unknown>, daemonToken?: string)
|
|||||||
{
|
{
|
||||||
githubToken: undefined,
|
githubToken: undefined,
|
||||||
validateModelPresets: vi.fn(() => undefined),
|
validateModelPresets: vi.fn(() => undefined),
|
||||||
|
sanitizeBooleanSetting: vi.fn((name: string, value: unknown) => {
|
||||||
|
if (value === undefined || value === null) return undefined;
|
||||||
|
if (typeof value !== "boolean") throw new Error(`${name} must be a boolean`);
|
||||||
|
return value;
|
||||||
|
}),
|
||||||
sanitizeOverlapIgnorePaths: vi.fn(() => undefined),
|
sanitizeOverlapIgnorePaths: vi.fn(() => undefined),
|
||||||
discoverDashboardPiExtensions: vi.fn(async () => ({
|
discoverDashboardPiExtensions: vi.fn(async () => ({
|
||||||
manifestPaths: [],
|
manifestPaths: [],
|
||||||
@@ -183,6 +188,29 @@ describe("register-settings-memory-routes worktrunk gate", () => {
|
|||||||
expect(scopedStore.updateSettings).toHaveBeenCalledTimes(1);
|
expect(scopedStore.updateSettings).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sanitizes required recommendation policy as a project boolean", async () => {
|
||||||
|
const { app, scopedStore } = createApp();
|
||||||
|
|
||||||
|
expect((await patchSettings(app, { requireTaskRecommendations: true })).status).toBe(200);
|
||||||
|
expect(scopedStore.updateSettings).toHaveBeenCalledWith(
|
||||||
|
{ requireTaskRecommendations: true },
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
|
||||||
|
scopedStore.updateSettings.mockClear();
|
||||||
|
expect((await patchSettings(app, { requireTaskRecommendations: false })).status).toBe(200);
|
||||||
|
expect(scopedStore.updateSettings).toHaveBeenCalledWith(
|
||||||
|
{ requireTaskRecommendations: false },
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
|
||||||
|
scopedStore.updateSettings.mockClear();
|
||||||
|
const malformed = await patchSettings(app, { requireTaskRecommendations: "true" });
|
||||||
|
expect(malformed.status).toBe(400);
|
||||||
|
expect(malformed.body.error).toContain("requireTaskRecommendations must be a boolean");
|
||||||
|
expect(scopedStore.updateSettings).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects an empty built-in workflow set before persistence", async () => {
|
it("rejects an empty built-in workflow set before persistence", async () => {
|
||||||
const { app, scopedStore } = createApp();
|
const { app, scopedStore } = createApp();
|
||||||
|
|
||||||
|
|||||||
@@ -647,6 +647,10 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
|
|||||||
if (Object.prototype.hasOwnProperty.call(clientSettings, "ignoreHiddenOverlapPaths")) {
|
if (Object.prototype.hasOwnProperty.call(clientSettings, "ignoreHiddenOverlapPaths")) {
|
||||||
clientSettings.ignoreHiddenOverlapPaths = sanitizeBooleanSetting("ignoreHiddenOverlapPaths", clientSettings.ignoreHiddenOverlapPaths);
|
clientSettings.ignoreHiddenOverlapPaths = sanitizeBooleanSetting("ignoreHiddenOverlapPaths", clientSettings.ignoreHiddenOverlapPaths);
|
||||||
}
|
}
|
||||||
|
// FNXC:TaskRecommendations 2026-08-19-13:05: Keep the required-completion policy boolean-safe at the HTTP boundary while canonical project-key routing handles scope and persistence.
|
||||||
|
if (Object.prototype.hasOwnProperty.call(clientSettings, "requireTaskRecommendations")) {
|
||||||
|
clientSettings.requireTaskRecommendations = sanitizeBooleanSetting("requireTaskRecommendations", clientSettings.requireTaskRecommendations);
|
||||||
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(clientSettings, "overlapIgnorePaths")) {
|
if (Object.prototype.hasOwnProperty.call(clientSettings, "overlapIgnorePaths")) {
|
||||||
clientSettings.overlapIgnorePaths = sanitizeOverlapIgnorePaths(clientSettings.overlapIgnorePaths);
|
clientSettings.overlapIgnorePaths = sanitizeOverlapIgnorePaths(clientSettings.overlapIgnorePaths);
|
||||||
}
|
}
|
||||||
@@ -819,6 +823,7 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
|
|||||||
|| errorMessage.includes("must include both provider and modelId")
|
|| errorMessage.includes("must include both provider and modelId")
|
||||||
|| errorMessage.includes("mutually exclusive")
|
|| errorMessage.includes("mutually exclusive")
|
||||||
|| errorMessage.includes("enabledBuiltinWorkflowIds")
|
|| errorMessage.includes("enabledBuiltinWorkflowIds")
|
||||||
|
|| errorMessage.includes("requireTaskRecommendations must be a boolean")
|
||||||
) ? 400 : 500;
|
) ? 400 : 500;
|
||||||
throw new ApiError(status, errorMessage);
|
throw new ApiError(status, errorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,37 @@ describe("executor prompt: completion recommendations", () => {
|
|||||||
expect(prompt()).toContain("at most 3");
|
expect(prompt()).toContain("at most 3");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requires explicit quality-first evaluation when enabled", () => {
|
||||||
|
const text = prompt({ maxRecommendationsPerTask: 3, requireTaskRecommendations: true });
|
||||||
|
expect(text).toContain("MUST explicitly evaluate");
|
||||||
|
expect(text).toContain("Aim toward 3 distinct");
|
||||||
|
expect(text).toContain("shorter list or `recommendations: []` is correct");
|
||||||
|
expect(text).toContain("scope drift");
|
||||||
|
expect(text).not.toContain("optionally evaluate");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps cap zero authoritative over required mode", () => {
|
||||||
|
const text = prompt({ maxRecommendationsPerTask: 0, requireTaskRecommendations: true });
|
||||||
|
expect(text).toContain("Recommendation capture is disabled for this project");
|
||||||
|
expect(text).not.toContain("MUST explicitly evaluate");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends required guidance consistently to custom and withheld-tool prompts", () => {
|
||||||
|
const text = getExecutorSystemPrompt({
|
||||||
|
maxRecommendationsPerTask: 2,
|
||||||
|
requireTaskRecommendations: true,
|
||||||
|
agentPrompts: {
|
||||||
|
templates: [{ id: "custom-executor", name: "Custom", role: "executor", prompt: "Custom executor instructions.", builtIn: false }],
|
||||||
|
roleAssignments: { executor: "custom-executor" },
|
||||||
|
},
|
||||||
|
} as never, { taskCreateWithheld: true, delegateWithheld: true });
|
||||||
|
|
||||||
|
expect(text).toContain("Custom executor instructions.");
|
||||||
|
expect(text).toContain("MUST explicitly evaluate");
|
||||||
|
expect(text).toContain("required completion evaluation");
|
||||||
|
expect(text).not.toContain("Recommendation capture is disabled");
|
||||||
|
});
|
||||||
|
|
||||||
it("tells the executor to send nothing when capture is disabled", () => {
|
it("tells the executor to send nothing when capture is disabled", () => {
|
||||||
const text = prompt({ maxRecommendationsPerTask: 0 });
|
const text = prompt({ maxRecommendationsPerTask: 0 });
|
||||||
expect(text).toContain("Recommendation capture is disabled for this project");
|
expect(text).toContain("Recommendation capture is disabled for this project");
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function completionTask() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProductionTaskDoneTool(maximum: number | undefined = 3, recommendationMailboxNoticeEnabled?: boolean) {
|
function createProductionTaskDoneTool(maximum: number | undefined = 3, recommendationMailboxNoticeEnabled?: boolean, requireTaskRecommendations = false) {
|
||||||
const store = createMockStore();
|
const store = createMockStore();
|
||||||
const task = completionTask();
|
const task = completionTask();
|
||||||
store._setRow(task.id, task);
|
store._setRow(task.id, task);
|
||||||
@@ -44,16 +44,18 @@ function createProductionTaskDoneTool(maximum: number | undefined = 3, recommend
|
|||||||
worktreeInitCommand: undefined,
|
worktreeInitCommand: undefined,
|
||||||
...(maximum === undefined ? {} : { maxRecommendationsPerTask: maximum }),
|
...(maximum === undefined ? {} : { maxRecommendationsPerTask: maximum }),
|
||||||
...(recommendationMailboxNoticeEnabled === undefined ? {} : { recommendationMailboxNoticeEnabled }),
|
...(recommendationMailboxNoticeEnabled === undefined ? {} : { recommendationMailboxNoticeEnabled }),
|
||||||
|
...(requireTaskRecommendations ? { requireTaskRecommendations: true } : {}),
|
||||||
});
|
});
|
||||||
const executor = new TaskExecutor(store as any, "/repo");
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
const onDone = vi.fn();
|
||||||
const tool = (executor as any).createTaskDoneTool(
|
const tool = (executor as any).createTaskDoneTool(
|
||||||
task.id,
|
task.id,
|
||||||
task.worktree,
|
task.worktree,
|
||||||
"# Task\n## Steps\n### Step 0: Implement\n- [x] Complete",
|
"# Task\n## Steps\n### Step 0: Implement\n- [x] Complete",
|
||||||
new Map(),
|
new Map(),
|
||||||
vi.fn(),
|
onDone,
|
||||||
);
|
);
|
||||||
return { store, task, tool };
|
return { store, task, tool, onDone };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("fn_task_done recommendation validation", () => {
|
describe("fn_task_done recommendation validation", () => {
|
||||||
@@ -82,6 +84,57 @@ describe("fn_task_done recommendation validation", () => {
|
|||||||
}], 3)).not.toBeTypeOf("string");
|
}], 3)).not.toBeTypeOf("string");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requires an explicit quality-first evaluation without mutating refused completion", async () => {
|
||||||
|
const { store, task, tool, onDone } = createProductionTaskDoneTool(3, undefined, true);
|
||||||
|
|
||||||
|
const refused = await tool.execute("call-required-omitted", {});
|
||||||
|
|
||||||
|
expect(refused.content[0].text).toContain("requires an explicit recommendations array");
|
||||||
|
expect(refused.content[0].text).toContain("recommendations: []");
|
||||||
|
expect((await store.getTask(task.id)).recommendations).toBeUndefined();
|
||||||
|
expect(store.updateTask).not.toHaveBeenCalled();
|
||||||
|
expect(onDone).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts shorter, at-cap, and empty explicit arrays when required", async () => {
|
||||||
|
const short = createProductionTaskDoneTool(3, undefined, true);
|
||||||
|
await short.tool.execute("call-required-short", { recommendations: [recommendation] });
|
||||||
|
expect((await short.store.getTask(short.task.id)).recommendations).toEqual([recommendation]);
|
||||||
|
|
||||||
|
const empty = createProductionTaskDoneTool(3, undefined, true);
|
||||||
|
await empty.tool.execute("call-required-empty", { recommendations: [] });
|
||||||
|
expect((await empty.store.getTask(empty.task.id)).recommendations).toEqual([]);
|
||||||
|
|
||||||
|
const atCap = createProductionTaskDoneTool(3, undefined, true);
|
||||||
|
await atCap.tool.execute("call-required-cap", {
|
||||||
|
recommendations: [recommendation, { ...recommendation, id: "rec-second" }, { ...recommendation, id: "rec-third" }],
|
||||||
|
});
|
||||||
|
expect((await atCap.store.getTask(atCap.task.id)).recommendations).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps blocked exits outside required recommendation enforcement", async () => {
|
||||||
|
const { store, task, tool } = createProductionTaskDoneTool(3, undefined, true);
|
||||||
|
const blocked = await tool.execute("call-required-blocked", {
|
||||||
|
outcome: "blocked",
|
||||||
|
reason: "Waiting for the upstream API contract.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(blocked.content[0].text).toContain("Task parked as blocked");
|
||||||
|
expect((await store.getTask(task.id)).recommendations).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires an explicit result for no-op completion too", async () => {
|
||||||
|
const { store, task, tool, onDone } = createProductionTaskDoneTool(3, undefined, true);
|
||||||
|
const refused = await tool.execute("call-required-no-op", {
|
||||||
|
summary: "PREMISE STALE: the requested behavior is already present on HEAD",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(refused.content[0].text).toContain("requires an explicit recommendations array");
|
||||||
|
expect((await store.getTask(task.id)).recommendations).toBeUndefined();
|
||||||
|
expect(store.updateTask).not.toHaveBeenCalled();
|
||||||
|
expect(onDone).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("persists and deterministically replaces recommendations through the production completion tool", async () => {
|
it("persists and deterministically replaces recommendations through the production completion tool", async () => {
|
||||||
const { store, task, tool } = createProductionTaskDoneTool();
|
const { store, task, tool } = createProductionTaskDoneTool();
|
||||||
const first = await tool.execute("call-1", { recommendations: [recommendation] });
|
const first = await tool.execute("call-1", { recommendations: [recommendation] });
|
||||||
@@ -155,7 +208,7 @@ describe("fn_task_done recommendation validation", () => {
|
|||||||
it("documents the prompted payload and persists its equivalent through the production tool", async () => {
|
it("documents the prompted payload and persists its equivalent through the production tool", async () => {
|
||||||
const { store, task, tool } = createProductionTaskDoneTool();
|
const { store, task, tool } = createProductionTaskDoneTool();
|
||||||
expect(tool.description).toContain("recommendations: []");
|
expect(tool.description).toContain("recommendations: []");
|
||||||
expect(tool.description).toContain("empty list is accepted for compatibility");
|
expect(tool.description).toContain("a shorter list or [] is valid when relevance does not support more");
|
||||||
expect(tool.parameters.properties.recommendations.description).toContain("unique stable ids");
|
expect(tool.parameters.properties.recommendations.description).toContain("unique stable ids");
|
||||||
expect(tool.parameters.properties.recommendations.description).toContain("populated input is rejected");
|
expect(tool.parameters.properties.recommendations.description).toContain("populated input is rejected");
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function createTaskDoneTool(
|
|||||||
"documentation is updated — call as the final action after finishing all work; automatically marks all " +
|
"documentation is updated — call as the final action after finishing all work; automatically marks all " +
|
||||||
"remaining steps as done. At this accepted final checkpoint, when recommendation capture is enabled, submit up to " +
|
"remaining steps as done. At this accepted final checkpoint, when recommendation capture is enabled, submit up to " +
|
||||||
"the project cap of genuine, task-ready out-of-scope recommendations with stable unique ids, or explicitly send " +
|
"the project cap of genuine, task-ready out-of-scope recommendations with stable unique ids, or explicitly send " +
|
||||||
"recommendations: [] when none qualify; at cap 0, omit recommendations (an empty list is accepted for compatibility). " +
|
"recommendations: [] when none qualify; when required by project policy, an explicit array is mandatory, but a shorter list or [] is valid when relevance does not support more; at cap 0, omit recommendations. " +
|
||||||
"Do not use recommendations for required fixes, blockers, secrets, commands, or reasoning. " +
|
"Do not use recommendations for required fixes, blockers, secrets, commands, or reasoning. " +
|
||||||
"With outcome=\"blocked\": honestly park the task when the work genuinely cannot proceed (upstream API break, " +
|
"With outcome=\"blocked\": honestly park the task when the work genuinely cannot proceed (upstream API break, " +
|
||||||
"missing dependency task, unresolvable external blocker). Blocked is NOT a completion claim — it does not " +
|
"missing dependency task, unresolvable external blocker). Blocked is NOT a completion claim — it does not " +
|
||||||
@@ -117,7 +117,7 @@ export function createTaskDoneTool(
|
|||||||
title: Type.String(),
|
title: Type.String(),
|
||||||
description: Type.String(),
|
description: Type.String(),
|
||||||
category: Type.Union([Type.Literal("improvement"), Type.Literal("feature"), Type.Literal("bug"), Type.Literal("other")]),
|
category: Type.Union([Type.Literal("improvement"), Type.Literal("feature"), Type.Literal("bug"), Type.Literal("other")]),
|
||||||
}), { description: "For accepted completed outcomes when capture is enabled: submit at most the project cap of task-ready out-of-scope suggestions with unique stable ids, or [] when none qualify. At cap 0, omit this field; an empty list is accepted for compatibility but populated input is rejected. Never send for blocked/refused outcomes or include mandatory fixes, secrets, executable commands, or reasoning." })),
|
}), { description: "For accepted completed outcomes when capture is enabled: submit at most the project cap of task-ready out-of-scope suggestions with unique stable ids, or [] when none qualify. When requireTaskRecommendations is enabled and the cap is positive, this field is mandatory, but a shorter list or [] is valid when relevance does not support more. At cap 0, omit this field; populated input is rejected. Never send for blocked/refused outcomes or include mandatory fixes, secrets, executable commands, or reasoning." })),
|
||||||
/*
|
/*
|
||||||
FNXC:Lifecycle 2026-07-16-10:20:
|
FNXC:Lifecycle 2026-07-16-10:20:
|
||||||
FN-8141 laundered a genuinely-impossible task into `done`: fn_task_done only expressed success, the bulk-completion
|
FN-8141 laundered a genuinely-impossible task into `done`: fn_task_done only expressed success, the bulk-completion
|
||||||
@@ -429,9 +429,21 @@ export function createTaskDoneTool(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maximumRecommendations = settings.maxRecommendationsPerTask ?? 3;
|
||||||
|
/*
|
||||||
|
FNXC:TaskRecommendations 2026-08-19-13:05:
|
||||||
|
Required mode is an explicit evaluation contract, not a quota. Refuse only an omitted array at the live accepted-completion boundary; valid empty, short, and at-cap arrays remain quality-first outcomes.
|
||||||
|
*/
|
||||||
|
if (settings.requireTaskRecommendations === true && maximumRecommendations > 0 && params.recommendations === undefined) {
|
||||||
|
const message = `Cannot mark task done yet — project policy requires an explicit recommendations array when maxRecommendationsPerTask is ${maximumRecommendations}. Send relevant recommendations or recommendations: [] and call fn_task_done() again.`;
|
||||||
|
return {
|
||||||
|
content: [{ type: "text" as const, text: message }],
|
||||||
|
details: { error: message },
|
||||||
|
};
|
||||||
|
}
|
||||||
const completionRecommendations = params.recommendations === undefined
|
const completionRecommendations = params.recommendations === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: validateCompletionRecommendations(params.recommendations, settings.maxRecommendationsPerTask ?? 3);
|
: validateCompletionRecommendations(params.recommendations, maximumRecommendations);
|
||||||
if (typeof completionRecommendations === "string") {
|
if (typeof completionRecommendations === "string") {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text" as const, text: `Cannot mark task done yet — ${completionRecommendations}.` }],
|
content: [{ type: "text" as const, text: `Cannot mark task done yet — ${completionRecommendations}.` }],
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ policy rather than malfunction. It is appended last so it wins over the base tex
|
|||||||
applies to a custom operator prompt too (an operator who overrode the prompt still gets a
|
applies to a custom operator prompt too (an operator who overrode the prompt still gets a
|
||||||
truthful statement of what this session may do).
|
truthful statement of what this session may do).
|
||||||
*/
|
*/
|
||||||
function getCompletionRecommendationGuidance(maximum: number): string {
|
function getCompletionRecommendationGuidance(maximum: number, required: boolean): string {
|
||||||
/*
|
/*
|
||||||
FNXC:TaskRecommendations 2026-08-09-04:06:
|
FNXC:TaskRecommendations 2026-08-09-04:06:
|
||||||
Engine-appended guidance preserves the accepted-completion recommendation contract even when an
|
Engine-appended guidance preserves the accepted-completion recommendation contract even when an
|
||||||
@@ -276,10 +276,15 @@ function getCompletionRecommendationGuidance(maximum: number): string {
|
|||||||
return `## Completion recommendations
|
return `## Completion recommendations
|
||||||
|
|
||||||
Recommendation capture is disabled for this project (maxRecommendationsPerTask is 0). Ignore any earlier generic recommendation guidance: do not send recommendations, including \`recommendations: []\`; use an honest summary or task log for non-blocking context, and do not fabricate a finding.`;
|
Recommendation capture is disabled for this project (maxRecommendationsPerTask is 0). Ignore any earlier generic recommendation guidance: do not send recommendations, including \`recommendations: []\`; use an honest summary or task log for non-blocking context, and do not fabricate a finding.`;
|
||||||
|
}
|
||||||
|
if (required) {
|
||||||
|
return `## Completion recommendations
|
||||||
|
|
||||||
|
At the final accepted \`fn_task_done(outcome="completed")\` checkpoint, you MUST explicitly evaluate optional, non-blocking work discovered outside this task by sending a \`recommendations\` array. Aim toward ${maximum} distinct, grounded, task-ready recommendations from concrete source-task or worktree findings, but stop before relevance degrades: a shorter list or \`recommendations: []\` is correct when fewer candidates qualify. Reject scope drift, restatements of this task, duplicates, speculation, filler, required current-task work, blockers, secrets, executable commands, and reasoning. Each item needs a stable unique \`id\`, \`title\`, \`description\`, and \`category\`. Recommendations are only for completed outcomes; never send them with \`outcome="blocked"\`. Use immediate task creation/delegation only for an explicit task requirement, necessary dependency coordination, or operator direction.`;
|
||||||
}
|
}
|
||||||
return `## Completion recommendations
|
return `## Completion recommendations
|
||||||
|
|
||||||
At the final accepted \`fn_task_done(outcome="completed")\` checkpoint, evaluate optional, non-blocking work discovered outside this task. Send at most ${maximum} task-ready recommendations, each with a stable unique \`id\`, \`title\`, \`description\`, and \`category\`, or explicitly send \`recommendations: []\` when none genuinely qualify. Example populated payload: \`recommendations: [{ id: "follow-up-export", title: "Add task export", description: "Provide a CSV export for completed tasks.", category: "feature" }]\`. Do not fabricate filler or include required current-task work, blockers, secrets, executable commands, reasoning, or duplicate ids. Recommendations are only for completed outcomes; never send them with \`outcome="blocked"\`. Use immediate task creation/delegation only for an explicit task requirement, necessary dependency coordination, or operator direction.`;
|
At the final accepted \`fn_task_done(outcome="completed")\` checkpoint, optionally evaluate grounded, non-blocking work discovered outside this task. Send at most ${maximum} task-ready recommendations, each with a stable unique \`id\`, \`title\`, \`description\`, and \`category\`, or explicitly send \`recommendations: []\` when none genuinely qualify. Example populated payload: \`recommendations: [{ id: "follow-up-export", title: "Add task export", description: "Provide CSV export outside the completed task's scope.", category: "feature" }]\`. Stop before relevance degrades: do not fabricate filler or include scope drift, restatements, duplicates, speculation, required current-task work, blockers, secrets, executable commands, or reasoning. Recommendations are only for completed outcomes; never send them with \`outcome="blocked"\`. Use immediate task creation/delegation only for an explicit task requirement, necessary dependency coordination, or operator direction.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -289,14 +294,16 @@ partial restore recovered this function from a pre-FN-8850 base, so a withheld s
|
|||||||
at fn_task_log instead of the completion recommendation route its validator accepts — and when capture is
|
at fn_task_log instead of the completion recommendation route its validator accepts — and when capture is
|
||||||
disabled the prompt must say so rather than invite an unavailable write.
|
disabled the prompt must say so rather than invite an unavailable write.
|
||||||
*/
|
*/
|
||||||
function getWithheldTaskCreationGuidance(taskCreateWithheld: boolean, delegateWithheld: boolean, maximum: number): string {
|
function getWithheldTaskCreationGuidance(taskCreateWithheld: boolean, delegateWithheld: boolean, maximum: number, required: boolean): string {
|
||||||
if (!taskCreateWithheld && !delegateWithheld) return "";
|
if (!taskCreateWithheld && !delegateWithheld) return "";
|
||||||
const withheld = [
|
const withheld = [
|
||||||
...(taskCreateWithheld ? ["`fn_task_create`"] : []),
|
...(taskCreateWithheld ? ["`fn_task_create`"] : []),
|
||||||
...(delegateWithheld ? ["`fn_delegate_task`"] : []),
|
...(delegateWithheld ? ["`fn_delegate_task`"] : []),
|
||||||
].join(" and ");
|
].join(" and ");
|
||||||
const recommendationRoute = maximum > 0
|
const recommendationRoute = maximum > 0
|
||||||
? `For optional, non-blocking discoveries, use the available completion recommendation route at accepted completion (or \`recommendations: []\` if none qualify).`
|
? required
|
||||||
|
? `For non-blocking discoveries, the required completion evaluation must use the available recommendation route at accepted completion; aim toward ${maximum} grounded candidates, but send a shorter list or \`recommendations: []\` when relevance does not support more.`
|
||||||
|
: `For optional, non-blocking discoveries, use the available completion recommendation route at accepted completion (or \`recommendations: []\` if none qualify).`
|
||||||
: "Recommendation capture is disabled, so retain non-blocking context in an honest task log or completion summary without inventing a follow-up.";
|
: "Recommendation capture is disabled, so retain non-blocking context in an honest task log or completion summary without inventing a follow-up.";
|
||||||
return `## Follow-up task creation is disabled for this session
|
return `## Follow-up task creation is disabled for this session
|
||||||
|
|
||||||
@@ -323,14 +330,17 @@ export function getExecutorSystemPrompt(
|
|||||||
the validator it pairs with — the two must be added or removed together.
|
the validator it pairs with — the two must be added or removed together.
|
||||||
*/
|
*/
|
||||||
const maximumRecommendations = settings.maxRecommendationsPerTask ?? 3;
|
const maximumRecommendations = settings.maxRecommendationsPerTask ?? 3;
|
||||||
|
// FNXC:TaskRecommendations 2026-08-19-13:05: Cap zero is the final disabled state; otherwise required mode makes the executor submit an explicit quality-first evaluation at completion.
|
||||||
|
const requireTaskRecommendations = settings.requireTaskRecommendations === true && maximumRecommendations > 0;
|
||||||
const sections = [
|
const sections = [
|
||||||
basePrompt,
|
basePrompt,
|
||||||
isResearchToolSurfaceEnabled(settings) ? getResearchGuidanceForSurface("executor") : "",
|
isResearchToolSurfaceEnabled(settings) ? getResearchGuidanceForSurface("executor") : "",
|
||||||
getCompletionRecommendationGuidance(maximumRecommendations),
|
getCompletionRecommendationGuidance(maximumRecommendations, requireTaskRecommendations),
|
||||||
getWithheldTaskCreationGuidance(
|
getWithheldTaskCreationGuidance(
|
||||||
toolAvailability?.taskCreateWithheld === true,
|
toolAvailability?.taskCreateWithheld === true,
|
||||||
toolAvailability?.delegateWithheld === true,
|
toolAvailability?.delegateWithheld === true,
|
||||||
maximumRecommendations,
|
maximumRecommendations,
|
||||||
|
requireTaskRecommendations,
|
||||||
),
|
),
|
||||||
].filter((section) => section.trim());
|
].filter((section) => section.trim());
|
||||||
return sections.join("\n\n");
|
return sections.join("\n\n");
|
||||||
|
|||||||
@@ -6133,6 +6133,7 @@
|
|||||||
"ephemeralAgentTaskCreationPolicyDeny": "Deny",
|
"ephemeralAgentTaskCreationPolicyDeny": "Deny",
|
||||||
"quickChatCloseOnOutsideClickHint": "When enabled, clicking outside the Quick Chat window closes it. Disable to keep it open until you close it explicitly. Default: enabled.",
|
"quickChatCloseOnOutsideClickHint": "When enabled, clicking outside the Quick Chat window closes it. Disable to keep it open until you close it explicitly. Default: enabled.",
|
||||||
"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).",
|
"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).",
|
||||||
|
"builtinWorkflowAtLeastOneEnabled": "At least one built-in workflow must remain enabled.",
|
||||||
"aiUndoTaskWorkflow": "AI-undo task workflow",
|
"aiUndoTaskWorkflow": "AI-undo task workflow",
|
||||||
"aiUndoTaskWorkflowInherit": "Inherit project default workflow",
|
"aiUndoTaskWorkflowInherit": "Inherit project default workflow",
|
||||||
"aiUndoTaskWorkflowHelp": "Workflow assigned to AI-undo (revert) tasks, which reverse already-shipped code and warrant stricter review. Choose \"Inherit project default workflow\" to leave them on the project default. Default: review-heavy.",
|
"aiUndoTaskWorkflowHelp": "Workflow assigned to AI-undo (revert) tasks, which reverse already-shipped code and warrant stricter review. Choose \"Inherit project default workflow\" to leave them on the project default. Default: review-heavy.",
|
||||||
@@ -6170,6 +6171,8 @@
|
|||||||
"reportTargetByActionHelp": "Optional per-action override of the default report target for bug, feedback, idea, or help. No default — unset actions inherit reportTarget / action defaults.",
|
"reportTargetByActionHelp": "Optional per-action override of the default report target for bug, feedback, idea, or help. No default — unset actions inherit reportTarget / action defaults.",
|
||||||
"maxRecommendationsPerTask": "Maximum recommendations per task",
|
"maxRecommendationsPerTask": "Maximum recommendations per task",
|
||||||
"maxRecommendationsPerTaskHelp": "Default: 3. Set 0 to disable recommendations; choose a whole number from 1 to 20 to cap each completed task.",
|
"maxRecommendationsPerTaskHelp": "Default: 3. Set 0 to disable recommendations; choose a whole number from 1 to 20 to cap each completed task.",
|
||||||
|
"requireTaskRecommendations": "Require automatic task recommendations",
|
||||||
|
"requireTaskRecommendationsHelp": "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.",
|
||||||
"recommendationMailboxNoticeEnabled": "Recommendation mailbox notices",
|
"recommendationMailboxNoticeEnabled": "Recommendation mailbox notices",
|
||||||
"recommendationMailboxNoticeEnabledHelp": "Default: enabled. When a completed task captures recommendations, send a summary to your mailbox. Turning this off does not change whether recommendations are captured."
|
"recommendationMailboxNoticeEnabledHelp": "Default: enabled. When a completed task captures recommendations, send a summary to your mailbox. Turning this off does not change whether recommendations are captured."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6089,6 +6089,7 @@
|
|||||||
"allowEphemeralAgentsToCreateTasksHint": "",
|
"allowEphemeralAgentsToCreateTasksHint": "",
|
||||||
"quickChatCloseOnOutsideClickHint": "",
|
"quickChatCloseOnOutsideClickHint": "",
|
||||||
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"builtinWorkflowAtLeastOneEnabled": "",
|
||||||
"autoTranslateImportedIssues": "",
|
"autoTranslateImportedIssues": "",
|
||||||
"autoTranslateImportedIssuesHelp": "",
|
"autoTranslateImportedIssuesHelp": "",
|
||||||
"translationTargetLanguage": "",
|
"translationTargetLanguage": "",
|
||||||
@@ -6146,6 +6147,8 @@
|
|||||||
"refinementTaskWorkflowHelp": "",
|
"refinementTaskWorkflowHelp": "",
|
||||||
"maxRecommendationsPerTask": "",
|
"maxRecommendationsPerTask": "",
|
||||||
"maxRecommendationsPerTaskHelp": "",
|
"maxRecommendationsPerTaskHelp": "",
|
||||||
|
"requireTaskRecommendations": "",
|
||||||
|
"requireTaskRecommendationsHelp": "",
|
||||||
"recommendationMailboxNoticeEnabled": "",
|
"recommendationMailboxNoticeEnabled": "",
|
||||||
"recommendationMailboxNoticeEnabledHelp": ""
|
"recommendationMailboxNoticeEnabledHelp": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6089,6 +6089,7 @@
|
|||||||
"allowEphemeralAgentsToCreateTasksHint": "",
|
"allowEphemeralAgentsToCreateTasksHint": "",
|
||||||
"quickChatCloseOnOutsideClickHint": "",
|
"quickChatCloseOnOutsideClickHint": "",
|
||||||
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"builtinWorkflowAtLeastOneEnabled": "",
|
||||||
"autoTranslateImportedIssues": "",
|
"autoTranslateImportedIssues": "",
|
||||||
"autoTranslateImportedIssuesHelp": "",
|
"autoTranslateImportedIssuesHelp": "",
|
||||||
"translationTargetLanguage": "",
|
"translationTargetLanguage": "",
|
||||||
@@ -6146,6 +6147,8 @@
|
|||||||
"refinementTaskWorkflowHelp": "",
|
"refinementTaskWorkflowHelp": "",
|
||||||
"maxRecommendationsPerTask": "",
|
"maxRecommendationsPerTask": "",
|
||||||
"maxRecommendationsPerTaskHelp": "",
|
"maxRecommendationsPerTaskHelp": "",
|
||||||
|
"requireTaskRecommendations": "",
|
||||||
|
"requireTaskRecommendationsHelp": "",
|
||||||
"recommendationMailboxNoticeEnabled": "",
|
"recommendationMailboxNoticeEnabled": "",
|
||||||
"recommendationMailboxNoticeEnabledHelp": ""
|
"recommendationMailboxNoticeEnabledHelp": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6088,6 +6088,7 @@
|
|||||||
"gitLabEnabledHint": "",
|
"gitLabEnabledHint": "",
|
||||||
"allowEphemeralAgentsToCreateTasksHint": "",
|
"allowEphemeralAgentsToCreateTasksHint": "",
|
||||||
"quickChatCloseOnOutsideClickHint": "",
|
"quickChatCloseOnOutsideClickHint": "",
|
||||||
|
"builtinWorkflowAtLeastOneEnabled": "",
|
||||||
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
||||||
"autoTranslateImportedIssues": "",
|
"autoTranslateImportedIssues": "",
|
||||||
"autoTranslateImportedIssuesHelp": "",
|
"autoTranslateImportedIssuesHelp": "",
|
||||||
@@ -6146,6 +6147,8 @@
|
|||||||
"refinementTaskWorkflowHelp": "",
|
"refinementTaskWorkflowHelp": "",
|
||||||
"maxRecommendationsPerTask": "",
|
"maxRecommendationsPerTask": "",
|
||||||
"maxRecommendationsPerTaskHelp": "",
|
"maxRecommendationsPerTaskHelp": "",
|
||||||
|
"requireTaskRecommendations": "",
|
||||||
|
"requireTaskRecommendationsHelp": "",
|
||||||
"recommendationMailboxNoticeEnabled": "",
|
"recommendationMailboxNoticeEnabled": "",
|
||||||
"recommendationMailboxNoticeEnabledHelp": ""
|
"recommendationMailboxNoticeEnabledHelp": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6097,6 +6097,7 @@
|
|||||||
"ephemeralAgentTaskCreationPolicyDeny": "Negar",
|
"ephemeralAgentTaskCreationPolicyDeny": "Negar",
|
||||||
"quickChatCloseOnOutsideClickHint": "Quando ativado, clicar fora da janela do Chat Rápido a fecha. Desative para mantê-la aberta até que você a feche explicitamente. Padrão: ativado.",
|
"quickChatCloseOnOutsideClickHint": "Quando ativado, clicar fora da janela do Chat Rápido a fecha. Desative para mantê-la aberta até que você a feche explicitamente. Padrão: ativado.",
|
||||||
"disabledFusionWorkflowsAreHiddenFromWorkflow": "Fluxos de trabalho do Fusion desativados ficam ocultos nos seletores de fluxo de trabalho. Tarefas existentes que já usam um deles continuam sendo processadas normalmente. Padrão: todos os fluxos de trabalho integrados ativados (não definido).",
|
"disabledFusionWorkflowsAreHiddenFromWorkflow": "Fluxos de trabalho do Fusion desativados ficam ocultos nos seletores de fluxo de trabalho. Tarefas existentes que já usam um deles continuam sendo processadas normalmente. Padrão: todos os fluxos de trabalho integrados ativados (não definido).",
|
||||||
|
"builtinWorkflowAtLeastOneEnabled": "",
|
||||||
"aiUndoTaskWorkflow": "Fluxo de trabalho de tarefas de reversão por IA",
|
"aiUndoTaskWorkflow": "Fluxo de trabalho de tarefas de reversão por IA",
|
||||||
"aiUndoTaskWorkflowInherit": "Herdar fluxo de trabalho padrão do projeto",
|
"aiUndoTaskWorkflowInherit": "Herdar fluxo de trabalho padrão do projeto",
|
||||||
"aiUndoTaskWorkflowHelp": "Fluxo de trabalho atribuído a tarefas de reversão por IA (AI-undo), que revertem código já publicado e exigem revisão mais rigorosa. Escolha \"Herdar fluxo de trabalho padrão do projeto\" para mantê-las no padrão do projeto. Padrão: com foco intenso em revisão.",
|
"aiUndoTaskWorkflowHelp": "Fluxo de trabalho atribuído a tarefas de reversão por IA (AI-undo), que revertem código já publicado e exigem revisão mais rigorosa. Escolha \"Herdar fluxo de trabalho padrão do projeto\" para mantê-las no padrão do projeto. Padrão: com foco intenso em revisão.",
|
||||||
@@ -6135,7 +6136,9 @@
|
|||||||
"recommendationMailboxNoticeEnabled": "",
|
"recommendationMailboxNoticeEnabled": "",
|
||||||
"recommendationMailboxNoticeEnabledHelp": "",
|
"recommendationMailboxNoticeEnabledHelp": "",
|
||||||
"maxRecommendationsPerTask": "Máximo de recomendações por tarefa",
|
"maxRecommendationsPerTask": "Máximo de recomendações por tarefa",
|
||||||
"maxRecommendationsPerTaskHelp": "Padrão: 3. Defina 0 para desativar recomendações; escolha um número inteiro de 1 a 20 para limitar cada tarefa concluída."
|
"maxRecommendationsPerTaskHelp": "Padrão: 3. Defina 0 para desativar recomendações; escolha um número inteiro de 1 a 20 para limitar cada tarefa concluída.",
|
||||||
|
"requireTaskRecommendations": "",
|
||||||
|
"requireTaskRecommendationsHelp": ""
|
||||||
},
|
},
|
||||||
"mobileNav": {
|
"mobileNav": {
|
||||||
"label": "Seção de configurações"
|
"label": "Seção de configurações"
|
||||||
|
|||||||
@@ -6089,6 +6089,7 @@
|
|||||||
"allowEphemeralAgentsToCreateTasksHint": "",
|
"allowEphemeralAgentsToCreateTasksHint": "",
|
||||||
"quickChatCloseOnOutsideClickHint": "",
|
"quickChatCloseOnOutsideClickHint": "",
|
||||||
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"builtinWorkflowAtLeastOneEnabled": "",
|
||||||
"autoTranslateImportedIssues": "",
|
"autoTranslateImportedIssues": "",
|
||||||
"autoTranslateImportedIssuesHelp": "",
|
"autoTranslateImportedIssuesHelp": "",
|
||||||
"translationTargetLanguage": "",
|
"translationTargetLanguage": "",
|
||||||
@@ -6146,6 +6147,8 @@
|
|||||||
"refinementTaskWorkflowHelp": "",
|
"refinementTaskWorkflowHelp": "",
|
||||||
"maxRecommendationsPerTask": "",
|
"maxRecommendationsPerTask": "",
|
||||||
"maxRecommendationsPerTaskHelp": "",
|
"maxRecommendationsPerTaskHelp": "",
|
||||||
|
"requireTaskRecommendations": "",
|
||||||
|
"requireTaskRecommendationsHelp": "",
|
||||||
"recommendationMailboxNoticeEnabled": "",
|
"recommendationMailboxNoticeEnabled": "",
|
||||||
"recommendationMailboxNoticeEnabledHelp": ""
|
"recommendationMailboxNoticeEnabledHelp": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6089,6 +6089,7 @@
|
|||||||
"allowEphemeralAgentsToCreateTasksHint": "",
|
"allowEphemeralAgentsToCreateTasksHint": "",
|
||||||
"quickChatCloseOnOutsideClickHint": "",
|
"quickChatCloseOnOutsideClickHint": "",
|
||||||
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
"disabledFusionWorkflowsAreHiddenFromWorkflow": "",
|
||||||
|
"builtinWorkflowAtLeastOneEnabled": "",
|
||||||
"autoTranslateImportedIssues": "",
|
"autoTranslateImportedIssues": "",
|
||||||
"autoTranslateImportedIssuesHelp": "",
|
"autoTranslateImportedIssuesHelp": "",
|
||||||
"translationTargetLanguage": "",
|
"translationTargetLanguage": "",
|
||||||
@@ -6146,6 +6147,8 @@
|
|||||||
"refinementTaskWorkflowHelp": "",
|
"refinementTaskWorkflowHelp": "",
|
||||||
"maxRecommendationsPerTask": "",
|
"maxRecommendationsPerTask": "",
|
||||||
"maxRecommendationsPerTaskHelp": "",
|
"maxRecommendationsPerTaskHelp": "",
|
||||||
|
"requireTaskRecommendations": "",
|
||||||
|
"requireTaskRecommendationsHelp": "",
|
||||||
"recommendationMailboxNoticeEnabled": "",
|
"recommendationMailboxNoticeEnabled": "",
|
||||||
"recommendationMailboxNoticeEnabledHelp": ""
|
"recommendationMailboxNoticeEnabledHelp": ""
|
||||||
},
|
},
|
||||||
|
|||||||
3
packages/i18n/src/resources.d.ts
vendored
3
packages/i18n/src/resources.d.ts
vendored
@@ -6201,6 +6201,7 @@ export default interface Resources {
|
|||||||
"deleteInboxOutboxMessagesOlderThanThisMany": "Delete inbox/outbox messages older than this many days. Default: Off. 7 days is the suggested setting.",
|
"deleteInboxOutboxMessagesOlderThanThisMany": "Delete inbox/outbox messages older than this many days. Default: Off. 7 days is the suggested setting.",
|
||||||
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "Disabled built-in workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve.",
|
"disabledBuiltInWorkflowsAreHiddenFromWorkflow": "Disabled built-in workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve.",
|
||||||
"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).",
|
"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).",
|
||||||
|
"builtinWorkflowAtLeastOneEnabled": "At least one built-in workflow must remain enabled.",
|
||||||
"enableSummarizationInProjectModelsToConfigureThatModel": " Enable summarization in Project Models to configure that model.",
|
"enableSummarizationInProjectModelsToConfigureThatModel": " Enable summarization in Project Models to configure that model.",
|
||||||
"ephemeralAgentTaskCreationPolicy": "Ephemeral agent follow-up tasks",
|
"ephemeralAgentTaskCreationPolicy": "Ephemeral agent follow-up tasks",
|
||||||
"ephemeralAgentTaskCreationPolicyAllow": "Allow",
|
"ephemeralAgentTaskCreationPolicyAllow": "Allow",
|
||||||
@@ -6217,6 +6218,8 @@ export default interface Resources {
|
|||||||
"loweringThisWindowMeansReliabilityMetricsChartsAnd": " Lowering this window means Reliability metrics/charts and the Activity feed will not show history older than the selected range. Per-task task detail history is unaffected. Default: 30 days. ",
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": " Lowering this window means Reliability metrics/charts and the Activity feed will not show history older than the selected range. Per-task task detail history is unaffected. Default: 30 days. ",
|
||||||
"maxRecommendationsPerTask": "Maximum recommendations per task",
|
"maxRecommendationsPerTask": "Maximum recommendations per task",
|
||||||
"maxRecommendationsPerTaskHelp": "Default: 3. Set 0 to disable recommendations; choose a whole number from 1 to 20 to cap each completed task.",
|
"maxRecommendationsPerTaskHelp": "Default: 3. Set 0 to disable recommendations; choose a whole number from 1 to 20 to cap each completed task.",
|
||||||
|
"requireTaskRecommendations": "Require automatic task recommendations",
|
||||||
|
"requireTaskRecommendationsHelp": "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.",
|
||||||
"mobileNavPrimaryItems": "Mobile footer quick actions",
|
"mobileNavPrimaryItems": "Mobile footer quick actions",
|
||||||
"mobileNavPrimaryItemsHint": "Default: Dashboard, Tasks, Agents, Missions, Chat, Mailbox. Add eligible destinations; unselected destinations remain in More.",
|
"mobileNavPrimaryItemsHint": "Default: Dashboard, Tasks, Agents, Missions, Chat, Mailbox. Add eligible destinations; unselected destinations remain in More.",
|
||||||
"moveNavItemEarlier": "Move {{item}} earlier",
|
"moveNavItemEarlier": "Move {{item}} earlier",
|
||||||
|
|||||||
Reference in New Issue
Block a user