fix(FN-2392): normalize skill-facing tool references to fn_*

- Update Fusion skill docs, prompts, and capability references to use public fn_* tool names consistently
- Align engine system prompts and tool schemas for messaging/task actions with fn_send_message, fn_read_messages, fn_task_* naming
- Refresh related tests across CLI, engine, dashboard, and core to match normalized tool naming and behavior
- Add a patch changeset for @runfusion/fusion describing the skill-tool namespace normalization
This commit is contained in:
Fusion
2026-04-24 03:11:09 -07:00
committed by gsxdsm
parent fa0cbe2327
commit eef56af706
33 changed files with 927 additions and 764 deletions

View File

@@ -396,7 +396,7 @@ describe("HeartbeatMonitor", () => {
capturedTools = [];
});
it("includes send_message and read_messages tools when messageStore is available", () => {
it("includes fn_send_message and fn_read_messages tools when messageStore is available", () => {
const messageStore = createMockMessageStore();
const customMonitor = new HeartbeatMonitor({
store,
@@ -408,8 +408,8 @@ describe("HeartbeatMonitor", () => {
const tools = customMonitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001", undefined, undefined, messageStore);
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("send_message");
expect(toolNames).toContain("read_messages");
expect(toolNames).toContain("fn_send_message");
expect(toolNames).toContain("fn_read_messages");
});
it("does not include message tools when messageStore is not provided", () => {
@@ -422,8 +422,8 @@ describe("HeartbeatMonitor", () => {
const tools = customMonitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
const toolNames = tools.map((t) => t.name);
expect(toolNames).not.toContain("send_message");
expect(toolNames).not.toContain("read_messages");
expect(toolNames).not.toContain("fn_send_message");
expect(toolNames).not.toContain("fn_read_messages");
});
it("does not include message tools when messageStore is undefined even if other params are passed", () => {
@@ -443,8 +443,8 @@ describe("HeartbeatMonitor", () => {
);
const toolNames = tools.map((t) => t.name);
expect(toolNames).not.toContain("send_message");
expect(toolNames).not.toContain("read_messages");
expect(toolNames).not.toContain("fn_send_message");
expect(toolNames).not.toContain("fn_read_messages");
});
});
});
@@ -1474,7 +1474,7 @@ describe("HeartbeatMonitor", () => {
expect(result.resultJson).toEqual({ reason: "no_assignment" });
});
it("identity agent without task receives correct tools (task_create, list_agents, delegate_task, heartbeat_done)", async () => {
it("identity agent without task receives correct tools (fn_task_create, fn_list_agents, fn_delegate_task, fn_heartbeat_done)", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
@@ -1487,20 +1487,20 @@ describe("HeartbeatMonitor", () => {
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
// Should have task_create, list_agents, delegate_task
expect(toolNames).toContain("task_create");
expect(toolNames).toContain("list_agents");
expect(toolNames).toContain("delegate_task");
// Should have heartbeat_done
expect(toolNames).toContain("heartbeat_done");
// Should have fn_task_create, fn_list_agents, fn_delegate_task
expect(toolNames).toContain("fn_task_create");
expect(toolNames).toContain("fn_list_agents");
expect(toolNames).toContain("fn_delegate_task");
// Should have fn_heartbeat_done
expect(toolNames).toContain("fn_heartbeat_done");
// Should have memory tools
expect(toolNames).toContain("memory_search");
expect(toolNames).toContain("memory_append");
expect(toolNames).toContain("fn_memory_search");
expect(toolNames).toContain("fn_memory_append");
// Should NOT have task_log, task_document_write, task_document_read (they require taskId)
expect(toolNames).not.toContain("task_log");
expect(toolNames).not.toContain("task_document_write");
expect(toolNames).not.toContain("task_document_read");
// Should NOT have fn_task_log, fn_task_document_write, fn_task_document_read (they require taskId)
expect(toolNames).not.toContain("fn_task_log");
expect(toolNames).not.toContain("fn_task_document_write");
expect(toolNames).not.toContain("fn_task_document_read");
});
it("no-task run receives HEARTBEAT_NO_TASK_SYSTEM_PROMPT as system prompt", async () => {
@@ -1517,17 +1517,17 @@ describe("HeartbeatMonitor", () => {
const systemPrompt = callArgs.systemPrompt;
expect(systemPrompt).toContain(HEARTBEAT_NO_TASK_SYSTEM_PROMPT);
expect(systemPrompt).not.toContain("task_log");
expect(systemPrompt).not.toContain("task_document_write");
expect(systemPrompt).not.toContain("task_document_read");
expect(systemPrompt).toContain("task_create");
expect(systemPrompt).toContain("list_agents");
expect(systemPrompt).toContain("delegate_task");
expect(systemPrompt).toContain("read_messages");
expect(systemPrompt).toContain("send_message");
expect(systemPrompt).toContain("memory_search");
expect(systemPrompt).toContain("memory_append");
expect(systemPrompt).toContain("heartbeat_done");
expect(systemPrompt).not.toContain("fn_task_log");
expect(systemPrompt).not.toContain("fn_task_document_write");
expect(systemPrompt).not.toContain("fn_task_document_read");
expect(systemPrompt).toContain("fn_task_create");
expect(systemPrompt).toContain("fn_list_agents");
expect(systemPrompt).toContain("fn_delegate_task");
expect(systemPrompt).toContain("fn_read_messages");
expect(systemPrompt).toContain("fn_send_message");
expect(systemPrompt).toContain("fn_memory_search");
expect(systemPrompt).toContain("fn_memory_append");
expect(systemPrompt).toContain("fn_heartbeat_done");
});
it("identity agent without task receives no-task execution prompt mentioning 'no assigned task'", async () => {
@@ -1543,13 +1543,13 @@ describe("HeartbeatMonitor", () => {
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const systemPrompt = callArgs.systemPrompt;
expect(systemPrompt).toContain(HEARTBEAT_NO_TASK_SYSTEM_PROMPT);
expect(systemPrompt).not.toContain("task_log");
expect(systemPrompt).not.toContain("task_document_write");
expect(systemPrompt).not.toContain("task_document_read");
expect(systemPrompt).not.toContain("fn_task_log");
expect(systemPrompt).not.toContain("fn_task_document_write");
expect(systemPrompt).not.toContain("fn_task_document_read");
expect(systemPrompt).not.toContain("Task Documents:");
expect(systemPrompt).toContain("task_create");
expect(systemPrompt).toContain("heartbeat_done");
expect(systemPrompt).toContain("memory_append");
expect(systemPrompt).toContain("fn_task_create");
expect(systemPrompt).toContain("fn_heartbeat_done");
expect(systemPrompt).toContain("fn_memory_append");
// The execution prompt is passed to session.prompt by promptWithFallback mock
const promptCalls = mockSession.prompt.mock.calls;
@@ -1560,9 +1560,9 @@ describe("HeartbeatMonitor", () => {
expect(executionPrompt).toContain("No assigned task");
// Should describe ambient work capabilities
expect(executionPrompt).toContain("ambient work");
expect(executionPrompt).toContain("task_create");
expect(executionPrompt).toContain("list_agents");
expect(executionPrompt).toContain("delegate_task");
expect(executionPrompt).toContain("fn_task_create");
expect(executionPrompt).toContain("fn_list_agents");
expect(executionPrompt).toContain("fn_delegate_task");
// Should NOT include task-specific content
expect(executionPrompt).not.toContain("Assigned task:");
expect(executionPrompt).not.toContain("Task description:");
@@ -1582,8 +1582,8 @@ describe("HeartbeatMonitor", () => {
const systemPrompt = callArgs.systemPrompt;
expect(systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
expect(systemPrompt).toContain("task_log");
expect(systemPrompt).toContain("task_document_write");
expect(systemPrompt).toContain("fn_task_log");
expect(systemPrompt).toContain("fn_task_document_write");
expect(systemPrompt).toContain("Task Documents:");
});
@@ -1650,8 +1650,8 @@ describe("HeartbeatMonitor", () => {
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
// Should have messaging tools when messageStore is available
expect(toolNames).toContain("send_message");
expect(toolNames).toContain("read_messages");
expect(toolNames).toContain("fn_send_message");
expect(toolNames).toContain("fn_read_messages");
});
it("identity agent without task does NOT include messaging tools when messageStore is unavailable", async () => {
@@ -1668,8 +1668,8 @@ describe("HeartbeatMonitor", () => {
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
// Should NOT have messaging tools when messageStore is not available
expect(toolNames).not.toContain("send_message");
expect(toolNames).not.toContain("read_messages");
expect(toolNames).not.toContain("fn_send_message");
expect(toolNames).not.toContain("fn_read_messages");
});
it("identity agent without task fetches messages and includes them in prompt for timer trigger", async () => {
@@ -2406,7 +2406,7 @@ describe("HeartbeatMonitor", () => {
expect(executionPrompt).toContain("dashboard");
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const sendMessageTool = callArgs.customTools?.find((tool: { name: string }) => tool.name === "send_message");
const sendMessageTool = callArgs.customTools?.find((tool: { name: string }) => tool.name === "fn_send_message");
expect(sendMessageTool).toBeDefined();
await sendMessageTool!.execute(
@@ -2613,29 +2613,29 @@ describe("HeartbeatMonitor", () => {
});
describe("execution", () => {
it("no-task system prompt does not reference task_log or task_document tools", () => {
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_log");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_document_write");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_document_read");
it("no-task system prompt does not reference fn_task_log or task_document tools", () => {
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_log");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_document_write");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_document_read");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_document");
});
it("no-task system prompt references only available tools", () => {
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("task_create");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("list_agents");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("delegate_task");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("send_message");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("read_messages");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("memory_search");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("memory_get");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("memory_append");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("heartbeat_done");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_task_create");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_list_agents");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_delegate_task");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_send_message");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_read_messages");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_memory_search");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_memory_get");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_memory_append");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_heartbeat_done");
});
it("task-scoped system prompt still references task_log and task_document tools", () => {
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("task_log");
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("task_document_write");
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("task_document tools");
it("task-scoped system prompt still references fn_task_log and fn_task_document tools", () => {
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("fn_task_log");
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("fn_task_document_write");
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("fn_task_document tools");
});
it("both prompts include memory boundaries section", () => {
@@ -2648,9 +2648,9 @@ describe("HeartbeatMonitor", () => {
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("reply_to_message_id");
});
it("no-task system prompt processing messages section does not reference task_log", () => {
it("no-task system prompt processing messages section does not reference fn_task_log", () => {
const processingMessagesSection = HEARTBEAT_NO_TASK_SYSTEM_PROMPT.split("## Processing Messages")[1] ?? "";
expect(processingMessagesSection).not.toContain("task_log");
expect(processingMessagesSection).not.toContain("fn_task_log");
});
it("creates session with enriched system prompt and expected tools", async () => {
@@ -2678,24 +2678,24 @@ describe("HeartbeatMonitor", () => {
expect(callArgs.systemPrompt).toContain("Recent runs found flaky tests in integration suites.");
expect(callArgs.systemPrompt).toContain("Always log blockers with actionable next steps.");
expect(callArgs.systemPrompt).toContain("## Project Memory");
expect(callArgs.systemPrompt).toContain("memory_search");
expect(callArgs.systemPrompt).toContain("task_log");
expect(callArgs.systemPrompt).toContain("task_document_write");
expect(callArgs.systemPrompt).toContain("fn_memory_search");
expect(callArgs.systemPrompt).toContain("fn_task_log");
expect(callArgs.systemPrompt).toContain("fn_task_document_write");
expect(callArgs.tools).toBe("readonly");
// Tools: task_create, task_log, task_document_write, task_document_read, list_agents, delegate_task,
// memory_search, memory_get, memory_append, heartbeat_done
// Tools: fn_task_create, fn_task_log, fn_task_document_write, fn_task_document_read, fn_list_agents, fn_delegate_task,
// fn_memory_search, fn_memory_get, fn_memory_append, fn_heartbeat_done
expect(callArgs.customTools).toHaveLength(10);
expect(callArgs.customTools![0]!.name).toBe("task_create");
expect(callArgs.customTools![1]!.name).toBe("task_log");
expect(callArgs.customTools![2]!.name).toBe("task_document_write");
expect(callArgs.customTools![3]!.name).toBe("task_document_read");
expect(callArgs.customTools![4]!.name).toBe("list_agents");
expect(callArgs.customTools![5]!.name).toBe("delegate_task");
expect(callArgs.customTools![6]!.name).toBe("memory_search");
expect(callArgs.customTools![7]!.name).toBe("memory_get");
expect(callArgs.customTools![8]!.name).toBe("memory_append");
// heartbeat_done is last (terminal tool)
expect(callArgs.customTools![9]!.name).toBe("heartbeat_done");
expect(callArgs.customTools![0]!.name).toBe("fn_task_create");
expect(callArgs.customTools![1]!.name).toBe("fn_task_log");
expect(callArgs.customTools![2]!.name).toBe("fn_task_document_write");
expect(callArgs.customTools![3]!.name).toBe("fn_task_document_read");
expect(callArgs.customTools![4]!.name).toBe("fn_list_agents");
expect(callArgs.customTools![5]!.name).toBe("fn_delegate_task");
expect(callArgs.customTools![6]!.name).toBe("fn_memory_search");
expect(callArgs.customTools![7]!.name).toBe("fn_memory_get");
expect(callArgs.customTools![8]!.name).toBe("fn_memory_append");
// fn_heartbeat_done is last (terminal tool)
expect(callArgs.customTools![9]!.name).toBe("fn_heartbeat_done");
});
it("includes memory instructions even when agent has no custom instructions", async () => {
@@ -2736,12 +2736,12 @@ describe("HeartbeatMonitor", () => {
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
expect(callArgs.systemPrompt).not.toContain("## Project Memory");
expect(toolNames).not.toContain("memory_search");
expect(toolNames).not.toContain("memory_get");
expect(toolNames).not.toContain("memory_append");
expect(toolNames).not.toContain("fn_memory_search");
expect(toolNames).not.toContain("fn_memory_get");
expect(toolNames).not.toContain("fn_memory_append");
});
it("wires user-created agent memory into the memory_search tool", async () => {
it("wires user-created agent memory into the fn_memory_search tool", async () => {
const store = createStoreWithAgentForExec({
name: "CEO",
memory: "Prioritize roadmap sequencing and delegate implementation follow-ups.",
@@ -2759,7 +2759,7 @@ describe("HeartbeatMonitor", () => {
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const memorySearch = callArgs.customTools!.find((tool: any) => tool.name === "memory_search") as any;
const memorySearch = callArgs.customTools!.find((tool: any) => tool.name === "fn_memory_search") as any;
expect(memorySearch).toBeDefined();
const result = await memorySearch.execute("call-1", {
query: "roadmap delegate",
@@ -2784,11 +2784,11 @@ describe("HeartbeatMonitor", () => {
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const toolNames = callArgs.customTools!.map((t: any) => t.name);
expect(toolNames).toContain("task_document_write");
expect(toolNames).toContain("task_document_read");
expect(toolNames).toContain("fn_task_document_write");
expect(toolNames).toContain("fn_task_document_read");
});
it("heartbeat_done is the terminal tool (last in array)", async () => {
it("fn_heartbeat_done is the terminal tool (last in array)", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
@@ -2801,8 +2801,8 @@ describe("HeartbeatMonitor", () => {
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const toolNames = callArgs.customTools!.map((t: any) => t.name);
// heartbeat_done should be last for stable terminal signaling
expect(toolNames[toolNames.length - 1]).toBe("heartbeat_done");
// fn_heartbeat_done should be last for stable terminal signaling
expect(toolNames[toolNames.length - 1]).toBe("fn_heartbeat_done");
});
it("calls promptWithFallback with task context", async () => {
@@ -2929,10 +2929,10 @@ describe("HeartbeatMonitor", () => {
// Should have fetched the override task
expect(mockTaskStore.getTask).toHaveBeenCalledWith("FN-OVERRIDE");
// task_log tool should use the override task ID
// fn_task_log tool should use the override task ID
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const taskLogTool = callArgs.customTools![1]!;
expect(taskLogTool.name).toBe("task_log");
expect(taskLogTool.name).toBe("fn_task_log");
});
it("passes model config from agent runtimeConfig to createFnAgent", async () => {
@@ -3035,20 +3035,20 @@ describe("HeartbeatMonitor", () => {
});
});
describe("heartbeat_done tool", () => {
it("captures summary from heartbeat_done in resultJson", async () => {
describe("fn_heartbeat_done tool", () => {
it("captures summary from fn_heartbeat_done in resultJson", async () => {
const store = createStoreWithAgentForExec();
let capturedDoneTool: any;
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
// heartbeat_done is last in the customTools array (index 4)
// fn_heartbeat_done is last in the customTools array (index 4)
capturedDoneTool = opts.customTools[opts.customTools.length - 1];
return { session: mockSession as any };
});
// Simulate: when prompt is called, invoke the heartbeat_done tool
// Simulate: when prompt is called, invoke the fn_heartbeat_done tool
mockSession.prompt = vi.fn().mockImplementation(async (prompt: string) => {
// Simulate the agent calling heartbeat_done
// Simulate the agent calling fn_heartbeat_done
const result = await capturedDoneTool.execute("call-1", { summary: "Checked task, all good" });
expect(result.content[0].text).toContain("Heartbeat complete");
expect(result.content[0].text).toContain("Checked task, all good");
@@ -3062,7 +3062,7 @@ describe("HeartbeatMonitor", () => {
expect((run.resultJson as any).summary).toBe("Checked task, all good");
});
it("works without summary in heartbeat_done", async () => {
it("works without summary in fn_heartbeat_done", async () => {
const store = createStoreWithAgentForExec();
let capturedDoneTool: any;
const mockSession = createMockAgentSession();
@@ -3084,13 +3084,13 @@ describe("HeartbeatMonitor", () => {
});
});
describe("task_create tool", () => {
it("creates a task in the store when task_create tool is called", async () => {
describe("fn_task_create tool", () => {
it("creates a task in the store when fn_task_create tool is called", async () => {
const store = createStoreWithAgentForExec();
let capturedCreateTool: any;
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
capturedCreateTool = opts.customTools[0]; // task_create
capturedCreateTool = opts.customTools[0]; // fn_task_create
return { session: mockSession as any };
});
@@ -3512,22 +3512,22 @@ describe("HeartbeatMonitor", () => {
mockTaskStore = createMockTaskStoreForTools();
});
it("returns task_create, task_log, task_document_write, and task_document_read tools", () => {
it("returns fn_task_create, fn_task_log, fn_task_document_write, and fn_task_document_read tools", () => {
const store = createMockStore();
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
expect(tools).toHaveLength(6);
expect(tools[0]!.name).toBe("task_create");
expect(tools[1]!.name).toBe("task_log");
expect(tools[2]!.name).toBe("task_document_write");
expect(tools[3]!.name).toBe("task_document_read");
expect(tools[4]!.name).toBe("list_agents");
expect(tools[5]!.name).toBe("delegate_task");
expect(tools[0]!.name).toBe("fn_task_create");
expect(tools[1]!.name).toBe("fn_task_log");
expect(tools[2]!.name).toBe("fn_task_document_write");
expect(tools[3]!.name).toBe("fn_task_document_read");
expect(tools[4]!.name).toBe("fn_list_agents");
expect(tools[5]!.name).toBe("fn_delegate_task");
});
it("task_create tool creates a task in triage via TaskStore", async () => {
it("fn_task_create tool creates a task in triage via TaskStore", async () => {
const store = createMockStore();
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
@@ -3548,7 +3548,7 @@ describe("HeartbeatMonitor", () => {
expect(result.details).toEqual({ taskId: "FN-100" });
});
it("task_create details includes taskId matching mock store return", async () => {
it("fn_task_create details includes taskId matching mock store return", async () => {
const store = createMockStore();
const matchingStore = createMockTaskStoreForTools({
createTask: vi.fn().mockResolvedValue({
@@ -3566,7 +3566,7 @@ describe("HeartbeatMonitor", () => {
expect((result.details as any).taskId).toBe("ZX-321");
});
it("task_create tracking uses details.taskId for non-standard ID prefixes", async () => {
it("fn_task_create tracking uses details.taskId for non-standard ID prefixes", async () => {
const store = createMockStore();
const prefixedTaskStore = createMockTaskStoreForTools({
createTask: vi.fn().mockResolvedValue({
@@ -3589,10 +3589,10 @@ describe("HeartbeatMonitor", () => {
);
});
it("task_create tracking falls back to unknown when details has no taskId", async () => {
it("fn_task_create tracking falls back to unknown when details has no taskId", async () => {
const store = createMockStore();
const createTaskCreateToolSpy = vi.spyOn(agentTools, "createTaskCreateTool").mockReturnValue({
name: "task_create",
name: "fn_task_create",
label: "Create Task",
description: "Create a task",
parameters: {} as any,
@@ -3618,7 +3618,7 @@ describe("HeartbeatMonitor", () => {
}
});
it("task_create tracking handles missing details gracefully", async () => {
it("fn_task_create tracking handles missing details gracefully", async () => {
const store = createMockStore();
const missingDetailsTaskStore = createMockTaskStoreForTools({
createTask: vi.fn().mockResolvedValue({
@@ -3685,12 +3685,12 @@ describe("HeartbeatMonitor", () => {
expect(mockTaskStore.createTask).toHaveBeenCalled();
});
it("task_document_write tool persists documents via TaskStore", async () => {
it("fn_task_document_write tool persists documents via TaskStore", async () => {
const store = createMockStore();
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
const writeTool = tools.find((t) => t.name === "task_document_write")!;
const writeTool = tools.find((t) => t.name === "fn_task_document_write")!;
const result = await writeTool.execute("call-1", { key: "plan", content: "Implementation plan here" }, undefined as any, undefined as any, undefined as any);
@@ -3705,7 +3705,7 @@ describe("HeartbeatMonitor", () => {
expect(responseText).toContain("plan");
});
it("task_document_read tool reads specific document by key", async () => {
it("fn_task_document_read tool reads specific document by key", async () => {
const store = createMockStore();
mockTaskStore.getTaskDocument = vi.fn().mockResolvedValue({
id: "doc-1",
@@ -3720,7 +3720,7 @@ describe("HeartbeatMonitor", () => {
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
const readTool = tools.find((t) => t.name === "task_document_read")!;
const readTool = tools.find((t) => t.name === "fn_task_document_read")!;
const result = await readTool.execute("call-1", { key: "plan" }, undefined as any, undefined as any, undefined as any);
@@ -3731,7 +3731,7 @@ describe("HeartbeatMonitor", () => {
expect(responseText).toContain("Implementation plan content");
});
it("task_document_read tool lists all documents when key is omitted", async () => {
it("fn_task_document_read tool lists all documents when key is omitted", async () => {
const store = createMockStore();
mockTaskStore.getTaskDocuments = vi.fn().mockResolvedValue([
{ id: "doc-1", taskId: "FN-001", key: "plan", content: "", revision: 1, author: "agent", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
@@ -3740,7 +3740,7 @@ describe("HeartbeatMonitor", () => {
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
const readTool = tools.find((t) => t.name === "task_document_read")!;
const readTool = tools.find((t) => t.name === "fn_task_document_read")!;
const result = await readTool.execute("call-1", { key: undefined }, undefined as any, undefined as any, undefined as any);
@@ -5027,8 +5027,8 @@ describe("HeartbeatTriggerScheduler", () => {
// Create tools with run context
const tools = monitor.createHeartbeatTools("agent-456", mockTaskStore, "FN-001", runContext);
// Find the task_log tool and execute it
const taskLogTool = tools.find(t => t.name === "task_log");
// Find the fn_task_log tool and execute it
const taskLogTool = tools.find(t => t.name === "fn_task_log");
expect(taskLogTool).toBeDefined();
const result = await taskLogTool!.execute("call-1", { message: "Test log entry", outcome: undefined }, undefined as any, undefined as any, undefined as any);
@@ -5066,8 +5066,8 @@ describe("HeartbeatTriggerScheduler", () => {
// Create tools with run context
const tools = monitor.createHeartbeatTools("agent-abc", mockTaskStore, "FN-001", runContext);
// Find the task_create tool and execute it
const taskCreateTool = tools.find(t => t.name === "task_create");
// Find the fn_task_create tool and execute it
const taskCreateTool = tools.find(t => t.name === "fn_task_create");
expect(taskCreateTool).toBeDefined();
const result = await taskCreateTool!.execute("call-1", { description: "New task created" }, undefined as any, undefined as any, undefined as any);
@@ -5103,8 +5103,8 @@ describe("HeartbeatTriggerScheduler", () => {
// Create tools without run context
const tools = monitor.createHeartbeatTools("agent-456", mockTaskStore, "FN-001");
// Find the task_log tool and execute it
const taskLogTool = tools.find(t => t.name === "task_log");
// Find the fn_task_log tool and execute it
const taskLogTool = tools.find(t => t.name === "fn_task_log");
expect(taskLogTool).toBeDefined();
const result = await taskLogTool!.execute("call-1", { message: "Test log entry", outcome: undefined }, undefined as any, undefined as any, undefined as any);

View File

@@ -8,7 +8,7 @@
*
* When `executeHeartbeat()` is called (via API, timer, or assignment),
* the system wakes the agent, checks its assigned task from AgentStore,
* executes work in a lightweight agent session with `task_create` capability,
* executes work in a lightweight agent session with `fn_task_create` capability,
* records results, and transitions the run to completed.
*
* Callback pattern (not EventEmitter):
@@ -62,7 +62,7 @@ export interface HeartbeatMonitorOptions {
onRunStarted?: (agentId: string, run: AgentHeartbeatRun) => void;
/** Callback when a run completes */
onRunCompleted?: (agentId: string, run: AgentHeartbeatRun) => void;
/** TaskStore for task_create and task_log tools during heartbeat execution.
/** TaskStore for fn_task_create and fn_task_log tools during heartbeat execution.
* When not provided, executeHeartbeat() will throw. */
taskStore?: TaskStore;
/** Project root directory for agent session CWD.
@@ -125,21 +125,21 @@ export function isBlockedStateDuplicate(current: BlockedStateSnapshot, previous:
/**
* System prompt for heartbeat agent sessions.
* Instructs the agent to perform a single-pass check on its assigned task
* and use `task_create` / `task_log` / task documents to record findings or spawn follow-up work.
* and use `fn_task_create` / `fn_task_log` / `fn_task_document_*` tools to record findings or spawn follow-up work.
*/
export const HEARTBEAT_SYSTEM_PROMPT = `You are a heartbeat agent running in a short execution window.
Your job:
1. Check your assigned task — read the description and PROMPT.md if present.
2. Do ONE useful action: analyze, review, create follow-up tasks, or log findings.
3. Use task_create to spawn follow-up work, task_log to record observations.
4. Use task_document_write to save durable findings, plans, or research notes.
5. Call heartbeat_done when finished with an optional summary of what was accomplished.
3. Use fn_task_create to spawn follow-up work, fn_task_log to record observations.
4. Use fn_task_document_write to save durable findings, plans, or research notes.
5. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
Keep work lightweight — this is a single-pass check, not a full implementation run.
You have readonly file access plus task_create, task_log, and task_document tools.
You have readonly file access plus fn_task_create, fn_task_log, and fn_task_document tools.
**Task Documents:** Save important findings with task_document_write(key="...", content="...").
**Task Documents:** Save important findings with fn_task_document_write(key="...", content="...").
Documents persist across sessions and are visible in the dashboard's Documents tab.
## Memory Boundaries
@@ -152,12 +152,12 @@ You may receive an Agent Memory section and a Project Memory section.
## Processing Messages
When you are woken by an incoming message (source includes "wake-on-message"), you should:
1. Use read_messages to check your inbox for unread messages.
1. Use fn_read_messages to check your inbox for unread messages.
2. Review each message and determine the appropriate action:
- If the message requires a response, use send_message to reply.
- When replying, include 'reply_to_message_id' with the original message ID from read_messages output.
- If the message is informational, acknowledge it by logging with task_log.
- If the message requests work, create a follow-up task with task_create or handle it directly.
- If the message requires a response, use fn_send_message to reply.
- When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output.
- If the message is informational, acknowledge it by logging with fn_task_log.
- If the message requests work, create a follow-up task with fn_task_create or handle it directly.
3. After processing messages, continue with your normal heartbeat duties.
When sending messages:
@@ -175,17 +175,17 @@ export const HEARTBEAT_NO_TASK_SYSTEM_PROMPT = `You are a heartbeat agent runnin
Your job:
1. Review your context — check messages, memory, and project state.
2. Do ONE useful action: analyze, create follow-up tasks, delegate work, or update memory.
3. Use task_create to spawn follow-up work.
4. Use list_agents and delegate_task to coordinate with other agents.
5. Call heartbeat_done when finished with an optional summary of what was accomplished.
3. Use fn_task_create to spawn follow-up work.
4. Use fn_list_agents and fn_delegate_task to coordinate with other agents.
5. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
Keep work lightweight — this is a single-pass ambient check, not a full implementation run.
You have readonly file access plus:
- task_create
- list_agents and delegate_task
- memory_search, memory_get, and memory_append
- heartbeat_done
- send_message and read_messages when messaging is enabled for this run (they may not always be available)
- fn_task_create
- fn_list_agents and fn_delegate_task
- fn_memory_search, fn_memory_get, and fn_memory_append
- fn_heartbeat_done
- fn_send_message and fn_read_messages when messaging is enabled for this run (they may not always be available)
## Memory Boundaries
@@ -197,12 +197,12 @@ You may receive an Agent Memory section and a Project Memory section.
## Processing Messages
When you are woken by an incoming message (source includes "wake-on-message"), you should:
1. If read_messages is available, use it to check your inbox for unread messages.
1. If fn_read_messages is available, use it to check your inbox for unread messages.
2. Review each message and determine the appropriate action:
- If the message requires a response and send_message is available, use send_message to reply.
- When replying, include 'reply_to_message_id' with the original message ID from read_messages output.
- If the message is informational, acknowledge it and respond via send_message when appropriate.
- If the message requests work, create a follow-up task with task_create.
- If the message requires a response and fn_send_message is available, use fn_send_message to reply.
- When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output.
- If the message is informational, acknowledge it and respond via fn_send_message when appropriate.
- If the message requests work, create a follow-up task with fn_task_create.
3. After processing messages, continue with your ambient work.
When sending messages:
@@ -214,7 +214,7 @@ When sending messages:
// Backward-compatible alias; prefer HEARTBEAT_NO_TASK_SYSTEM_PROMPT.
export const HEARTBEAT_SYSTEM_PROMPT_NO_TASK = HEARTBEAT_NO_TASK_SYSTEM_PROMPT;
/** Parameter schema for the heartbeat_done tool */
/** Parameter schema for the fn_heartbeat_done tool */
const heartbeatDoneParams = Type.Object({
summary: Type.Optional(Type.String({ description: "Summary of what was accomplished this heartbeat" })),
});
@@ -726,7 +726,7 @@ export class HeartbeatMonitor {
* Implements the Paperclip-style execution model:
* 1. Wake — start a heartbeat run record
* 2. Check inbox — resolve the agent's assigned task
* 3. Work — run a lightweight agent session with readonly tools + task_create/task_log
* 3. Work — run a lightweight agent session with readonly tools + fn_task_create/fn_task_log
* 4. Exit — record results and complete the run
*
* Budget governance:
@@ -1053,9 +1053,9 @@ export class HeartbeatMonitor {
stdoutExcerpt += delta.slice(0, remaining);
};
// Create heartbeat_done tool
// Create fn_heartbeat_done tool
const heartbeatDoneTool: ToolDefinition = {
name: "heartbeat_done",
name: "fn_heartbeat_done",
label: "Heartbeat Done",
description: "Signal that the heartbeat execution is complete. Call when finished.",
parameters: heartbeatDoneParams,
@@ -1079,13 +1079,13 @@ export class HeartbeatMonitor {
const { buildSessionSkillContextSync } = await import("./session-skill-context.js");
// Build tools with task creation tracking and run context for mutation correlation
// For no-task runs, exclude task_log and document tools (they require a taskId)
// For no-task runs, exclude fn_task_log and document tools (they require a taskId)
let heartbeatTools: ToolDefinition[];
if (isNoTaskRun) {
// No-task runs: task_create, list_agents, delegate_task, messaging, memory, heartbeat_done
// No-task runs: fn_task_create, fn_list_agents, fn_delegate_task, messaging, memory, fn_heartbeat_done
heartbeatTools = [];
// task_create tool (no tracking needed for no-task runs)
// fn_task_create tool (no tracking needed for no-task runs)
heartbeatTools.push(createTaskCreateTool(taskStore));
// Agent delegation tools
@@ -1098,7 +1098,7 @@ export class HeartbeatMonitor {
heartbeatTools.push(createReadMessagesTool(this.messageStore, agentId));
}
} else {
// Task-scoped runs: full tool set including task_log and document tools
// Task-scoped runs: full tool set including fn_task_log and document tools
// taskId is guaranteed to be defined here because isNoTaskRun = !taskId
heartbeatTools = this.createHeartbeatTools(agentId, taskStore, taskId!, runContext, audit, this.messageStore);
}
@@ -1220,16 +1220,16 @@ export class HeartbeatMonitor {
"You have identity (soul, instructions, and/or memory) loaded, which means you can perform",
"useful ambient work. Here are some things you can do:",
"",
"1. **Check your messages** — Use read_messages to review any pending messages",
" and use send_message with reply_to_message_id when responding.",
"1. **Check your messages** — Use fn_read_messages to review any pending messages",
" and use fn_send_message with reply_to_message_id when responding.",
"",
"2. **Create new tasks** — Use task_create to spawn follow-up work that needs",
"2. **Create new tasks** — Use fn_task_create to spawn follow-up work that needs",
" to be done. This is useful for surfacing issues or ideas you discover.",
"",
"3. **Delegate work** — Use list_agents to discover available agents and",
" delegate_task to assign work to them.",
"3. **Delegate work** — Use fn_list_agents to discover available agents and",
" fn_delegate_task to assign work to them.",
"",
"4. **Update your memory** — Use memory_append to persist important learnings",
"4. **Update your memory** — Use fn_memory_append to persist important learnings",
" or context that will help you in future sessions.",
"",
"5. **Monitor the project** — Review the task board and identify any issues",
@@ -1238,7 +1238,7 @@ export class HeartbeatMonitor {
"",
"Your soul, instructions, and memory are already loaded in the system prompt.",
"Focus on work that benefits the project without requiring a specific task context.",
"Call heartbeat_done when finished.",
"Call fn_heartbeat_done when finished.",
].join("\n");
} else {
// Task-scoped heartbeat: agent has an assigned task
@@ -1307,7 +1307,7 @@ export class HeartbeatMonitor {
...triggeringCommentLines,
...pendingMessagesLines,
"",
"Review the task status and take appropriate action. Call heartbeat_done when finished.",
"Review the task status and take appropriate action. Call fn_heartbeat_done when finished.",
].join("\n");
}
@@ -1431,7 +1431,7 @@ export class HeartbeatMonitor {
*
* @param agentId - The agent ID (used for tracking and logging)
* @param taskStore - TaskStore for task creation and logging
* @param taskId - The assigned task ID (for task_log context)
* @param taskId - The assigned task ID (for fn_task_log context)
* @param runContext - Optional run context for mutation correlation
* @param audit - Optional run auditor for audit trail (FN-1404)
* @param messageStore - Optional MessageStore for messaging tools
@@ -1480,7 +1480,7 @@ export class HeartbeatMonitor {
};
tools.push(trackedCreateTool);
// task_log tool (with run context for mutation correlation)
// fn_task_log tool (with run context for mutation correlation)
tools.push(createTaskLogToolWithContext(taskStore, taskId, runContext));
// Document tools for persisting durable findings

View File

@@ -38,7 +38,7 @@ describe("summarizeToolArgs", () => {
});
it("falls back to first short string arg for unknown tools", () => {
expect(summarizeToolArgs("task_update", { step: 1, status: "done" })).toBe("done");
expect(summarizeToolArgs("fn_task_update", { step: 1, status: "done" })).toBe("done");
});
it("returns undefined when no args or empty args", () => {
@@ -140,10 +140,10 @@ describe("AgentLogger", () => {
const store = createMockStore();
const logger = new AgentLogger({ store, taskId: "FN-005" });
logger.onToolStart("task_done", { count: 42 });
logger.onToolStart("fn_task_done", { count: 42 });
await vi.advanceTimersByTimeAsync(0);
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-005", "task_done", "tool", undefined, undefined);
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-005", "fn_task_done", "tool", undefined, undefined);
});
it("flush() clears timer and writes remaining text", async () => {

View File

@@ -115,22 +115,22 @@ describe("createMemoryTools", () => {
expect(createMemoryTools("/repo", { memoryEnabled: false }).map((tool) => tool.name)).toEqual([]);
});
it("omits memory_append for read-only memory backends", () => {
it("omits fn_memory_append for read-only memory backends", () => {
expect(createMemoryTools("/repo", { memoryBackendType: "readonly" }).map((tool) => tool.name)).toEqual([
"memory_search",
"memory_get",
"fn_memory_search",
"fn_memory_get",
]);
});
it("includes memory_append for writable memory backends", () => {
it("includes fn_memory_append for writable memory backends", () => {
expect(createMemoryTools("/repo", { memoryBackendType: "file" }).map((tool) => tool.name)).toEqual([
"memory_search",
"memory_get",
"memory_append",
"fn_memory_search",
"fn_memory_get",
"fn_memory_append",
]);
});
it("searches per-agent memory through the memory_search tool", async () => {
it("searches per-agent memory through the fn_memory_search tool", async () => {
const [searchTool, getTool] = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
agentMemory: {
agentId: "ceo-agent",
@@ -180,7 +180,7 @@ describe("createMemoryTools", () => {
.resolves.toContain("Agent Daily Memory");
});
it("appends to this agent's daily memory through memory_append", async () => {
it("appends to this agent's daily memory through fn_memory_append", async () => {
const tools = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
agentMemory: {
agentId: "ceo-agent",
@@ -188,7 +188,7 @@ describe("createMemoryTools", () => {
memory: "The CEO agent should prioritize roadmap sequencing and delegation.",
},
});
const appendTool = tools.find((tool) => tool.name === "memory_append")!;
const appendTool = tools.find((tool) => tool.name === "fn_memory_append")!;
const result = await (appendTool as any).execute("call-1", {
scope: "agent",
@@ -202,7 +202,7 @@ describe("createMemoryTools", () => {
expect(result.details).toEqual({ scope: "agent", layer: "daily" });
});
it("memory_get reads agent dreams returned by memory_search", async () => {
it("fn_memory_get reads agent dreams returned by fn_memory_search", async () => {
const [, getTool, appendTool] = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
agentMemory: {
agentId: "ceo-agent",
@@ -318,7 +318,7 @@ describe("createMemoryTools", () => {
memory: "Roadmap delegation priorities are tracked here.",
},
});
const appendTool = tools.find((tool) => tool.name === "memory_append")!;
const appendTool = tools.find((tool) => tool.name === "fn_memory_append")!;
const result = await (appendTool as any).execute("call-1", {
scope: "agent",
@@ -376,8 +376,8 @@ describe("createSendMessageTool", () => {
return tool.execute("call-1", params, undefined, undefined, undefined);
};
it("creates a tool with name 'send_message'", () => {
expect(tool.name).toBe("send_message");
it("creates a tool with name 'fn_send_message'", () => {
expect(tool.name).toBe("fn_send_message");
});
it("creates a tool with correct label", () => {
@@ -545,8 +545,8 @@ describe("createReadMessagesTool", () => {
return tool.execute("call-1", params, undefined, undefined, undefined);
};
it("creates a tool with name 'read_messages'", () => {
expect(tool.name).toBe("read_messages");
it("creates a tool with name 'fn_read_messages'", () => {
expect(tool.name).toBe("fn_read_messages");
});
it("creates a tool with correct label", () => {

View File

@@ -80,7 +80,7 @@ export const sendMessageParams = Type.Object({
Type.Literal("agent-to-user"),
], { description: "Message type (defaults to 'agent-to-agent')" })),
reply_to_message_id: Type.Optional(
Type.String({ description: "Optional ID of the message you are replying to (use IDs from read_messages output)" }),
Type.String({ description: "Optional ID of the message you are replying to (use IDs from fn_read_messages output)" }),
),
});
@@ -95,7 +95,7 @@ export const memorySearchParams = Type.Object({
});
export const memoryGetParams = Type.Object({
path: Type.String({ description: "Memory path from memory_search, e.g. .fusion/memory/MEMORY.md or .fusion/memory/YYYY-MM-DD.md" }),
path: Type.String({ description: "Memory path from fn_memory_search, e.g. .fusion/memory/MEMORY.md or .fusion/memory/YYYY-MM-DD.md" }),
startLine: Type.Optional(Type.Number({ description: "1-based start line (default: 1)" })),
lineCount: Type.Optional(Type.Number({ description: "Number of lines to read (default: 120, max: 400)" })),
});
@@ -415,14 +415,14 @@ async function getAgentMemoryWindow(rootDir: string, agentMemory: AgentMemoryCon
// ── Tool factory functions ────────────────────────────────────────────────
/**
* Create a `task_create` tool that creates a new task in triage.
* Create a `fn_task_create` tool that creates a new task in triage.
*
* @param store - TaskStore for task persistence
* @returns ToolDefinition for the `task_create` tool
* @returns ToolDefinition for the `fn_task_create` tool
*/
export function createTaskCreateTool(store: TaskStore): ToolDefinition {
return {
name: "task_create",
name: "fn_task_create",
label: "Create Task",
description:
"Create a new task for out-of-scope work discovered during execution. " +
@@ -449,15 +449,15 @@ export function createTaskCreateTool(store: TaskStore): ToolDefinition {
}
/**
* Create a `task_log` tool that logs an entry for a specific task.
* Create a `fn_task_log` tool that logs an entry for a specific task.
*
* @param store - TaskStore for task persistence
* @param taskId - The task ID to log entries against
* @returns ToolDefinition for the `task_log` tool
* @returns ToolDefinition for the `fn_task_log` tool
*/
export function createTaskLogTool(store: TaskStore, taskId: string): ToolDefinition {
return {
name: "task_log",
name: "fn_task_log",
label: "Log Entry",
description:
"Log an important action, decision, or issue for this task. " +
@@ -474,16 +474,16 @@ export function createTaskLogTool(store: TaskStore, taskId: string): ToolDefinit
}
/**
* Create a `task_log` tool with run context for mutation correlation.
* Create a `fn_task_log` tool with run context for mutation correlation.
*
* @param store - TaskStore for task persistence
* @param taskId - The task ID to log entries against
* @param runContext - Optional run context for mutation correlation
* @returns ToolDefinition for the `task_log` tool
* @returns ToolDefinition for the `fn_task_log` tool
*/
export function createTaskLogToolWithContext(store: TaskStore, taskId: string, runContext?: RunMutationContext): ToolDefinition {
return {
name: "task_log",
name: "fn_task_log",
label: "Log Entry",
description:
"Log an important action, decision, or issue for this task. " +
@@ -500,15 +500,15 @@ export function createTaskLogToolWithContext(store: TaskStore, taskId: string, r
}
/**
* Create a `task_document_write` tool that stores a named task document.
* Create a `fn_task_document_write` tool that stores a named task document.
*
* @param store - TaskStore for task document persistence
* @param taskId - The task ID to write documents against
* @returns ToolDefinition for the `task_document_write` tool
* @returns ToolDefinition for the `fn_task_document_write` tool
*/
export function createTaskDocumentWriteTool(store: TaskStore, taskId: string): ToolDefinition {
return {
name: "task_document_write",
name: "fn_task_document_write",
label: "Write Document",
description:
"Save a named document for this task (for example plan, notes, or research). " +
@@ -545,15 +545,15 @@ export function createTaskDocumentWriteTool(store: TaskStore, taskId: string): T
}
/**
* Create a `task_document_read` tool that reads task-scoped documents.
* Create a `fn_task_document_read` tool that reads task-scoped documents.
*
* @param store - TaskStore for task document reads
* @param taskId - The task ID to read documents from
* @returns ToolDefinition for the `task_document_read` tool
* @returns ToolDefinition for the `fn_task_document_read` tool
*/
export function createTaskDocumentReadTool(store: TaskStore, taskId: string): ToolDefinition {
return {
name: "task_document_read",
name: "fn_task_document_read",
label: "Read Document",
description:
"Read a named document for this task, or list all documents when no key is provided.",
@@ -614,11 +614,11 @@ export function createTaskDocumentReadTool(store: TaskStore, taskId: string): To
export function createMemorySearchTool(rootDir: string, settings?: MemoryToolSettings, options?: MemoryToolOptions): ToolDefinition {
return {
name: "memory_search",
name: "fn_memory_search",
label: "Search Memory",
description:
"Search durable project memory and this agent's own memory, returning small snippets with file paths and line ranges. " +
"Use this before memory_get; do not read all memory by default.",
"Use this before fn_memory_get; do not read all memory by default.",
parameters: memorySearchParams,
execute: async (_id: string, params: Static<typeof memorySearchParams>) => {
const limit = params.limit ?? 5;
@@ -653,10 +653,10 @@ export function createMemorySearchTool(rootDir: string, settings?: MemoryToolSet
export function createMemoryGetTool(rootDir: string, settings?: MemoryToolSettings, options?: MemoryToolOptions): ToolDefinition {
return {
name: "memory_get",
name: "fn_memory_get",
label: "Get Memory",
description:
"Read a bounded line window from a memory file returned by memory_search. " +
"Read a bounded line window from a memory file returned by fn_memory_search. " +
"Allowed files include project memory under .fusion/memory/ and this agent's own .fusion/agent-memory/{agentId}/MEMORY.md file.",
parameters: memoryGetParams,
execute: async (_id: string, params: Static<typeof memoryGetParams>) => {
@@ -690,7 +690,7 @@ export function createMemoryGetTool(rootDir: string, settings?: MemoryToolSettin
export function createMemoryAppendTool(rootDir: string, settings?: MemoryToolSettings, options?: MemoryToolOptions): ToolDefinition {
return {
name: "memory_append",
name: "fn_memory_append",
label: "Append Memory",
description:
"Append concise Markdown to project memory. Use long-term only for durable conventions/decisions/pitfalls; " +
@@ -755,7 +755,7 @@ export function createMemoryTools(rootDir: string, settings?: MemoryToolSettings
}
/**
* Create a `reflect_on_performance` tool that asks the reflection service to
* Create a `fn_reflect_on_performance` tool that asks the reflection service to
* analyze recent agent performance and return actionable insights.
*/
export function createReflectOnPerformanceTool(
@@ -763,7 +763,7 @@ export function createReflectOnPerformanceTool(
agentId: string,
): ToolDefinition {
return {
name: "reflect_on_performance",
name: "fn_reflect_on_performance",
label: "Reflect on Performance",
description:
'Review your past task performance and generate insights for improvement. Optionally focus on a specific area like "code quality", "speed", or "testing".',
@@ -803,14 +803,14 @@ export function createReflectOnPerformanceTool(
}
/**
* Create a `list_agents` tool that lists all available agents.
* Create a `fn_list_agents` tool that lists all available agents.
*
* @param agentStore - AgentStore for agent discovery
* @returns ToolDefinition for the `list_agents` tool
* @returns ToolDefinition for the `fn_list_agents` tool
*/
export function createListAgentsTool(agentStore: AgentStore): ToolDefinition {
return {
name: "list_agents",
name: "fn_list_agents",
label: "List Agents",
description:
"List all available agents in the system. Shows each agent's name, role, state, " +
@@ -860,20 +860,20 @@ export function createListAgentsTool(agentStore: AgentStore): ToolDefinition {
}
/**
* Create a `delegate_task` tool that creates and assigns a task to a specific agent.
* Create a `fn_delegate_task` tool that creates and assigns a task to a specific agent.
*
* @param agentStore - AgentStore for agent lookup
* @param taskStore - TaskStore for task creation
* @returns ToolDefinition for the `delegate_task` tool
* @returns ToolDefinition for the `fn_delegate_task` tool
*/
export function createDelegateTaskTool(agentStore: AgentStore, taskStore: TaskStore): ToolDefinition {
return {
name: "delegate_task",
name: "fn_delegate_task",
label: "Delegate Task",
description:
"Create a new task and assign it to a specific agent for execution. The task goes to " +
"'todo' and will be picked up by the target agent on their next heartbeat cycle. " +
"Use list_agents first to find available agents and their capabilities.",
"Use fn_list_agents first to find available agents and their capabilities.",
parameters: delegateTaskParams,
execute: async (_id: string, params: Static<typeof delegateTaskParams>) => {
// Validate target agent exists
@@ -915,15 +915,15 @@ export function createDelegateTaskTool(agentStore: AgentStore, taskStore: TaskSt
}
/**
* Create a `send_message` tool that sends a message to another agent or user.
* Create a `fn_send_message` tool that sends a message to another agent or user.
*
* @param messageStore - MessageStore for message persistence
* @param fromAgentId - The agent ID sending the message
* @returns ToolDefinition for the `send_message` tool
* @returns ToolDefinition for the `fn_send_message` tool
*/
export function createSendMessageTool(messageStore: MessageStore, fromAgentId: string): ToolDefinition {
return {
name: "send_message",
name: "fn_send_message",
label: "Send Message",
description:
"Send a message to another agent or user. The recipient will be woken if they have " +
@@ -988,15 +988,15 @@ export function createSendMessageTool(messageStore: MessageStore, fromAgentId: s
}
/**
* Create a `read_messages` tool that reads inbox messages for an agent.
* Create a `fn_read_messages` tool that reads inbox messages for an agent.
*
* @param messageStore - MessageStore for message retrieval
* @param agentId - The agent ID whose inbox to read
* @returns ToolDefinition for the `read_messages` tool
* @returns ToolDefinition for the `fn_read_messages` tool
*/
export function createReadMessagesTool(messageStore: MessageStore, agentId: string): ToolDefinition {
return {
name: "read_messages",
name: "fn_read_messages",
label: "Read Messages",
description: "Read your inbox messages. Returns unread messages by default.",
parameters: readMessagesParams,

File diff suppressed because it is too large Load Diff

View File

@@ -73,9 +73,9 @@ const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"
/** Maximum retry attempts for workflow step hard failures before giving up */
const MAX_WORKFLOW_STEP_RETRIES = 3;
/** Maximum in-session retries when an agent exits without calling task_done(). */
/** Maximum in-session retries when an agent exits without calling fn_task_done(). */
const MAX_TASK_DONE_SESSION_RETRIES = 3;
/** Maximum todo requeues after exhausting in-session task_done retries. */
/** Maximum todo requeues after exhausting in-session fn_task_done retries. */
const MAX_TASK_DONE_REQUEUE_RETRIES = 3;
/**
@@ -197,7 +197,7 @@ const spawnAgentParams = Type.Object({
task: Type.String({ description: "Task description for the child agent to execute" }),
});
/** Result returned from spawn_agent tool */
/** Result returned from fn_spawn_agent tool */
interface SpawnAgentResult {
agentId: string;
name: string;
@@ -260,22 +260,22 @@ You are working in a git worktree isolated from the main branch. Your job is to
You have tools to report progress. The board updates in real-time.
**Step lifecycle:**
- Before starting a step: \`task_update(step=N, status="in-progress")\`
- After completing a step: \`task_update(step=N, status="done")\`
- If skipping a step: \`task_update(step=N, status="skipped")\`
- Before starting a step: \`fn_task_update(step=N, status="in-progress")\`
- After completing a step: \`fn_task_update(step=N, status="done")\`
- If skipping a step: \`fn_task_update(step=N, status="skipped")\`
**Logging important actions:** \`task_log(message="what happened")\`
**Logging important actions:** \`fn_task_log(message="what happened")\`
**Out-of-scope work found during execution:** \`task_create(description="what needs doing")\`
**Out-of-scope work found during execution:** \`fn_task_create(description="what needs doing")\`
When creating multiple related tasks, declare dependencies between them:
\`task_create(description="load door sounds", dependencies=[])\` → returns KB-050
\`task_create(description="play sound on door open/close", dependencies=["KB-050"])\`
\`fn_task_create(description="load door sounds", dependencies=[])\` → returns KB-050
\`fn_task_create(description="play sound on door open/close", dependencies=["KB-050"])\`
**Discovered a dependency:** \`task_add_dep(task_id="KB-XXX")\` — use when you discover mid-execution that another task must be completed first. This will return a warning first — you must call again with \`confirm=true\` to proceed. Adding a dependency stops execution, discards current work, and moves the task to triage for re-specification.
**Discovered a dependency:** \`fn_task_add_dep(task_id="KB-XXX")\` — use when you discover mid-execution that another task must be completed first. This will return a warning first — you must call again with \`confirm=true\` to proceed. Adding a dependency stops execution, discards current work, and moves the task to triage for re-specification.
## Cross-model review via review_step tool
## Cross-model review via fn_review_step tool
You have a \`review_step\` tool. It spawns a SEPARATE reviewer agent (different
You have a \`fn_review_step\` tool. It spawns a SEPARATE reviewer agent (different
model, read-only access) to independently assess your work.
**When to call it** — based on the Review Level in the PROMPT.md:
@@ -283,8 +283,8 @@ model, read-only access) to independently assess your work.
| Review Level | Before implementing | After implementing + committing |
|-------------|--------------------|---------------------------------|
| 0 (None) | — | — |
| 1 (Plan) | \`review_step(step, "plan", step_name)\` | — |
| 2 (Plan+Code) | \`review_step(step, "plan", step_name)\` | \`review_step(step, "code", step_name, baseline)\` |
| 1 (Plan) | \`fn_review_step(step, "plan", step_name)\` | — |
| 2 (Plan+Code) | \`fn_review_step(step, "plan", step_name)\` | \`fn_review_step(step, "code", step_name, baseline)\` |
| 3 (Full) | plan review | code review + test review |
**Skip reviews for** Step 0 (Preflight) and the final documentation/delivery step.
@@ -293,13 +293,13 @@ model, read-only access) to independently assess your work.
1. Before starting a step, capture baseline: \`git rev-parse HEAD\`
2. Implement the step
3. Commit
4. Call \`review_step\` with the baseline SHA so the reviewer sees only your changes
4. Call \`fn_review_step\` with the baseline SHA so the reviewer sees only your changes
**Handling verdicts:**
- **APPROVE** → proceed to next step
- **REVISE (code review)** → **enforced**. You MUST fix the issues, commit again,
and re-run \`review_step(type="code")\` before the step can be marked done.
\`task_update(status="done")\` will be rejected until the code review passes.
and re-run \`fn_review_step(type="code")\` before the step can be marked done.
\`fn_task_update(status="done")\` will be rejected until the code review passes.
- **REVISE (plan review)** → advisory. Incorporate the feedback at your discretion
and proceed with implementation. No re-review is required.
- **RETHINK (code review)** → your code changes have been reverted and conversation rewound. Read the feedback carefully and take a fundamentally different approach. Do NOT repeat the rejected strategy.
@@ -309,13 +309,13 @@ model, read-only access) to independently assess your work.
You can save and retrieve named documents for this task. Use these to store planning notes, research findings, or any persistent data that should survive across sessions.
- **Save a document:** \`task_document_write(key="plan", content="...")\`
- **Read a document:** \`task_document_read(key="plan")\`
- **List all documents:** \`task_document_read()\` (no key)
- **Save a document:** \`fn_task_document_write(key="plan", content="...")\`
- **Read a document:** \`fn_task_document_read(key="plan")\`
- **List all documents:** \`fn_task_document_read()\` (no key)
Documents are versioned — each write creates a new revision. Use meaningful keys like "plan", "notes", "research", "architecture".
**IMPORTANT — Save your deliverables as documents:** When your task produces written output (documentation, specifications, reports, API references, README updates, guides, or any other content), you MUST save that content as a task document using \`task_document_write\`. Use a key that describes the deliverable (e.g., key="readme", key="api-docs", key="changelog"). Do this in addition to writing the file to disk — the document persists in the task for review even after the worktree is cleaned up.
**IMPORTANT — Save your deliverables as documents:** When your task produces written output (documentation, specifications, reports, API references, README updates, guides, or any other content), you MUST save that content as a task document using \`fn_task_document_write\`. Use a key that describes the deliverable (e.g., key="readme", key="api-docs", key="changelog"). Do this in addition to writing the file to disk — the document persists in the task for review even after the worktree is cleaned up.
If the task's PROMPT.md includes a "Documentation Requirements" section listing files to update, save each updated file's final content as a task document with a matching key.
@@ -341,7 +341,7 @@ If you attempt to write to a path outside the worktree, the file tools will reje
- Read "Context to Read First" files before starting
- Follow the "Do NOT" section strictly
- If tests, lint, build, or typecheck fail and the fix requires touching code outside the declared File Scope, fix those failures directly and keep the repo green
- Use \`task_create\` for genuinely separate follow-up work, not for mandatory fixes required to make this task land cleanly
- Use \`fn_task_create\` for genuinely separate follow-up work, not for mandatory fixes required to make this task land cleanly
- Update documentation listed in "Must Update" and check "Check If Affected"
- NEVER delete, remove, or gut modules, interfaces, settings, exports, or test files outside your File Scope
- NEVER remove features as "cleanup" — if something seems unused, create a task for investigation instead
@@ -352,14 +352,14 @@ If you attempt to write to a path outside the worktree, the file tools will reje
You can spawn child agents to handle parallel work or specialized sub-tasks:
**When to use \`spawn_agent\`:**
**When to use \`fn_spawn_agent\`:**
- Parallel work that can be divided into independent chunks
- Specialized tasks requiring different expertise or tools
- Delegation of sub-tasks to specialized agents
**How to spawn:**
\`\`\`javascript
spawn_agent({
fn_spawn_agent({
name: "researcher",
role: "engineer",
task: "Research best practices for authentication in React applications"
@@ -369,7 +369,7 @@ spawn_agent({
**Child agent behavior:**
- Each child runs in its own git worktree (branched from your worktree)
- Children execute autonomously and report completion
- When you end (task_done), all spawned children are terminated
- When you end (fn_task_done), all spawned children are terminated
- Check AgentStore for spawned agent status
**Limits:**
@@ -379,13 +379,13 @@ spawn_agent({
## Completion
After all steps are done, lint passes, tests pass, typecheck passes, and docs are updated:
\`\`\`bash
Call \`task_done()\` to signal completion.
Call \`fn_task_done()\` to signal completion.
\`\`\`
If a project build command is listed in the prompt, it is a hard completion gate:
- Run the exact build command in the current worktree before \`task_done()\`
- Run the exact build command in the current worktree before \`fn_task_done()\`
- Do not claim the build passes unless you actually ran it and got exit code 0
- If the build fails, do NOT call \`task_done()\`; keep working until it passes
- If the build fails, do NOT call \`fn_task_done()\`; keep working until it passes
Lint, tests, and typecheck are also hard quality gates:
- Keep fixing failures until lint, the configured/full test suite, and typecheck all pass
@@ -448,7 +448,7 @@ export interface TaskExecutorOptions {
reflectionService?: AgentReflectionService;
/** Plugin runner for invoking plugin hooks and providing plugin tools. */
pluginRunner?: PluginRunner;
/** MessageStore for sending messages to other agents. When provided, executor agents gain send_message capability. */
/** MessageStore for sending messages to other agents. When provided, executor agents gain fn_send_message capability. */
messageStore?: import("@fusion/core").MessageStore;
missionStore?: MissionStore;
onSliceComplete?: (slice: Slice) => void;
@@ -786,8 +786,8 @@ export class TaskExecutor {
/**
* Check whether a task's work is complete — all steps are done or skipped.
* Used to detect tasks that called task_done() but never transitioned to in-review
* (e.g., killed by stuck detector after task_done but before moveTask).
* Used to detect tasks that called fn_task_done() but never transitioned to in-review
* (e.g., killed by stuck detector after fn_task_done but before moveTask).
*/
private isTaskWorkComplete(task: Task): boolean {
if (task.steps.length === 0) return false;
@@ -796,7 +796,7 @@ export class TaskExecutor {
private isNoProgressNoTaskDoneFailure(task: Task): boolean {
return task.status === "failed" &&
task.error?.includes("without calling task_done") === true &&
task.error?.includes("without calling fn_task_done") === true &&
task.steps.every((step) => step.status === "pending");
}
@@ -1022,7 +1022,7 @@ export class TaskExecutor {
}
if (this.isNoProgressNoTaskDoneFailure(task)) {
executorLog.log(`${task.id} failed without task_done and has no step progress — leaving for self-healing requeue`);
executorLog.log(`${task.id} failed without fn_task_done and has no step progress — leaving for self-healing requeue`);
continue;
}
@@ -1676,7 +1676,7 @@ export class TaskExecutor {
// ── Single-Session Path (default) ────────────────────────────────
// Build custom tools for the worker
// Track the last code review verdict per step so we can enforce REVISE
// (block task_update status="done" until the agent re-reviews and gets APPROVE).
// (block fn_task_update status="done" until the agent re-reviews and gets APPROVE).
const codeReviewVerdicts = new Map<number, ReviewVerdict>();
let wasPaused = false;
@@ -1695,7 +1695,7 @@ export class TaskExecutor {
// Log fast mode status
if (executionMode === "fast") {
executorLog.log(`${task.id}: fast mode — review_step tool not injected`);
executorLog.log(`${task.id}: fast mode — fn_review_step tool not injected`);
}
const customTools = [
@@ -1704,7 +1704,7 @@ export class TaskExecutor {
this.createTaskCreateTool(),
this.createTaskAddDepTool(task.id),
this.createTaskDoneTool(task.id, () => { taskDone = true; }),
// Skip review_step tool in fast mode — fast mode bypasses automated review gates
// Skip fn_review_step tool in fast mode — fast mode bypasses automated review gates
...(executionMode !== "fast" ? [
this.createReviewStepTool(task.id, worktreePath, detail.prompt, codeReviewVerdicts, sessionRef, stepCheckpoints, detail, stuckDetector),
] : []),
@@ -1816,7 +1816,7 @@ export class TaskExecutor {
}
}
// Make session available to custom tools (task_update checkpoint capture, review_step rewind)
// Make session available to custom tools (fn_task_update checkpoint capture, fn_review_step rewind)
sessionRef.current = session;
// Register session so the pause listener can terminate it
@@ -1913,7 +1913,7 @@ export class TaskExecutor {
"3. Review the PROMPT.md steps to see which are still pending",
"",
"Take a DIFFERENT approach from what you were doing before.",
"If the current step is complete, call task_update to mark it done and move to the next step.",
"If the current step is complete, call fn_task_update to mark it done and move to the next step.",
"If you're stuck on a problem, try a simpler or alternative solution.",
"",
"Continue the task from where you left off.",
@@ -1960,7 +1960,7 @@ export class TaskExecutor {
return;
}
// If the agent didn't explicitly call task_done, check whether
// If the agent didn't explicitly call fn_task_done, check whether
// all steps are already complete — treat as implicit done to avoid
// unnecessary retry sessions for context-overflow / compaction cases.
if (!taskDone) {
@@ -1968,8 +1968,8 @@ export class TaskExecutor {
if (implicitCheck.steps.length > 0 &&
implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
taskDone = true;
executorLog.log(`${task.id} all steps done — treating as implicit task_done`);
await this.store.logEntry(task.id, "All steps complete — implicit task_done (agent did not call tool explicitly)", undefined, this.currentRunContext);
executorLog.log(`${task.id} all steps done — treating as implicit fn_task_done`);
await this.store.logEntry(task.id, "All steps complete — implicit fn_task_done (agent did not call tool explicitly)", undefined, this.currentRunContext);
}
}
@@ -2016,11 +2016,11 @@ export class TaskExecutor {
while (!taskDone && taskDoneSessionRetries < MAX_TASK_DONE_SESSION_RETRIES) {
taskDoneSessionRetries++;
executorLog.log(
`${task.id} finished without task_done — retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
`${task.id} finished without fn_task_done — retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
);
await this.store.logEntry(
task.id,
`Agent finished without calling task_done — retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
`Agent finished without calling fn_task_done — retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
undefined,
this.currentRunContext,
);
@@ -2067,10 +2067,10 @@ export class TaskExecutor {
stuckDetector?.trackTask(task.id, retrySession);
const retryPrompt = [
"Your previous session ended without calling the task_done tool.",
"Your previous session ended without calling the fn_task_done tool.",
"The task may already be complete — review the current state of the worktree and either:",
"1. If the work is done, call task_done with a summary of what was accomplished.",
"2. If there is remaining work, finish it and then call task_done.",
"1. If the work is done, call fn_task_done with a summary of what was accomplished.",
"2. If there is remaining work, finish it and then call fn_task_done.",
"",
"Original task:",
buildExecutionPrompt(detail, this.rootDir, settings, worktreePath),
@@ -2085,8 +2085,8 @@ export class TaskExecutor {
if (implicitCheck.steps.length > 0 &&
implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
taskDone = true;
executorLog.log(`${task.id} all steps done — treating as implicit task_done`);
await this.store.logEntry(task.id, "All steps complete — implicit task_done (agent did not call tool explicitly)", undefined, this.currentRunContext);
executorLog.log(`${task.id} all steps done — treating as implicit fn_task_done`);
await this.store.logEntry(task.id, "All steps complete — implicit fn_task_done (agent did not call tool explicitly)", undefined, this.currentRunContext);
}
}
}
@@ -2123,7 +2123,7 @@ export class TaskExecutor {
} else {
const priorRequeues = task.taskDoneRetryCount ?? 0;
const nextRequeueCount = priorRequeues + 1;
const errorMessage = `Agent finished without calling task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
const errorMessage = `Agent finished without calling fn_task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
if (priorRequeues < MAX_TASK_DONE_REQUEUE_RETRIES) {
await this.store.updateTask(task.id, {
@@ -2143,7 +2143,7 @@ export class TaskExecutor {
await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
await this.store.logEntry(task.id, `${errorMessage} — moved to in-review for inspection`, undefined, this.currentRunContext);
await this.store.moveTask(task.id, "in-review");
executorLog.log(`${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries — no task_done → in-review`);
executorLog.log(`${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries — no fn_task_done → in-review`);
}
this.options.onError?.(task, new Error(errorMessage));
}
@@ -2277,7 +2277,7 @@ export class TaskExecutor {
"2. Identify the most critical remaining work",
"3. Complete it with a simpler, more focused approach",
"",
"Do not repeat what's already been done. Just complete the task and call task_done.",
"Do not repeat what's already been done. Just complete the task and call fn_task_done.",
].join("\n");
await promptWithFallback(activeEntry.session, reducedPrompt);
@@ -2469,7 +2469,7 @@ export class TaskExecutor {
): ToolDefinition {
const store = this.store;
return {
name: "task_update",
name: "fn_task_update",
label: "Update Step",
description:
"Update a step's status. Call before starting a step (in-progress), " +
@@ -2489,14 +2489,14 @@ export class TaskExecutor {
// Enforce code review REVISE: block advancing to "done" when the last
// code review for this step returned REVISE. The agent must fix the
// issues and call review_step(type="code") again before proceeding.
// issues and call fn_review_step(type="code") again before proceeding.
if (status === "done" && codeReviewVerdicts.get(step) === "REVISE") {
return {
content: [{
type: "text" as const,
text: `Cannot mark Step ${step} as done — the last code review returned REVISE. ` +
`Fix the issues from the code review, commit your changes, and call ` +
`review_step(step=${step}, type="code") again. The step can only advance ` +
`fn_review_step(step=${step}, type="code") again. The step can only advance ` +
`after the code review passes.`,
}],
details: {},
@@ -2544,7 +2544,7 @@ export class TaskExecutor {
private createTaskAddDepTool(taskId: string): ToolDefinition {
const store = this.store;
return {
name: "task_add_dep",
name: "fn_task_add_dep",
label: "Add Dependency",
description:
"Declare a dependency on an existing task. Use when you discover " +
@@ -2637,7 +2637,7 @@ export class TaskExecutor {
private createTaskDoneTool(taskId: string, onDone: () => void): ToolDefinition {
const store = this.store;
return {
name: "task_done",
name: "fn_task_done",
label: "Mark Task Done",
description:
"Signal that all steps are complete, tests pass, and documentation is updated. " +
@@ -2656,7 +2656,7 @@ export class TaskExecutor {
return {
content: [{
type: "text" as const,
text: `Cannot mark task done yet — ${completionBlocker}. Resolve the blocker before calling task_done().`,
text: `Cannot mark task done yet — ${completionBlocker}. Resolve the blocker before calling fn_task_done().`,
}],
details: {},
};
@@ -2687,7 +2687,7 @@ export class TaskExecutor {
}
/**
* Create the review_step tool for the executor agent.
* Create the fn_review_step tool for the executor agent.
*
* When the reviewer returns a RETHINK verdict, this tool:
* 1. Runs `git reset --hard <baseline>` to revert file changes
@@ -2709,7 +2709,7 @@ export class TaskExecutor {
const options = this.options;
return {
name: "review_step",
name: "fn_review_step",
label: "Review Step",
description:
"Spawn a reviewer agent to evaluate your plan or code for a step. " +
@@ -2782,7 +2782,7 @@ export class TaskExecutor {
case "REVISE":
if (reviewType === "code") {
text = `REVISE — this step cannot be marked done until the code review passes.\n\n` +
`Fix the issues below, commit your changes, and call review_step(step=${step}, ` +
`Fix the issues below, commit your changes, and call fn_review_step(step=${step}, ` +
`type="code", step_name="${step_name}", baseline="<new SHA>") again.\n\n${result.review}`;
} else {
text = `REVISE\n\n${result.review}`;
@@ -3022,7 +3022,7 @@ export class TaskExecutor {
// All prior steps stay done — agent applies the feedback as an in-place
// patch rather than re-planning or re-executing earlier steps.
const scopeLine = "All prior steps remain **done**. Apply the feedback above as an in-place fix (make the necessary code changes, commit, and call `task_done()` when complete). Do **not** re-run or re-plan any earlier step unless the feedback explicitly calls it out.";
const scopeLine = "All prior steps remain **done**. Apply the feedback above as an in-place fix (make the necessary code changes, commit, and call `fn_task_done()` when complete). Do **not** re-run or re-plan any earlier step unless the feedback explicitly calls it out.";
// Check for existing Workflow Revision Instructions section
const revisionSectionHeader = "## Workflow Revision Instructions";
@@ -4305,7 +4305,7 @@ and show an appropriate message to the user.\`
/**
* When the engine restarts mid-step, an `in-progress` step may have already
* passed its code review (log: `code review Step N: APPROVE`) but not yet
* been flipped to `done` by the agent's next `task_update` call. Without
* been flipped to `done` by the agent's next `fn_task_update` call. Without
* intervention, the next executor pass re-enters the step and replays plan
* + code review, which we've measured at 520 min of pure waste per restart.
*
@@ -4635,17 +4635,17 @@ and show an appropriate message to the user.\`
}
/**
* Create the spawn_agent tool definition.
* Create the fn_spawn_agent tool definition.
* Allows the parent agent to spawn child agents with delegated tasks.
*/
private createSpawnAgentTool(taskId: string, worktreePath: string, settings: Settings): ToolDefinition {
return {
name: "spawn_agent",
name: "fn_spawn_agent",
label: "Spawn Agent",
description:
"Spawn a child agent to handle parallel work or specialized sub-tasks. " +
"Each child runs in its own git worktree (branched from your worktree) and executes autonomously. " +
"When you end (task_done), all spawned children are terminated.",
"When you end (fn_task_done), all spawned children are terminated.",
parameters: spawnAgentParams,
execute: async (_id: string, params: Static<typeof spawnAgentParams>) => {
const { name, role, task: taskPrompt } = params;
@@ -4902,10 +4902,10 @@ ${attachmentsSection}${commandsSection}${memorySection}${progressSection}${steer
${reviewLevel === 0 ? "No reviews required. Implement directly." : ""}
${reviewLevel >= 1 ? `Before implementing each step (except Step 0 and the final step), call:
\`review_step(step=N, type="plan", step_name="...")\`` : ""}
\`fn_review_step(step=N, type="plan", step_name="...")\`` : ""}
${reviewLevel >= 2 ? `After implementing + committing each step, call:
\`review_step(step=N, type="code", step_name="...", baseline="<SHA from before step>")\`` : ""}
${reviewLevel >= 3 ? `After tests, also call review_step with type="code" for test review.` : ""}
\`fn_review_step(step=N, type="code", step_name="...", baseline="<SHA from before step>")\`` : ""}
${reviewLevel >= 3 ? `After tests, also call fn_review_step with type="code" for test review.` : ""}
## Worktree Boundaries
@@ -4921,18 +4921,18 @@ You are running in an **isolated git worktree**. This means:
${hasProgress
? `Resume from Step ${task.currentStep}. Do NOT redo completed steps.`
: "Start with Step 0 (Preflight). Work through each step in order."}
Use \`task_update\` to report progress on every step transition.
Use \`task_log\` for important actions and decisions.
Use \`task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
Use \`fn_task_update\` to report progress on every step transition.
Use \`fn_task_log\` for important actions and decisions.
Use \`fn_task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N — description"${authorArg}\`
When all steps are complete: call \`task_done()\`
When all steps are complete: call \`fn_task_done()\`
If a build command is configured, run that exact command in this worktree before calling \`task_done()\`.
If a build command is configured, run that exact command in this worktree before calling \`fn_task_done()\`.
Treat a non-zero exit code as a blocking failure. Do not claim success without a real passing run.
Run the configured/full test suite and fix failures even when that requires edits outside the original File Scope.
If the repo has a lint command (e.g. \`pnpm lint\`, \`npm run lint\`), run it before \`task_done()\` and fix any failures it reports.
If the repo has a typecheck command, run it before \`task_done()\` and fix any failures it reports.
Use \`task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
If the repo has a lint command (e.g. \`pnpm lint\`, \`npm run lint\`), run it before \`fn_task_done()\` and fix any failures it reports.
If the repo has a typecheck command, run it before \`fn_task_done()\` and fix any failures it reports.
Use \`fn_task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
If lint is configured and failing, fix that too before completion.
**CRITICAL: Resolve ALL test failures (and any lint/typecheck failures) before completing the task, even if they appear unrelated or pre-existing.** Unrelated failures left unfixed accumulate technical debt and block future integrations. Investigate and fix or suppress them — do not defer them to a separate task.`;
}

View File

@@ -2496,7 +2496,7 @@ describe("aiMergeTask — reset cleanup failure diagnostics", () => {
const resetFailureMessage = "lock file busy";
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
const reportTool = opts.customTools?.find((t: any) => t.name === "report_build_failure");
const reportTool = opts.customTools?.find((t: any) => t.name === "fn_report_build_failure");
return {
session: {
prompt: vi.fn().mockImplementation(async () => {
@@ -2881,7 +2881,7 @@ describe("aiMergeTask — build verification", () => {
expect(capturedSystemPrompt).toContain("## Build verification");
expect(capturedSystemPrompt).toContain("build verification is a hard gate");
expect(capturedSystemPrompt).toContain("Do not assume the build passes");
expect(capturedSystemPrompt).toContain("report_build_failure");
expect(capturedSystemPrompt).toContain("fn_report_build_failure");
});
it("includes build command in merge prompt when configured", async () => {
@@ -2927,11 +2927,11 @@ describe("aiMergeTask — build verification", () => {
// Verify custom tool was passed
expect(capturedArgs.customTools).toBeDefined();
expect(capturedArgs.customTools.some((t: any) => t.name === "report_build_failure")).toBe(true);
expect(capturedArgs.customTools.some((t: any) => t.name === "fn_report_build_failure")).toBe(true);
expect(capturedPrompt).toContain("Build command: `pnpm build`");
expect(capturedPrompt).toContain("This command is mandatory before commit.");
expect(capturedPrompt).toContain("Only commit if it exits 0.");
expect(capturedPrompt).toContain("call `report_build_failure`");
expect(capturedPrompt).toContain("call `fn_report_build_failure`");
});
it("merge succeeds when build passes (agent reports success)", async () => {
@@ -2974,10 +2974,10 @@ describe("aiMergeTask — build verification", () => {
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
});
it("merge aborts when build fails via report_build_failure tool", async () => {
// Mock agent that calls the report_build_failure tool execute method
it("merge aborts when build fails via fn_report_build_failure tool", async () => {
// Mock agent that calls the fn_report_build_failure tool execute method
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
const reportTool = opts.customTools?.find((t: any) => t.name === "report_build_failure");
const reportTool = opts.customTools?.find((t: any) => t.name === "fn_report_build_failure");
return {
session: {
prompt: vi.fn().mockImplementation(async () => {
@@ -3052,7 +3052,7 @@ describe("aiMergeTask — build verification", () => {
const resetFailureMessage = "reset failed: dirty working tree";
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
const reportTool = opts.customTools?.find((t: any) => t.name === "report_build_failure");
const reportTool = opts.customTools?.find((t: any) => t.name === "fn_report_build_failure");
return {
session: {
prompt: vi.fn().mockImplementation(async () => {

View File

@@ -1251,7 +1251,7 @@ and the bash tool returned exit code 0.
1. Run the build command (shown in the prompt context below)
2. If the build succeeds (exit code 0), proceed with the commit
3. If the build fails (non-zero exit code), DO NOT commit. Instead:
- Call the \`report_build_failure\` tool with the real error details
- Call the \`fn_report_build_failure\` tool with the real error details
- Stop immediately and do not run \`git commit\`
- Do not claim success in plain text
@@ -1291,7 +1291,7 @@ and the bash tool returned exit code 0.
1. Run the build command (shown in the prompt context below)
2. If the build succeeds (exit code 0), proceed with the commit
3. If the build fails (non-zero exit code), DO NOT commit. Instead:
- Call the \`report_build_failure\` tool with the real error details
- Call the \`fn_report_build_failure\` tool with the real error details
- Stop immediately and do not run \`git commit\`
- Do not claim success in plain text
@@ -2800,7 +2800,7 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
// Create custom tool for reporting build failures
const reportBuildFailureTool: ToolDefinition = {
name: "report_build_failure",
name: "fn_report_build_failure",
label: "Report Build Failure",
description: "Report that the build verification failed. Use this when the build command returns a non-zero exit code. Provide the error details in the message parameter.",
parameters: Type.Object({
@@ -3076,7 +3076,7 @@ export function buildMergePrompt(params: MergePromptParams): string {
"This command is mandatory before commit.",
"Run it with the bash tool in the current worktree and inspect the actual exit code.",
"Only proceed if it exits 0.",
"If it exits non-zero, call `report_build_failure` with the concrete error output and stop without committing.",
"If it exits non-zero, call `fn_report_build_failure` with the concrete error output and stop without committing.",
);
}
@@ -3090,7 +3090,7 @@ export function buildMergePrompt(params: MergePromptParams): string {
"This command is mandatory before commit.",
"Run it with the bash tool in the current worktree and inspect the actual exit code.",
"Only commit if it exits 0.",
"If it exits non-zero, call `report_build_failure` with the concrete error output and stop without committing.",
"If it exits non-zero, call `fn_report_build_failure` with the concrete error output and stop without committing.",
);
}

View File

@@ -264,7 +264,7 @@ describe("worktree path boundary helpers", () => {
it("wraps only file tools, not other tools", async () => {
const mockTaskTool = {
name: "task_create",
name: "fn_task_create",
label: "Create Task",
description: "Create a task",
parameters: {},
@@ -279,7 +279,7 @@ describe("worktree path boundary helpers", () => {
"/project",
);
// task_create should be unchanged (not wrapped)
// fn_task_create should be unchanged (not wrapped)
expect(wrapped[0]).toBe(mockTaskTool);
});

View File

@@ -76,6 +76,8 @@ type AgentToolHookSession = AgentSession & {
__fusionMessageContentGuardInstalled?: boolean;
};
const FN_MEMORY_APPEND_TOOL_NAME = "fn_memory_append";
function getSessionStateError(session: AgentSession): string {
const state = (session as any).state;
const error = state?.errorMessage ?? state?.error;
@@ -286,7 +288,7 @@ async function flushMemoryBeforeSessionCompaction(session: AgentSession): Promis
const flushPrompt = [
"Before context compaction, preserve only unresolved durable memory if needed.",
"If memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
"If fn_memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
"Use layer=\"long-term\" for durable facts and layer=\"daily\" for running notes/open loops.",
"If there is nothing durable to save, reply exactly: NONE.",
].join("\n");
@@ -965,7 +967,7 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
const { session } = sessionResult;
installToolResultContentGuard(session as AgentToolHookSession);
installMessageContentGuard(session as AgentToolHookSession, sessionManager as unknown as SessionManagerLike);
(session as any).__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
(session as any).__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
const promptableSession = session as PromptableSession;
promptableSession.promptWithFallback = async (prompt: string, promptOptions?: unknown) => {
@@ -1026,7 +1028,7 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
fallbackSession as unknown as AgentToolHookSession,
sessionManager as unknown as SessionManagerLike,
);
(fallbackSession as any).__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
(fallbackSession as any).__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
if (options.defaultThinkingLevel) {
fallbackSession.setThinkingLevel(options.defaultThinkingLevel as any);

View File

@@ -216,9 +216,9 @@ function mockAgentFailure(error = "agent crashed") {
}
/**
* Create a mock agent that auto-triggers the task_done tool when prompt is called.
* This simulates a successful task execution where the agent calls task_done(),
* preventing the executor from entering the "finished without task_done — retrying
* Create a mock agent that auto-triggers the fn_task_done tool when prompt is called.
* This simulates a successful task execution where the agent calls fn_task_done(),
* preventing the executor from entering the "finished without fn_task_done — retrying
* with new session" branch. Follows the same pattern as executor.test.ts.
*
* Uses per-session local customTools capture to avoid race conditions when
@@ -230,8 +230,8 @@ function createAgentWithTaskDone() {
const localCustomTools = opts?.customTools || [];
const session = {
prompt: vi.fn().mockImplementation(async () => {
// Find and execute task_done tool to set taskDone = true
const taskDoneTool = localCustomTools.find((t: any) => t.name === "task_done");
// Find and execute fn_task_done tool to set taskDone = true
const taskDoneTool = localCustomTools.find((t: any) => t.name === "fn_task_done");
if (taskDoneTool) {
await taskDoneTool.execute("tool-1", {});
}
@@ -290,7 +290,7 @@ describe("In-progress task resume after restart", () => {
const taskDone = makeTask("FN-003", "done");
store.listTasks.mockResolvedValue([task1, task2, taskDone]);
// Use deterministic mock that calls task_done to prevent retry-with-new-session
// Use deterministic mock that calls fn_task_done to prevent retry-with-new-session
createAgentWithTaskDone();
const executor = new TaskExecutor(store, "/tmp/test");
@@ -589,11 +589,11 @@ describe("In-progress task resume after restart", () => {
expect(recoverSpy).toHaveBeenCalledWith(completedTask);
});
it("resumeOrphaned() leaves no-progress no-task_done failures for self-healing", async () => {
it("resumeOrphaned() leaves no-progress no-fn_task_done failures for self-healing", async () => {
const store = createMockStore();
const failedTask = makeTask("FN-1473", "in-progress", {
status: "failed",
error: "Agent finished without calling task_done (after retry)",
error: "Agent finished without calling fn_task_done (after retry)",
steps: [],
});
store.listTasks.mockResolvedValue([failedTask]);
@@ -1027,7 +1027,7 @@ describe("Crash scenario edge cases", () => {
vi.clearAllMocks();
store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("FN-090", "in-progress"));
// Use deterministic mock that calls task_done to prevent retry-with-new-session
// Use deterministic mock that calls fn_task_done to prevent retry-with-new-session
createAgentWithTaskDone();
await executor.resumeOrphaned();
@@ -1407,8 +1407,8 @@ describe("Engine pause/unpause cycle", () => {
previous: { enginePaused: false },
});
// Session continues normally and completes by calling task_done
const taskDoneTool = opts?.customTools?.find((t: any) => t.name === "task_done");
// Session continues normally and completes by calling fn_task_done
const taskDoneTool = opts?.customTools?.find((t: any) => t.name === "fn_task_done");
if (taskDoneTool) {
await taskDoneTool.execute("tool-1", {});
}

View File

@@ -160,7 +160,7 @@ describe("reviewStep — spec review type", () => {
const opts = mockedCreateFnAgent.mock.calls[0][0];
expect(opts.systemPrompt).toContain("## Project Memory");
expect(opts.systemPrompt).toContain("Do not update memory during review");
expect(opts.customTools?.map((tool: any) => tool.name)).toEqual(["memory_search", "memory_get"]);
expect(opts.customTools?.map((tool: any) => tool.name)).toEqual(["fn_memory_search", "fn_memory_get"]);
});
it("omits reviewer memory tools and instructions when memory is disabled", async () => {
@@ -462,10 +462,10 @@ describe("REVIEWER_SYSTEM_PROMPT", () => {
);
});
it("instructs planner to use task_create for undersplit tasks", () => {
it("instructs planner to use fn_task_create for undersplit tasks", () => {
// The reviewer's REVISE feedback must explicitly direct the planner to
// create child tasks via task_create rather than just flagging the issue.
expect(REVIEWER_SYSTEM_PROMPT).toContain("task_create");
// create child tasks via fn_task_create rather than just flagging the issue.
expect(REVIEWER_SYSTEM_PROMPT).toContain("fn_task_create");
expect(REVIEWER_SYSTEM_PROMPT).toContain(
"create 25 child tasks",
);

View File

@@ -2,7 +2,7 @@
* Reviewer — spawns a separate pi agent to review a worker's plan or code.
*
* Replicates taskplane's cross-model review pattern:
* - Worker calls review_step(step, type) during execution
* - Worker calls fn_review_step(step, type) during execution
* - A separate reviewer agent is spawned with read-only tools
* - Reviewer writes a structured verdict: APPROVE, REVISE, or RETHINK
* - Verdict + feedback is returned to the worker
@@ -138,12 +138,12 @@ When reviewing specs, actively assess whether the task should have been broken i
Say explicitly: "This task should be broken into subtasks because [specific reason]."
Recommend the number of child tasks (2-5) and what each should cover.
**Critically**, instruct the planner to take these actions in your REVISE feedback:
1. Use the \`task_create\` tool to create 25 child tasks from the oversized spec
1. Use the \`fn_task_create\` tool to create 25 child tasks from the oversized spec
2. Do NOT write a parent PROMPT.md — the parent will be closed automatically after children are created
3. Each child task should cover one coherent deliverable with clear scope boundaries
Example REVISE feedback for an undersplit task:
"This task should be broken into 3 subtasks because it spans the engine, dashboard, and CLI packages with independent deliverables. Use task_create to create: (1) engine logic, (2) dashboard UI, (3) CLI integration. Do not write a parent PROMPT."
"This task should be broken into 3 subtasks because it spans the engine, dashboard, and CLI packages with independent deliverables. Use fn_task_create to create: (1) engine logic, (2) dashboard UI, (3) CLI integration. Do not write a parent PROMPT."
**Do NOT flag if:**
- Steps are sequential and tightly coupled (e.g., a pipeline where each step depends on the previous)

View File

@@ -520,10 +520,10 @@ Some freeform text without checkboxes.`;
expect(result).not.toContain("Project Commands");
});
it("includes task_done instruction at the end", () => {
it("includes fn_task_done instruction at the end", () => {
const task = makeTaskDetail({ prompt: fullPrompt });
const result = buildStepPrompt(task, 1);
expect(result).toContain("task_done()");
expect(result).toContain("fn_task_done()");
});
it("does not include content from other steps", () => {
@@ -2134,7 +2134,7 @@ describe("StepSessionExecutor tool availability", () => {
return captured;
}
it("includes list_agents and delegate_task when agentStore is available", async () => {
it("includes fn_list_agents and fn_delegate_task when agentStore is available", async () => {
const mockAgentStore = {
listAgents: vi.fn().mockResolvedValue([]),
getAgent: vi.fn().mockResolvedValue(null),
@@ -2145,19 +2145,19 @@ describe("StepSessionExecutor tool availability", () => {
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).toContain("list_agents");
expect(toolNames).toContain("delegate_task");
expect(toolNames).toContain("fn_list_agents");
expect(toolNames).toContain("fn_delegate_task");
});
it("excludes delegation tools when agentStore is not provided", async () => {
const tools = await captureCustomTools({});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).not.toContain("list_agents");
expect(toolNames).not.toContain("delegate_task");
expect(toolNames).not.toContain("fn_list_agents");
expect(toolNames).not.toContain("fn_delegate_task");
});
it("includes send_message and read_messages when messageStore and assignedAgentId are available", async () => {
it("includes fn_send_message and fn_read_messages when messageStore and assignedAgentId are available", async () => {
const mockMessageStore = {
sendMessage: vi.fn().mockReturnValue({ id: "msg-001" }),
getInbox: vi.fn().mockReturnValue([]),
@@ -2169,8 +2169,8 @@ describe("StepSessionExecutor tool availability", () => {
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).toContain("send_message");
expect(toolNames).toContain("read_messages");
expect(toolNames).toContain("fn_send_message");
expect(toolNames).toContain("fn_read_messages");
});
it("excludes messaging tools when messageStore is not provided", async () => {
@@ -2179,8 +2179,8 @@ describe("StepSessionExecutor tool availability", () => {
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).not.toContain("send_message");
expect(toolNames).not.toContain("read_messages");
expect(toolNames).not.toContain("fn_send_message");
expect(toolNames).not.toContain("fn_read_messages");
});
it("excludes messaging tools when assignedAgentId is not provided", async () => {
@@ -2194,16 +2194,16 @@ describe("StepSessionExecutor tool availability", () => {
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).not.toContain("send_message");
expect(toolNames).not.toContain("read_messages");
expect(toolNames).not.toContain("fn_send_message");
expect(toolNames).not.toContain("fn_read_messages");
});
it("includes task_log and task_create when store is available", async () => {
it("includes fn_task_log and fn_task_create when store is available", async () => {
const tools = await captureCustomTools({});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).toContain("task_log");
expect(toolNames).toContain("task_create");
expect(toolNames).toContain("fn_task_log");
expect(toolNames).toContain("fn_task_create");
});
});

View File

@@ -421,12 +421,12 @@ export function buildStepPrompt(
if (isLastStep) {
parts.push(
"",
"**Document your deliverables:** When this task produces written output (documentation, specifications, reports, API references, README updates, guides, or any other content), save that content as a task document using `task_document_write(key='...', content='...')`. Use a key that describes the deliverable (e.g., key=\"readme\", key=\"api-docs\"). The document persists in the task for review even after the worktree is cleaned up.",
"**Document your deliverables:** When this task produces written output (documentation, specifications, reports, API references, README updates, guides, or any other content), save that content as a task document using `fn_task_document_write(key='...', content='...')`. Use a key that describes the deliverable (e.g., key=\"readme\", key=\"api-docs\"). The document persists in the task for review even after the worktree is cleaned up.",
"",
);
}
parts.push("After completing this step, commit your changes and call task_done(). Do NOT proceed to subsequent steps.");
parts.push("After completing this step, commit your changes and call fn_task_done(). Do NOT proceed to subsequent steps.");
return parts.join("\n");
}
@@ -526,7 +526,7 @@ function buildReducedStepPrompt(taskDetail: TaskDetail, stepIndex: number): stri
"IMPORTANT: Your previous attempt hit the context window limit.",
"Do NOT repeat work that's already been done.",
"Check git status and git log to see what's been committed.",
"Complete the remaining work and call task_done().",
"Complete the remaining work and call fn_task_done().",
];
return parts.join("\n").replace(/\n{3,}/g, "\n\n"); // Collapse multiple blank lines

View File

@@ -264,7 +264,7 @@ describe("buildSpecificationPrompt", () => {
);
expect(prompt).toContain("## Subtask Breakdown Requested");
expect(prompt).toContain("If splitting: use the \\\`task_create\\\` tool");
expect(prompt).toContain("If splitting: use the \\\`fn_task_create\\\` tool");
expect(prompt).not.toContain("## Subtask Consideration");
});
@@ -285,8 +285,8 @@ describe("buildSpecificationPrompt", () => {
);
expect(prompt).toContain("Specify this task");
expect(prompt).toContain("## Project Memory");
expect(prompt).toContain("memory_search");
expect(prompt).toContain("memory_get");
expect(prompt).toContain("fn_memory_search");
expect(prompt).toContain("fn_memory_get");
});
it("excludes memory instructions when memoryEnabled: false", () => {
@@ -315,8 +315,8 @@ describe("buildSpecificationPrompt", () => {
);
expect(prompt).toContain("Specify this task");
expect(prompt).toContain("## Project Memory");
expect(prompt).toContain("memory_search");
expect(prompt).toContain("memory_get");
expect(prompt).toContain("fn_memory_search");
expect(prompt).toContain("fn_memory_get");
});
});
@@ -380,8 +380,8 @@ describe("buildSpecificationPrompt", () => {
expect(prompt).toContain("## Project Memory");
// QMD should NOT unconditionally reference .fusion/memory/
expect(prompt).not.toContain(".fusion/memory/");
expect(prompt).toContain("memory_search");
expect(prompt).toContain("memory_get");
expect(prompt).toContain("fn_memory_search");
expect(prompt).toContain("fn_memory_get");
});
it("QMD prompt has actionable memory instructions", () => {
@@ -402,7 +402,7 @@ describe("buildSpecificationPrompt", () => {
expect(prompt).toContain("## Project Memory");
// QMD should NOT contain .fusion/memory/
expect(prompt).not.toContain(".fusion/memory/");
expect(prompt).toContain("memory_search");
expect(prompt).toContain("fn_memory_search");
});
});
@@ -710,7 +710,7 @@ describe("TriageProcessor", () => {
expect(store.on).toHaveBeenCalledWith("settings:updated", expect.any(Function));
});
it("re-reads settings when review_spec runs so reviewer uses the latest validator model", async () => {
it("re-reads settings when fn_review_spec runs so reviewer uses the latest validator model", async () => {
const taskId = "FN-001";
const testRootDir = await createTriageFixtureRoot("fusion-triage-review-spec-");
try {
@@ -1194,8 +1194,8 @@ describe("taskCreate tool model inheritance", () => {
}));
});
describe("proactive subtask creation (task_create always available)", () => {
it("task_create tool is included in triage tools regardless of breakIntoSubtasks", () => {
describe("proactive subtask creation (fn_task_create always available)", () => {
it("fn_task_create tool is included in triage tools regardless of breakIntoSubtasks", () => {
const store = createMockStore();
const processor = new TriageProcessor(store, "/test/root");
const createdSubtasksRef = { current: [] };
@@ -1207,13 +1207,13 @@ describe("taskCreate tool model inheritance", () => {
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).toContain("task_create");
expect(toolNames).toContain("task_list");
expect(toolNames).toContain("task_get");
expect(toolNames).toContain("fn_task_create");
expect(toolNames).toContain("fn_task_list");
expect(toolNames).toContain("fn_task_get");
expect(tools).toHaveLength(3);
});
it("task_create tool succeeds and tracks created subtask", async () => {
it("fn_task_create tool succeeds and tracks created subtask", async () => {
const parentTask: Task = {
id: "FN-400",
description: "Large task without breakIntoSubtasks",
@@ -1252,7 +1252,7 @@ describe("taskCreate tool model inheritance", () => {
createdSubtasksRef,
});
const taskCreateTool = tools.find((t: any) => t.name === "task_create");
const taskCreateTool = tools.find((t: any) => t.name === "fn_task_create");
const result = await taskCreateTool.execute("call-1", {
description: "Child task description",
title: "Child Task",
@@ -1275,7 +1275,7 @@ describe("taskCreate tool model inheritance", () => {
}));
});
it("task_create rejects a dependency on the parent task being split", async () => {
it("fn_task_create rejects a dependency on the parent task being split", async () => {
// Regression: triage used to accept any id in `dependencies`. If the AI
// named the parent, the parent got deleted after the split and the child
// was blocked forever by a nonexistent dep (FN-2163/FN-2164 incident).
@@ -1303,7 +1303,7 @@ describe("taskCreate tool model inheritance", () => {
allowTaskCreate: true,
createdSubtasksRef,
});
const taskCreateTool = tools.find((t: any) => t.name === "task_create");
const taskCreateTool = tools.find((t: any) => t.name === "fn_task_create");
const result = await taskCreateTool.execute("call-1", {
description: "Child that tries to wait for the parent",
@@ -1319,7 +1319,7 @@ describe("taskCreate tool model inheritance", () => {
expect(createdSubtasksRef.current).toEqual([]);
});
it("task_create accepts dependencies on sibling subtasks created earlier in the same split", async () => {
it("fn_task_create accepts dependencies on sibling subtasks created earlier in the same split", async () => {
// The valid case: two siblings where the second depends on the first.
const parentTask: Task = {
id: "FN-700",
@@ -1352,7 +1352,7 @@ describe("taskCreate tool model inheritance", () => {
allowTaskCreate: true,
createdSubtasksRef,
});
const taskCreateTool = tools.find((t: any) => t.name === "task_create");
const taskCreateTool = tools.find((t: any) => t.name === "fn_task_create");
const firstRes = await taskCreateTool.execute("c1", {
description: "Sibling 1",
@@ -1374,7 +1374,7 @@ describe("taskCreate tool model inheritance", () => {
expect(createdSubtasksRef.current).toEqual(["FN-701", "FN-702"]);
});
it("task_create rejects an unknown dependency id that is neither sibling nor existing task", async () => {
it("fn_task_create rejects an unknown dependency id that is neither sibling nor existing task", async () => {
const parentTask: Task = {
id: "FN-800",
description: "Parent",
@@ -1402,7 +1402,7 @@ describe("taskCreate tool model inheritance", () => {
allowTaskCreate: true,
createdSubtasksRef,
});
const taskCreateTool = tools.find((t: any) => t.name === "task_create");
const taskCreateTool = tools.find((t: any) => t.name === "fn_task_create");
const result = await taskCreateTool.execute("c1", {
description: "Child naming a nonexistent dep",
@@ -1418,7 +1418,7 @@ describe("taskCreate tool model inheritance", () => {
it("closes parent after proactive split even when breakIntoSubtasks is undefined", async () => {
// Test that the post-session closure path doesn't gate on breakIntoSubtasks.
// Strategy: capture the customTools from createFnAgent, then have
// promptWithFallback invoke the task_create tool to simulate the agent
// promptWithFallback invoke the fn_task_create tool to simulate the agent
// proactively splitting an oversized task.
const task: Task = {
id: "FN-500",
@@ -1487,13 +1487,13 @@ describe("taskCreate tool model inheritance", () => {
};
});
// Make promptWithFallback invoke the task_create tool twice to simulate
// Make promptWithFallback invoke the fn_task_create tool twice to simulate
// the agent proactively splitting the oversized task
const { promptWithFallback } = await import("./pi.js");
(promptWithFallback as ReturnType<typeof vi.fn>).mockImplementationOnce(
async () => {
const taskCreateTool = capturedCustomTools.find(
(t: any) => t.name === "task_create",
(t: any) => t.name === "fn_task_create",
);
expect(taskCreateTool).toBeDefined();
// Simulate agent creating two child tasks
@@ -1527,7 +1527,7 @@ describe("taskCreate tool model inheritance", () => {
});
describe("bounded recovery retries for triage", () => {
it("requeues triage with backoff when the agent exits without calling review_spec", async () => {
it("requeues triage with backoff when the agent exits without calling fn_review_spec", async () => {
const task = {
id: "FN-202",
description: "Test triage task",
@@ -1572,7 +1572,7 @@ describe("taskCreate tool model inheritance", () => {
}));
expect(store.logEntry).toHaveBeenCalledWith(
"FN-202",
expect.stringContaining("Spec review not approved (review_spec was never called)"),
expect.stringContaining("Spec review not approved (fn_review_spec was never called)"),
);
});
@@ -2140,7 +2140,7 @@ describe("stale approval detection", () => {
expect(fp1).toBe(fp2);
});
it("captures fingerprint on review_spec APPROVE", async () => {
it("captures fingerprint on fn_review_spec APPROVE", async () => {
const taskId = "FN-CAP";
const taskDir = join(rootDir, ".fusion", "tasks", taskId);
await mkdir(taskDir, { recursive: true });
@@ -2184,14 +2184,14 @@ describe("stale approval detection", () => {
{},
);
// Execute review_spec — should capture fingerprint at APPROVE time
// Execute fn_review_spec — should capture fingerprint at APPROVE time
await tool.execute({});
// Verify fingerprint was captured from the user comments at approval time
expect(approvedCommentFingerprintRef.current).toBe("c1");
});
it("fingerprint is empty string when review_spec returns REVISE (no capture)", async () => {
it("fingerprint is empty string when fn_review_spec returns REVISE (no capture)", async () => {
const taskId = "FN-REV";
const taskDir = join(rootDir, ".fusion", "tasks", taskId);
await mkdir(taskDir, { recursive: true });
@@ -3034,7 +3034,7 @@ describe("TriageProcessor delegation tools", () => {
};
}
it("createTriageTools returns task_list, task_get, task_create (no delegation tools — those are in customTools)", () => {
it("createTriageTools returns fn_task_list, fn_task_get, fn_task_create (no delegation tools — those are in customTools)", () => {
const store = createMockStore();
const processor = new TriageProcessor(store as any, "/tmp/root");
@@ -3045,12 +3045,12 @@ describe("TriageProcessor delegation tools", () => {
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).toContain("task_list");
expect(toolNames).toContain("task_get");
expect(toolNames).toContain("task_create");
// list_agents and delegate_task are added in customTools, not createTriageTools
expect(toolNames).not.toContain("list_agents");
expect(toolNames).not.toContain("delegate_task");
expect(toolNames).toContain("fn_task_list");
expect(toolNames).toContain("fn_task_get");
expect(toolNames).toContain("fn_task_create");
// fn_list_agents and fn_delegate_task are added in customTools, not createTriageTools
expect(toolNames).not.toContain("fn_list_agents");
expect(toolNames).not.toContain("fn_delegate_task");
});
it("delegation tools are accessible when agentStore is available", () => {

View File

@@ -117,8 +117,8 @@ Follow this structure exactly:
### Step {N}: Documentation & Delivery
- [ ] Update relevant documentation
- [ ] Save documentation deliverables as task documents via \`task_document_write\` (key="docs", content=...)
- [ ] Out-of-scope findings created as new tasks via \`task_create\` tool
- [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...)
- [ ] Out-of-scope findings created as new tasks via \`fn_task_create\` tool
## Documentation Requirements
@@ -151,7 +151,7 @@ Commits at step boundaries. All commits include the task ID:
- Refuse necessary fixes just because they touch files outside the initial File Scope
- Commit without the task ID prefix
- Remove, delete, or gut modules, settings, interfaces, exports, or test files outside the File Scope
- Remove features as "cleanup" — if something seems unused, create a task via \`task_create\`
- Remove features as "cleanup" — if something seems unused, create a task via \`fn_task_create\`
## Changeset Requirements
@@ -173,13 +173,13 @@ tests. Manual verification is NOT a test.
as part of this task (not just skipping tests)
## Duplicate check
Before writing a spec, call \`task_list\` to see existing tasks.
Before writing a spec, call \`fn_task_list\` to see existing tasks.
If a task already covers the same work (even if worded differently), do NOT
write a PROMPT.md. Instead, write a single line to the output file:
\`DUPLICATE: {existing-task-id}\`
## Dependency awareness
When you plan to list a task in the \`## Dependencies\` section, first call \`task_get\` on that task ID to read its PROMPT.md.
When you plan to list a task in the \`## Dependencies\` section, first call \`fn_task_get\` on that task ID to read its PROMPT.md.
Use what you learn — file scope, APIs, patterns, completion criteria — to make the new spec accurate: reference the right paths, avoid conflicting assumptions, and describe what the dependency must deliver before this task starts.
If the dependency task has no PROMPT.md yet (not yet specified), note that in the Dependencies section.
@@ -187,8 +187,8 @@ If the dependency task has no PROMPT.md yet (not yet specified), note that in th
When the task includes \`breakIntoSubtasks: true\`, first decide whether it should be split.
- Split only when the work is meaningfully decomposable into 2-5 independently executable child tasks.
- If splitting: use the \`task_create\` tool to create child tasks in triage, include clear descriptions and dependencies between them, then stop. Do NOT write a PROMPT.md for the parent task.
- **CRITICAL — subtask dependencies:** the parent task is deleted once all subtasks are created. \`dependencies\` on a new subtask may ONLY reference sibling subtasks you have created earlier in this same split (or unrelated existing tasks). **Never depend on the parent task's id.** If a child conceptually "waits for the parent's remaining work", create a sibling subtask that does that work and depend on the sibling instead. The \`task_create\` tool will reject parent-id dependencies with an error.
- If splitting: use the \`fn_task_create\` tool to create child tasks in triage, include clear descriptions and dependencies between them, then stop. Do NOT write a PROMPT.md for the parent task.
- **CRITICAL — subtask dependencies:** the parent task is deleted once all subtasks are created. \`dependencies\` on a new subtask may ONLY reference sibling subtasks you have created earlier in this same split (or unrelated existing tasks). **Never depend on the parent task's id.** If a child conceptually "waits for the parent's remaining work", create a sibling subtask that does that work and depend on the sibling instead. The \`fn_task_create\` tool will reject parent-id dependencies with an error.
- If not splitting: proceed with a normal PROMPT.md specification.
## Proactive Subtask Breakdown for M/L Tasks
@@ -211,20 +211,20 @@ For tasks you assess as Size M or L, proactively evaluate whether splitting into
## Triage tools
You have these extra tools during triage:
- \`task_list\` — list existing active tasks
- \`task_get\` — inspect a task and its PROMPT.md
- \`task_create\` — create a child/follow-up task while triaging
- \`task_document_write\` — save a planning document (e.g., key="plan")
- \`task_document_read\` — read back a previously saved document
- \`fn_task_list\` — list existing active tasks
- \`fn_task_get\` — inspect a task and its PROMPT.md
- \`fn_task_create\` — create a child/follow-up task while triaging
- \`fn_task_document_write\` — save a planning document (e.g., key="plan")
- \`fn_task_document_read\` — read back a previously saved document
When the planning conversation produces a structured plan, save it as a document with \`task_document_write(key='plan', content='...')\` so the executor can reference it during implementation.
When the planning conversation produces a structured plan, save it as a document with \`fn_task_document_write(key='plan', content='...')\` so the executor can reference it during implementation.
## Guidelines
- Read the project structure and relevant source files to understand context BEFORE writing
- Be specific — name actual files, functions, and patterns from the codebase
- Steps should express OUTCOMES, not micro-instructions (2-5 checkboxes per step)
- Always include a testing step and a documentation step
- For tasks whose primary deliverable is documentation (updating docs, writing README, API references), include an explicit step or checkbox instructing the executor to save the final documentation content via \`task_document_write\`
- For tasks whose primary deliverable is documentation (updating docs, writing README, API references), include an explicit step or checkbox instructing the executor to save the final documentation content via \`fn_task_document_write\`
- Include a "Do NOT" section with project-appropriate guardrails
- Size assessment: S (<2h), M (2-4h), L (4-8h). Split if XL (8h+)
- Review level scoring: Blast radius (0-2), Pattern novelty (0-2), Security (0-2), Reversibility (0-2)
@@ -238,16 +238,16 @@ package.json when explicit commands are provided.
## Spec Review
After writing the PROMPT.md, call \`review_spec()\` to get an independent quality review.
After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent quality review.
- **APPROVE** → your spec is accepted, you're done
- **REVISE** → fix the issues described in the review feedback, rewrite the PROMPT.md, and call \`review_spec()\` again. Repeat until approved.
- **REVISE** → fix the issues described in the review feedback, rewrite the PROMPT.md, and call \`fn_review_spec()\` again. Repeat until approved.
- **RETHINK** → your approach was fundamentally rejected. The conversation will rewind. Read the feedback carefully and take a completely different approach. Do NOT repeat the rejected strategy.
You MUST call \`review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict.
You MUST call \`fn_review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict.
## Output
Write the PROMPT.md directly using the write tool, then call \`review_spec()\` for review.
Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` for review.
## Frontend UX Criteria Injection
@@ -621,11 +621,11 @@ export class TriageProcessor {
/**
* Specify a triage task by spawning an AI agent to generate a PROMPT.md.
*
* After the agent writes the PROMPT.md, it calls `review_spec()` to spawn
* After the agent writes the PROMPT.md, it calls `fn_review_spec()` to spawn
* an independent reviewer agent that evaluates the specification quality.
* The review loop works as follows:
* - **APPROVE**: the spec is accepted and the task moves to `todo`
* - **REVISE**: the agent revises the spec and calls `review_spec()` again.
* - **REVISE**: the agent revises the spec and calls `fn_review_spec()` again.
* If the agent finishes without getting APPROVE, the task is NOT moved to
* `todo` — a post-session gate requires an explicit APPROVE verdict.
* - **RETHINK**: the conversation rewinds to a pre-specification checkpoint
@@ -670,7 +670,7 @@ export class TriageProcessor {
// Mutable ref — populated after createFnAgent, tools access lazily via closure
const sessionRef: { current: AgentSession | null } = { current: null };
// Checkpoint for RETHINK rewind — captured lazily on first review_spec call
// Checkpoint for RETHINK rewind — captured lazily on first fn_review_spec call
const checkpointRef: { current: string | null } = { current: null };
// Track the last spec review verdict for post-session enforcement
const specReviewVerdictRef: { current: ReviewVerdict | null } = {
@@ -789,7 +789,7 @@ export class TriageProcessor {
"triage",
);
// Make session available to review_spec tool (for RETHINK rewind)
// Make session available to fn_review_spec tool (for RETHINK rewind)
sessionRef.current = session;
// Register session so the global pause listener can terminate it
@@ -875,7 +875,7 @@ export class TriageProcessor {
triageLog.log(`${task.id} split into subtasks (${childTaskIds}) and closed`);
} catch (err: unknown) {
// deleteTask refuses when live tasks still depend on this id.
// If task_create's validation worked correctly this branch is
// If fn_task_create's validation worked correctly this branch is
// unreachable, but we keep it as defense-in-depth: leaving the
// parent alive is always safer than stranding dependents.
const msg = err instanceof Error ? err.message : String(err);
@@ -902,7 +902,7 @@ export class TriageProcessor {
if (canRetryWithPlanningFallback) {
const verdictDesc =
specReviewVerdictRef.current === null
? "review_spec was never called"
? "fn_review_spec was never called"
: `verdict was ${specReviewVerdictRef.current}`;
const fallbackDesc = `${planningFallbackProvider}/${planningFallbackModelId}`;
triageLog.warn(
@@ -979,7 +979,7 @@ export class TriageProcessor {
if (specReviewVerdictRef.current !== "APPROVE") {
const verdictDesc =
specReviewVerdictRef.current === null
? "review_spec was never called"
? "fn_review_spec was never called"
: `verdict was ${specReviewVerdictRef.current}`;
const decision = computeRecoveryDecision({
recoveryRetryCount: task.recoveryRetryCount,
@@ -1195,7 +1195,7 @@ export class TriageProcessor {
});
const taskList: ToolDefinition = {
name: "task_list",
name: "fn_task_list",
label: "List Tasks",
description:
"List all tasks that aren't done. Returns ID, description, column, " +
@@ -1225,7 +1225,7 @@ export class TriageProcessor {
};
const taskGet: ToolDefinition = {
name: "task_get",
name: "fn_task_get",
label: "Get Task",
description:
"Get full details of a specific task including its PROMPT.md content. " +
@@ -1254,7 +1254,7 @@ export class TriageProcessor {
};
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
triageLog.warn(`${options.parentTaskId}: task_get lookup failed for ${params.id}: ${msg}`);
triageLog.warn(`${options.parentTaskId}: fn_task_get lookup failed for ${params.id}: ${msg}`);
return {
content: [
{ type: "text" as const, text: `Task ${params.id} not found.` },
@@ -1266,7 +1266,7 @@ export class TriageProcessor {
};
const taskCreate: ToolDefinition = {
name: "task_create",
name: "fn_task_create",
label: "Create Child Task",
description:
"Create a child task (subtask) while breaking a larger task into smaller pieces. " +
@@ -1282,7 +1282,7 @@ export class TriageProcessor {
_callId: string,
params: Static<typeof taskCreateParams>,
) => {
// task_create is always available during triage to support both
// fn_task_create is always available during triage to support both
// explicit breakIntoSubtasks and proactive splitting of oversized tasks.
try {
// Validate dependencies before creating the child:
@@ -1328,8 +1328,8 @@ export class TriageProcessor {
{
type: "text" as const,
text:
`ERROR: task_create rejected. Invalid dependencies:\n${summary}\n\n` +
`Remove or replace these ids and call task_create again.`,
`ERROR: fn_task_create rejected. Invalid dependencies:\n${summary}\n\n` +
`Remove or replace these ids and call fn_task_create again.`,
},
],
details: { rejectedDependencies: rejected },
@@ -1342,7 +1342,7 @@ export class TriageProcessor {
parentTask = await store.getTask(options.parentTaskId);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
triageLog.warn(`${options.parentTaskId}: failed to load parent task for task_create inheritance: ${msg}`);
triageLog.warn(`${options.parentTaskId}: failed to load parent task for fn_task_create inheritance: ${msg}`);
// Parent task not found or error - proceed without inheritance
parentTask = undefined;
}
@@ -1389,13 +1389,13 @@ export class TriageProcessor {
}
/**
* Create the `review_spec` tool for the triage agent.
* Create the `fn_review_spec` tool for the triage agent.
*
* Spawns an independent reviewer agent to evaluate the generated PROMPT.md.
* Verdict handling:
* - **APPROVE**: returns "APPROVE" — the triage agent's work is done.
* - **REVISE**: returns the review feedback. The triage agent must fix the
* PROMPT.md and call `review_spec` again. A post-session gate in
* PROMPT.md and call `fn_review_spec` again. A post-session gate in
* `specifyTask()` prevents moving to `todo` if the last verdict is REVISE.
* - **RETHINK**: rewinds the conversation to a pre-specification checkpoint
* using `session.navigateTree()`. Returns a re-prompt instructing the agent
@@ -1421,7 +1421,7 @@ export class TriageProcessor {
const options = this.options;
return {
name: "review_spec",
name: "fn_review_spec",
label: "Review Specification",
description:
"Spawn a reviewer agent to evaluate the generated PROMPT.md specification. " +
@@ -1448,7 +1448,7 @@ export class TriageProcessor {
"utf-8",
).catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
triageLog.warn(`${taskId}: failed to read PROMPT.md for review_spec (${promptPath}): ${msg}`);
triageLog.warn(`${taskId}: failed to read PROMPT.md for fn_review_spec (${promptPath}): ${msg}`);
return "";
});
@@ -1457,7 +1457,7 @@ export class TriageProcessor {
content: [
{
type: "text" as const,
text: "UNAVAILABLE — PROMPT.md file not found or empty. Write the specification first, then call review_spec.",
text: "UNAVAILABLE — PROMPT.md file not found or empty. Write the specification first, then call fn_review_spec.",
},
],
details: {},
@@ -1525,7 +1525,7 @@ export class TriageProcessor {
text = "APPROVE";
break;
case "REVISE":
text = `REVISE — fix the issues below, rewrite the PROMPT.md, and call review_spec() again.\n\n${result.review}`;
text = `REVISE — fix the issues below, rewrite the PROMPT.md, and call fn_review_spec() again.\n\n${result.review}`;
break;
case "RETHINK": {
// Rewind conversation to pre-specification checkpoint
@@ -1900,12 +1900,12 @@ The user has requested that this task be broken into smaller subtasks if it is c
**How to split:**
1. First, analyze the task to determine if it should be split
2. If splitting: use the \\\`task_create\\\` tool to create child tasks in order, setting up dependencies as needed
2. If splitting: use the \\\`fn_task_create\\\` tool to create child tasks in order, setting up dependencies as needed
3. Include clear descriptions and acceptance criteria for each child task
4. After creating all subtasks, stop — do NOT write a PROMPT.md for the parent task
5. If NOT splitting: proceed with a normal PROMPT.md specification for this task
**Subtask dependencies rule:** \`dependencies\` on a child may only reference **sibling subtasks created earlier in this same split** or **pre-existing tasks in the store**. They must NEVER reference the parent task being split — the parent is deleted after the split completes, and a dependency on a deleted task permanently blocks the dependent. If a child "needs the rest of the parent's work to finish first", create another sibling subtask for that remaining work and depend on the sibling. The \`task_create\` tool rejects parent-id dependencies.
**Subtask dependencies rule:** \`dependencies\` on a child may only reference **sibling subtasks created earlier in this same split** or **pre-existing tasks in the store**. They must NEVER reference the parent task being split — the parent is deleted after the split completes, and a dependency on a deleted task permanently blocks the dependent. If a child "needs the rest of the parent's work to finish first", create another sibling subtask for that remaining work and depend on the sibling. The \`fn_task_create\` tool rejects parent-id dependencies.
**Important:** If you create subtasks, this parent task will be closed and replaced by the children. Make sure each child is a complete, executable task.`;
} else {
@@ -1931,7 +1931,7 @@ The user did not explicitly request subtask breakdown, so you should first asses
- Adding a small feature to one module with 5 steps
**How to decide:**
- If you choose to split: use the \\\`task_create\\\` tool to create the child tasks, set dependencies where needed, and then stop without writing a PROMPT.md for the parent task.
- If you choose to split: use the \\\`fn_task_create\\\` tool to create the child tasks, set dependencies where needed, and then stop without writing a PROMPT.md for the parent task.
- **Subtask dependencies must only reference sibling subtasks created earlier in this same split, or pre-existing tasks. NEVER depend on the parent task being split — the parent is deleted after splitting, and the tool will reject parent-id dependencies.**
- If the work appears to be Size S, or if an M/L task genuinely has 5 or fewer focused steps with a clear scope, proceed with a normal PROMPT.md specification.
- If size is uncertain at first, make a quick assessment from the available context before deciding.`;