feat(FN-4899): complete Step 4 — surface duplicate lineage in CLI outputs

Fusion-Task-Id: FN-4899
Fusion-Task-Lineage: 2ae24667-6635-4268-a620-d6012266d8f2
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 16:03:00 -07:00
committed by gsxdsm
parent 67d6a292c1
commit 990c1896dd
4 changed files with 95 additions and 21 deletions

View File

@@ -854,22 +854,10 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
describe("fn_task_show", () => {
it("shows task details", async () => {
const createTool = api.tools.get("fn_task_create")!;
await createTool.execute(
"c1",
{ description: "Implement caching layer" },
undefined,
undefined,
makeCtx(tmpDir),
);
await createTool.execute("c1", { description: "Implement caching layer" }, undefined, undefined, makeCtx(tmpDir));
const showTool = api.tools.get("fn_task_show")!;
const result = await showTool.execute(
"call-1",
{ id: "FN-001" },
undefined,
undefined,
makeCtx(tmpDir),
);
const result = await showTool.execute("call-1", { id: "FN-001" }, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("FN-001");
expect(result.content[0].text).toContain("Implement caching layer");
@@ -891,10 +879,7 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
sourceMetadata: { agentName: "Scout" },
},
});
await store.createTask({
description: "UI created",
source: { sourceType: "dashboard_ui" },
});
await store.createTask({ description: "UI created", source: { sourceType: "dashboard_ui" } });
const showTool = api.tools.get("fn_task_show")!;
const agentResult = await showTool.execute("call-2", { id: "FN-001" }, undefined, undefined, makeCtx(tmpDir));
@@ -902,8 +887,23 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
expect(agentResult.content[0].text).toContain("Created via: Agent (Scout)");
expect(dashboardResult.content[0].text).toContain("Created via: Dashboard");
expect(agentResult.details.task.sourceMetadata?.agentName).toBe("Scout");
expect(agentResult.details.task.sourceAgentId).toBe("agent-999");
});
it("shows duplicate lineage with archived annotation", async () => {
const store = new TaskStore(tmpDir);
await store.init();
const archivedSource = await store.createTask({ description: "Archived source" });
await store.moveTask(archivedSource.id, "done");
await store.archiveTask(archivedSource.id);
await store.createTask({
description: "Dup task",
source: { sourceType: "chat_session", sourceMetadata: { duplicateOfTaskIds: [archivedSource.id, "FN-404"] } },
});
const showTool = api.tools.get("fn_task_show")!;
const result = await showTool.execute("call-4", { id: "FN-002" }, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain(`Duplicate of: ${archivedSource.id} (archived), FN-404`);
});
});

View File

@@ -42,6 +42,17 @@ vi.mock("@fusion/core", () => {
TaskStore: vi.fn(),
COLUMNS,
COLUMN_LABELS,
getTaskDuplicateLineage: vi.fn((task: { sourceType?: string; sourceParentTaskId?: string; sourceMetadata?: any }) => {
const ids: string[] = [];
if (task.sourceType === "task_duplicate" && task.sourceParentTaskId) ids.push(task.sourceParentTaskId);
const metadata = task.sourceMetadata?.duplicateOfTaskIds;
if (Array.isArray(metadata)) {
for (const id of metadata) {
if (typeof id === "string" && !ids.includes(id)) ids.push(id);
}
}
return ids;
}),
CentralCore: vi.fn().mockImplementation(function() {
return {
init: vi.fn().mockResolvedValue(undefined),
@@ -219,6 +230,28 @@ describe("runTaskShow", () => {
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
expect(output).not.toContain("Source:");
});
it("prints duplicate lineage from metadata and task_duplicate parent", async () => {
mockTaskStoreGetTask(makeTask({
sourceType: "task_duplicate",
sourceParentTaskId: "FN-2905",
sourceMetadata: { duplicateOfTaskIds: ["FN-9"] },
}));
await runTaskShow("FN-001");
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
expect(output).toContain("Duplicate of: FN-2905, FN-9");
});
it("prints duplicate lineage from metadata list", async () => {
mockTaskStoreGetTask(makeTask({ sourceMetadata: { duplicateOfTaskIds: ["FN-1", "FN-2"] } }));
await runTaskShow("FN-001");
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
expect(output).toContain("Duplicate of: FN-1, FN-2");
});
});
describe("task node overrides", () => {

View File

@@ -1,6 +1,6 @@
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { TaskStore, COLUMNS, COLUMN_LABELS, CentralCore, type Settings, type Column, type StepStatus, type AgentLogType, type AgentLogEntry } from "@fusion/core";
import { TaskStore, COLUMNS, COLUMN_LABELS, CentralCore, getTaskDuplicateLineage, type Settings, type Column, type StepStatus, type AgentLogType, type AgentLogEntry } from "@fusion/core";
import { aiMergeTask, listBranchRecoveryCandidates, type BranchRecoveryCandidate } from "@fusion/engine";
import { createInterface } from "node:readline/promises";
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
@@ -59,6 +59,22 @@ function getResearchSourceContext(sourceMetadata: unknown): string | undefined {
return typeof runId === "string" && runId.length > 0 ? runId : undefined;
}
async function formatTaskDuplicateLineage(task: Awaited<ReturnType<TaskStore["getTask"]>>, store: TaskStore): Promise<string | null> {
const lineage = getTaskDuplicateLineage(task);
if (lineage.length === 0) return null;
const labels = await Promise.all(lineage.map(async (id) => {
try {
const linked = await store.getTask(id);
return linked.column === "archived" ? `${id} (archived)` : id;
} catch {
return id;
}
}));
return labels.join(", ");
}
function formatTaskSource(task: {
sourceType?: string;
sourceAgentId?: string;
@@ -677,6 +693,10 @@ export async function runTaskShow(id: string, projectName?: string) {
if (sourceSummary) {
console.log(` Source: ${sourceSummary}`);
}
const duplicateLineage = await formatTaskDuplicateLineage(task, store);
if (duplicateLineage) {
console.log(` Duplicate of: ${duplicateLineage}`);
}
console.log();
// Steps

View File

@@ -19,6 +19,7 @@ import {
resolveResearchSettings,
canAgentTakeImplementationTaskForExplicitRouting,
formatRoleMismatchReason,
getTaskDuplicateLineage,
resolveAgentProvisioningPolicy,
TASK_PRIORITIES,
resolveSecretAccessPolicy,
@@ -269,6 +270,22 @@ function getTaskSourceLabel(task: Pick<Task, "sourceType" | "sourceMetadata" | "
}
}
async function formatDuplicateLineageLine(task: Task, store: TaskStore): Promise<string | null> {
const lineage = getTaskDuplicateLineage(task);
if (lineage.length === 0) return null;
const labels = await Promise.all(lineage.map(async (id) => {
try {
const linked = await store.getTask(id);
return linked.column === "archived" ? `${id} (archived)` : id;
} catch {
return id;
}
}));
return `Duplicate of: ${labels.join(", ")}`;
}
function formatTaskLine(t: Task): string {
const label =
t.title || t.description.slice(0, 60) + (t.description.length > 60 ? "…" : "");
@@ -755,6 +772,10 @@ export default function kbExtension(pi: ExtensionAPI) {
if (sourceLabel) {
lines.push(`Created via: ${sourceLabel}`);
}
const duplicateLineage = await formatDuplicateLineageLine(task, store);
if (duplicateLineage) {
lines.push(duplicateLineage);
}
if (task.paused) lines.push("Status: PAUSED");
lines.push("");