From 8a0885ad48acfbeb3e24f63c900200fb874f8818 Mon Sep 17 00:00:00 2001 From: Fusion Date: Fri, 24 Apr 2026 03:11:09 -0700 Subject: [PATCH] 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 --- .changeset/fn-2392-skill-tool-prefix.md | 5 + packages/cli/skill/fusion/SKILL.md | 2 + .../fusion/references/extension-tools.md | 2 + .../fusion/references/fusion-capabilities.md | 2 + .../skill/fusion/workflows/specifications.md | 2 + .../skill/fusion/workflows/task-management.md | 2 + packages/cli/src/__tests__/skill-sync.test.ts | 82 +++- packages/core/src/agent-store.test.ts | 2 +- packages/core/src/plugin-loader.test.ts | 4 +- packages/core/src/plugin-loader.ts | 1 - packages/core/src/project-memory.ts | 24 +- .../dashboard/app/components/AgentsView.tsx | 1 + .../app/components/ClaudeCliProviderCard.tsx | 154 +++--- packages/dashboard/app/styles.css | 68 ++- packages/dashboard/src/routes.test.ts | 2 +- packages/engine/src/agent-heartbeat.test.ts | 256 +++++----- packages/engine/src/agent-heartbeat.ts | 88 ++-- packages/engine/src/agent-logger.test.ts | 6 +- packages/engine/src/agent-tools.test.ts | 32 +- packages/engine/src/agent-tools.ts | 74 +-- packages/engine/src/executor.test.ts | 456 +++++++++--------- packages/engine/src/executor.ts | 154 +++--- packages/engine/src/merger.test.ts | 16 +- packages/engine/src/merger.ts | 10 +- .../engine/src/pi-create-fn-agent.test.ts | 4 +- packages/engine/src/pi.ts | 8 +- .../engine/src/restart.integration.test.ts | 22 +- packages/engine/src/reviewer.test.ts | 8 +- packages/engine/src/reviewer.ts | 6 +- .../engine/src/step-session-executor.test.ts | 34 +- packages/engine/src/step-session-executor.ts | 6 +- packages/engine/src/triage.test.ts | 74 +-- packages/engine/src/triage.ts | 84 ++-- 33 files changed, 927 insertions(+), 764 deletions(-) create mode 100644 .changeset/fn-2392-skill-tool-prefix.md diff --git a/.changeset/fn-2392-skill-tool-prefix.md b/.changeset/fn-2392-skill-tool-prefix.md new file mode 100644 index 000000000..23657877c --- /dev/null +++ b/.changeset/fn-2392-skill-tool-prefix.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Normalize Fusion skill-facing tool naming to the public `fn_*` namespace and clarify the boundary between extension tools and internal engine runtime tools across skill docs. diff --git a/packages/cli/skill/fusion/SKILL.md b/packages/cli/skill/fusion/SKILL.md index 9be29f2cf..58a00a527 100644 --- a/packages/cli/skill/fusion/SKILL.md +++ b/packages/cli/skill/fusion/SKILL.md @@ -21,6 +21,8 @@ Mission → Milestone → Slice → Feature → Task **Available tools:** Fusion registers tools via the pi extension (prefixed `fn_*`). No CLI commands or Bash needed — use the registered tools directly. +**Naming boundary:** The published skill surface always uses `fn_*` tool names (for example `fn_task_create`, `fn_mission_create`). Internal engine runtime tools like `task_create`, `task_update`, `task_log`, and `task_done` are intentionally unprefixed and not part of this skill. + **Tool categories:** - **Task tools** — `fn_task_create`, `fn_task_update`, `fn_task_list`, `fn_task_show`, `fn_task_attach`, `fn_task_pause`, `fn_task_unpause`, `fn_task_retry`, `fn_task_duplicate`, `fn_task_refine`, `fn_task_archive`, `fn_task_unarchive`, `fn_task_delete`, `fn_task_plan` - **GitHub tools** — `fn_task_import_github`, `fn_task_import_github_issue`, `fn_task_browse_github_issues` diff --git a/packages/cli/skill/fusion/references/extension-tools.md b/packages/cli/skill/fusion/references/extension-tools.md index 5d2ac8696..2f9288ebf 100644 --- a/packages/cli/skill/fusion/references/extension-tools.md +++ b/packages/cli/skill/fusion/references/extension-tools.md @@ -2,6 +2,8 @@ All tools are registered via the pi extension. They are available in any pi agent session when the Fusion extension is installed. +> Naming contract: all externally exposed Fusion extension tools are `fn_*` (for example `fn_task_create`). Internal engine/executor runtime tools (`task_create`, `task_update`, `task_log`, `task_done`, etc.) are separate and intentionally out of scope for this skill surface. + ## Task Tools ### fn_task_create diff --git a/packages/cli/skill/fusion/references/fusion-capabilities.md b/packages/cli/skill/fusion/references/fusion-capabilities.md index c16305175..349b32b64 100644 --- a/packages/cli/skill/fusion/references/fusion-capabilities.md +++ b/packages/cli/skill/fusion/references/fusion-capabilities.md @@ -7,6 +7,8 @@ Triage → Todo → In Progress → In Review → Done → Archived ## Pi Extension Tools (Available to Agents) +All skill/extension tool invocations in this catalog use the public `fn_*` namespace. Engine runtime tools (for example `task_create`, `task_update`, `task_log`, `task_done`) are internal and intentionally not listed here. + | Tool | Purpose | |------|---------| | `fn_task_create` | Create a new task in triage | diff --git a/packages/cli/skill/fusion/workflows/specifications.md b/packages/cli/skill/fusion/workflows/specifications.md index 9ae5129e5..95e1cb786 100644 --- a/packages/cli/skill/fusion/workflows/specifications.md +++ b/packages/cli/skill/fusion/workflows/specifications.md @@ -5,6 +5,8 @@ Guide the agent through creating well-specified tasks and organizing work using the mission hierarchy for complex multi-phase projects. + +All tool examples in this workflow intentionally use the public `fn_*` extension namespace. diff --git a/packages/cli/skill/fusion/workflows/task-management.md b/packages/cli/skill/fusion/workflows/task-management.md index a4bbc9f05..b48326f01 100644 --- a/packages/cli/skill/fusion/workflows/task-management.md +++ b/packages/cli/skill/fusion/workflows/task-management.md @@ -5,6 +5,8 @@ Guide the agent through creating, viewing, and managing tasks on the Fusion board using pi extension tools. + +Use only the public `fn_*` extension tools in this workflow. Do not substitute internal engine runtime tools like `task_create`, `task_update`, `task_log`, or `task_done`. diff --git a/packages/cli/src/__tests__/skill-sync.test.ts b/packages/cli/src/__tests__/skill-sync.test.ts index ee2a67ad4..6ae53b593 100644 --- a/packages/cli/src/__tests__/skill-sync.test.ts +++ b/packages/cli/src/__tests__/skill-sync.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { readFileSync, existsSync } from "node:fs"; -import { resolve, dirname } from "node:path"; +import { readFileSync, existsSync, readdirSync } from "node:fs"; +import { resolve, dirname, relative, join } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -52,6 +52,25 @@ function getCapabilitiesToolNames(): string[] { return matches.map((m) => m[1]).sort(); } +function collectMarkdownFiles(dir: string): string[] { + const files: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectMarkdownFiles(fullPath)); + continue; + } + if (entry.isFile() && entry.name.endsWith(".md")) { + files.push(fullPath); + } + } + return files.sort(); +} + +function escapeRegex(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + describe("Skill-Extension Sync", () => { it("skill directory structure exists", () => { expect(existsSync(resolve(skillDir, "SKILL.md"))).toBe(true); @@ -105,6 +124,65 @@ describe("Skill-Extension Sync", () => { expect(missingFromCaps).toEqual([]); }); + it("covers the full Fusion skill markdown surface", () => { + const markdownFiles = collectMarkdownFiles(skillDir).map((filePath) => + relative(skillDir, filePath), + ); + + expect(markdownFiles).toEqual([ + "SKILL.md", + "references/best-practices.md", + "references/cli-commands.md", + "references/extension-tools.md", + "references/fusion-capabilities.md", + "references/skill-patterns.md", + "references/task-structure.md", + "workflows/dashboard-cli.md", + "workflows/specifications.md", + "workflows/task-lifecycle.md", + "workflows/task-management.md", + ]); + }); + + it("enforces fn_* naming across extension + all skill markdown for public tools", () => { + const extensionTools = getExtensionToolNames(); + const publicSuffixes = extensionTools.map((toolName) => + toolName.replace(/^fn_/, ""), + ); + + // These names are intentionally unprefixed engine/runtime tools and are allowed + // to appear in docs that explain capability boundaries. + const allowedUnprefixedInternalTools = new Set([ + "task_create", + "task_update", + "task_log", + "task_done", + "review_step", + "spawn_agent", + ]); + + const forbiddenSuffixes = publicSuffixes.filter( + (suffix) => !allowedUnprefixedInternalTools.has(suffix), + ); + + const filesToScan = [extensionPath, ...collectMarkdownFiles(skillDir)]; + const violations: string[] = []; + + for (const filePath of filesToScan) { + const content = readFileSync(filePath, "utf-8"); + const relativePath = relative(cliRoot, filePath); + + for (const suffix of forbiddenSuffixes) { + const regex = new RegExp(`(? { const skillMd = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8"); expect(skillMd).toContain("/fn"); diff --git a/packages/core/src/agent-store.test.ts b/packages/core/src/agent-store.test.ts index dc5fa769d..34f0ed2d1 100644 --- a/packages/core/src/agent-store.test.ts +++ b/packages/core/src/agent-store.test.ts @@ -1741,7 +1741,7 @@ describe("AgentStore", () => { const activeRun = await store.getActiveHeartbeatRun(agent.id); expect(activeRun).not.toBeNull(); expect(activeRun!.id).toBe(run.id); - }); + }, 15_000); it("throws for non-existent agent", async () => { await expect( diff --git a/packages/core/src/plugin-loader.test.ts b/packages/core/src/plugin-loader.test.ts index d1ec563c4..909cc731d 100644 --- a/packages/core/src/plugin-loader.test.ts +++ b/packages/core/src/plugin-loader.test.ts @@ -931,7 +931,7 @@ describe("PluginLoader", () => { "Failed to load plugin bad-load-all-log:", expect.any(Error), ); - }); + }, 15_000); it("logs invokeHook failures", async () => { await pluginStore.init(); @@ -1412,7 +1412,7 @@ describe("PluginLoader", () => { expect(runtimes).toHaveLength(2); expect(runtimes.find((r) => r.pluginId === "plugin-a")?.runtime.metadata.runtimeId).toBe("runtime-a"); expect(runtimes.find((r) => r.pluginId === "plugin-b")?.runtime.metadata.runtimeId).toBe("runtime-b"); - }); + }, 15_000); it("skips plugins without runtime registration when other plugins have runtimes", async () => { await pluginStore.init(); diff --git a/packages/core/src/plugin-loader.ts b/packages/core/src/plugin-loader.ts index 364e679fc..7eb758da0 100644 --- a/packages/core/src/plugin-loader.ts +++ b/packages/core/src/plugin-loader.ts @@ -289,7 +289,6 @@ export class PluginLoader extends EventEmitter<{ } else { mod = await import(moduleUrl); } - this.loadedModules.set(path, mod); return mod; } diff --git a/packages/core/src/project-memory.ts b/packages/core/src/project-memory.ts index c36f4b3b3..7a1b9c34c 100644 --- a/packages/core/src/project-memory.ts +++ b/packages/core/src/project-memory.ts @@ -387,8 +387,8 @@ This project has OpenClaw-style memory files: - \`.fusion/memory/YYYY-MM-DD.md\` — append-only daily notes for running context **Before writing the specification:** -1. Use \`memory_search\` first for task-relevant context -2. Use \`memory_get\` only for specific memory files/line ranges returned by search +1. Use \`fn_memory_search\` first for task-relevant context +2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search 3. Incorporate relevant learnings into your specification — reference actual patterns, constraints, and conventions documented there Do not read all memory directly by default. If memory is irrelevant, skip it. @@ -402,8 +402,8 @@ Do not read all memory directly by default. If memory is irrelevant, skip it. This project has a memory system that stores durable project learnings. **Before writing the specification:** -1. Use \`memory_search\` first for task-relevant context -2. Use \`memory_get\` only for specific memory files/line ranges returned by search +1. Use \`fn_memory_search\` first for task-relevant context +2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search 3. Incorporate useful learnings into your specification **If the memory contains useful context for this task, reference it in the specification.** @@ -465,12 +465,12 @@ This project has OpenClaw-style memory files: - \`.fusion/memory/YYYY-MM-DD.md\` — append-only daily notes for running observations and open loops **At the start of execution:** -1. Use \`memory_search\` first for task-relevant context -2. Use \`memory_get\` only for specific memory files/line ranges returned by search +1. Use \`fn_memory_search\` first for task-relevant context +2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search 3. Apply relevant learnings to your implementation — follow documented patterns and avoid known pitfalls 4. Do not load all memory directly by default. Skip memory reads when memory is irrelevant or context is tight. -**At the end of execution (before calling \`task_done()\`):** +**At the end of execution (before calling \`fn_task_done()\`):** 1. Review what you learned during this task that would genuinely benefit future runs 2. Write durable decisions, conventions, and pitfalls to \`.fusion/memory/MEMORY.md\` 3. Write running observations, unresolved context, and open loops to today's \`.fusion/memory/YYYY-MM-DD.md\` @@ -501,11 +501,11 @@ This project has OpenClaw-style memory files: This project has a memory system that stores durable project learnings accumulated from past task runs. **At the start of execution:** -1. Use \`memory_search\` first for task-relevant context -2. Use \`memory_get\` only for specific memory files/line ranges returned by search +1. Use \`fn_memory_search\` first for task-relevant context +2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search 3. Apply useful learnings to your implementation -**At the end of execution (before calling \`task_done()\`):** +**At the end of execution (before calling \`fn_task_done()\`):** 1. Review what you learned during this task that would genuinely benefit future runs 2. **If nothing durable was learned, skip the memory update entirely** — do not append trivial or task-specific notes 3. Only write when you have genuinely durable, reusable insights such as: @@ -538,8 +538,8 @@ export function buildReviewerMemoryInstructions( This project has a memory system that stores durable project learnings. **During review:** -1. Use \`memory_search\` for task-relevant project conventions, pitfalls, and prior decisions when they could affect your verdict -2. Use \`memory_get\` only for specific memory files/line ranges returned by search +1. Use \`fn_memory_search\` for task-relevant project conventions, pitfalls, and prior decisions when they could affect your verdict +2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search 3. Treat documented durable conventions and pitfalls as review evidence when deciding APPROVE, REVISE, or RETHINK 4. Do not update memory during review; reviewer memory access is read-only 5. Skip memory reads when they are not relevant to the reviewed plan or code diff --git a/packages/dashboard/app/components/AgentsView.tsx b/packages/dashboard/app/components/AgentsView.tsx index 923216a0a..7badcf7af 100644 --- a/packages/dashboard/app/components/AgentsView.tsx +++ b/packages/dashboard/app/components/AgentsView.tsx @@ -350,6 +350,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) { .filter((n): n is OrgTreeNode => n !== null); }, [orgTree, showSystemAgents]); + useEffect(() => { if (agentView !== "org") return; diff --git a/packages/dashboard/app/components/ClaudeCliProviderCard.tsx b/packages/dashboard/app/components/ClaudeCliProviderCard.tsx index 590ea4f1e..7952981ad 100644 --- a/packages/dashboard/app/components/ClaudeCliProviderCard.tsx +++ b/packages/dashboard/app/components/ClaudeCliProviderCard.tsx @@ -130,9 +130,81 @@ export function ClaudeCliProviderCard({ ); + const actions = ( + <> + + {currentlyEnabled ? ( + + ) : ( + + )} + + ); + + // Compact layout mirrors `.auth-provider-card` so it slots cleanly into + // the Settings > Authentication list and picks up the shared mobile rules. + if (compact) { + return ( +
+
+
+ + Anthropic — via Claude CLI + +
+
{actions}
+
+
+ Details +
+ {description} + + {lastAction && } +
+
+
+ ); + } + return (
@@ -142,69 +214,33 @@ export function ClaudeCliProviderCard({ Anthropic — via Claude CLI - {compact ? ( -
- - Details - - {description} - -
- ) : ( - <> - {description} - - - )} + {description} +
-
- - {currentlyEnabled ? ( - - ) : ( - - )} -
- {lastAction && ( - - )} +
{actions}
+ {lastAction && }
); } +function ClaudeCliBadge({ + status, + authenticated, +}: { + status: ClaudeCliStatus | null; + authenticated: boolean; +}) { + const enabled = status?.enabled ?? authenticated; + const available = status?.binary.available ?? false; + if (enabled) { + return ✓ Active; + } + if (!available && status) { + return ✗ Not installed; + } + return ✗ Not connected; +} + /** * One-line health summary. Renders different text for "binary missing" * vs "binary ok but disabled" vs "fully ready" so the user can quickly diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index 9a660328e..bcd43ad83 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -25387,37 +25387,69 @@ html .column.drag-over * { width: min(calc(var(--space-2xl) * 10), 100%); } -.onboarding-provider-card--compact { - padding: var(--space-sm) var(--space-md); - gap: var(--space-sm); - align-items: center; +.auth-provider-card--cli .auth-provider-info { + gap: 8px; } -.onboarding-provider-card--compact .onboarding-provider-card__name { +.auth-provider-card--cli .auth-provider-info strong { font-size: 13px; } -.onboarding-provider-card__details { - font-size: 12px; - color: var(--text-muted); - margin-top: 2px; +.auth-provider-cli-actions { + display: flex; + gap: var(--space-xs); + flex-shrink: 0; } -.onboarding-provider-card__details-summary { +.auth-provider-cli-details { + border-top: 1px solid var(--border); + font-size: 12px; + color: var(--text-muted); +} + +.auth-provider-cli-details > summary { cursor: pointer; - color: var(--text-muted); - font-size: 12px; - list-style: revert; user-select: none; + padding: 8px 16px; + font-size: 12px; + color: var(--text-muted); + list-style: revert; } -.onboarding-provider-card__details[open] .onboarding-provider-card__details-summary { - margin-bottom: var(--space-xs); +.auth-provider-cli-details > summary:hover { + color: var(--text); } -.onboarding-provider-card__details > *:not(summary) { - display: block; - margin-top: var(--space-xs); +.auth-provider-cli-details-body { + display: flex; + flex-direction: column; + gap: var(--space-xs); + padding: 0 16px 12px; + line-height: 1.5; +} + +@media (max-width: 768px) { + .auth-provider-card--cli .auth-provider-info { + flex-wrap: wrap; + } + + .auth-provider-cli-actions { + width: 100%; + justify-content: flex-end; + } + + .auth-provider-cli-actions .btn { + flex: 1; + min-width: 0; + } + + .auth-provider-cli-details > summary { + padding: 8px 14px; + } + + .auth-provider-cli-details-body { + padding: 0 14px 12px; + } } /* === Onboarding API Key Form === */ diff --git a/packages/dashboard/src/routes.test.ts b/packages/dashboard/src/routes.test.ts index 27e983f13..9feddd0f0 100644 --- a/packages/dashboard/src/routes.test.ts +++ b/packages/dashboard/src/routes.test.ts @@ -15182,7 +15182,7 @@ describe("POST /workflow-step-templates/:id/create", () => { templateId: "qa-check", name: "QA Check", description: "Run lint, tests, and typecheck; verify they pass and check for obvious bugs", - prompt: expect.stringContaining("Quality Gate Execution"), + prompt: expect.stringContaining("QA tester"), toolMode: "coding", enabled: true, }); diff --git a/packages/engine/src/agent-heartbeat.test.ts b/packages/engine/src/agent-heartbeat.test.ts index 8fd06d888..d03a714f0 100644 --- a/packages/engine/src/agent-heartbeat.test.ts +++ b/packages/engine/src/agent-heartbeat.test.ts @@ -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); diff --git a/packages/engine/src/agent-heartbeat.ts b/packages/engine/src/agent-heartbeat.ts index 161af8d15..fc47c84a0 100644 --- a/packages/engine/src/agent-heartbeat.ts +++ b/packages/engine/src/agent-heartbeat.ts @@ -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 diff --git a/packages/engine/src/agent-logger.test.ts b/packages/engine/src/agent-logger.test.ts index 45d4225d0..59c928b4c 100644 --- a/packages/engine/src/agent-logger.test.ts +++ b/packages/engine/src/agent-logger.test.ts @@ -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 () => { diff --git a/packages/engine/src/agent-tools.test.ts b/packages/engine/src/agent-tools.test.ts index 0bb4411cc..649518750 100644 --- a/packages/engine/src/agent-tools.test.ts +++ b/packages/engine/src/agent-tools.test.ts @@ -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", () => { diff --git a/packages/engine/src/agent-tools.ts b/packages/engine/src/agent-tools.ts index 6b462b5fb..8ba988776 100644 --- a/packages/engine/src/agent-tools.ts +++ b/packages/engine/src/agent-tools.ts @@ -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) => { 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) => { @@ -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) => { // 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, diff --git a/packages/engine/src/executor.test.ts b/packages/engine/src/executor.test.ts index 2efb2b4dd..55449ac61 100644 --- a/packages/engine/src/executor.test.ts +++ b/packages/engine/src/executor.test.ts @@ -505,11 +505,11 @@ describe("TaskExecutor worktreeInitCommand", () => { ); // The init command failure itself does not abort execution, but the mocked - // agent still exits without task_done. After 3 retries it requeues to todo + // agent still exits without fn_task_done. After 3 retries it requeues to todo // and reports an error. expect(onError).toHaveBeenCalledWith( expect.objectContaining({ id: "FN-010" }), - expect.objectContaining({ message: "Agent finished without calling task_done (after 3 retries)" }), + expect.objectContaining({ message: "Agent finished without calling fn_task_done (after 3 retries)" }), ); // Agent should still have been created @@ -2290,7 +2290,7 @@ describe("buildExecutionPrompt", () => { } as any); expect(result).toContain("fix failures even when that requires edits outside the original File Scope"); - expect(result).toContain("If the repo has a typecheck command, run it before `task_done()`"); + expect(result).toContain("If the repo has a typecheck command, run it before `fn_task_done()`"); expect(result).toContain("not for fixes required to get tests, build, or typecheck back to green"); }); @@ -2490,7 +2490,7 @@ describe("buildExecutionPrompt", () => { updatedAt: new Date().toISOString(), }); - // Called four times: initial execution + 3 retries when agent finishes without task_done + // Called four times: initial execution + 3 retries when agent finishes without fn_task_done expect(mockPrompt).toHaveBeenCalledTimes(4); const agentPrompt = mockPrompt.mock.calls[0][0]; expect(agentPrompt).toContain("## Project Commands"); @@ -2533,7 +2533,7 @@ describe("buildExecutionPrompt", () => { } as any); // Should instruct selective writes, not unconditional appends expect(result).toMatch(/skip.*memory.*update|selectively|durable.*learnings/i); - expect(result).toMatch(/end of execution|before calling.*task_done/i); + expect(result).toMatch(/end of execution|before calling.*fn_task_done/i); // Should forbid task-specific trivia expect(result).toMatch(/avoid.*trivia|task-specific.*trivia|per-task.*log/i); // Should allow consolidation/editing @@ -2593,8 +2593,8 @@ describe("buildExecutionPrompt", () => { const memorySection = memorySectionMatch![1]; // QMD should NOT unconditionally reference .fusion/memory/ in the memory section expect(memorySection).not.toContain(".fusion/memory/"); - expect(memorySection).toContain("memory_search"); - expect(memorySection).toContain("memory_get"); + expect(memorySection).toContain("fn_memory_search"); + expect(memorySection).toContain("fn_memory_get"); }); it("QMD prompt has actionable memory instructions", () => { @@ -2610,7 +2610,7 @@ describe("buildExecutionPrompt", () => { const memorySection = memorySectionMatch![1]; // QMD should NOT contain .fusion/memory/ expect(memorySection).not.toContain(".fusion/memory/"); - expect(memorySection).toContain("memory_search"); + expect(memorySection).toContain("fn_memory_search"); // Contains "end of execution" write guidance expect(memorySection).toMatch(/end of execution/i); }); @@ -2707,7 +2707,7 @@ describe("summarizeToolArgs", () => { }); it("returns first 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 provided", () => { @@ -2948,7 +2948,7 @@ describe("TaskExecutor pause behavior", () => { // Wait for async execution to start await new Promise((r) => setTimeout(r, 50)); - // Agent created at least twice: initial resume + retry when agent finishes without task_done + // Agent created at least twice: initial resume + retry when agent finishes without fn_task_done // (async worktree validation may allow additional retry cycles within the timeout) expect(mockedCreateFnAgent.mock.calls.length).toBeGreaterThanOrEqual(2); expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Resuming execution after unpause", undefined, undefined); @@ -3094,7 +3094,7 @@ describe("TaskExecutor pause behavior", () => { updatedAt: new Date().toISOString(), }); - // At least two agent creations (initial + retry without task_done), but no duplicate from the unpause event + // At least two agent creations (initial + retry without fn_task_done), but no duplicate from the unpause event // (async worktree validation may allow additional retry cycles) expect(mockedCreateFnAgent.mock.calls.length).toBeGreaterThanOrEqual(2); }); @@ -3164,7 +3164,7 @@ describe("TaskExecutor pause behavior", () => { await executePromise; - // Four agent sessions (initial + 3 retries without task_done) — the unpause during active session was a no-op + // Four agent sessions (initial + 3 retries without fn_task_done) — the unpause during active session was a no-op expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4); }); @@ -3419,7 +3419,7 @@ describe("swallowed async store failure observability", () => { return { session: { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = capturedCustomTools.find((tool) => tool.name === "task_done"); + const taskDoneTool = capturedCustomTools.find((tool) => tool.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("call-1", { summary: "done" }); } @@ -3531,7 +3531,7 @@ describe("swallowed async store failure observability", () => { return { session: { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "task_done"); + const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("call-1", { summary: "done" }); } @@ -3950,7 +3950,7 @@ describe("TaskExecutor global pause behavior", () => { expect(store.updateTask).not.toHaveBeenCalledWith("FN-001", { status: "failed" }); }); - it("finalizes to in-review when global pause hits after task_done", async () => { + it("finalizes to in-review when global pause hits after fn_task_done", async () => { const store = createMockStore(); mockedCreateFnAgent.mockImplementation(async (opts: any) => { @@ -3958,7 +3958,7 @@ describe("TaskExecutor global pause behavior", () => { return { session: { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("tool-1", {}); } @@ -4002,7 +4002,7 @@ describe("TaskExecutor global pause behavior", () => { settings: { globalPause: false }, previous: { globalPause: false }, }); - const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "task_done"); + const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("call-1", { summary: "done" }); } @@ -4037,7 +4037,7 @@ describe("TaskExecutor global pause behavior", () => { settings: { globalPause: true }, previous: { globalPause: true }, }); - const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "task_done"); + const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("call-1", { summary: "done" }); } @@ -4080,7 +4080,7 @@ describe("TaskExecutor enginePaused soft pause (no agent termination)", () => { settings: { enginePaused: true }, previous: { enginePaused: false }, }); - const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "task_done"); + const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("call-1", { summary: "done" }); } @@ -4119,7 +4119,7 @@ describe("TaskExecutor enginePaused soft pause (no agent termination)", () => { settings: { enginePaused: true }, previous: { enginePaused: false }, }); - const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "task_done"); + const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("call-1", { summary: "done" }); } @@ -4154,7 +4154,7 @@ describe("TaskExecutor enginePaused soft pause (no agent termination)", () => { settings: { enginePaused: false }, previous: { enginePaused: false }, }); - const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "task_done"); + const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("call-1", { summary: "done" }); } @@ -4189,7 +4189,7 @@ describe("TaskExecutor enginePaused soft pause (no agent termination)", () => { settings: { enginePaused: true }, previous: { enginePaused: true }, }); - const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "task_done"); + const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("call-1", { summary: "done" }); } @@ -4282,7 +4282,7 @@ describe("Code review verdict tracking", () => { }); const tools = await captureTools(); - const result = await tools.review_step("call1", { + const result = await tools.fn_review_step("call1", { step: 1, type: "code", step_name: "Implement", @@ -4292,8 +4292,8 @@ describe("Code review verdict tracking", () => { expect(result.content[0].text).toContain("REVISE"); expect(result.content[0].text).toContain("cannot be marked done"); - // Now task_update(step=1, status="done") should be blocked - const updateResult = await tools.task_update("call2", { step: 1, status: "done" }); + // Now fn_task_update(step=1, status="done") should be blocked + const updateResult = await tools.fn_task_update("call2", { step: 1, status: "done" }); expect(updateResult.content[0].text).toContain("Cannot mark Step 1 as done"); expect(updateResult.content[0].text).toContain("REVISE"); }); @@ -4307,7 +4307,7 @@ describe("Code review verdict tracking", () => { }); const tools = await captureTools(); - await tools.review_step("call1", { + await tools.fn_review_step("call1", { step: 1, type: "code", step_name: "Implement", @@ -4315,7 +4315,7 @@ describe("Code review verdict tracking", () => { }); // Verify it's blocked - const blocked = await tools.task_update("call2", { step: 1, status: "done" }); + const blocked = await tools.fn_task_update("call2", { step: 1, status: "done" }); expect(blocked.content[0].text).toContain("Cannot mark Step 1 as done"); // Now: APPROVE @@ -4325,15 +4325,15 @@ describe("Code review verdict tracking", () => { summary: "All good", }); - await tools.review_step("call3", { + await tools.fn_review_step("call3", { step: 1, type: "code", step_name: "Implement", baseline: "def456", }); - // Now task_update should succeed - const updateResult = await tools.task_update("call4", { step: 1, status: "done" }); + // Now fn_task_update should succeed + const updateResult = await tools.fn_task_update("call4", { step: 1, status: "done" }); expect(updateResult.content[0].text).toContain("→ done"); }); @@ -4345,7 +4345,7 @@ describe("Code review verdict tracking", () => { }); const tools = await captureTools(); - const result = await tools.review_step("call1", { + const result = await tools.fn_review_step("call1", { step: 1, type: "plan", step_name: "Implement", @@ -4355,19 +4355,19 @@ describe("Code review verdict tracking", () => { expect(result.content[0].text).toContain("REVISE"); expect(result.content[0].text).not.toContain("cannot be marked done"); - // task_update should still work (plan reviews are advisory) - const updateResult = await tools.task_update("call2", { step: 1, status: "done" }); + // fn_task_update should still work (plan reviews are advisory) + const updateResult = await tools.fn_task_update("call2", { step: 1, status: "done" }); expect(updateResult.content[0].text).toContain("→ done"); }); }); -describe("Code review verdict enforcement - task_update blocking", () => { +describe("Code review verdict enforcement - fn_task_update blocking", () => { beforeEach(() => { vi.clearAllMocks(); mockedExistsSync.mockReturnValue(true); }); - it("task_update(status='done') is rejected when last code review was REVISE", async () => { + it("fn_task_update(status='done') is rejected when last code review was REVISE", async () => { mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Fix issues", @@ -4375,38 +4375,38 @@ describe("Code review verdict enforcement - task_update blocking", () => { }); const tools = await captureTools(); - await tools.review_step("call1", { + await tools.fn_review_step("call1", { step: 1, type: "code", step_name: "Implement", baseline: "abc", }); - const result = await tools.task_update("call2", { step: 1, status: "done" }); + const result = await tools.fn_task_update("call2", { step: 1, status: "done" }); expect(result.content[0].text).toContain("Cannot mark Step 1 as done"); - expect(result.content[0].text).toContain("review_step"); + expect(result.content[0].text).toContain("fn_review_step"); }); - it("task_update succeeds after a subsequent APPROVE", async () => { + it("fn_task_update succeeds after a subsequent APPROVE", async () => { const tools = await captureTools(); // REVISE first mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Fix", summary: "Bad" }); - await tools.review_step("c1", { step: 1, type: "code", step_name: "Impl", baseline: "a" }); + await tools.fn_review_step("c1", { step: 1, type: "code", step_name: "Impl", baseline: "a" }); // Then APPROVE mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "OK", summary: "Good" }); - await tools.review_step("c2", { step: 1, type: "code", step_name: "Impl", baseline: "b" }); + await tools.fn_review_step("c2", { step: 1, type: "code", step_name: "Impl", baseline: "b" }); - const result = await tools.task_update("c3", { step: 1, status: "done" }); + const result = await tools.fn_task_update("c3", { step: 1, status: "done" }); expect(result.content[0].text).toContain("→ done"); }); - it("task_update succeeds when no code review was requested (review level 0)", async () => { + it("fn_task_update succeeds when no code review was requested (review level 0)", async () => { const tools = await captureTools(); - // No review_step calls at all - const result = await tools.task_update("c1", { step: 1, status: "done" }); + // No fn_review_step calls at all + const result = await tools.fn_task_update("c1", { step: 1, status: "done" }); expect(result.content[0].text).toContain("→ done"); }); @@ -4414,9 +4414,9 @@ describe("Code review verdict enforcement - task_update blocking", () => { mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Rethink", summary: "Plan issue" }); const tools = await captureTools(); - await tools.review_step("c1", { step: 1, type: "plan", step_name: "Impl" }); + await tools.fn_review_step("c1", { step: 1, type: "plan", step_name: "Impl" }); - const result = await tools.task_update("c2", { step: 1, status: "done" }); + const result = await tools.fn_task_update("c2", { step: 1, status: "done" }); expect(result.content[0].text).toContain("→ done"); }); @@ -4424,14 +4424,14 @@ describe("Code review verdict enforcement - task_update blocking", () => { mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Fix", summary: "Bad" }); const tools = await captureTools(); - await tools.review_step("c1", { step: 1, type: "code", step_name: "Step1", baseline: "a" }); + await tools.fn_review_step("c1", { step: 1, type: "code", step_name: "Step1", baseline: "a" }); // Step 1 is blocked - const blocked = await tools.task_update("c2", { step: 1, status: "done" }); + const blocked = await tools.fn_task_update("c2", { step: 1, status: "done" }); expect(blocked.content[0].text).toContain("Cannot mark Step 1 as done"); // Step 2 is NOT blocked (no review for step 2) - const allowed = await tools.task_update("c3", { step: 2, status: "done" }); + const allowed = await tools.fn_task_update("c3", { step: 2, status: "done" }); expect(allowed.content[0].text).toContain("→ done"); }); @@ -4439,10 +4439,10 @@ describe("Code review verdict enforcement - task_update blocking", () => { mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Bug found", summary: "Issues" }); const tools = await captureTools(); - const result = await tools.review_step("c1", { step: 1, type: "code", step_name: "Implement", baseline: "abc" }); + const result = await tools.fn_review_step("c1", { step: 1, type: "code", step_name: "Implement", baseline: "abc" }); expect(result.content[0].text).toContain("cannot be marked done"); - expect(result.content[0].text).toContain("review_step"); + expect(result.content[0].text).toContain("fn_review_step"); expect(result.content[0].text).toContain('type="code"'); }); @@ -4487,14 +4487,14 @@ describe("Code review verdict enforcement - task_update blocking", () => { // The direct test for EXECUTOR_SYSTEM_PROMPT is skipped because of module caching issues in vitest. // The buildExecutionPrompt test verifies the CRITICAL language is included in execution prompts. - it("task_update with non-done status is not blocked by REVISE", async () => { + it("fn_task_update with non-done status is not blocked by REVISE", async () => { mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Fix", summary: "Bad" }); const tools = await captureTools(); - await tools.review_step("c1", { step: 1, type: "code", step_name: "Step1", baseline: "a" }); + await tools.fn_review_step("c1", { step: 1, type: "code", step_name: "Step1", baseline: "a" }); // "in-progress" should still work even with REVISE - const result = await tools.task_update("c2", { step: 1, status: "in-progress" }); + const result = await tools.fn_task_update("c2", { step: 1, status: "in-progress" }); expect(result.content[0].text).toContain("→ in-progress"); }); }); @@ -4515,7 +4515,7 @@ describe("RETHINK verdict handling", () => { updatedAt: new Date().toISOString(), }); - /** Return value for store.updateStep that satisfies the task_update tool. */ + /** Return value for store.updateStep that satisfies the fn_task_update tool. */ function makeStepResult(stepIndex: number, status: string) { const steps = Array.from({ length: Math.max(stepIndex + 1, 3) }, (_, i) => ({ name: `Step ${i}`, @@ -4575,13 +4575,13 @@ describe("RETHINK verdict handling", () => { }); const { toolMap } = await captureRethinkTools(store); - const reviewTool = toolMap.get("review_step"); + const reviewTool = toolMap.get("fn_review_step"); - // First call task_update to set in-progress (captures checkpoint) - const updateTool = toolMap.get("task_update"); + // First call fn_task_update to set in-progress (captures checkpoint) + const updateTool = toolMap.get("fn_task_update"); await updateTool.execute("call-1", { step: 1, status: "in-progress" }); - // Now call review_step with a baseline + // Now call fn_review_step with a baseline const result = await reviewTool.execute("call-2", { step: 1, type: "code", @@ -4611,11 +4611,11 @@ describe("RETHINK verdict handling", () => { const { toolMap, mockNavigateTree } = await captureRethinkTools(store); // Capture checkpoint - const updateTool = toolMap.get("task_update"); + const updateTool = toolMap.get("fn_task_update"); await updateTool.execute("call-1", { step: 1, status: "in-progress" }); // Trigger RETHINK - await toolMap.get("review_step").execute("call-2", { + await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "code", step_name: "Test Step", @@ -4640,10 +4640,10 @@ describe("RETHINK verdict handling", () => { const { toolMap } = await captureRethinkTools(store); - const updateTool = toolMap.get("task_update"); + const updateTool = toolMap.get("fn_task_update"); await updateTool.execute("call-1", { step: 1, status: "in-progress" }); - await toolMap.get("review_step").execute("call-2", { + await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "code", step_name: "Test Step", @@ -4668,10 +4668,10 @@ describe("RETHINK verdict handling", () => { const { toolMap } = await captureRethinkTools(store); - const updateTool = toolMap.get("task_update"); + const updateTool = toolMap.get("fn_task_update"); await updateTool.execute("call-1", { step: 1, status: "in-progress" }); - const result = await toolMap.get("review_step").execute("call-2", { + const result = await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "code", step_name: "Test Step", @@ -4699,11 +4699,11 @@ describe("RETHINK verdict handling", () => { const { toolMap, mockNavigateTree } = await captureRethinkTools(store); - const updateTool = toolMap.get("task_update"); + const updateTool = toolMap.get("fn_task_update"); await updateTool.execute("call-1", { step: 1, status: "in-progress" }); - // Call review_step WITHOUT baseline - await toolMap.get("review_step").execute("call-2", { + // Call fn_review_step WITHOUT baseline + await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "code", step_name: "Test Step", @@ -4734,10 +4734,10 @@ describe("RETHINK verdict handling", () => { const { toolMap, mockNavigateTree } = await captureRethinkTools(store); - // Do NOT call task_update for step 2, so no checkpoint exists + // Do NOT call fn_task_update for step 2, so no checkpoint exists - // Call review_step for step 2 — should not crash - const result = await toolMap.get("review_step").execute("call-2", { + // Call fn_review_step for step 2 — should not crash + const result = await toolMap.get("fn_review_step").execute("call-2", { step: 2, type: "code", step_name: "Test Step", @@ -4751,7 +4751,7 @@ describe("RETHINK verdict handling", () => { expect(result.content[0].text).toContain("RETHINK"); }); - it("pre-step checkpoint is captured when task_update sets status to in-progress", async () => { + it("pre-step checkpoint is captured when fn_task_update sets status to in-progress", async () => { const store = createMockStore(); store.updateStep.mockImplementation(async (_id: string, step: number, status: string) => makeStepResult(step, status), @@ -4759,7 +4759,7 @@ describe("RETHINK verdict handling", () => { const { toolMap, mockSessionManager } = await captureRethinkTools(store); - const updateTool = toolMap.get("task_update"); + const updateTool = toolMap.get("fn_task_update"); await updateTool.execute("call-1", { step: 1, status: "in-progress" }); // Verify getLeafId was called @@ -4804,10 +4804,10 @@ describe("RETHINK verdict handling", () => { for (const tool of capturedTools) toolMap.set(tool.name, tool); // Capture checkpoint - await toolMap.get("task_update").execute("call-1", { step: 1, status: "in-progress" }); + await toolMap.get("fn_task_update").execute("call-1", { step: 1, status: "in-progress" }); // Trigger RETHINK - await toolMap.get("review_step").execute("call-2", { + await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "code", step_name: "Test Step", @@ -4896,10 +4896,10 @@ describe("Plan RETHINK verdict handling", () => { const { toolMap, mockNavigateTree } = await capturePlanRethinkTools(store); // Capture checkpoint by starting step - await toolMap.get("task_update").execute("call-1", { step: 1, status: "in-progress" }); + await toolMap.get("fn_task_update").execute("call-1", { step: 1, status: "in-progress" }); // Trigger plan RETHINK - await toolMap.get("review_step").execute("call-2", { + await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "plan", step_name: "Test Step", @@ -4923,10 +4923,10 @@ describe("Plan RETHINK verdict handling", () => { const { toolMap } = await capturePlanRethinkTools(store); - await toolMap.get("task_update").execute("call-1", { step: 1, status: "in-progress" }); + await toolMap.get("fn_task_update").execute("call-1", { step: 1, status: "in-progress" }); // Even if baseline is passed, plan RETHINK should NOT git reset - await toolMap.get("review_step").execute("call-2", { + await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "plan", step_name: "Test Step", @@ -4954,9 +4954,9 @@ describe("Plan RETHINK verdict handling", () => { const { toolMap } = await capturePlanRethinkTools(store); - await toolMap.get("task_update").execute("call-1", { step: 1, status: "in-progress" }); + await toolMap.get("fn_task_update").execute("call-1", { step: 1, status: "in-progress" }); - await toolMap.get("review_step").execute("call-2", { + await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "plan", step_name: "Test Step", @@ -4980,9 +4980,9 @@ describe("Plan RETHINK verdict handling", () => { const { toolMap } = await capturePlanRethinkTools(store); - await toolMap.get("task_update").execute("call-1", { step: 1, status: "in-progress" }); + await toolMap.get("fn_task_update").execute("call-1", { step: 1, status: "in-progress" }); - const result = await toolMap.get("review_step").execute("call-2", { + const result = await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "plan", step_name: "Test Step", @@ -5010,10 +5010,10 @@ describe("Plan RETHINK verdict handling", () => { const { toolMap, mockNavigateTree } = await capturePlanRethinkTools(store); - // Do NOT call task_update for step 2, so no checkpoint exists + // Do NOT call fn_task_update for step 2, so no checkpoint exists - // Call review_step for step 2 — should not crash - const result = await toolMap.get("review_step").execute("call-2", { + // Call fn_review_step for step 2 — should not crash + const result = await toolMap.get("fn_review_step").execute("call-2", { step: 2, type: "plan", step_name: "Test Step", @@ -5041,9 +5041,9 @@ describe("Plan RETHINK verdict handling", () => { const { toolMap } = await capturePlanRethinkTools(store); - await toolMap.get("task_update").execute("call-1", { step: 1, status: "in-progress" }); + await toolMap.get("fn_task_update").execute("call-1", { step: 1, status: "in-progress" }); - await toolMap.get("review_step").execute("call-2", { + await toolMap.get("fn_review_step").execute("call-2", { step: 1, type: "plan", step_name: "Test Step", @@ -5133,11 +5133,11 @@ describe("E2E review pipeline — multi-verdict sequence", () => { const { tools } = await captureE2ETools(store); // Step 1: Start the step - await tools.task_update("u1", { step: 1, status: "in-progress" }); + await tools.fn_task_update("u1", { step: 1, status: "in-progress" }); // Step 2: Plan review → APPROVE (advisory, no blocking) mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "Good plan", summary: "Approved" }); - const planResult = await tools.review_step("r1", { + const planResult = await tools.fn_review_step("r1", { step: 1, type: "plan", step_name: "Implement", }); expect(planResult.content[0].text).toBe("APPROVE"); @@ -5146,26 +5146,26 @@ describe("E2E review pipeline — multi-verdict sequence", () => { mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Missing error handling in fetchUser()", summary: "Needs fixes", }); - const reviseResult = await tools.review_step("r2", { + const reviseResult = await tools.fn_review_step("r2", { step: 1, type: "code", step_name: "Implement", baseline: "sha-1", }); expect(reviseResult.content[0].text).toContain("cannot be marked done"); // Step 4: Attempt to mark done — should be blocked - const blockedResult = await tools.task_update("u2", { step: 1, status: "done" }); + const blockedResult = await tools.fn_task_update("u2", { step: 1, status: "done" }); expect(blockedResult.content[0].text).toContain("Cannot mark Step 1 as done"); // Step 5: Fix issues, re-submit code review → APPROVE mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "Error handling added correctly", summary: "All good", }); - const approveResult = await tools.review_step("r3", { + const approveResult = await tools.fn_review_step("r3", { step: 1, type: "code", step_name: "Implement", baseline: "sha-2", }); expect(approveResult.content[0].text).toBe("APPROVE"); // Step 6: Now marking done should succeed - const doneResult = await tools.task_update("u3", { step: 1, status: "done" }); + const doneResult = await tools.fn_task_update("u3", { step: 1, status: "done" }); expect(doneResult.content[0].text).toContain("→ done"); }); @@ -5178,13 +5178,13 @@ describe("E2E review pipeline — multi-verdict sequence", () => { const { tools, mockNavigateTree } = await captureE2ETools(store); // Step 1: Start the step (captures checkpoint) - await tools.task_update("u1", { step: 1, status: "in-progress" }); + await tools.fn_task_update("u1", { step: 1, status: "in-progress" }); // Step 2: Code review → RETHINK (rewind everything) mockedReviewStep.mockResolvedValue({ verdict: "RETHINK", review: "Using polling instead of events is wrong", summary: "Bad approach", }); - const rethinkResult = await tools.review_step("r1", { + const rethinkResult = await tools.fn_review_step("r1", { step: 1, type: "code", step_name: "Implement", baseline: "sha-bad", }); @@ -5199,19 +5199,19 @@ describe("E2E review pipeline — multi-verdict sequence", () => { expect(store.updateStep).toHaveBeenCalledWith("FN-E2E", 1, "pending"); // Step 3: Restart the step (new approach) - await tools.task_update("u2", { step: 1, status: "in-progress" }); + await tools.fn_task_update("u2", { step: 1, status: "in-progress" }); // Step 4: Code review → APPROVE on second attempt mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "Event-driven approach is correct", summary: "Approved", }); - const approveResult = await tools.review_step("r2", { + const approveResult = await tools.fn_review_step("r2", { step: 1, type: "code", step_name: "Implement", baseline: "sha-good", }); expect(approveResult.content[0].text).toBe("APPROVE"); // Step 5: Mark done — should succeed (no REVISE blocking) - const doneResult = await tools.task_update("u3", { step: 1, status: "done" }); + const doneResult = await tools.fn_task_update("u3", { step: 1, status: "done" }); expect(doneResult.content[0].text).toContain("→ done"); }); @@ -5224,19 +5224,19 @@ describe("E2E review pipeline — multi-verdict sequence", () => { const { tools } = await captureE2ETools(store); // Step 1: Complete with APPROVE - await tools.task_update("u1", { step: 1, status: "in-progress" }); + await tools.fn_task_update("u1", { step: 1, status: "in-progress" }); mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "OK", summary: "Good" }); - await tools.review_step("r1", { step: 1, type: "code", step_name: "Implement", baseline: "sha-1" }); - const step1Done = await tools.task_update("u2", { step: 1, status: "done" }); + await tools.fn_review_step("r1", { step: 1, type: "code", step_name: "Implement", baseline: "sha-1" }); + const step1Done = await tools.fn_task_update("u2", { step: 1, status: "done" }); expect(step1Done.content[0].text).toContain("→ done"); // Step 2: Gets REVISE - await tools.task_update("u3", { step: 2, status: "in-progress" }); + await tools.fn_task_update("u3", { step: 2, status: "in-progress" }); mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Tests insufficient", summary: "Bad" }); - await tools.review_step("r2", { step: 2, type: "code", step_name: "Tests", baseline: "sha-2" }); + await tools.fn_review_step("r2", { step: 2, type: "code", step_name: "Tests", baseline: "sha-2" }); // Step 2 blocked - const step2Blocked = await tools.task_update("u4", { step: 2, status: "done" }); + const step2Blocked = await tools.fn_task_update("u4", { step: 2, status: "done" }); expect(step2Blocked.content[0].text).toContain("Cannot mark Step 2 as done"); // Step 1 remains unaffected — if agent tries to re-update step 1, it still works @@ -5252,13 +5252,13 @@ describe("E2E review pipeline — multi-verdict sequence", () => { const { tools, mockNavigateTree } = await captureE2ETools(store); // Start step - await tools.task_update("u1", { step: 1, status: "in-progress" }); + await tools.fn_task_update("u1", { step: 1, status: "in-progress" }); // Plan review → RETHINK mockedReviewStep.mockResolvedValue({ verdict: "RETHINK", review: "Plan ignores edge cases", summary: "Bad plan", }); - const rethinkResult = await tools.review_step("r1", { + const rethinkResult = await tools.fn_review_step("r1", { step: 1, type: "plan", step_name: "Implement", }); expect(rethinkResult.content[0].text).toContain("Your plan was rejected"); @@ -5273,25 +5273,25 @@ describe("E2E review pipeline — multi-verdict sequence", () => { expect(mockNavigateTree).toHaveBeenCalled(); // Restart step with new plan - await tools.task_update("u2", { step: 1, status: "in-progress" }); + await tools.fn_task_update("u2", { step: 1, status: "in-progress" }); // Plan review → APPROVE mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "Good plan", summary: "Approved" }); - await tools.review_step("r2", { step: 1, type: "plan", step_name: "Implement" }); + await tools.fn_review_step("r2", { step: 1, type: "plan", step_name: "Implement" }); // Code phase: APPROVE directly mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "Clean code", summary: "Good" }); - await tools.review_step("r3", { step: 1, type: "code", step_name: "Implement", baseline: "sha-1" }); + await tools.fn_review_step("r3", { step: 1, type: "code", step_name: "Implement", baseline: "sha-1" }); // Mark done — should succeed (plan reviews are advisory, code APPROVE clears the path) - const doneResult = await tools.task_update("u3", { step: 1, status: "done" }); + const doneResult = await tools.fn_task_update("u3", { step: 1, status: "done" }); expect(doneResult.content[0].text).toContain("→ done"); }); }); -// ── task_add_dep tool tests ────────────────────────────────────────── +// ── fn_task_add_dep tool tests ────────────────────────────────────────── -describe("task_add_dep tool", () => { +describe("fn_task_add_dep tool", () => { /** * Helper: run executor with a customized mock store and capture custom tools. * The mock store's getTask is configured to: @@ -5392,7 +5392,7 @@ describe("task_add_dep tool", () => { it("adds a valid dependency via store.updateTask when confirm=true", async () => { const { tools, store } = await captureAddDepTools(); - const result = await tools.task_add_dep("call1", { task_id: "FN-OTHER", confirm: true }); + const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER", confirm: true }); expect(result.content[0].text).toContain("Added dependency"); expect(result.content[0].text).toContain("triage"); @@ -5404,7 +5404,7 @@ describe("task_add_dep tool", () => { it("returns error for self-dependency", async () => { const { tools, store } = await captureAddDepTools(); - const result = await tools.task_add_dep("call1", { task_id: "FN-TEST" }); + const result = await tools.fn_task_add_dep("call1", { task_id: "FN-TEST" }); expect(result.content[0].text).toContain("Cannot add self-dependency"); expect(result.content[0].text).toContain("FN-TEST cannot depend on itself"); @@ -5419,7 +5419,7 @@ describe("task_add_dep tool", () => { it("returns error for non-existent target task", async () => { const { tools, store } = await captureAddDepTools({ targetExists: false }); - const result = await tools.task_add_dep("call1", { task_id: "FN-OTHER" }); + const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER" }); expect(result.content[0].text).toContain("FN-OTHER not found"); expect(result.content[0].text).toContain("Cannot add dependency on a non-existent task"); @@ -5432,7 +5432,7 @@ describe("task_add_dep tool", () => { it("returns informational message for duplicate dependency without duplicating", async () => { const { tools, store } = await captureAddDepTools({ existingDeps: ["FN-OTHER"] }); - const result = await tools.task_add_dep("call1", { task_id: "FN-OTHER" }); + const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER" }); expect(result.content[0].text).toContain("already a dependency"); expect(result.content[0].text).toContain("No changes made"); @@ -5445,7 +5445,7 @@ describe("task_add_dep tool", () => { it("logs the dependency addition via store.logEntry when confirm=true", async () => { const { tools, store } = await captureAddDepTools(); - await tools.task_add_dep("call1", { task_id: "FN-OTHER", confirm: true }); + await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER", confirm: true }); expect(store.logEntry).toHaveBeenCalledWith("FN-TEST", "Added dependency on FN-OTHER — stopping execution for re-specification"); }); @@ -5453,7 +5453,7 @@ describe("task_add_dep tool", () => { it("appends to existing dependencies without overwriting when confirm=true", async () => { const { tools, store } = await captureAddDepTools({ existingDeps: ["FN-001"] }); - const result = await tools.task_add_dep("call1", { task_id: "FN-OTHER", confirm: true }); + const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER", confirm: true }); expect(result.content[0].text).toContain("Added dependency"); expect(store.updateTask).toHaveBeenCalledWith("FN-TEST", { @@ -5464,14 +5464,14 @@ describe("task_add_dep tool", () => { it("is registered in customTools array", async () => { const { tools } = await captureAddDepTools(); - expect(tools.task_add_dep).toBeDefined(); - expect(typeof tools.task_add_dep).toBe("function"); + expect(tools.fn_task_add_dep).toBeDefined(); + expect(typeof tools.fn_task_add_dep).toBe("function"); }); it("returns warning without confirm=true and does NOT add dependency", async () => { const { tools, store } = await captureAddDepTools(); - const result = await tools.task_add_dep("call1", { task_id: "FN-OTHER" }); + const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER" }); expect(result.content[0].text).toContain("stop execution and discard current work"); expect(result.content[0].text).toContain("confirm=true"); @@ -5490,17 +5490,17 @@ describe("task_add_dep tool", () => { it("validation errors (self-dep, not-found, dedup) return immediately without requiring confirm", async () => { // Self-dep — no confirm needed const { tools: tools1 } = await captureAddDepTools(); - const selfResult = await tools1.task_add_dep("call1", { task_id: "FN-TEST" }); + const selfResult = await tools1.fn_task_add_dep("call1", { task_id: "FN-TEST" }); expect(selfResult.content[0].text).toContain("Cannot add self-dependency"); // Not found — no confirm needed const { tools: tools2 } = await captureAddDepTools({ targetExists: false }); - const notFoundResult = await tools2.task_add_dep("call1", { task_id: "FN-OTHER" }); + const notFoundResult = await tools2.fn_task_add_dep("call1", { task_id: "FN-OTHER" }); expect(notFoundResult.content[0].text).toContain("not found"); // Dedup — no confirm needed const { tools: tools3 } = await captureAddDepTools({ existingDeps: ["FN-OTHER"] }); - const dedupResult = await tools3.task_add_dep("call1", { task_id: "FN-OTHER" }); + const dedupResult = await tools3.fn_task_add_dep("call1", { task_id: "FN-OTHER" }); expect(dedupResult.content[0].text).toContain("already a dependency"); }); @@ -5550,8 +5550,8 @@ describe("task_add_dep tool", () => { return { session: { prompt: vi.fn().mockImplementation(async () => { - // The agent calls task_add_dep with confirm=true during execution - const addDepTool = capturedTools.find((t: any) => t.name === "task_add_dep"); + // The agent calls fn_task_add_dep with confirm=true during execution + const addDepTool = capturedTools.find((t: any) => t.name === "fn_task_add_dep"); await addDepTool.execute("call1", { task_id: "FN-TARGET", confirm: true }); // After dispose is called, session.prompt throws throw new Error("Session terminated"); @@ -6087,7 +6087,7 @@ describe("TaskExecutor bounded recovery retries", () => { updatedAt: new Date().toISOString(), }); - // Exhausted no-task_done retries now requeue immediately to todo. + // Exhausted no-fn_task_done retries now requeue immediately to todo. expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo"); }); }); @@ -6796,12 +6796,12 @@ describe("Invalid transition error handling", () => { updatedAt: new Date().toISOString(), }); - // A missing task_done triggers 3 retries. The final requeue-to-todo move + // A missing fn_task_done triggers 3 retries. The final requeue-to-todo move // then throws the Invalid transition error, // which is caught by the outer handler. expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "failed", - error: "Agent finished without calling task_done (after 3 retries)", + error: "Agent finished without calling fn_task_done (after 3 retries)", taskDoneRetryCount: 1, }); @@ -6890,7 +6890,7 @@ describe("Invalid transition error handling", () => { }); }); -describe("TaskExecutor task_done with summary", () => { +describe("TaskExecutor fn_task_done with summary", () => { beforeEach(() => { vi.clearAllMocks(); mockedExistsSync.mockReturnValue(true); @@ -6901,8 +6901,8 @@ describe("TaskExecutor task_done with summary", () => { let capturedTool: any = null; mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => { - // Capture the task_done tool - capturedTool = customTools?.find((t: any) => t.name === "task_done"); + // Capture the fn_task_done tool + capturedTool = customTools?.find((t: any) => t.name === "fn_task_done"); return { session: { prompt: vi.fn().mockResolvedValue(undefined), @@ -6927,9 +6927,9 @@ describe("TaskExecutor task_done with summary", () => { updatedAt: new Date().toISOString(), }); - // Verify task_done tool was created + // Verify fn_task_done tool was created expect(capturedTool).toBeDefined(); - expect(capturedTool.name).toBe("task_done"); + expect(capturedTool.name).toBe("fn_task_done"); // Verify the tool accepts summary parameter expect(capturedTool.parameters).toBeDefined(); @@ -6949,7 +6949,7 @@ describe("TaskExecutor task_done with summary", () => { let capturedTool: any = null; mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => { - capturedTool = customTools?.find((t: any) => t.name === "task_done"); + capturedTool = customTools?.find((t: any) => t.name === "fn_task_done"); return { session: { prompt: vi.fn().mockResolvedValue(undefined), @@ -6987,13 +6987,13 @@ describe("TaskExecutor task_done with summary", () => { }); }); -describe("TaskExecutor task_done blockers", () => { +describe("TaskExecutor fn_task_done blockers", () => { beforeEach(() => { vi.clearAllMocks(); mockedExistsSync.mockReturnValue(true); }); - it("rejects task_done when the task is explicitly blocked", async () => { + it("rejects fn_task_done when the task is explicitly blocked", async () => { const store = createMockStore(); let capturedTool: any = null; @@ -7021,7 +7021,7 @@ describe("TaskExecutor task_done blockers", () => { }); mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => { - capturedTool = customTools?.find((t: any) => t.name === "task_done"); + capturedTool = customTools?.find((t: any) => t.name === "fn_task_done"); return { session: { prompt: vi.fn().mockResolvedValue(undefined), @@ -7064,8 +7064,8 @@ describe("Workflow Steps Execution", () => { }); /** - * 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(). + * 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(). */ function createAgentWithTaskDone() { let capturedCustomTools: any[] = []; @@ -7074,8 +7074,8 @@ describe("Workflow Steps Execution", () => { capturedCustomTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - // Find and execute task_done tool to set taskDone = true - const taskDoneTool = capturedCustomTools.find((t: any) => t.name === "task_done"); + // Find and execute fn_task_done tool to set taskDone = true + const taskDoneTool = capturedCustomTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) { await taskDoneTool.execute("tool-1", {}); } @@ -7090,7 +7090,7 @@ describe("Workflow Steps Execution", () => { }) as any); } - it("requeues to todo after 3 retries when the agent exits without calling task_done", async () => { + it("requeues to todo after 3 retries when the agent exits without calling fn_task_done", async () => { const store = createMockStore(); store.getTask.mockResolvedValue({ id: "FN-001", @@ -7137,27 +7137,27 @@ describe("Workflow Steps Execution", () => { // Should have been called four times: initial + 3 retries expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4); - // Retries still didn't call task_done, so it fails and requeues immediately. + // Retries still didn't call fn_task_done, so it fails and requeues immediately. expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "failed", - error: "Agent finished without calling task_done (after 3 retries)", + error: "Agent finished without calling fn_task_done (after 3 retries)", taskDoneRetryCount: 1, }); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo"); expect(store.logEntry).toHaveBeenCalledWith( "FN-001", - "Agent finished without calling task_done (after 3 retries) — requeued to todo immediately (1/3)", + "Agent finished without calling fn_task_done (after 3 retries) — requeued to todo immediately (1/3)", undefined, expect.objectContaining({ agentId: "executor" }), ); expect(onError).toHaveBeenCalledWith( expect.objectContaining({ id: "FN-001" }), - expect.objectContaining({ message: "Agent finished without calling task_done (after 3 retries)" }), + expect.objectContaining({ message: "Agent finished without calling fn_task_done (after 3 retries)" }), ); expect(onComplete).not.toHaveBeenCalled(); }); - it("moves task to in-review once task_done requeue budget is exhausted", async () => { + it("moves task to in-review once fn_task_done requeue budget is exhausted", async () => { const store = createMockStore(); store.getTask.mockResolvedValue({ id: "FN-001", @@ -7205,13 +7205,13 @@ describe("Workflow Steps Execution", () => { expect(mockedCreateFnAgent.mock.calls.length).toBeGreaterThanOrEqual(1); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "failed", - error: "Agent finished without calling task_done (after 3 retries)", + error: "Agent finished without calling fn_task_done (after 3 retries)", }); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review"); expect(store.moveTask).not.toHaveBeenCalledWith("FN-001", "todo"); expect(onError).toHaveBeenCalledWith( expect.objectContaining({ id: "FN-001" }), - expect.objectContaining({ message: "Agent finished without calling task_done (after 3 retries)" }), + expect.objectContaining({ message: "Agent finished without calling fn_task_done (after 3 retries)" }), ); }); @@ -7244,16 +7244,16 @@ describe("Workflow Steps Execution", () => { updatedAt: new Date().toISOString(), }); - // First call: main agent with task_done, subsequent calls: simple mocks for workflow step agents + // First call: main agent with fn_task_done, subsequent calls: simple mocks for workflow step agents let callIdx = 0; mockedCreateFnAgent.mockImplementation((async (opts: any) => { callIdx++; if (callIdx === 1) { - // Main execution — find and trigger task_done + // Main execution — find and trigger fn_task_done const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -7345,7 +7345,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -7426,7 +7426,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -7608,7 +7608,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -7700,7 +7700,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -7799,7 +7799,7 @@ describe("Workflow Steps Execution", () => { return Buffer.from(""); }); - // Main agent with task_done + // Main agent with fn_task_done createAgentWithTaskDone(); const onComplete = vi.fn(); @@ -7965,7 +7965,7 @@ describe("Workflow Steps Execution", () => { // Should reset all steps to pending // Check that updateStep was called with "pending" for step 0 - // (There may be multiple calls - first from task_done marking it done, second from sendTaskBackForFix resetting it) + // (There may be multiple calls - first from fn_task_done marking it done, second from sendTaskBackForFix resetting it) const updateStepCalls = store.updateStep.mock.calls; const hasResetToPending = updateStepCalls.some( (call: any[]) => call[0] === "FN-001" && call[1] === 0 && call[2] === "pending" @@ -8097,7 +8097,7 @@ describe("Workflow Steps Execution", () => { // Should reset all steps to pending // Check that updateStep was called with "pending" for step 0 - // (There may be multiple calls - first from task_done marking it done, second from sendTaskBackForFix resetting it) + // (There may be multiple calls - first from fn_task_done marking it done, second from sendTaskBackForFix resetting it) const updateStepCalls = store.updateStep.mock.calls; const hasResetToPending = updateStepCalls.some( (call: any[]) => call[0] === "FN-001" && call[1] === 0 && call[2] === "pending" @@ -8177,7 +8177,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -8339,7 +8339,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -8439,7 +8439,7 @@ describe("Workflow Steps Execution", () => { return undefined; }); - // Main agent calls task_done, then a workflow step agent for pre-merge only + // Main agent calls fn_task_done, then a workflow step agent for pre-merge only let callIdx = 0; mockedCreateFnAgent.mockImplementation((async (opts: any) => { callIdx++; @@ -8447,7 +8447,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -8538,7 +8538,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -8674,7 +8674,7 @@ describe("Workflow Steps Execution", () => { updatedAt: new Date().toISOString(), }); - // First call: main agent with task_done + // First call: main agent with fn_task_done // Second call: workflow step agent that returns REQUEST REVISION let callIdx = 0; let subscribeHandler: any; @@ -8685,7 +8685,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -8800,7 +8800,7 @@ describe("Workflow Steps Execution", () => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -9518,12 +9518,12 @@ describe("TaskExecutor context limit error recovery", () => { "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"); expect(reducedPrompt).toContain("context window limit"); expect(reducedPrompt).toContain("git status"); - expect(reducedPrompt).toContain("task_done"); + expect(reducedPrompt).toContain("fn_task_done"); expect(reducedPrompt).toContain("Do not repeat what's already been done"); }); }); @@ -9643,15 +9643,15 @@ describe("Agent Spawning", () => { vi.mocked(execSync).mockReturnValue(""); }); - it("spawn_agent tool is registered in customTools", async () => { + it("fn_spawn_agent tool is registered in customTools", async () => { const { tools } = await captureToolsWithAgentStore(); - expect(tools.spawn_agent).toBeDefined(); - expect(typeof tools.spawn_agent).toBe("function"); + expect(tools.fn_spawn_agent).toBeDefined(); + expect(typeof tools.fn_spawn_agent).toBe("function"); }); it("returns error when AgentStore is not configured", async () => { const { tools } = await captureToolsWithAgentStore(undefined); - const result = await tools.spawn_agent("call1", { + const result = await tools.fn_spawn_agent("call1", { name: "researcher", role: "engineer", task: "Research something", @@ -9666,7 +9666,7 @@ describe("Agent Spawning", () => { const agentStore = createMockAgentStore(); const { tools } = await captureToolsWithAgentStore(agentStore); - const result = await tools.spawn_agent("call1", { + const result = await tools.fn_spawn_agent("call1", { name: "researcher", role: "engineer", task: "Research authentication patterns", @@ -9685,7 +9685,7 @@ describe("Agent Spawning", () => { const agentStore = createMockAgentStore(); const { tools } = await captureToolsWithAgentStore(agentStore); - const result = await tools.spawn_agent("call1", { + const result = await tools.fn_spawn_agent("call1", { name: "researcher", role: "engineer", task: "Research authentication patterns", @@ -9710,7 +9710,7 @@ describe("Agent Spawning", () => { const agentStore = createMockAgentStore(); const { tools } = await captureToolsWithAgentStore(agentStore); - await tools.spawn_agent("call1", { + await tools.fn_spawn_agent("call1", { name: "worker", role: "custom", task: "Do some work", @@ -9728,7 +9728,7 @@ describe("Agent Spawning", () => { const agentStore = createMockAgentStore(); const { tools } = await captureToolsWithAgentStore(agentStore); - await tools.spawn_agent("call1", { + await tools.fn_spawn_agent("call1", { name: "worker", role: "engineer", task: "Do some work", @@ -9753,11 +9753,11 @@ describe("Agent Spawning", () => { }); // Spawn 2 agents (limit) - await tools.spawn_agent("call1", { name: "a1", role: "engineer", task: "task 1" }); - await tools.spawn_agent("call2", { name: "a2", role: "engineer", task: "task 2" }); + await tools.fn_spawn_agent("call1", { name: "a1", role: "engineer", task: "task 1" }); + await tools.fn_spawn_agent("call2", { name: "a2", role: "engineer", task: "task 2" }); // 3rd should be rejected - const result = await tools.spawn_agent("call3", { name: "a3", role: "engineer", task: "task 3" }); + const result = await tools.fn_spawn_agent("call3", { name: "a3", role: "engineer", task: "task 3" }); expect(result.content[0].text).toContain("Per-parent spawn limit reached"); expect(result.content[0].text).toContain("2/2"); expect(result.details.state).toBe("error"); @@ -9770,12 +9770,12 @@ describe("Agent Spawning", () => { }); // Spawn 3 agents (global limit) - await tools.spawn_agent("call1", { name: "a1", role: "engineer", task: "task 1" }); - await tools.spawn_agent("call2", { name: "a2", role: "engineer", task: "task 2" }); - await tools.spawn_agent("call3", { name: "a3", role: "engineer", task: "task 3" }); + await tools.fn_spawn_agent("call1", { name: "a1", role: "engineer", task: "task 1" }); + await tools.fn_spawn_agent("call2", { name: "a2", role: "engineer", task: "task 2" }); + await tools.fn_spawn_agent("call3", { name: "a3", role: "engineer", task: "task 3" }); // 4th should hit global limit - const result = await tools.spawn_agent("call4", { name: "a4", role: "engineer", task: "task 4" }); + const result = await tools.fn_spawn_agent("call4", { name: "a4", role: "engineer", task: "task 4" }); expect(result.content[0].text).toContain("Global spawn limit reached"); expect(result.content[0].text).toContain("3/3"); expect(result.details.state).toBe("error"); @@ -9787,7 +9787,7 @@ describe("Agent Spawning", () => { const { tools } = await captureToolsWithAgentStore(agentStore); // Should be able to spawn (defaults: 5 per parent, 20 global) - const result = await tools.spawn_agent("call1", { + const result = await tools.fn_spawn_agent("call1", { name: "worker", role: "engineer", task: "task 1", @@ -9802,7 +9802,7 @@ describe("Agent Spawning", () => { const { tools } = await captureToolsWithAgentStore(agentStore); - const result = await tools.spawn_agent("call1", { + const result = await tools.fn_spawn_agent("call1", { name: "worker", role: "engineer", task: "task 1", @@ -9817,7 +9817,7 @@ describe("Agent Spawning", () => { const agentStore = createMockAgentStore(); const { tools } = await captureToolsWithAgentStore(agentStore); - await tools.spawn_agent("call1", { + await tools.fn_spawn_agent("call1", { name: " researcher ", role: "engineer", task: "task 1", @@ -9832,7 +9832,7 @@ describe("Agent Spawning", () => { const { tools } = await captureToolsWithAgentStore(agentStore); const longTask = "A".repeat(200); - const result = await tools.spawn_agent("call1", { + const result = await tools.fn_spawn_agent("call1", { name: "worker", role: "engineer", task: longTask, @@ -10592,7 +10592,7 @@ describe("TaskExecutor agent execution flow (FN-978)", () => { // The executing guard prevents duplicate execution from the event handler. // Note: createFnAgent may be called a second time if the agent finishes - // without calling task_done (retry path), but the initial trigger should + // without calling fn_task_done (retry path), but the initial trigger should // only cause one execution, not two. // Verify that store.on was called with task:moved (listener registered) expect(store.on).toHaveBeenCalledWith("task:moved", expect.any(Function)); @@ -11084,7 +11084,7 @@ describe("StepSessionExecutor integration", () => { // Give it time to set up the step executor await new Promise((r) => setTimeout(r, 50)); - // Simulate dep-abort by directly triggering the task_add_dep cleanup logic + // Simulate dep-abort by directly triggering the fn_task_add_dep cleanup logic // The dep-abort flag should cause the step-session path to handle cleanup // We can test this by checking that when depAborted is set, cleanup is called // For now, just resolve and verify cleanup runs @@ -11707,7 +11707,7 @@ describe("TaskExecutor messaging tools", () => { return captured; } - it("includes send_message when messageStore and assignedAgentId are available", async () => { + it("includes fn_send_message when messageStore and assignedAgentId are available", async () => { const mockMessageStore = { sendMessage: vi.fn().mockReturnValue({ id: "msg-001" }), }; @@ -11717,10 +11717,10 @@ describe("TaskExecutor messaging tools", () => { }); const toolNames = tools.map((t: any) => t.name); - expect(toolNames).toContain("send_message"); + expect(toolNames).toContain("fn_send_message"); }); - it("includes read_messages when messageStore and assignedAgentId are available", async () => { + it("includes fn_read_messages when messageStore and assignedAgentId are available", async () => { const mockMessageStore = { sendMessage: vi.fn().mockReturnValue({ id: "msg-001" }), getInbox: vi.fn().mockReturnValue([]), @@ -11731,19 +11731,19 @@ describe("TaskExecutor messaging tools", () => { }); const toolNames = tools.map((t: any) => t.name); - expect(toolNames).toContain("read_messages"); + expect(toolNames).toContain("fn_read_messages"); }); - it("excludes read_messages when messageStore is not provided", async () => { + it("excludes fn_read_messages when messageStore is not provided", async () => { const tools = await captureCustomTools({ assignedAgentId: "agent-001", }); const toolNames = tools.map((t: any) => t.name); - expect(toolNames).not.toContain("read_messages"); + expect(toolNames).not.toContain("fn_read_messages"); }); - it("excludes read_messages when assignedAgentId is not provided", async () => { + it("excludes fn_read_messages when assignedAgentId is not provided", async () => { const mockMessageStore = { sendMessage: vi.fn().mockReturnValue({ id: "msg-001" }), getInbox: vi.fn().mockReturnValue([]), @@ -11753,7 +11753,7 @@ describe("TaskExecutor messaging tools", () => { }); const toolNames = tools.map((t: any) => t.name); - expect(toolNames).not.toContain("read_messages"); + expect(toolNames).not.toContain("fn_read_messages"); }); it("excludes messaging tools when messageStore is not provided", async () => { @@ -11762,11 +11762,11 @@ describe("TaskExecutor messaging tools", () => { }); 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 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), @@ -11776,16 +11776,16 @@ describe("TaskExecutor messaging tools", () => { }); 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"); }); describe("fast mode", () => { @@ -11794,39 +11794,39 @@ describe("TaskExecutor messaging tools", () => { mockedExistsSync.mockReturnValue(true); }); - it("excludes review_step tool when executionMode is 'fast'", async () => { + it("excludes fn_review_step tool when executionMode is 'fast'", async () => { const tools = await captureCustomTools({ executionMode: "fast", }); const toolNames = tools.map((t: any) => t.name); - expect(toolNames).not.toContain("review_step"); + expect(toolNames).not.toContain("fn_review_step"); }); - it("includes task_update and task_done tools in fast mode", async () => { + it("includes fn_task_update and fn_task_done tools in fast mode", async () => { const tools = await captureCustomTools({ executionMode: "fast", }); const toolNames = tools.map((t: any) => t.name); - expect(toolNames).toContain("task_update"); - expect(toolNames).toContain("task_done"); + expect(toolNames).toContain("fn_task_update"); + expect(toolNames).toContain("fn_task_done"); }); - it("includes review_step tool when executionMode is 'standard'", async () => { + it("includes fn_review_step tool when executionMode is 'standard'", async () => { const tools = await captureCustomTools({ executionMode: "standard", }); const toolNames = tools.map((t: any) => t.name); - expect(toolNames).toContain("review_step"); + expect(toolNames).toContain("fn_review_step"); }); - it("includes review_step tool when executionMode is undefined (defaults to standard)", async () => { + it("includes fn_review_step tool when executionMode is undefined (defaults to standard)", async () => { const tools = await captureCustomTools({}); const toolNames = tools.map((t: any) => t.name); - expect(toolNames).toContain("review_step"); + expect(toolNames).toContain("fn_review_step"); }); it("logs executor model usage when execution starts", async () => { @@ -11910,12 +11910,12 @@ describe("TaskExecutor messaging tools", () => { updatedAt: new Date().toISOString(), }); - // Mock agent with task_done + // Mock agent with fn_task_done mockedCreateFnAgent.mockImplementation((async (opts: any) => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), @@ -11995,11 +11995,11 @@ describe("TaskExecutor messaging tools", () => { const session = { prompt: vi.fn().mockImplementation(async () => { if (callIdx === 1) { - // Main execution — find and trigger task_done - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + // Main execution — find and trigger fn_task_done + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); } else { - // Workflow step — no task_done needed + // Workflow step — no fn_task_done needed } }), dispose: vi.fn(), @@ -12036,7 +12036,7 @@ describe("TaskExecutor messaging tools", () => { expect(mockedCreateFnAgent).toHaveBeenCalledTimes(2); }); - it("still enforces task_done requirement in fast mode", async () => { + it("still enforces fn_task_done requirement in fast mode", async () => { const store = createMockStore(); // Task in fast mode @@ -12055,7 +12055,7 @@ describe("TaskExecutor messaging tools", () => { updatedAt: new Date().toISOString(), }); - // Mock agent that exits WITHOUT calling task_done + // Mock agent that exits WITHOUT calling fn_task_done mockedCreateFnAgent.mockResolvedValue({ session: { prompt: vi.fn().mockResolvedValue(undefined), @@ -12084,14 +12084,14 @@ describe("TaskExecutor messaging tools", () => { updatedAt: new Date().toISOString(), }); - // Fast mode should still enforce task_done requirement. + // Fast mode should still enforce fn_task_done requirement. // After 3 retries it should fail and requeue. expect(onError).toHaveBeenCalled(); expect(store.updateTask).toHaveBeenCalledWith( "FN-001", expect.objectContaining({ status: "failed", - error: "Agent finished without calling task_done (after 3 retries)", + error: "Agent finished without calling fn_task_done (after 3 retries)", taskDoneRetryCount: 1, }), ); @@ -12117,12 +12117,12 @@ describe("TaskExecutor messaging tools", () => { updatedAt: new Date().toISOString(), }); - // Mock agent with task_done + // Mock agent with fn_task_done mockedCreateFnAgent.mockImplementation((async (opts: any) => { const customTools = opts.customTools || []; const session = { prompt: vi.fn().mockImplementation(async () => { - const taskDoneTool = customTools.find((t: any) => t.name === "task_done"); + const taskDoneTool = customTools.find((t: any) => t.name === "fn_task_done"); if (taskDoneTool) await taskDoneTool.execute("tool-1", {}); }), dispose: vi.fn(), diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index f3fdb2bed..acfbf4df4 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -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(); 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 ` 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="") 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) => { 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="")\`` : ""} -${reviewLevel >= 3 ? `After tests, also call review_step with type="code" for test review.` : ""} +\`fn_review_step(step=N, type="code", step_name="...", baseline="")\`` : ""} +${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.`; } diff --git a/packages/engine/src/merger.test.ts b/packages/engine/src/merger.test.ts index 050e0b53c..d0bbd47b5 100644 --- a/packages/engine/src/merger.test.ts +++ b/packages/engine/src/merger.test.ts @@ -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 () => { diff --git a/packages/engine/src/merger.ts b/packages/engine/src/merger.ts index 7e1b607e2..ae0aa84bf 100644 --- a/packages/engine/src/merger.ts +++ b/packages/engine/src/merger.ts @@ -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.", ); } diff --git a/packages/engine/src/pi-create-fn-agent.test.ts b/packages/engine/src/pi-create-fn-agent.test.ts index 713508c29..8c29907eb 100644 --- a/packages/engine/src/pi-create-fn-agent.test.ts +++ b/packages/engine/src/pi-create-fn-agent.test.ts @@ -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); }); diff --git a/packages/engine/src/pi.ts b/packages/engine/src/pi.ts index 666dd04cc..e85c98246 100644 --- a/packages/engine/src/pi.ts +++ b/packages/engine/src/pi.ts @@ -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 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 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); diff --git a/packages/engine/src/restart.integration.test.ts b/packages/engine/src/restart.integration.test.ts index cc74e60c7..95cc64918 100644 --- a/packages/engine/src/restart.integration.test.ts +++ b/packages/engine/src/restart.integration.test.ts @@ -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", {}); } diff --git a/packages/engine/src/reviewer.test.ts b/packages/engine/src/reviewer.test.ts index 4a5ce132b..68ca9a2d6 100644 --- a/packages/engine/src/reviewer.test.ts +++ b/packages/engine/src/reviewer.test.ts @@ -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", ); diff --git a/packages/engine/src/reviewer.ts b/packages/engine/src/reviewer.ts index 4dae53540..f4b9acea5 100644 --- a/packages/engine/src/reviewer.ts +++ b/packages/engine/src/reviewer.ts @@ -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) diff --git a/packages/engine/src/step-session-executor.test.ts b/packages/engine/src/step-session-executor.test.ts index b3303765f..a84c4360c 100644 --- a/packages/engine/src/step-session-executor.test.ts +++ b/packages/engine/src/step-session-executor.test.ts @@ -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"); }); }); diff --git a/packages/engine/src/step-session-executor.ts b/packages/engine/src/step-session-executor.ts index f84d640a8..140f4587f 100644 --- a/packages/engine/src/step-session-executor.ts +++ b/packages/engine/src/step-session-executor.ts @@ -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 diff --git a/packages/engine/src/triage.test.ts b/packages/engine/src/triage.test.ts index f2da18aba..77c3fc718 100644 --- a/packages/engine/src/triage.test.ts +++ b/packages/engine/src/triage.test.ts @@ -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).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", () => { diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 104e11daa..49a69eca1 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -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, ) => { - // 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.`;