FN-036: make task title summarization project-controlled

Make generated task titles project-controlled while preserving useful summaries at any length.

- Add project-level title summarization settings and UI controls.
- Make summarization length-independent across task creation, planning, GitHub, and agent flows.
- Update localization, documentation, and regression coverage.

Files changed:
 .../fn-036-systematic-title-summarization.md       |   7 +
 docs/settings-reference.md                         |   2 +-
 docs/storage.md                                    |   2 +-
 docs/task-management.md                            |   4 +-
 packages/core/src/__tests__/ai-summarize.test.ts   |  34 ++--
 .../__tests__/store-create-intake-column.test.ts   | 186 +++++++++++++++++++++
 packages/core/src/ai/ai-summarize.ts               |  32 ++--
 packages/core/src/task-store/task-creation.ts      |  83 +++++++--
 packages/core/src/types/settings/settings-scope.ts |   9 +-
 packages/core/src/types/task/task-core.ts          |   2 +-
 .../dashboard/app/__tests__/api-projects.test.ts   |   4 +-
 .../app/__tests__/settings-sections.test.tsx       |   2 +-
 .../dashboard/app/api/planning/ai-summarize.ts     |   2 +-
 .../__tests__/SettingsModal.search.test.ts         |   5 +-
 .../search/__tests__/settings-search-index.test.ts |   2 +-
 .../sections/ProjectModelsSection.search.ts        |   4 +-
 .../settings/sections/ProjectModelsSection.tsx     |  11 +-
 .../src/__tests__/github-tracking.test.ts          |  12 +-
 .../src/__tests__/routes-planning.test.ts          |  28 +++-
 .../dashboard/src/__tests__/routes-tasks.test.ts   |  19 +++
 packages/dashboard/src/github-tracking.ts          |   9 +-
 .../routes/register-ai-text-assistant-routes.ts    |   7 +-
 .../src/__tests__/agent-tools-delegation.test.ts   |   1 +
 .../src/__tests__/heartbeat-executor.test.ts       |   1 -
 packages/engine/src/__tests__/triage.test.ts       |   7 +-
 packages/engine/src/agent-tools.ts                 |   7 +-
 packages/engine/src/triage.ts                      |  12 +-
 packages/i18n/locales/en/app.json                  |   6 +-
 packages/i18n/locales/pt-BR/app.json               |   8 +-
 packages/i18n/src/resources.d.ts                   |   6 +-
 30 files changed, 418 insertions(+), 96 deletions(-)

Fusion-Task-Id: FN-036

Fusion-Task-Lineage: df603386-e63e-4be8-a8e5-bae9aa1d4a80

Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
Fusion Agent
2026-08-19 14:54:46 +00:00
parent 06649a0288
commit a81c9b80d9
30 changed files with 418 additions and 96 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Make automatic task-title summarization project-controlled for every non-empty description.
category: fix
dev: Reuses the project `autoSummarizeTitles` setting while preserving explicit titles and manual `summarize:true` requests.

View File

@@ -773,7 +773,7 @@ Database backups work with both external PostgreSQL and Fusion's default embedde
| `memoryBackupRetention` | `number` | `14` | Number of memory backups to retain. |
| `memoryBackupDir` | `string` | `".fusion/backups/memory"` | Relative memory backup directory path. |
| `memoryBackupScope` | `"project" \| "agents" \| "all"` | `"all"` | Backup scope: project memory, agent memory, or both. |
| `autoSummarizeTitles` | `boolean` | `false` | Auto-generate AI titles only for untitled descriptions longer than 200 characters across dashboard/API task creation. Generated titles match the operator's input language from the task description. Shorter untitled descriptions use a setting-independent deterministic planning title, which normal prompt-heading writeback persists. Agent-created tasks from `fn_task_create` and `fn_delegate_task` always request AI summarization for long untitled tasks, regardless of this setting. |
| `autoSummarizeTitles` | `boolean` | `false` | Project-scoped automatic policy for every non-empty untitled task description across dashboard/API, direct store, agent, scheduled, signal, and CLI-backed creates. Enabled creates attempt an AI title regardless of description length; disabled creates use the deterministic planner fallback. Explicit titles are preserved, `summarize:true` and Task Detail/manual requests remain explicit force paths, and the setting is snapshotted per create rather than applied retroactively. |
| `taskDefinitionInInputLanguage` | `boolean` | `false` | When enabled, generated task-definition (`PROMPT.md`) prose uses a confidently detected supported input language: Spanish (`es`), French (`fr`), Korean (`ko`), or Chinese (`zh-CN`). Only planner-authored prose is localized; headings, markers, the verbatim Original Description, code, paths, tool names, and commit conventions stay canonical English for deterministic parsing. Chinese always authors as `zh-CN`; Traditional Chinese is not variant-detected. English, short/uncertain, and unsupported input such as Japanese fall back to English. Configure in **Settings → Project Models**. |
| `useAiMergeCommitSummary` | `boolean` | `true` | Use AI-generated merge commit summaries (subject + bullet body + diff-stat) instead of raw step-commit subject lists. |
| `titleSummarizerProvider` | `string` | `undefined` | Provider for title summarization. |

View File

@@ -447,7 +447,7 @@ API endpoints reviewed:
| `autoBackupSchedule` | Project | `GET/PUT /api/settings` | Backup cron schedule |
| `autoBackupRetention` | Project | `GET/PUT /api/settings` | Backup retention count |
| `autoBackupDir` | Project | `GET/PUT /api/settings` | Backup directory |
| `autoSummarizeTitles` | Project | `GET/PUT /api/settings` | Auto-title generation |
| `autoSummarizeTitles` | Project | `GET/PUT /api/settings` | All-length automatic AI title policy for non-empty untitled task descriptions; default `false`. Explicit titles and manual/force requests are unaffected, and the value is snapshotted per create. |
| `titleSummarizerProvider` | Project | `GET/PUT /api/settings` | Title model provider |
| `titleSummarizerModelId` | Project | `GET/PUT /api/settings` | Title model id |
| `titleSummarizerFallbackProvider` | Project | `GET/PUT /api/settings` | Title fallback provider |

View File

