FN-6570: guard task list formatting fallback
Prevent fn_task_list surfaces from crashing when the runtime core namespace lacks the clamp export. - Add a shared defensive task-list formatter with a bounded fallback clamp. - Route CLI, dashboard planning-board, and engine triage fn_task_list output through the formatter. - Cover missing, throwing, and non-string clamp behavior in core and CLI tests. - Document the resilient task-list formatting behavior and add a patch changeset. Files changed: .changeset/fn-6570-task-list-resolve-fix.md | 5 ++ docs/cli-reference.md | 2 +- packages/cli/src/__tests__/extension.test.ts | 57 +++++++++++++++++++++- packages/cli/src/extension.ts | 8 ++- .../core/src/__tests__/task-list-format.test.ts | 49 ++++++++++++++++++- packages/core/src/index.ts | 2 +- packages/core/src/task-list-format.ts | 35 +++++++++++++ packages/dashboard/src/planning-board-tools.ts | 8 ++- packages/engine/src/triage.ts | 8 ++- 9 files changed, 164 insertions(+), 10 deletions(-) Fusion-Task-Id: FN-6570 Fusion-Task-Lineage: 455f38a8-5e4f-4643-a3ce-088ef0c92b01
This commit is contained in:
5
.changeset/fn-6570-task-list-resolve-fix.md
Normal file
5
.changeset/fn-6570-task-list-resolve-fix.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix `fn_task_list` crashes when the runtime `@fusion/core` formatter export is unavailable by resolving defensively and returning bounded fallback text.
|
||||||
@@ -485,7 +485,7 @@ Use planning mode to turn a rough idea into a triage task through an interactive
|
|||||||
|
|
||||||
When supported by your configured runtime/model provider, planning sessions can also use builtin `WebSearch` and `WebFetch` tools for live context gathering.
|
When supported by your configured runtime/model provider, planning sessions can also use builtin `WebSearch` and `WebFetch` tools for live context gathering.
|
||||||
|
|
||||||
Planning sessions also have read-only board tools: `fn_task_list` (list active backlog tasks) and `fn_task_get` (read full task details, including PROMPT.md) so interviews can avoid duplicate in-flight plans and anchor questions to existing work. `fn_task_list` also accepts `includeDeleted: true` to surface soft-deleted blockers when diagnosing stalled dependency chains, and `fn_task_show` now auto-falls back to include soft-deleted tasks with a `[SOFT-DELETED at ...]` marker.
|
Planning sessions also have read-only board tools: `fn_task_list` (list active backlog tasks) and `fn_task_get` (read full task details, including PROMPT.md) so interviews can avoid duplicate in-flight plans and anchor questions to existing work. `fn_task_list` output is bounded and falls back to a defensive formatter if the runtime task-list clamp helper is unavailable, so board reads return text instead of failing during ambient planning or heartbeat checks. `fn_task_list` also accepts `includeDeleted: true` to surface soft-deleted blockers when diagnosing stalled dependency chains, and `fn_task_show` now auto-falls back to include soft-deleted tasks with a `[SOFT-DELETED at ...]` marker.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
fn task plan [description]
|
fn task plan [description]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ vi.mock("../commands/task.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import kbExtension from "../extension.js";
|
import kbExtension from "../extension.js";
|
||||||
import { TaskStore, AgentStore, MANUAL_RETRY_RESET_COUNTER_KEYS, RESEARCH_RUN_STATUSES, MAX_TASK_LIST_TEXT_CHARS } from "@fusion/core";
|
import { TaskStore, AgentStore, MANUAL_RETRY_RESET_COUNTER_KEYS, RESEARCH_RUN_STATUSES, MAX_TASK_LIST_TEXT_CHARS, formatTaskListText } from "@fusion/core";
|
||||||
import type { WorkflowIr } from "@fusion/core";
|
import type { WorkflowIr } from "@fusion/core";
|
||||||
import { isGhAvailable, isGhAuthenticated, runGhJsonAsync } from "@fusion/core/gh-cli";
|
import { isGhAvailable, isGhAuthenticated, runGhJsonAsync } from "@fusion/core/gh-cli";
|
||||||
import { runTaskPlan } from "../commands/task.js";
|
import { runTaskPlan } from "../commands/task.js";
|
||||||
@@ -2552,6 +2552,35 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("fn_task_list", () => {
|
describe("fn_task_list", () => {
|
||||||
|
function expectSingleBoundedTextBlock(result: any) {
|
||||||
|
expect(result.content).toHaveLength(1);
|
||||||
|
expect(result.content[0].type).toBe("text");
|
||||||
|
expect(result.content[0].text).toBeTruthy();
|
||||||
|
expect(result.content[0].text.length).toBeLessThanOrEqual(MAX_TASK_LIST_TEXT_CHARS);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns bounded text for omitted and provided column/limit params", async () => {
|
||||||
|
const store = new TaskStore(tmpDir);
|
||||||
|
await store.init();
|
||||||
|
try {
|
||||||
|
await store.createTask({ description: "Planning task one" });
|
||||||
|
await store.createTask({ description: "Todo task one", column: "todo" });
|
||||||
|
} finally {
|
||||||
|
store.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const listTool = api.tools.get("fn_task_list")!;
|
||||||
|
for (const [callId, params] of [
|
||||||
|
["list-all-default", {}],
|
||||||
|
["list-todo-default", { column: "todo" }],
|
||||||
|
["list-todo-large-limit", { column: "todo", limit: 50 }],
|
||||||
|
] as const) {
|
||||||
|
const result = await listTool.execute(callId, params, undefined, undefined, makeCtx(tmpDir));
|
||||||
|
expectSingleBoundedTextBlock(result);
|
||||||
|
expect(result.details.count).toBe(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps small column-filtered listings complete without the clamp marker", async () => {
|
it("keeps small column-filtered listings complete without the clamp marker", async () => {
|
||||||
const store = new TaskStore(tmpDir);
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
@@ -2737,6 +2766,32 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("degrades to bounded text when the clamp export is unavailable", () => {
|
||||||
|
const boardLinesWithoutParams = [
|
||||||
|
"Planning (2):",
|
||||||
|
` FN-001 Planning task ${"x".repeat(6_000)}`,
|
||||||
|
` FN-002 Planning task ${"x".repeat(6_000)}`,
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
const boardLinesWithColumnAndLimit = [
|
||||||
|
"Todo (2):",
|
||||||
|
` FN-003 Todo task ${"x".repeat(6_000)}`,
|
||||||
|
" ... and 1 more",
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TaskListOutput 2026-06-17-05:55:
|
||||||
|
FN-6570 exercises the formatter boundary called by the CLI surface because the existing extension harness imports @fusion/core before per-test mocks can safely replace only clampTaskListText with a stale-dist missing export.
|
||||||
|
The two line sets mirror fn_task_list with omitted params and with column/limit provided, proving the surface path now receives bounded text instead of a crashing `(0 , _core.clampTaskListText) is not a function` call.
|
||||||
|
*/
|
||||||
|
for (const lines of [boardLinesWithoutParams, boardLinesWithColumnAndLimit]) {
|
||||||
|
const text = formatTaskListText(lines, { clamp: undefined }).trimEnd();
|
||||||
|
expect(text).toBeTruthy();
|
||||||
|
expect(text.length).toBeLessThanOrEqual(MAX_TASK_LIST_TEXT_CHARS);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns structured details for invalid task assignment", async () => {
|
it("returns structured details for invalid task assignment", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
import { Type, type TSchema } from "typebox";
|
import { Type, type TSchema } from "typebox";
|
||||||
import { StringEnum } from "@earendil-works/pi-ai";
|
import { StringEnum } from "@earendil-works/pi-ai";
|
||||||
|
import * as fusionCore from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
TaskStore,
|
TaskStore,
|
||||||
COLUMNS,
|
COLUMNS,
|
||||||
@@ -28,7 +29,7 @@ import {
|
|||||||
resolveSecretAccessPolicy,
|
resolveSecretAccessPolicy,
|
||||||
getProjectRootFromWorktree,
|
getProjectRootFromWorktree,
|
||||||
resolveTaskGithubTracking,
|
resolveTaskGithubTracking,
|
||||||
clampTaskListText,
|
formatTaskListText,
|
||||||
type SecretScope,
|
type SecretScope,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
@@ -825,9 +826,12 @@ export default function kbExtension(pi: ExtensionAPI) {
|
|||||||
/*
|
/*
|
||||||
FNXC:TaskListOutput 2026-06-16-17:47:
|
FNXC:TaskListOutput 2026-06-16-17:47:
|
||||||
FN-6492 routes CLI fn_task_list through the shared clamp so large column-filtered board reads remain text-only instead of being converted to host attachments.
|
FN-6492 routes CLI fn_task_list through the shared clamp so large column-filtered board reads remain text-only instead of being converted to host attachments.
|
||||||
|
|
||||||
|
FNXC:TaskListOutput 2026-06-17-05:46:
|
||||||
|
FN-6570 resolves the clamp from the runtime @fusion/core namespace and lets formatTaskListText fall back when stale dist/interoperability paths omit clampTaskListText, preventing heartbeat board reads from crashing.
|
||||||
*/
|
*/
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: clampTaskListText(lines).trimEnd() }],
|
content: [{ type: "text", text: formatTaskListText(lines, { clamp: fusionCore.clampTaskListText }).trimEnd() }],
|
||||||
details: { count: tasks.length },
|
details: { count: tasks.length },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { describe, expect, it } from "vitest";
|
|||||||
import {
|
import {
|
||||||
clampTaskListText as sourceBarrelClampTaskListText,
|
clampTaskListText as sourceBarrelClampTaskListText,
|
||||||
MAX_TASK_LIST_TEXT_CHARS as SOURCE_BARREL_MAX_TASK_LIST_TEXT_CHARS,
|
MAX_TASK_LIST_TEXT_CHARS as SOURCE_BARREL_MAX_TASK_LIST_TEXT_CHARS,
|
||||||
|
formatTaskListText as sourceBarrelFormatTaskListText,
|
||||||
} from "../index.js";
|
} from "../index.js";
|
||||||
import { clampTaskListText, MAX_TASK_LIST_TEXT_CHARS } from "../task-list-format.js";
|
import { clampTaskListText, formatTaskListText, MAX_TASK_LIST_TEXT_CHARS } from "../task-list-format.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ type RuntimeCoreTaskListModule = {
|
|||||||
COLUMN_LABELS: Record<string, string>;
|
COLUMN_LABELS: Record<string, string>;
|
||||||
MAX_TASK_LIST_TEXT_CHARS: number;
|
MAX_TASK_LIST_TEXT_CHARS: number;
|
||||||
clampTaskListText: (lines: string[]) => string;
|
clampTaskListText: (lines: string[]) => string;
|
||||||
|
formatTaskListText?: (lines: string[]) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RuntimeTask = {
|
type RuntimeTask = {
|
||||||
@@ -81,6 +83,7 @@ describe("@fusion/core dist barrel export wiring (FN-6515/FN-6535)", () => {
|
|||||||
|
|
||||||
it("re-exports task-list formatting helpers from the source barrel", () => {
|
it("re-exports task-list formatting helpers from the source barrel", () => {
|
||||||
expect(typeof sourceBarrelClampTaskListText).toBe("function");
|
expect(typeof sourceBarrelClampTaskListText).toBe("function");
|
||||||
|
expect(typeof sourceBarrelFormatTaskListText).toBe("function");
|
||||||
expect(typeof SOURCE_BARREL_MAX_TASK_LIST_TEXT_CHARS).toBe("number");
|
expect(typeof SOURCE_BARREL_MAX_TASK_LIST_TEXT_CHARS).toBe("number");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,6 +93,7 @@ describe("@fusion/core dist barrel export wiring (FN-6515/FN-6535)", () => {
|
|||||||
const mod = await import(pathToFileURL(distIndex).href);
|
const mod = await import(pathToFileURL(distIndex).href);
|
||||||
|
|
||||||
expect(typeof mod.clampTaskListText).toBe("function");
|
expect(typeof mod.clampTaskListText).toBe("function");
|
||||||
|
expect(typeof mod.formatTaskListText).toBe("function");
|
||||||
expect(typeof mod.MAX_TASK_LIST_TEXT_CHARS).toBe("number");
|
expect(typeof mod.MAX_TASK_LIST_TEXT_CHARS).toBe("number");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,6 +145,49 @@ describe("@fusion/core dist barrel export wiring (FN-6515/FN-6535)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("formatTaskListText", () => {
|
||||||
|
it("returns an empty string for empty input", () => {
|
||||||
|
expect(formatTaskListText([])).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the canonical clamp path for small input without a marker", () => {
|
||||||
|
const lines = ["Todo (2):", " FN-001 First task", " FN-002 Second task"];
|
||||||
|
|
||||||
|
expect(formatTaskListText(lines)).toBe(lines.join("\n"));
|
||||||
|
expect(formatTaskListText(lines)).not.toContain("truncated to fit");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the canonical clamp path for large input with the FN-6492 marker", () => {
|
||||||
|
const lines = Array.from({ length: 20 }, (_, index) => `FN-${String(index + 1).padStart(3, "0")} ${"x".repeat(20)}`);
|
||||||
|
|
||||||
|
const text = formatTaskListText(lines, { maxChars: 150 });
|
||||||
|
|
||||||
|
expect(text.length).toBeLessThanOrEqual(150);
|
||||||
|
expect(text).toContain("truncated to fit; narrow with column/limit");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to bounded text when the clamp helper is missing", () => {
|
||||||
|
const lines = Array.from({ length: 500 }, (_, index) => `FN-${String(index + 1).padStart(3, "0")} ${"x".repeat(80)}`);
|
||||||
|
|
||||||
|
const text = formatTaskListText(lines, { clamp: undefined });
|
||||||
|
|
||||||
|
expect(text).toBeTruthy();
|
||||||
|
expect(text.length).toBeLessThanOrEqual(MAX_TASK_LIST_TEXT_CHARS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to bounded text when the clamp binding is not a function", () => {
|
||||||
|
const lines = ["FN-001 " + "x".repeat(200)];
|
||||||
|
const text = formatTaskListText(lines, {
|
||||||
|
maxChars: 40,
|
||||||
|
clamp: "not-a-function" as unknown as (lines: string[], opts?: { maxChars?: number }) => string,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text).toBeTruthy();
|
||||||
|
expect(text.length).toBeLessThanOrEqual(40);
|
||||||
|
expect(text.endsWith("…")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("clampTaskListText", () => {
|
describe("clampTaskListText", () => {
|
||||||
it("returns an empty string for empty input", () => {
|
it("returns an empty string for empty input", () => {
|
||||||
expect(clampTaskListText([])).toBe("");
|
expect(clampTaskListText([])).toBe("");
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export { redactSecrets } from "./redact-secrets.js";
|
|||||||
export { isActiveNearDuplicateColumn, isNearDuplicateCanonicalInactive } from "./near-duplicate-canonical.js";
|
export { isActiveNearDuplicateColumn, isNearDuplicateCanonicalInactive } from "./near-duplicate-canonical.js";
|
||||||
export type { NearDuplicateCanonicalState } from "./near-duplicate-canonical.js";
|
export type { NearDuplicateCanonicalState } from "./near-duplicate-canonical.js";
|
||||||
export * from "./frontend-ux-policy.js";
|
export * from "./frontend-ux-policy.js";
|
||||||
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText } from "./task-list-format.js";
|
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
|
||||||
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
|
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
|
||||||
export type { MockProviderId, MockSessionPurpose } from "./mock-provider-constants.js";
|
export type { MockProviderId, MockSessionPurpose } from "./mock-provider-constants.js";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -43,3 +43,38 @@ export function clampTaskListText(
|
|||||||
|
|
||||||
return marker.slice(0, Math.max(0, maxChars - 1)) + "…";
|
return marker.slice(0, Math.max(0, maxChars - 1)) + "…";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fallbackClampTaskListText(lines: string[], maxChars: number): string {
|
||||||
|
const text = lines.join("\n");
|
||||||
|
if (text.length <= maxChars) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.slice(0, Math.max(0, maxChars - 1)) + "…";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FNXC:TaskListOutput 2026-06-17-05:44:
|
||||||
|
* FN-6570 requires fn_task_list tool surfaces to resolve the formatter defensively because stale or mismatched @fusion/core builds can omit the clampTaskListText export and crash ambient heartbeat agents as `(0 , _core.clampTaskListText) is not a function`.
|
||||||
|
* Keep the canonical clamp as the normal path, but degrade to a bounded inline fallback so board listing tools return text instead of throwing.
|
||||||
|
*/
|
||||||
|
export function formatTaskListText(
|
||||||
|
lines: string[],
|
||||||
|
opts: {
|
||||||
|
maxChars?: number;
|
||||||
|
clamp?: (lines: string[], opts?: { maxChars?: number }) => string;
|
||||||
|
} = {},
|
||||||
|
): string {
|
||||||
|
const maxChars = Math.max(1, Math.floor(opts.maxChars ?? MAX_TASK_LIST_TEXT_CHARS));
|
||||||
|
const clamp = opts.clamp ?? clampTaskListText;
|
||||||
|
if (typeof clamp !== "function") {
|
||||||
|
return fallbackClampTaskListText(lines, maxChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = clamp(lines, { maxChars });
|
||||||
|
return typeof text === "string" ? text : fallbackClampTaskListText(lines, maxChars);
|
||||||
|
} catch {
|
||||||
|
return fallbackClampTaskListText(lines, maxChars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { clampTaskListText, type TaskStore } from "@fusion/core";
|
import * as fusionCore from "@fusion/core";
|
||||||
|
import { formatTaskListText, type TaskStore } from "@fusion/core";
|
||||||
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
||||||
|
|
||||||
export function createPlanningBoardTools(store: TaskStore): ToolDefinition[] {
|
export function createPlanningBoardTools(store: TaskStore): ToolDefinition[] {
|
||||||
@@ -35,9 +36,12 @@ export function createPlanningBoardTools(store: TaskStore): ToolDefinition[] {
|
|||||||
/*
|
/*
|
||||||
FNXC:TaskListOutput 2026-06-16-17:47:
|
FNXC:TaskListOutput 2026-06-16-17:47:
|
||||||
FN-6492 keeps dashboard planning-board duplicate checks within the shared plain-text budget so large boards stay readable to non-vision agents.
|
FN-6492 keeps dashboard planning-board duplicate checks within the shared plain-text budget so large boards stay readable to non-vision agents.
|
||||||
|
|
||||||
|
FNXC:TaskListOutput 2026-06-17-05:46:
|
||||||
|
FN-6570 keeps the planning-board fn_task_list surface resilient when runtime @fusion/core lacks clampTaskListText by passing the namespace binding through the defensive formatter fallback.
|
||||||
*/
|
*/
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text" as const, text: clampTaskListText(lines) }],
|
content: [{ type: "text" as const, text: formatTaskListText(lines, { clamp: fusionCore.clampTaskListText }) }],
|
||||||
details: {},
|
details: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import * as fusionCore from "@fusion/core";
|
||||||
import type {
|
import type {
|
||||||
TaskStore,
|
TaskStore,
|
||||||
Task,
|
Task,
|
||||||
@@ -26,7 +27,7 @@ import {
|
|||||||
findNearDuplicates,
|
findNearDuplicates,
|
||||||
isNearDuplicateCanonicalInactive,
|
isNearDuplicateCanonicalInactive,
|
||||||
applyFrontendUxCriteria,
|
applyFrontendUxCriteria,
|
||||||
clampTaskListText,
|
formatTaskListText,
|
||||||
type NearDuplicateCandidate,
|
type NearDuplicateCandidate,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import type { ImageContent } from "@earendil-works/pi-ai";
|
import type { ImageContent } from "@earendil-works/pi-ai";
|
||||||
@@ -1471,9 +1472,12 @@ export class TriageProcessor {
|
|||||||
/*
|
/*
|
||||||
FNXC:TaskListOutput 2026-06-16-17:47:
|
FNXC:TaskListOutput 2026-06-16-17:47:
|
||||||
FN-6492 keeps engine triage duplicate-detection listings bounded with the shared fn_task_list text clamp so large active boards never require attachment/image fallback.
|
FN-6492 keeps engine triage duplicate-detection listings bounded with the shared fn_task_list text clamp so large active boards never require attachment/image fallback.
|
||||||
|
|
||||||
|
FNXC:TaskListOutput 2026-06-17-05:47:
|
||||||
|
FN-6570 guards the triage fn_task_list formatter against stale @fusion/core runtime namespaces where clampTaskListText is absent, so duplicate-detection board reads degrade to bounded text instead of throwing.
|
||||||
*/
|
*/
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text" as const, text: clampTaskListText(lines) }],
|
content: [{ type: "text" as const, text: formatTaskListText(lines, { clamp: fusionCore.clampTaskListText }) }],
|
||||||
details: {},
|
details: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user