diff --git a/bin/install.js b/bin/install.js index 994d723ae..b37c0a83d 100755 --- a/bin/install.js +++ b/bin/install.js @@ -1545,11 +1545,17 @@ function convertGeminiToolName(claudeTool) { // Task/Agent: exclude — agents are auto-registered as callable tools. // AskUserQuestion: exclude — Gemini CLI does not expose an ask_user tool; // emitting it causes frontmatter validation errors (#3362). + // Skill/SlashCommand: exclude — Gemini CLI has no 'skill' built-in tool; + // the lowercase fallback would emit an invalid 'skill'/'slashcommand' name + // that fails frontmatter validation (tools.N: Invalid tool name) and aborts + // the entire agent load (#1394). if ( claudeTool === 'Task' || claudeTool === 'Agent' || claudeTool === 'AskUserQuestion' || - claudeTool === 'ask_user' + claudeTool === 'ask_user' || + claudeTool === 'Skill' || + claudeTool === 'SlashCommand' ) { return null; } diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index fd9a8072a..024802045 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -1812,11 +1812,17 @@ function convertGeminiToolName(claudeTool) { // Task/Agent: exclude — agents are auto-registered as callable tools. // AskUserQuestion: exclude — Gemini CLI does not expose an ask_user tool; // emitting it causes frontmatter validation errors (#3362). + // Skill/SlashCommand: exclude — Gemini CLI has no 'skill' built-in tool; + // the lowercase fallback would emit an invalid 'skill'/'slashcommand' name + // that fails frontmatter validation (tools.N: Invalid tool name) and aborts + // the entire agent load (#1394). if ( claudeTool === 'Task' || claudeTool === 'Agent' || claudeTool === 'AskUserQuestion' || - claudeTool === 'ask_user' + claudeTool === 'ask_user' || + claudeTool === 'Skill' || + claudeTool === 'SlashCommand' ) { return null; } diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs index 3edccd6e6..1808213c3 100644 --- a/tests/runtime-converters.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -18,6 +18,7 @@ const { convertClaudeToOpencodeFrontmatter, convertClaudeToKiloFrontmatter, convertClaudeToGeminiAgent, + convertClaudeAgentToAntigravityAgent, convertClaudeCommandToOpencodeSkill, convertClaudeCommandToKiloSkill, neutralizeAgentReferences, @@ -292,6 +293,69 @@ Offer choices via AskUserQuestion when user input is needed. assert.ok(!result.includes('AskUserQuestion'), 'does not leave Claude-only tool references in the body'); assert.ok(result.includes('conversational prompting'), 'uses runtime-neutral body wording for user prompts'); }); + + describe('#1394 regression: excludes Skill/SlashCommand from Gemini frontmatter', () => { + // Skill/SlashCommand are Claude-only tools with no Gemini built-in equivalent. + // Without explicit exclusion they hit the lowercase fallback and emit an + // invalid 'skill'/'slashcommand' tool name, which fails Gemini frontmatter + // validation (tools.N: Invalid tool name) and aborts the entire agent load — + // previously killing 22 of 34 GSD agents on Gemini. + + // Direct unit assertion against the canonical converter (criterion 1). + test('convertGeminiToolName returns null for Skill and SlashCommand', () => { + const { convertGeminiToolName } = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs'); + assert.equal(convertGeminiToolName('Skill'), null, 'Skill is excluded, not lowercased to "skill"'); + assert.equal(convertGeminiToolName('SlashCommand'), null, 'SlashCommand is excluded, not lowercased to "slashcommand"'); + // Existing AskUserQuestion exclusion must still hold. + assert.equal(convertGeminiToolName('AskUserQuestion'), null, 'AskUserQuestion remains excluded'); + // Sanity: a mapped tool still converts. + assert.equal(convertGeminiToolName('Read'), 'read_file', 'mapped tools still convert'); + }); + + // Agent-level assertion against the live install path (criterion 2/3). + test('a Skill tools entry produces no skill/slashcommand in emitted frontmatter', () => { + const input = `--- +name: gsd-planner +description: Creates executable phase plans. +tools: Read, Write, Bash, Glob, Grep, Skill, WebFetch, SlashCommand +--- + + +Plan the phase. +`; + + const result = convertClaudeToGeminiAgent(input); + const frontmatter = result.split('---')[1] || ''; + + assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); + assert.ok(frontmatter.includes(' - web_fetch'), 'maps WebFetch -> web_fetch'); + assert.ok(!frontmatter.includes(' - skill'), 'does not emit invalid Gemini skill tool'); + assert.ok(!frontmatter.includes(' - slashcommand'), 'does not emit invalid Gemini slashcommand tool'); + }); + + // Antigravity reuses convertGeminiToolName (it runs on the Gemini backend), + // so the exclusion intentionally applies there too. Antigravity surfaces GSD + // skills through the skill surface (SKILL.md), not the agent tools: allowlist, + // so dropping the invalid 'skill' tool name does not remove skill access — + // this locks that cross-runtime behavior (criterion 4). + test('Antigravity conversion also excludes Skill/SlashCommand (shared Gemini backend)', () => { + const input = `--- +name: gsd-planner +description: Creates executable phase plans. +tools: Read, Write, Bash, Skill, WebFetch, SlashCommand +--- + +Plan the phase.`; + + const result = convertClaudeAgentToAntigravityAgent(input); + const toolsLine = result.split('\n').find(l => l.startsWith('tools:')) || ''; + + 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'); + }); + }); }); // ─── neutralizeAgentReferences (#766) ─────────────────────────────────────────