@@ -929,9 +929,9 @@ Users can apply presets at task creation; manual model selection can override th
## AI Title Summarization
Titleless descriptions of 200 characters or fewer do not use AI summarization. During triage, Fusion supplies a setting-independent deterministic title derived from the first meaningful description line (with the shared markdown normalization and 60-character safety cap). The planner's normal `# Task: ID - title` heading is then parsed and written back to the project-scoped task metadata, so board, CLI, and API consumers see the same durable title.
`autoSummarizeTitles` is a project-scoped boolean (default `false`) that controls automatic title attempts for every non-empty task description created without a title. When enabled, dashboard/API, direct store, agent, scheduled, signal, and CLI-backed creates use the configured title model regardless of description length. When disabled or unavailable, triage supplies a deterministic title derived from the first meaningful description line (with shared Markdown normalization and a 60-character safety cap), and the planner's normal `# Task: ID - title` heading is written back to project-scoped metadata.
When `autoSummarizeTitles` is enabled and a task has a long untitled description, Fusion can auto-generate a concise AI title. This applies to tasks created from the dashboard/API as well as tasks created by agents and tooling flows (`fn_task_create`, delegated tasks, and triage-created child tasks). GitHub tracking now waits for the `createTask`-level summarizer (explicit or auto-attached from settings) to settle before filing, then uses that resulting title and falls back to deterministic description-derived title generation only when summarization is unavailable.
Explicit titles always win. `summarize:true`, Task Detail **Summarize**, and the explicit summarization endpoint remain available even when automatic mode is disabled. The setting is read as a snapshot for each create: changing it does not rename existing tasks or cancel an already-started attempt. GitHub tracking uses its configured summarizer for any non-empty titleless task and falls back to deterministic description-derived title generation when summarization is unavailable.
If a configured title summarizer model is stale after a pi upgrade, Fusion logs a warning naming that provider/model and retries once with automatic model resolution before falling back to deterministic title generation. Genuine AI-service failures are not masked by this retry.

View File

