fix(pi-claude-cli): rewrite bare custom tool refs in system prompt to MCP names

Triage system prompts read "MUST call fn_review_spec()" but Sonnet 4.6 routed
through pi-claude-cli writes PROMPT.md and silently skips the call — even with
the addendum explaining the deferred-tool protocol. Confirmed by the FN-2564
agent log: model called other MCP tools (fn_memory_search, fn_task_list) fine
but consistently never reached fn_review_spec, leaving triage looping on
"fn_review_spec was never called" and falling back to zai/glm-5.1 every time.

Rewrite bare `fn_*` (and any non-built-in custom tool name) references in the
system prompt to their `mcp__custom-tools__fn_*` form before sending. The
prompt now literally says "call mcp__custom-tools__fn_review_spec()" so the
model has no inference step, and the deferred-tool reminder Claude Code injects
matches verbatim. Word-boundary safe (won't touch fn_review_specifier) and
idempotent (won't double-prefix already-MCP-named occurrences).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-25 16:50:30 -07:00
parent cd4cef38fc
commit 5e0004cca2
2 changed files with 100 additions and 1 deletions

View File

@@ -910,6 +910,67 @@ describe("buildSystemPrompt", () => {
expect(result).toContain("IMPORTANT:");
expect(result).toContain("tool results");
});
it("rewrites bare custom tool references to MCP-prefixed names", async () => {
vi.resetModules();
vi.doMock("node:fs", () => ({
existsSync: () => false,
readFileSync: () => "",
}));
const { buildSystemPrompt: bsp } = await import("../prompt-builder");
const context = {
systemPrompt:
"Write the PROMPT.md, then call `fn_review_spec()` for review. " +
"If REVISE, call fn_review_spec again. Do not call mcp__custom-tools__fn_review_spec twice manually.",
messages: [],
tools: [
{ name: "fn_review_spec", description: "review", parameters: {} },
{ name: "read", description: "builtin", parameters: {} },
],
} as unknown as any;
const result = bsp(context, "/some/project");
// Bare name occurrences are rewritten
expect(result).toContain(
"call `mcp__custom-tools__fn_review_spec()` for review",
);
expect(result).toContain(
"call mcp__custom-tools__fn_review_spec again",
);
// Already-prefixed occurrence is not double-prefixed
expect(result).not.toContain(
"mcp__custom-tools__mcp__custom-tools__fn_review_spec",
);
// Built-in pi tool names are not rewritten
expect(result).not.toContain("mcp__custom-tools__read");
// The addendum still lists the custom tool with its full mapping
expect(result).toContain("mcp__custom-tools__fn_review_spec");
});
it("does not rewrite identifier substrings that happen to overlap a tool name", async () => {
vi.resetModules();
vi.doMock("node:fs", () => ({
existsSync: () => false,
readFileSync: () => "",
}));
const { buildSystemPrompt: bsp } = await import("../prompt-builder");
const context = {
systemPrompt: "fn_review_specifier and fn_reviews are not the tool.",
messages: [],
tools: [
{ name: "fn_review", description: "x", parameters: {} },
{ name: "fn_review_spec", description: "y", parameters: {} },
],
} as unknown as any;
const result = bsp(context, "/some/project");
// Neither substring should be rewritten — they're different identifiers.
expect(result).toContain("fn_review_specifier");
expect(result).toContain("fn_reviews");
expect(result).not.toContain("mcp__custom-tools__fn_review_specifier");
expect(result).not.toContain("mcp__custom-tools__fn_reviews");
});
});
describe("buildResumePrompt", () => {