From 7d0c6339d05a7daa59d5cb7f69d71da76d972873 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 17 Sep 2026 08:12:18 -0400 Subject: [PATCH] fix(#4705): emit Antigravity-native tool names as a YAML sequence (#4822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(#4705): add failing-first coverage for native Antigravity tool sequences * fix(#4705): emit Antigravity-native tool names as a YAML sequence convertClaudeAgentToAntigravityAgent and the installer's twin emitted Gemini CLI tool names as a comma-separated scalar. Antigravity's documented subagent contract (antigravity.google/docs/subagents) wants a YAML sequence of native names — view_file, grep_search, run_command, replace_file_content are the documented examples, and wrong or malformed grants can hang the subagent per Antigravity's own warning. Map values move to the native vocabulary where documented (Read -> view_file, Edit -> replace_file_content, Bash -> run_command, Grep -> grep_search); undocumented entries keep their best-known grant rather than being dropped (dropping would silently remove a restriction). The emitter writes one '- name' item per line; an agent whose every tool was filtered emits an explicit tools: [] instead of an empty scalar. Pre-existing pins updated to the native vocabulary. * test(#4705): update the #4727 map-value pin to the Antigravity-native vocabulary The #4727-era pin held the map VALUES at the Gemini CLI dialect on the belief that Antigravity speaks it; the confirmed bug #4705 (with Antigravity's own documented subagent contract) supersedes that for the four documented names. Key/shape pinning is preserved; only the values move. * docs(#4705): backfill changeset PR number --------- Co-authored-by: sim --- .changeset/bold-zebras-run.md | 5 +++ bin/install.js | 17 ++++++---- src/runtime-artifact-conversion.cts | 23 ++++++++++--- tests/gemini-runtime-removed.test.cjs | 34 +++++++++++-------- tests/runtime-converters.test.cjs | 47 ++++++++++++++++++++++++--- 5 files changed, 96 insertions(+), 30 deletions(-) create mode 100644 .changeset/bold-zebras-run.md diff --git a/.changeset/bold-zebras-run.md b/.changeset/bold-zebras-run.md new file mode 100644 index 000000000..f5444bf9e --- /dev/null +++ b/.changeset/bold-zebras-run.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4822 +--- +**Antigravity agents get native tool names as a YAML sequence** — converted agents carried Gemini CLI tool names (`read_file`, `search_file_content`, `run_shell_command`) as a comma-separated scalar, while Antigravity's documented subagent contract wants a YAML sequence of native names (`view_file`, `grep_search`, `run_command`, `replace_file_content`); wrong or malformed grants can hang the subagent. The installer's twin converter changes in lockstep. (#4705) diff --git a/bin/install.js b/bin/install.js index 5022c5c9d..71ed04e28 100755 --- a/bin/install.js +++ b/bin/install.js @@ -1659,16 +1659,17 @@ const claudeToOpencodeTools = { // Tool name mapping from Claude Code to Antigravity // Antigravity uses Gemini's snake_case built-in tool names const claudeToAntigravityTools = { - Read: 'read_file', + // #4705: Antigravity-NATIVE tool names (see src/runtime-artifact-conversion.cts) + Read: 'view_file', Write: 'write_file', - Edit: 'replace', - Bash: 'run_shell_command', + Edit: 'replace_file_content', + Bash: 'run_command', Glob: 'glob', - Grep: 'search_file_content', + Grep: 'grep_search', WebSearch: 'google_web_search', WebFetch: 'web_fetch', TodoWrite: 'write_todos', -}; +} // Tool name mapping from Claude/GSD agents to Kimi CLI module paths. // Kimi custom agent YAML requires fully-qualified module paths. @@ -2527,7 +2528,11 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) { const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean); // #2876: quote description for the same reason as the skill variant. - let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${mappedTools.join(', ')}\n`; + // #4705: tools is a YAML SEQUENCE of native names (see the src twin). + const toolsBlock = mappedTools.length > 0 + ? `tools:\n${mappedTools.map((t) => `- ${t}`).join('\n')}\n` + : 'tools: []\n'; + let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n${toolsBlock}`; if (color) fm += `color: ${color}\n`; fm += '---'; diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index 10d39613e..fdf07cbc9 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -2431,13 +2431,20 @@ const claudeToCopilotTools = { // Tool name mapping from Claude Code to Antigravity // Antigravity uses Gemini's snake_case built-in tool names +// #4705: values are Antigravity-NATIVE tool names per the documented subagent +// contract (antigravity.google/docs/subagents: view_file, replace_file_content, +// grep_search, run_command are the documented examples; the catalog is +// non-exhaustive, so unmapped entries keep their best-known grant rather than +// being dropped — dropping would silently remove a restriction). The old +// values were Gemini CLI dialect names, which Antigravity's tool validation +// does not document and may hang the subagent on. const claudeToAntigravityTools = { - Read: 'read_file', + Read: 'view_file', Write: 'write_file', - Edit: 'replace', - Bash: 'run_shell_command', + Edit: 'replace_file_content', + Bash: 'run_command', Glob: 'glob', - Grep: 'search_file_content', + Grep: 'grep_search', WebSearch: 'google_web_search', WebFetch: 'web_fetch', TodoWrite: 'write_todos', @@ -2552,7 +2559,13 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) { const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean); // #2876: quote description for the same reason as the skill variant. - let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${mappedTools.join(', ')}\n`; + // #4705: tools is a YAML SEQUENCE of native names (one `- name` item per + // line), not a comma-separated scalar. Empty mapped set -> `tools: []` so an + // agent whose every tool was filtered stays explicitly restricted. + const toolsBlock = mappedTools.length > 0 + ? `tools:\n${mappedTools.map((t) => `- ${t}`).join('\n')}\n` + : 'tools: []\n'; + let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n${toolsBlock}`; if (color) fm += `color: ${color}\n`; fm += '---'; diff --git a/tests/gemini-runtime-removed.test.cjs b/tests/gemini-runtime-removed.test.cjs index baf9ad5ce..99acb03e1 100644 --- a/tests/gemini-runtime-removed.test.cjs +++ b/tests/gemini-runtime-removed.test.cjs @@ -217,13 +217,14 @@ describe('#1928 Antigravity preserved (shared surface with the removed gemini ru assert.strictEqual(getRuntimeLabel('antigravity'), 'Antigravity'); }); - test('the shared Gemini-backend tool vocabulary still powers Antigravity agent conversion', () => { + test('the Antigravity-native tool vocabulary powers Antigravity agent conversion (#4705)', () => { const input = ['---', 'name: gsd-x', 'description: d', 'tools: Read, Write, WebFetch, Skill', '---', '', 'body'].join('\n'); - const toolsLine = convertClaudeAgentToAntigravityAgent(input).split('\n').find((l) => l.startsWith('tools:')) || ''; - assert.ok(toolsLine.includes('read_file'), 'Read → read_file via the retained convertAntigravityToolName'); - assert.ok(toolsLine.includes('write_file'), 'Write → write_file'); - assert.ok(toolsLine.includes('web_fetch'), 'WebFetch → web_fetch'); - assert.ok(!/\bskill\b/.test(toolsLine), 'Skill is still excluded (would be an invalid backend tool name)'); + const result = convertClaudeAgentToAntigravityAgent(input); + const toolsBlock = result.slice(result.indexOf('tools:')).split('\n').filter((l) => l.startsWith('- ')); + assert.ok(toolsBlock.includes('- view_file'), 'Read → view_file (native name, #4705)'); + assert.ok(toolsBlock.includes('- write_file'), 'Write → write_file'); + assert.ok(toolsBlock.includes('- web_fetch'), 'WebFetch → web_fetch'); + assert.ok(!toolsBlock.some((l) => /\bskill\b/.test(l)), 'Skill is still excluded (would be an invalid backend tool name)'); }); test('#4727 the rename is complete: no gemini-named alias survives alongside the antigravity-named exports', () => { @@ -250,20 +251,25 @@ describe('#1928 Antigravity preserved (shared surface with the removed gemini ru 'the retired gemini-named function must not still be exported — a surviving alias means the rename never finished', ); - // The rename must be a pure identifier change: every value byte-identical, so an added OR - // removed key (not just a renamed export) fails this too. + // The #4727 rename remains a pure identifier change: same KEYS, same shape + // (an added OR removed key still fails). The VALUES moved from the Gemini + // CLI dialect to Antigravity-native names in #4705 — Antigravity's own + // documented subagent contract (antigravity.google/docs/subagents) names + // view_file / replace_file_content / grep_search / run_command as native, + // and its tool-validation warning says wrong names can hang the subagent. + // The old "Antigravity speaks the Gemini dialect" rationale was the belief + // at #4727 time; the confirmed bug #4705 supersedes it. assert.deepStrictEqual(mod.claudeToAntigravityTools, { - Read: 'read_file', + Read: 'view_file', Write: 'write_file', - Edit: 'replace', - Bash: 'run_shell_command', + Edit: 'replace_file_content', + Bash: 'run_command', Glob: 'glob', - Grep: 'search_file_content', + Grep: 'grep_search', WebSearch: 'google_web_search', WebFetch: 'web_fetch', TodoWrite: 'write_todos', - }, 'the tool map values are Gemini\'s built-in tool dialect, which Antigravity speaks — Google\'s ' - + 'contract, not GSD\'s to alter by renaming the map that carries it'); + }, 'the tool map keys are the #4727 rename surface (add/remove still fails); the VALUES are the #4705 Antigravity-native vocabulary'); // In-set / out-of-set boundary pair: the excluded ids still return null individually... for (const excluded of ['mcp__anything', 'Task', 'Agent', 'AskUserQuestion', 'ask_user', 'Skill', 'SlashCommand']) { diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs index 17fe2b649..81d4fbf44 100644 --- a/tests/runtime-converters.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -478,12 +478,12 @@ tools: Read, Write, Bash, Skill, WebFetch, SlashCommand Plan the phase.`; const result = convertClaudeAgentToAntigravityAgent(input); - const toolsLine = result.split('\n').find(l => l.startsWith('tools:')) || ''; + const toolsItems = result.split('\n').filter(l => l.startsWith('- ')).map(l => l.slice(2)); - assert.ok(toolsLine.includes('read_file'), 'maps Read -> read_file'); - assert.ok(toolsLine.includes('web_fetch'), 'maps WebFetch -> web_fetch'); - assert.ok(!/\bskill\b/.test(toolsLine), 'no invalid skill tool in Antigravity frontmatter'); - assert.ok(!/\bslashcommand\b/.test(toolsLine), 'no invalid slashcommand tool in Antigravity frontmatter'); + assert.ok(toolsItems.includes('view_file'), 'maps Read -> view_file (#4705 native name)'); + assert.ok(toolsItems.includes('web_fetch'), 'maps WebFetch -> web_fetch'); + assert.ok(!toolsItems.some(t => /\bskill\b/.test(t)), 'no invalid skill tool in Antigravity frontmatter'); + assert.ok(!toolsItems.some(t => /\bslashcommand\b/.test(t)), 'no invalid slashcommand tool in Antigravity frontmatter'); }); }); @@ -2667,3 +2667,40 @@ describe('#3706: the layout seam actually threads the variant', () => { } }); }); + +// ── #4705 — Antigravity agent tools: native names, YAML sequence ───────────── +// The converter emitted Gemini CLI tool names as a comma-separated scalar; +// Antigravity's documented subagent contract (antigravity.google/docs/subagents) +// wants a YAML sequence of native tool names (view_file, grep_search, +// run_command, replace_file_content are the documented examples). A scalar or a +// wrong-vocabulary grant can hang the subagent per Antigravity's own warning. +describe('#4705 — Antigravity agent tools are native names in a YAML sequence', () => { + const input = `--- +name: example-reviewer +description: Inspect source files +tools: Read, Grep, Bash +--- + +Inspect source files.`; + + test('emits the documented native tools as a YAML sequence (#4705)', () => { + const result = convertClaudeAgentToAntigravityAgent(input); + assert.match(result, /tools:\n- view_file\n- grep_search\n- run_command\n/, + 'tools must be a YAML sequence of Antigravity-native names (view_file, grep_search, run_command)'); + assert.doesNotMatch(result, /tools:.*,/m, 'no comma-separated scalar may survive'); + }); + + test('Edit maps to the documented replace_file_content (#4705)', () => { + const result = convertClaudeAgentToAntigravityAgent( + `---\nname: e\ndescription: d\ntools: Edit\n---\n\nbody`); + assert.match(result, /- replace_file_content/, 'Edit -> replace_file_content (documented native name)'); + assert.ok(!/- replace\b/.test(result), 'the Gemini CLI name replace must not survive'); + }); + + test('unmapped tools keep their lowercase grant (no silent restriction drop) (#4705)', () => { + const result = convertClaudeAgentToAntigravityAgent( + `---\nname: e\ndescription: d\ntools: Glob, WebFetch\n---\n\nbody`); + assert.match(result, /- glob/, 'undocumented tools keep their mapped/lowercase grant'); + assert.match(result, /- web_fetch/, 'WebFetch stays granted'); + }); +});