@@ -55,7 +55,7 @@ describe("ai-summarize", () => {
});
it("should have correct length limits", () => {
expect(MIN_DESCRIPTION_LENGTH).toBe(201);
expect(MIN_DESCRIPTION_LENGTH).toBe(1);
expect(MAX_DESCRIPTION_LENGTH).toBe(2000);
expect(MAX_TITLE_SUMMARIZE_INPUT_LENGTH).toBe(4000);
expect(MAX_TITLE_LENGTH).toBe(60);
@@ -69,8 +69,7 @@ describe("ai-summarize", () => {
// ── Validation ─────────────────────────────────────────────────────────────
describe("validateDescription", () => {
it("should accept valid description length", () => {
const desc = "a".repeat(201);
it.each(["a", "a".repeat(200), "a".repeat(201), "a".repeat(4001)])("accepts any non-empty description (%s)", (desc) => {
expect(validateDescription(desc)).toBe(desc);
});
@@ -88,15 +87,9 @@ describe("ai-summarize", () => {
expect(() => validateDescription(123)).toThrow("description must be a string");
});
it("should throw for description too short", () => {
const desc = "a".repeat(100);
it.each(["", " ", "\n\t"]) ("should reject empty or whitespace-only descriptions", (desc) => {
expect(() => validateDescription(desc)).toThrow(ValidationError);
expect(() => validateDescription(desc)).toThrow("at least 201 characters");
});
it("should accept description at minimum boundary", () => {
const desc = "a".repeat(201);
expect(validateDescription(desc)).toBe(desc);
expect(() => validateDescription(desc)).toThrow("description must not be empty");
});
it("should accept description at historical maximum boundary", () => {
@@ -166,9 +159,22 @@ describe("ai-summarize", () => {
// ── summarizeTitle ─────────────────────────────────────────────────────────
describe("summarizeTitle", () => {
it("should return null for descriptions <= 200 characters", async () => {
const result = await summarizeTitle("Short description", "/tmp");
expect(result).toBeNull();
it.each([1, 200, 201, 4001])("accepts a non-empty description of length %i", async (length) => {
const prompt = vi.fn().mockResolvedValue(undefined);
getFnAgentMock.mockResolvedValue(() => Promise.resolve({
session: {
prompt,
dispose: vi.fn(),
state: { messages: [{ role: "assistant", content: "Generated task title" }] },
},
}));
await expect(summarizeTitle("a".repeat(length), "/tmp")).resolves.toBe("Generated task title");
expect(getFnAgentMock).toHaveBeenCalledTimes(1);
});
it.each(["", " ", "\n\t"]) ("rejects invalid %j before creating an agent", async (description) => {
await expect(summarizeTitle(description, "/tmp")).rejects.toThrow(ValidationError);
expect(getFnAgentMock).not.toHaveBeenCalled();
});

View File

@@ -2,6 +2,7 @@ import { it, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import type { Task } from "../types.js";
import { setTaskCreatedHook } from "../tasks/task-creation-hooks.js";
import {
pgDescribe,
createSharedPgTaskStoreTestHarness,
@@ -11,6 +12,23 @@ import { buildBootstrapPrompt } from "../mesh/mesh-task-replication.js";
const pgTest = pgDescribe;
/*
FNXC:TitleSummarization 2026-08-19-14:10:
The PostgreSQL create path owns the automatic title policy and deferred lifecycle fence. Keep these
helpers on the real TaskStore so enabled/disabled settings, late writes, duplicate claims, and hook
ordering cannot be proven only through gateway mocks.
*/
function observeTaskCreatedHook(): Promise<Task> {
return new Promise((resolve) => {
setTaskCreatedHook((task) => resolve(task));
});
}
async function settleTaskCreatedHook(hook: Promise<Task>): Promise<Task> {
// The hook is invoked after the deferred title write, so it fences the persisted result.
return hook;
}
/*
FNXC:CodingIdeasWorkflow 2026-07-04-11:30:
Pin the createTask intake-column wiring: a task created against the Coding (Ideas) workflow (manual autoTriage:false intake) must land in the "ideas" column, not the legacy "triage" default, while the default Coding workflow keeps landing cards in "triage".
@@ -26,9 +44,177 @@ pgTest("createTask intake-column wiring (Coding (Ideas))", () => {
await h.beforeEach();
});
afterEach(async () => {
setTaskCreatedHook(undefined);
await h.afterEach();
});
/*
FNXC:TitleSummarization 2026-08-19-14:10:
Automatic title generation is project-controlled but independent of description length. These
cases use a controlled callback at the PostgreSQL TaskStore boundary, proving the setting snapshot
and late lifecycle behavior without making a real model call or adding timing sleeps.
*/
it.each([1, 200, 201, 4001])("summarizes a titleless description of length %i when enabled", async (length) => {
const store = h.store();
await store.updateSettings({ autoSummarizeTitles: true });
let summarizeCalls = 0;
const hook = observeTaskCreatedHook();
const created = await store.createTask(
{ description: "x".repeat(length) },
{
onSummarize: async () => {
summarizeCalls += 1;
return `Generated title ${length}`;
},
},
);
await settleTaskCreatedHook(hook);
const persisted = await store.getTask(created.id);
expect(summarizeCalls).toBe(1);
expect(persisted?.title).toBe(`Generated title ${length}`);
});
it.each([1, 200, 201, 4001])("does not summarize a titleless description of length %i when disabled", async (length) => {
const store = h.store();
await store.updateSettings({ autoSummarizeTitles: false });
let summarizeCalls = 0;
const hook = observeTaskCreatedHook();
const created = await store.createTask(
{ description: "x".repeat(length) },
{
onSummarize: async () => {
summarizeCalls += 1;
return "Should not be used";
},
},
);
await settleTaskCreatedHook(hook);
const persisted = await store.getTask(created.id);
expect(summarizeCalls).toBe(0);
expect(persisted?.title).toBeUndefined();
});
it("preserves an explicit title and allows summarize:true to force generation", async () => {
const store = h.store();
await store.updateSettings({ autoSummarizeTitles: false });
let summarizeCalls = 0;
const hook = observeTaskCreatedHook();
const explicit = await store.createTask(
{ title: "Operator title", description: "short description" },
{ onSummarize: async () => { summarizeCalls += 1; return "Unexpected title"; } },
);
await settleTaskCreatedHook(hook);
expect(summarizeCalls).toBe(0);
expect((await store.getTask(explicit.id))?.title).toBe("Operator title");
const forcedHook = observeTaskCreatedHook();
const forced = await store.createTask(
{ description: "short forced description", summarize: true },
{ onSummarize: async () => { summarizeCalls += 1; return "Forced title"; } },
);
await settleTaskCreatedHook(forcedHook);
expect(summarizeCalls).toBe(1);
expect((await store.getTask(forced.id))?.title).toBe("Forced title");
});
it("does not invoke a second summarizer for a proposal-claim replay", async () => {
const store = h.store();
await store.updateSettings({ autoSummarizeTitles: true });
let summarizeCalls = 0;
const hook = observeTaskCreatedHook();
const first = await store.createTask(
{ description: "replayed description", proposalClaimId: "title-summary-replay" },
{ onSummarize: async () => { summarizeCalls += 1; return "Replay-safe title"; } },
);
const replay = await store.createTask(
{ description: "replayed description", proposalClaimId: "title-summary-replay" },
{ onSummarize: async () => { summarizeCalls += 1; return "Wrong second title"; } },
);
await settleTaskCreatedHook(hook);
expect(replay.id).toBe(first.id);
expect(summarizeCalls).toBe(1);
expect((await store.getTask(first.id))?.title).toBe("Replay-safe title");
});
it("does not overwrite a title supplied while summarization is pending", async () => {
const store = h.store();
await store.updateSettings({ autoSummarizeTitles: true });
let releaseSummary!: (title: string | null) => void;
let markStarted!: () => void;
const summaryStarted = new Promise<void>((resolve) => { markStarted = resolve; });
const summary = new Promise<string | null>((resolve) => { releaseSummary = resolve; });
const hook = observeTaskCreatedHook();
const created = await store.createTask(
{ description: "concurrent title description" },
{
onSummarize: async () => {
markStarted();
return summary;
},
},
);
await summaryStarted;
await store.updateTask(created.id, { title: "Concurrent operator title" });
releaseSummary("Late AI title");
await settleTaskCreatedHook(hook);
expect((await store.getTask(created.id))?.title).toBe("Concurrent operator title");
});
it("does not overwrite a title written after the deferred preflight read", async () => {
const store = h.store();
await store.updateSettings({ autoSummarizeTitles: true });
const hook = observeTaskCreatedHook();
let created!: Task;
const originalGetTask = store.getTask.bind(store);
let injected = false;
store.getTask = async (id: string) => {
const current = await originalGetTask(id);
if (!injected && id === created.id && !current?.title) {
injected = true;
await store.updateTask(id, { title: "Concurrent operator title" });
}
return current;
};
created = await store.createTask(
{ description: "preflight race description" },
{ onSummarize: async () => "Late AI title" },
);
await settleTaskCreatedHook(hook);
expect(injected).toBe(true);
expect((await originalGetTask(created.id))?.title).toBe("Concurrent operator title");
});
it.each([
{ label: "null", result: null },
{ label: "throw", result: "throw" },
])("settles the task-created hook after a $label summary result", async ({ result }) => {
const store = h.store();
await store.updateSettings({ autoSummarizeTitles: true });
let summarizeCalls = 0;
const hook = observeTaskCreatedHook();
const created = await store.createTask(
{ description: `summary ${result}` },
{
onSummarize: async () => {
summarizeCalls += 1;
if (result === "throw") throw new Error("controlled summarizer failure");
return null;
},
},
);
await settleTaskCreatedHook(hook);
expect(summarizeCalls).toBe(1);
expect((await store.getTask(created.id))?.title).toBeUndefined();
});
/*
FNXC:MergedPlanningColumn 2026-07-29-14:55 (U11 post-merge audit):
This guarded "a default-workflow create lands in triage" and was byte-identical for as long as

View File

@@ -2,13 +2,14 @@
* AI Title Summarization Service
*
* Provides AI-powered title generation from task descriptions.
* Automatically generates concise titles (≤60 characters) from descriptions
* longer than 200 characters.
* Automatically generates concise titles (≤60 characters) from any non-empty
* task description when the caller's project policy enables the attempt.
*
* Features:
* - Rate limiting per IP (10 requests per hour)
* - Dynamic import of @fusion/engine for AI agent creation
* - Text length validation (minimum 201 characters; model input is truncated)
* - Text validation rejects only missing, non-string, empty, or whitespace-only input
* - Model input is truncated to a bounded prompt size
*/
import { getFnAgent, type AgentMessage } from "./ai-engine-loader.js";
@@ -51,8 +52,8 @@ export const MAX_DESCRIPTION_LENGTH = 2000;
*/
export const MAX_TITLE_SUMMARIZE_INPUT_LENGTH = 4000;
/** Minimum description length for summarization in characters */
export const MIN_DESCRIPTION_LENGTH = 201;
/** Compatibility name for the non-empty description minimum. */
export const MIN_DESCRIPTION_LENGTH = 1;
/** Maximum title length in characters */
export const MAX_TITLE_LENGTH = 60;
@@ -197,12 +198,13 @@ export function validateDescription(description: unknown): string {
throw new ValidationError("description must be a string");
}
// Validate description length floor. There is intentionally no upper bound:
// runTitleSummarizer truncates model input before prompting.
if (description.length < MIN_DESCRIPTION_LENGTH) {
throw new ValidationError(
`description must be at least ${MIN_DESCRIPTION_LENGTH} characters for summarization`
);
/*
FNXC:TitleSummarization 2026-08-19-13:43:
Title generation is intentionally length-independent. Reject only empty content here;
the model prompt remains bounded by MAX_TITLE_SUMMARIZE_INPUT_LENGTH below.
*/
if (description.trim().length === 0) {
throw new ValidationError("description must not be empty");
}
return description;
@@ -347,7 +349,7 @@ async function runTitleSummarizer(
/**
* Summarize a task description into a concise title using AI.
* @param description - The task description to summarize (must be >200 chars; model input is truncated)
* @param description - The non-empty task description to summarize; model input is truncated
* @param rootDir - Project root directory for AI agent context
* @param provider - Optional AI model provider (e.g., "anthropic")
* @param modelId - Optional AI model ID (e.g., "claude-sonnet-4-5")
@@ -359,10 +361,8 @@ export async function summarizeTitle(
provider?: string,
modelId?: string
): Promise<string | null> {
// Validate description length first
if (description.length <= 200) {
return null; // Too short for summarization
}
// Validate before creating an agent so invalid input never incurs model work.
validateDescription(description);
const createFnAgent = await getFnAgent();
if (!createFnAgent) {

View File

@@ -8,7 +8,8 @@
*/
import {TaskStore, storeLog} from "../store.js";
import {InvalidFileScopeError, SelfDefeatingDependencyError, detectSelfDefeatingDependency, TombstonedTaskResurrectionError} from "./errors.js";
import {mkdir, rename, rm, writeFile} from "node:fs/promises";
import {and, eq, isNull, sql} from "drizzle-orm";
import {mkdir, readFile, rename, rm, writeFile} from "node:fs/promises";
import {join} from "node:path";
import {existsSync} from "node:fs";
import {randomUUID} from "node:crypto";
@@ -30,12 +31,13 @@ import type {WorkflowIr} from "../workflows/workflow-ir-types.js";
import {DEFAULT_WORKFLOW_ID, getBuiltinWorkflow, isBuiltinWorkflowId} from "../workflows/builtin-workflows.js";
import {columnsWithFlag} from "../workflows/workflow-lifecycle-traits.js";
import {validateFileScopeInPromptContent} from "../task-store/file-scope.js";
import {__setTaskActivityLogLimitsForTesting} from "../task-store/comments.js";
import {__setTaskActivityLogLimitsForTesting, rewriteHeadingLine} from "../task-store/comments.js";
import {withTaskBranchContextInSourceMetadata} from "../task-store/branch-context.js";
import {resolveCreateDeclaredSymbols} from "../tasks/task-symbol-resolution.js";
import {softDeleteTaskRow as softDeleteTaskRowAsync, insertTaskRowInTransaction, isTaskIdConflictError} from "../task-store/async/async-persistence.js";
import {recordRunAuditEvent as recordRunAuditEventAsync} from "../task-store/async/async-audit.js";
import {recordRunAuditEventWithinTransaction} from "../postgres/data-layer.js";
import {recordRunAuditEventWithinTransaction, taskProjectScope} from "../postgres/data-layer.js";
import * as schema from "../postgres/schema/index.js";
import type {DbTransaction} from "../postgres/data-layer.js";
import { resolveTaskPrefix } from "./task-prefix.js";
import {assertValidProviderInstanceId} from "../provider-instance.js";
@@ -137,6 +139,64 @@ async function resolveDefaultWorkflowIntakeColumn(store: TaskStore): Promise<str
}
}
/*
FNXC:TitleSummarization 2026-08-19-14:29:
The deferred writer uses a fast preflight read for the common deleted/already-titled case, but the
PostgreSQL UPDATE predicate is the authority. A title supplied by another process after that read
must make the generated write a no-op rather than being overwritten by a later full-row update.
*/
async function persistDeferredTaskTitleIfUntitled(
store: TaskStore,
taskId: string,
title: string,
): Promise<boolean> {
const observed = await store.getTask(taskId).catch(() => undefined);
if (!observed || observed.title?.trim()) return false;
const layer = store.asyncLayer!;
const updated = await layer.transactionImmediate(async (tx) => {
const scope = taskProjectScope(layer);
const conditions = [
eq(schema.project.tasks.id, taskId),
isNull(schema.project.tasks.deletedAt),
sql`btrim(coalesce(${schema.project.tasks.title}, '')) = ''`,
...(scope ? [scope] : []),
];
const [row] = await tx
.update(schema.project.tasks)
.set({ title, updatedAt: new Date().toISOString() })
.where(and(...conditions))
.returning();
return row ? store.rowToTask(store.pgRowToTaskRow(row)) : undefined;
});
if (!updated) return false;
await store.writeTaskJsonFile(store.taskDir(taskId), updated);
if (store.isWatching) store.taskCache.set(taskId, { ...updated });
// FNXC:TitleSummarization 2026-08-19-14:29: Keep the operator-visible prompt heading aligned without a second title mutation.
try {
const promptPath = join(store.taskDir(taskId), "PROMPT.md");
if (existsSync(promptPath)) {
const existingPrompt = await readFile(promptPath, "utf-8");
const triageStyle = /^#\s+Task:\s+[A-Z]+-\d+\s+-\s+/m.test(existingPrompt);
const heading = triageStyle
? `Task: ${updated.id} - ${updated.title}`
: `${updated.id}: ${updated.title}`;
const nextPrompt = rewriteHeadingLine(existingPrompt, heading);
if (nextPrompt !== existingPrompt) await writeFile(promptPath, nextPrompt);
}
} catch (err) {
storeLog.warn(`[title-summarization] failed to sync PROMPT.md heading for ${taskId}`, {
taskId,
error: err instanceof Error ? err.message : String(err),
});
}
store.emitTaskLifecycleEventSafely("task:updated", [updated]);
return true;
}
export async function createTaskBackendImpl(store: TaskStore, input: TaskCreateInput, options?: {
onSummarize?: (description: string) => Promise<string | null>; settings?: { autoSummarizeTitles?: boolean }; invokeTaskCreatedHook?: boolean; onProposalClaimConflict?: (task: Task) => void; ownershipExemption?: IntakeOwnershipExemption; },): Promise<Task> {
/* FNXC:CredentialInstanceSelection 2026-08-01-05:43: validate task authoring input before persistence; ids are stored but runtime credential resolution remains unchanged. */
@@ -218,9 +278,15 @@ export async function createTaskBackendImpl(store: TaskStore, input: TaskCreateI
}
const title = input.title?.trim() || undefined;
/*
FNXC:TitleSummarization 2026-08-19-13:43:
The project-scoped autoSummarizeTitles snapshot is the sole automatic eligibility policy.
Do not reintroduce a description-length branch: every non-empty untitled create follows the
same setting, while summarize:true remains an explicit per-create force request.
*/
const shouldSummarize =
!title &&
input.description.length > 200 &&
input.description.trim().length > 0 &&
(input.summarize === true || resolvedSettings?.autoSummarizeTitles === true);
const hasPendingSummarization = shouldSummarize && typeof onSummarize === "function";
const shouldInvokeTaskCreatedHook = options?.invokeTaskCreatedHook !== false;
@@ -424,12 +490,9 @@ export async function createTaskBackendImpl(store: TaskStore, input: TaskCreateI
if (sanitizedTitle) {
await store.trackDeferredTaskCreatedWork(async () => {
if (store.closing) return;
const currentTask = await store.getTask(id);
if (currentTask && !currentTask.title) {
const normalizedTitle = normalizeTitleForTaskId(sanitizedTitle, id);
if (normalizedTitle.title && !store.closing) {
await store.updateTask(id, { title: normalizedTitle.title });
}
const normalizedTitle = normalizeTitleForTaskId(sanitizedTitle, id);
if (normalizedTitle.title && !store.closing) {
await persistDeferredTaskTitleIfUntitled(store, id, normalizedTitle.title);
}
});
}

View File

@@ -2125,9 +2125,12 @@ export interface ProjectSettings {
* - "all": backups both project and per-agent memory
* Default: "all". */
memoryBackupScope?: "project" | "agents" | "all";
/** When true, tasks created without titles but with descriptions longer than 200
* characters will automatically receive an AI-generated title (max 60 chars).
* Default: false. */
/*
FNXC:TitleSummarization 2026-08-19-13:43:
This project-scoped opt-in controls automatic title attempts for every non-empty task
description created without a title. It is a create-time snapshot, defaults to false, and
does not govern explicit per-request or manual summarization actions.
*/
autoSummarizeTitles?: boolean;
/*
FNXC:TaskDefinitionInputLanguage 2026-07-16-05:00:

View File

@@ -1597,7 +1597,7 @@ export interface TaskCreateInput {
planningThinkingLevel?: ThinkingLevel;
/** Independent per-task merger reasoning-effort override; unset inherits merger settings. */
mergerThinkingLevel?: ThinkingLevel;
/** When true, trigger AI title summarization if description is long and no title provided */
/** Explicitly force an AI title attempt for this create when no title is provided; project automatic policy is separate. */
summarize?: boolean;
/** Mission ID to link this task to (for mission hierarchy) */
missionId?: string;

View File

@@ -522,7 +522,7 @@ describe("summarizeTitle", () => {
});
global.fetch = mockFetch;
const result = await summarizeTitle("a".repeat(201));
const result = await summarizeTitle("a");
expect(result).toBe("Generated Title");
expect(mockFetch).toHaveBeenCalledWith(
@@ -532,7 +532,7 @@ describe("summarizeTitle", () => {
// `summarizeTitle` calls `fetch()` directly, bypassing `api()`, so no attribution header.
// See the note in test/apiRequestHeaders.ts — this is a MUTATION without attribution.
headers: API_JSON_HEADERS_NO_ATTRIBUTION,
body: JSON.stringify({ description: "a".repeat(201), provider: undefined, modelId: undefined }),
body: JSON.stringify({ description: "a", provider: undefined, modelId: undefined }),
})
);
});

View File

@@ -639,7 +639,7 @@ describe("ProjectModelsSection", () => {
const summarizationSection = screen.getByTestId("project-models-ai-summarization");
const heading = screen.getByRole("heading", { name: "AI Title and Git Commit Message Summarization" });
const autoSummarizeTitles = screen.getByRole("checkbox", { name: "Auto-summarize long descriptions as titles" });
const autoSummarizeTitles = screen.getByRole("checkbox", { name: "Auto-summarize task titles" });
const summarizationDropdown = screen.getByTestId("mock-model-dropdown-summarizationModel");
const fallbackDropdown = screen.getByTestId("mock-model-dropdown-titleSummarizerFallbackModel");
const defaultDropdown = screen.getByTestId("mock-model-dropdown-defaultModel");

View File

@@ -18,7 +18,7 @@ export interface SummarizeTitleResponse {
}
/** Summarize a task description into a concise title using AI.
* @param description - The task description to summarize (must be >200 chars; model input is truncated)
* @param description - The non-empty task description to summarize; model input is truncated
* @param provider - Optional AI model provider (e.g., "anthropic")
* @param modelId - Optional AI model ID (e.g., "claude-sonnet-4-5")
* @param projectId - Optional project ID for scoped settings resolution

View File

@@ -17,8 +17,8 @@ const searchTranslations: Record<string, string> = {
"settings.projectModels.chatDefaultModel": "Chat Default Model",
"settings.projectModels.chatDefaultAgent": "Chat Default Agent",
"settings.projectModels.aITitleAndGitCommitMessageSummarization": "AI Title and Git Commit Message Summarization",
"settings.projectModels.autoSummarizeLongDescriptionsAsTitles": "Auto-summarize long descriptions as titles",
"settings.projectModels.whenEnabledTasksCreatedWithoutATitleBut": "When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title",
"settings.projectModels.autoSummarizeLongDescriptionsAsTitles": "Auto-summarize task titles",
"settings.projectModels.whenEnabledTasksCreatedWithoutATitleBut": "When enabled, every non-empty task description created without a title receives an AI-generated title",
"settings.projectModels.aIMergeCommitSummaries": "AI merge commit summaries",
"settings.projectModels.whenEnabledMergeCommitMessagesIncludeAnAI": "When enabled, merge commit messages include an AI-generated subject plus body summary",
};
@@ -48,6 +48,7 @@ describe("SettingsModal Settings search index", () => {
"summarize",
"auto summarize",
"auto-summarize titles",
"task titles",
"title summarization",
"autoSummarizeTitles",
"AI title",

View File

@@ -227,7 +227,7 @@ describe("settings search ranking", () => {
sectionId: "project-models",
key: "autoSummarizeTitles",
labelKey: "settings.projectModels.autoSummarizeLongDescriptionsAsTitles",
labelFallback: "Auto-summarize long descriptions as titles",
labelFallback: "Auto-summarize task titles",
};
const results = rankSettingsSearchResults([autoSummarize], "summarize", resolveEnglish);
expect(results).toHaveLength(1);

View File

@@ -66,10 +66,10 @@ export const projectModelsSearchEntries: SettingsSearchEntry[] = [
sectionId: "project-models",
key: "autoSummarizeTitles",
labelKey: "settings.projectModels.autoSummarizeLongDescriptionsAsTitles",
labelFallback: " Auto-summarize long descriptions as titles ",
labelFallback: " Auto-summarize task titles ",
helpKey: "settings.projectModels.whenEnabledTasksCreatedWithoutATitleBut",
helpFallback:
" When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. ",
" When enabled, every non-empty task description created without a title receives an AI-generated title (max 60 characters). Explicit titles are preserved, and manual or explicit force requests remain available when this is disabled. The same model is also used for merge commit summaries and GitHub tracking issue titles. Default: disabled. ",
},
{
sectionId: "project-models",

View File

@@ -869,7 +869,7 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
<div className="settings-field-label-row">
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.aITitleAndGitCommitMessageSummarization", " AI Title and Git Commit Message Summarization ")}</h4>
<SettingsHelpTip settingKey="project-ai-summarization">
{t("settings.projectModels.configuresTheModelUsedForTwoShortSummary", " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ")}
{t("settings.projectModels.configuresTheModelUsedForTwoShortSummary", " Configures the model used for two short-summary jobs: auto-generating task titles from task descriptions, and generating merge commit summaries from step commits and diff stats. ")}
{(form.autoSummarizeTitles || form.useAiMergeCommitSummary || form.githubTrackingEnabledByDefault || false)
? t("settings.movedStub.summarizerModelInline", "These summarization model controls govern title auto-summarization, merge commit summaries, GitHub tracking titles, and PR metadata generation.")
: ""}
@@ -904,8 +904,13 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
<SettingsToggleRow
descriptor={{
key: "autoSummarizeTitles",
label: t("settings.projectModels.autoSummarizeLongDescriptionsAsTitles", " Auto-summarize long descriptions as titles "),
help: t("settings.projectModels.whenEnabledTasksCreatedWithoutATitleBut", " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. "),
/*
FNXC:TitleSummarization 2026-08-19-13:43:
The project toggle describes the all-length automatic policy. Keep explicit titles and
manual/force requests as separate precedence paths rather than implying this toggle is a permission gate.
*/
label: t("settings.projectModels.autoSummarizeLongDescriptionsAsTitles", " Auto-summarize task titles "),
help: t("settings.projectModels.whenEnabledTasksCreatedWithoutATitleBut", " When enabled, every non-empty task description created without a title receives an AI-generated title (max 60 characters). Explicit titles are preserved, and manual or explicit force requests remain available when this is disabled. The same model is also used for merge commit summaries and GitHub tracking issue titles. Default: disabled. "),
scope: "project",
}}
value={form.autoSummarizeTitles || false}

View File

@@ -826,17 +826,21 @@ describe("maybeCreateTrackingIssue", () => {
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("AI title summarizer failed"));
});
it("does not invoke the summarizer when the description is too short", async () => {
it("invokes the configured summarizer for a short titleless description", async () => {
summarizeTitleMock.mockResolvedValue("AI short title");
const updateTask = vi.fn().mockImplementation(async (_id, updates) => buildTask({ title: updates.title, description: "Short title fallback" }));
await maybeCreateTrackingIssue(buildTask({ title: "", description: "Short title fallback", githubTracking: { enabled: true } }), {
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn(), updateTask: vi.fn() } as any,
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn(), updateTask } as any,
projectSettings: { titleSummarizerProvider: "anthropic", titleSummarizerModelId: "claude" } as any,
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger: { warn: vi.fn(), info: vi.fn() },
});
expect(summarizeTitleMock).not.toHaveBeenCalled();
expect(createIssueMock).toHaveBeenCalledWith(expect.objectContaining({ title: "[FN-1] Short title fallback" }));
expect(summarizeTitleMock).toHaveBeenCalledWith("Short title fallback", rootDir, "anthropic", "claude");
expect(updateTask).toHaveBeenCalledWith("FN-1", { title: "AI short title" });
expect(createIssueMock).toHaveBeenCalledWith(expect.objectContaining({ title: "[FN-1] AI short title" }));
});
it("does not invoke the summarizer when no summarizer model is configured", async () => {

View File

@@ -5276,19 +5276,37 @@ describe("POST /api/ai/summarize-title", () => {
expect(res.body.error).toContain("description");
});
it("validates description length (minimum 200 characters)", async () => {
it("rejects empty descriptions without a length threshold", async () => {
const res = await REQUEST(
buildApp(),
"POST",
"/api/ai/summarize-title",
JSON.stringify({
description: "Short description",
}),
JSON.stringify({ description: " " }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("at least 201 characters");
expect(res.body.error).toContain("description must not be empty");
});
it("accepts a short non-empty description", async () => {
const fusionCore = await import("@fusion/core");
fusionCore.__resetSummarizeState();
const summarizeTitleSpy = vi
.spyOn(fusionCore, "summarizeTitle")
.mockResolvedValueOnce("Generated short title");
const res = await REQUEST(
buildApp(),
"POST",
"/api/ai/summarize-title",
JSON.stringify({ description: "Short description" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body).toEqual({ title: "Generated short title" });
expect(summarizeTitleSpy).toHaveBeenCalledWith("Short description", "/test/project", undefined, undefined);
});
it("accepts optional provider and modelId parameters", async () => {

View File

@@ -756,6 +756,25 @@ describe("POST /tasks", () => {
);
});
it("attaches automatic title summarization for short descriptions when the project option is enabled", async () => {
const createdTask = { ...FAKE_TASK_DETAIL, title: undefined, description: "short request" };
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockResolvedValue({ autoSummarizeTitles: true });
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValue(createdTask);
const res = await REQUEST(
buildApp(),
"POST",
"/api/tasks",
JSON.stringify({ description: "short request" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(201);
const [createInput, options] = (store.createTask as ReturnType<typeof vi.fn>).mock.calls[0];
expect(createInput).toMatchObject({ description: "short request", summarize: false });
expect(options.onSummarize).toEqual(expect.any(Function));
});
it("forwards no-workflow and explicit owner inputs but drops public exemption-shaped fields", async () => {
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
...FAKE_TASK_DETAIL,

View File

@@ -1,6 +1,5 @@
import {
AiServiceError,
MIN_DESCRIPTION_LENGTH,
columnsWithFlag,
declaresAnyLifecycleTrait,
parseRepoSlug,
@@ -432,9 +431,15 @@ export async function maybeCreateTrackingIssue(
const titleMissing = collapseWhitespace(latestTask.title ?? "").length === 0;
const resolvedSummarizer = resolveTrackingTitleSummarizerModel(deps.projectSettings, deps.globalSettings);
/*
FNXC:TitleSummarization 2026-08-19-13:43:
Tracking title generation uses the shared summarizer for every non-empty titleless task.
The project setting controls ordinary create-time automation; this configured tracking lane
retains its existing explicit integration behavior without a hidden length threshold.
*/
const canSummarizeTitle = titleMissing
&& typeof latestTask.description === "string"
&& latestTask.description.length >= MIN_DESCRIPTION_LENGTH
&& latestTask.description.trim().length > 0
&& Boolean(resolvedSummarizer.provider && resolvedSummarizer.modelId);
if (canSummarizeTitle) {

View File

@@ -278,8 +278,8 @@ router.post("/ai/draft-goal-description", async (req, res) => {
* Body: { description: string, provider?: string, modelId?: string }
* Returns: { title: string }
*
* Generates a concise title (≤60 characters) from descriptions longer than 200 characters.
* Long descriptions are accepted; core truncates model input before prompting.
* Generates a concise title (≤60 characters) from any non-empty description.
* Model input is bounded by the core summarizer before prompting.
* Rate limited: 10 requests per hour per IP
*/
router.post("/ai/summarize-title", async (req, res) => {
@@ -294,7 +294,6 @@ router.post("/ai/summarize-title", async (req, res) => {
getRateLimitResetTime,
summarizeTitle,
validateDescription,
MIN_DESCRIPTION_LENGTH,
RateLimitError: _RateLimitError4,
ValidationError: _ValidationError2,
AiServiceError: _AiServiceError2,
@@ -359,7 +358,7 @@ router.post("/ai/summarize-title", async (req, res) => {
const title = await summarizeTitle(description, rootDir, resolvedProvider, resolvedModelId);
if (!title) {
throw badRequest(`Description must be at least ${MIN_DESCRIPTION_LENGTH} characters for summarization`);
throw badRequest("AI returned empty title");
}
res.json({ title });

View File

@@ -253,6 +253,7 @@ describe("createDelegateTaskTool", () => {
source: expect.objectContaining({ sourceType: "api" }),
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
expect(vi.mocked(taskStore.createTask).mock.calls[0]?.[0]).toMatchObject({ column: "todo" });
expect(vi.mocked(taskStore.createTask).mock.calls[0]?.[0]).not.toHaveProperty("summarize");
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Delegated to Bob (agent-001)");

View File

@@ -4014,7 +4014,6 @@ describe("executeHeartbeat", () => {
description: "Follow-up task",
dependencies: undefined,
priority: undefined,
summarize: true,
source: expect.objectContaining({
sourceType: "agent_heartbeat",
sourceAgentId: "agent-001",

View File

@@ -531,7 +531,7 @@ describe("buildSpecificationPrompt", () => {
});
describe("short titleless task title fallback", () => {
it.each([199, 200])("uses the deterministic description title at the %i-character boundary", (length) => {
it.each([1, 199, 200, 201, 4001])("uses the deterministic description title at the %i-character boundary", (length) => {
const description = "a".repeat(length);
const prompt = buildSpecificationPrompt(
{ ...baseTask, title: undefined, description },
@@ -565,14 +565,15 @@ describe("buildSpecificationPrompt", () => {
expect(prompt).toContain("- **Title:** (none)");
});
it("keeps (none) for titleless descriptions over the AI threshold", () => {
it("keeps the deterministic fallback above the former AI threshold", () => {
const description = "a".repeat(201);
const prompt = buildSpecificationPrompt(
{ ...baseTask, title: undefined, description },
".fusion/tasks/KB-001/PROMPT.md",
);
expect(prompt).toContain("- **Title:** (none)");
expect(prompt).toContain(`- **Title:** ${deriveFallbackTaskTitle(description)}`);
expect(prompt).not.toContain("- **Title:** (none)");
});
it("uses the helper-safe first meaningful line for markdown and multiline text", () => {

View File

@@ -1524,7 +1524,12 @@ export async function createAgentTask(
? undefined
: computeParentIntentClaimId({ title: input.title, description: input.description, sourceParentTaskId }) ?? undefined
),
summarize: !input.title?.trim() ? true : undefined,
/*
FNXC:TitleSummarization 2026-08-19-13:43:
Agent-created tasks use the same project-scoped automatic title policy as every other
create gateway. Do not inject summarize:true for untitled tasks; that flag is reserved for
an explicit caller request.
*/
source: nextSource,
githubTracking: shouldPrefillGithubTrackingEnabled
? {

View File

@@ -58,7 +58,6 @@ import {
isEphemeralAgent,
resolveEffectiveAgentPermissionPolicy,
MAX_TASK_LIST_TEXT_CHARS,
MIN_DESCRIPTION_LENGTH,
deriveFallbackTaskTitle,
detectContentLanguage,
localeDisplayName,
@@ -5426,17 +5425,18 @@ function isMalformedTaskTitle(title: string): boolean {
/**
* Resolve the title shown to the planner for a task that has not received a title yet.
*
* FNXC:TriageTitleFallback 2026-08-19-05:51:
* Short descriptions never enter AI title summarization, so planning must provide a deterministic
* title instead of `(none)`. Use the shared sanitized first-line helper rather than interpolating
* raw multiline description text, which could corrupt the prompt's `**Title:**` structure.
* FNXC:TitleSummarization 2026-08-19-13:43:
* Planning must provide one deterministic title fallback whenever an untitled task has non-empty
* content, regardless of whether automatic project summarization was disabled or unavailable.
* Use the shared sanitized first-line helper rather than interpolating raw multiline Markdown,
* which could corrupt the prompt's `**Title:**` structure.
*/
function resolveSpecificationPromptTitle(task: Pick<TaskDetail, "title" | "description">): string {
const existingTitle = task.title?.trim();
if (existingTitle) return existingTitle;
const description = task.description ?? "";
if (description.trim() && description.length < MIN_DESCRIPTION_LENGTH) {
if (description.trim()) {
return deriveFallbackTaskTitle(description);
}

View File

@@ -6668,11 +6668,11 @@
"aITitleAndGitCommitMessageSummarization": " AI Title and Git Commit Message Summarization ",
"automaticallyCompactContextWhenApproachingThisTokenCount": "Automatically compact context when approaching this token count. Leave empty for no cap (compact only on overflow errors). Set a number to proactively compact when reaching this token count. No default — unset (no cap).",
"autoSelectPresetBasedOnTaskSize": " Auto-select preset based on task size ",
"autoSummarizeLongDescriptionsAsTitles": " Auto-summarize long descriptions as titles ",
"autoSummarizeLongDescriptionsAsTitles": " Auto-summarize task titles ",
"taskDefinitionInInputLanguage": "Write task definitions in the operator's input language",
"taskDefinitionInInputLanguageHelp": "When enabled, generated task-definition prose uses supported detectable input languages (Spanish, French, Korean, or Chinese as zh-CN). Headings, markers, and code stay English. Unsupported or undetectable input stays English. Default: disabled.",
"configuredPresets": "Configured presets",
"configuresTheModelUsedForTwoShortSummary": " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ",
"configuresTheModelUsedForTwoShortSummary": " Configures the model used for two short-summary jobs: auto-generating task titles from task descriptions, and generating merge commit summaries from step commits and diff stats. ",
"defaultWorkflowModelLaneActions": "Default workflow model lane actions",
"defaultWorkflowModelLanes": "Default workflow model lanes",
"delete": " Delete ",
@@ -6711,7 +6711,7 @@
"useDefault": "Use default",
"useWorkflowDefault": "Use workflow default",
"whenEnabledMergeCommitMessagesIncludeAnAI": " When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model. Default: enabled. ",
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. ",
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, every non-empty task description created without a title receives an AI-generated title (max 60 characters). Explicit titles are preserved, and manual or explicit force requests remain available when this is disabled. The same model is also used for merge commit summaries and GitHub tracking issue titles. Default: disabled. ",
"autoSelectModelPresetHint": "Default: disabled.",
"prTitlePromptInstructionsHelp": "Guides the AI-generated Create PR title. Leave blank to use the default PR metadata prompt. No default — unset.",
"prDescriptionPromptInstructionsHelp": "Guides the AI-generated Create PR summary, changes, and testing sections. Leave blank to use the default PR metadata prompt. No default — unset.",

View File

@@ -6520,7 +6520,7 @@
"aITitleAndGitCommitMessageSummarization": " Resumo de título e mensagem de commit do Git por IA ",
"automaticallyCompactContextWhenApproachingThisTokenCount": "Compacta o contexto automaticamente ao se aproximar desta contagem de tokens. Deixe em branco para não haver limite (compactação apenas em erros de overflow). Defina um número para compactar proativamente ao atingir essa contagem de tokens. Sem padrão — não definido (sem limite).",
"autoSelectPresetBasedOnTaskSize": " Selecionar predefinição automaticamente com base no tamanho da tarefa ",
"autoSummarizeLongDescriptionsAsTitles": " Resumir automaticamente descrições longas como títulos ",
"autoSummarizeLongDescriptionsAsTitles": " Resumir automaticamente títulos de tarefas ",
"taskDefinitionInInputLanguage": "Escrever definições de tarefa no idioma de entrada do operador",
"taskDefinitionInInputLanguageHelp": "Quando ativado, o texto gerado da definição de tarefa usa os idiomas de entrada detectáveis suportados (espanhol, francês, coreano ou chinês como zh-CN). Títulos, marcadores e código permanecem em inglês. Entradas não suportadas ou não detectáveis permanecem em inglês. Padrão: desativado.",
"configuredPresets": "Predefinições configuradas",
@@ -6559,7 +6559,7 @@
"useDefault": "Usar padrão",
"useWorkflowDefault": "Usar padrão do fluxo de trabalho",
"whenEnabledMergeCommitMessagesIncludeAnAI": " Quando ativado, as mensagens de commit de merge incluem um assunto gerado por IA além de um resumo no corpo (narrativa + tópicos + estatísticas de diff), em vez de apenas listar os assuntos dos commits das etapas. Usa o modelo de resumo de título. Padrão: ativado. ",
"whenEnabledTasksCreatedWithoutATitleBut": " Quando ativado, tarefas criadas sem título, mas com descrições com mais de 200 caracteres, recebem automaticamente um título gerado por IA (máximo de 60 caracteres). O mesmo modelo também é usado para gerar corpos de mensagem de commit de merge como fallback quando o log de commits da branch está vazio (por exemplo, merges squash sem commits exclusivos), e títulos de issues de rastreamento do GitHub quando uma tarefa rastreada ainda não tem título. Padrão: desativado. ",
"whenEnabledTasksCreatedWithoutATitleBut": " Quando ativado, toda descrição não vazia criada sem título recebe automaticamente um título gerado por IA (máximo de 60 caracteres). Títulos explícitos são preservados e solicitações manuais ou explícitas continuam disponíveis quando desativado. O mesmo modelo também é usado para resumos de commits de merge e títulos de issues de rastreamento do GitHub. Padrão: desativado. ",
"autoSelectModelPresetHint": "Padrão: desativado.",
"prTitlePromptInstructionsHelp": "Orienta o título gerado por IA ao criar um PR. Deixe em branco para usar o prompt padrão de metadados do PR. Sem padrão — não definido.",
"prDescriptionPromptInstructionsHelp": "Orienta o resumo, as alterações e as seções de teste gerados por IA ao criar um PR. Deixe em branco para usar o prompt padrão de metadados do PR. Sem padrão — não definido.",
@@ -6997,7 +6997,7 @@
"aITitleAndGitCommitMessageSummarization": " Resumo de título e mensagem de commit do Git por IA ",
"automaticallyCompactContextWhenApproachingThisTokenCount": "Compacta o contexto automaticamente ao se aproximar desta contagem de tokens. Deixe em branco para não haver limite (compactação apenas em erros de overflow). Defina um número para compactar proativamente ao atingir essa contagem de tokens. Sem padrão — não definido (sem limite).",
"autoSelectPresetBasedOnTaskSize": " Selecionar predefinição automaticamente com base no tamanho da tarefa ",
"autoSummarizeLongDescriptionsAsTitles": " Resumir automaticamente descrições longas como títulos ",
"autoSummarizeLongDescriptionsAsTitles": " Resumir automaticamente títulos de tarefas ",
"taskDefinitionInInputLanguage": "Escrever definições de tarefa no idioma de entrada do operador",
"taskDefinitionInInputLanguageHelp": "Quando ativado, o texto gerado da definição de tarefa usa os idiomas de entrada detectáveis suportados (espanhol, francês, coreano ou chinês como zh-CN). Títulos, marcadores e código permanecem em inglês. Entradas não suportadas ou não detectáveis permanecem em inglês. Padrão: desativado.",
"configuredPresets": "Predefinições configuradas",
@@ -7036,7 +7036,7 @@
"useDefault": "Usar padrão",
"useWorkflowDefault": "Usar padrão do fluxo de trabalho",
"whenEnabledMergeCommitMessagesIncludeAnAI": " Quando ativado, as mensagens de commit de merge incluem um assunto gerado por IA além de um resumo no corpo (narrativa + tópicos + estatísticas de diff), em vez de apenas listar os assuntos dos commits das etapas. Usa o modelo de resumo de título. Padrão: ativado. ",
"whenEnabledTasksCreatedWithoutATitleBut": " Quando ativado, tarefas criadas sem título, mas com descrições com mais de 200 caracteres, recebem automaticamente um título gerado por IA (máximo de 60 caracteres). O mesmo modelo também é usado para gerar corpos de mensagem de commit de merge como fallback quando o log de commits da branch está vazio (por exemplo, merges squash sem commits exclusivos), e títulos de issues de rastreamento do GitHub quando uma tarefa rastreada ainda não tem título. Padrão: desativado. ",
"whenEnabledTasksCreatedWithoutATitleBut": " Quando ativado, toda descrição não vazia criada sem título recebe automaticamente um título gerado por IA (máximo de 60 caracteres). Títulos explícitos são preservados e solicitações manuais ou explícitas continuam disponíveis quando desativado. O mesmo modelo também é usado para resumos de commits de merge e títulos de issues de rastreamento do GitHub. Padrão: desativado. ",
"autoSelectModelPresetHint": "Padrão: desativado.",
"prTitlePromptInstructionsHelp": "Orienta o título gerado por IA ao criar um PR. Deixe em branco para usar o prompt padrão de metadados do PR. Sem padrão — não definido.",
"prDescriptionPromptInstructionsHelp": "Orienta o resumo, as alterações e as seções de teste gerados por IA ao criar um PR. Deixe em branco para usar o prompt padrão de metadados do PR. Sem padrão — não definido.",

View File

@@ -6817,7 +6817,7 @@ export default interface Resources {
"advancedWorkflowPolicy": " Advanced workflow policy ",
"autoSelectModelPresetHint": "Default: disabled.",
"autoSelectPresetBasedOnTaskSize": " Auto-select preset based on task size ",
"autoSummarizeLongDescriptionsAsTitles": " Auto-summarize long descriptions as titles ",
"autoSummarizeLongDescriptionsAsTitles": " Auto-summarize task titles ",
"automaticallyCompactContextWhenApproachingThisTokenCount": "Automatically compact context when approaching this token count. Leave empty for no cap (compact only on overflow errors). Set a number to proactively compact when reaching this token count. No default — unset (no cap).",
"chatDefaultAgent": "Chat Default Agent",
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
@@ -6835,7 +6835,7 @@ export default interface Resources {
"chatNewSessionModeHelp": "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.",
"chatNewSessionModePrompt": "Prompt for model each time",
"configuredPresets": "Configured presets",
"configuresTheModelUsedForTwoShortSummary": " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ",
"configuresTheModelUsedForTwoShortSummary": " Configures the model used for two short-summary jobs: auto-generating task titles from task descriptions, and generating merge commit summaries from step commits and diff stats. ",
"defaultWorkflowModelLaneActions": "Default workflow model lane actions",
"defaultWorkflowModelLanes": "Default workflow model lanes",
"delete": " Delete ",
@@ -6880,7 +6880,7 @@ export default interface Resources {
"useDefault": "Use default",
"useWorkflowDefault": "Use workflow default",
"whenEnabledMergeCommitMessagesIncludeAnAI": " When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model. Default: enabled. ",
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. ",
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, every non-empty task description created without a title receives an AI-generated title (max 60 characters). Explicit titles are preserved, and manual or explicit force requests remain available when this is disabled. The same model is also used for merge commit summaries and GitHub tracking issue titles. Default: disabled. ",
"workflowLanesSubheading": "Workflow lanes"
},
"prompts": {