fix(#4705): emit Antigravity-native tool names as a YAML sequence (#4822)

* 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 <sim@local>
This commit is contained in:
Tom Boucher
2026-09-17 08:12:18 -04:00
committed by GitHub
parent be1b76dddd
commit 7d0c6339d0
5 changed files with 96 additions and 30 deletions

View File

@@ -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)

View File

@@ -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 += '---';

View File

@@ -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 += '---';

View File

@@ -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']) {

View File

@@ -478,12 +478,12 @@ tools: Read, Write, Bash, Skill, WebFetch, SlashCommand
<role>Plan the phase.</role>`;
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');
});
});