feat(FN-4620): complete Step 1 — add shared planning board tools

Fusion-Task-Id: FN-4620
Fusion-Task-Lineage: d3d05e54-e137-46ee-bb5d-d71aa03e5a48
This commit is contained in:
Fusion
2026-05-15 11:22:10 -07:00
committed by gsxdsm
parent 939b68e1fa
commit f522d79e5b
2 changed files with 152 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import { describe, expect, it, vi } from "vitest";
import type { TaskStore } from "@fusion/core";
import { createPlanningBoardTools } from "../planning-board-tools.js";
function createStoreMock(overrides?: {
listTasks?: TaskStore["listTasks"];
getTask?: TaskStore["getTask"];
}): TaskStore {
return {
listTasks: overrides?.listTasks ?? vi.fn(async () => []),
getTask: overrides?.getTask ?? vi.fn(async () => {
throw new Error("not found");
}),
} as unknown as TaskStore;
}
describe("createPlanningBoardTools", () => {
it("fn_task_list excludes done tasks, includes deps, and handles empty list", async () => {
const store = createStoreMock({
listTasks: vi.fn(async () => [
{
id: "FN-1",
column: "todo",
title: "Task one",
description: "Task one description",
dependencies: ["FN-0"],
},
{
id: "FN-2",
column: "done",
title: "Done",
description: "Done description",
dependencies: [],
},
]) as TaskStore["listTasks"],
});
const taskList = createPlanningBoardTools(store).find((tool) => tool.name === "fn_task_list");
expect(taskList).toBeDefined();
const result = await taskList!.execute("c1", {});
expect(result.content[0]?.text).toBe("FN-1 (todo): Task one [deps: FN-0]");
const emptyStore = createStoreMock({ listTasks: vi.fn(async () => []) as TaskStore["listTasks"] });
const emptyResult = await createPlanningBoardTools(emptyStore)
.find((tool) => tool.name === "fn_task_list")!
.execute("c2", {});
expect(emptyResult.content[0]?.text).toBe("No active tasks.");
});
it("fn_task_get returns full details and not-found fallback", async () => {
const store = createStoreMock({
getTask: vi.fn(async (id: string) => ({
id,
column: "in-progress",
description: "Detailed task",
dependencies: ["FN-5", "FN-6"],
prompt: "# Prompt body",
})) as TaskStore["getTask"],
});
const taskGet = createPlanningBoardTools(store).find((tool) => tool.name === "fn_task_get");
expect(taskGet).toBeDefined();
const result = await taskGet!.execute("c3", { id: "FN-10" });
expect(result.content[0]?.text).toContain("ID: FN-10");
expect(result.content[0]?.text).toContain("Column: in-progress");
expect(result.content[0]?.text).toContain("Description: Detailed task");
expect(result.content[0]?.text).toContain("Dependencies: FN-5, FN-6");
expect(result.content[0]?.text).toContain("PROMPT.md:");
expect(result.content[0]?.text).toContain("# Prompt body");
const notFoundStore = createStoreMock();
const missingResult = await createPlanningBoardTools(notFoundStore)
.find((tool) => tool.name === "fn_task_get")!
.execute("c4", { id: "FN-404" });
expect(missingResult.content[0]?.text).toBe("Task FN-404 not found.");
});
});

View File

@@ -0,0 +1,75 @@
import type { TaskStore } from "@fusion/core";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
export function createPlanningBoardTools(store: TaskStore): ToolDefinition[] {
const taskGetParams = {
type: "object",
properties: {
id: { type: "string", description: "Task ID (e.g. KB-001)" },
},
required: ["id"],
additionalProperties: false,
} as const;
const taskList: ToolDefinition = {
name: "fn_task_list",
label: "List Tasks",
description:
"List all tasks that aren't done. Returns ID, description, column, " +
"and dependencies for each. Use to check for duplicates before planning.",
parameters: { type: "object", properties: {}, additionalProperties: false },
execute: async () => {
const tasks = await store.listTasks({ slim: true, includeArchived: false });
const active = tasks.filter((t) => t.column !== "done");
if (active.length === 0) {
return {
content: [{ type: "text" as const, text: "No active tasks." }],
details: {},
};
}
const lines = active.map((t) => {
const desc = t.title || t.description.slice(0, 80);
const deps = t.dependencies.length ? ` [deps: ${t.dependencies.join(", ")}]` : "";
return `${t.id} (${t.column}): ${desc}${deps}`;
});
return {
content: [{ type: "text" as const, text: lines.join("\n") }],
details: {},
};
},
};
const taskGet: ToolDefinition = {
name: "fn_task_get",
label: "Get Task",
description:
"Get full details of a specific task including its PROMPT.md content. " +
"Use to verify duplicates and to read dependency task specs before writing a new PROMPT.md.",
parameters: taskGetParams,
execute: async (_callId: string, params: { id: string }) => {
try {
const task = await store.getTask(params.id);
const parts = [
`ID: ${task.id}`,
`Column: ${task.column}`,
`Description: ${task.description}`,
task.dependencies.length ? `Dependencies: ${task.dependencies.join(", ")}` : null,
"",
"PROMPT.md:",
task.prompt || "(not yet specified)",
].filter(Boolean);
return {
content: [{ type: "text" as const, text: parts.join("\n") }],
details: {},
};
} catch {
return {
content: [{ type: "text" as const, text: `Task ${params.id} not found.` }],
details: {},
};
}
},
};
return [taskList, taskGet];
}