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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
@@ -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 5–20 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.`;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", {});
|
||||
}
|
||||
|
||||
@@ -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 2–5 child tasks",
|
||||
);
|
||||
|
||||
@@ -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 2–5 child tasks from the oversized spec
|
||||
1. Use the \`fn_task_create\` tool to create 2–5 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)
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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.`;
|
||||
|
||||
Reference in New Issue
Block a user