From 5660b6fc0b890f8d6ffc1c3f0c407f1a702a5f72 Mon Sep 17 00:00:00 2001 From: Cristian Uibar Date: Fri, 30 Jan 2026 03:43:01 +0200 Subject: [PATCH] fix: Gemini CLI agent loading errors (#347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: convert agent frontmatter for Gemini CLI compatibility - Add claudeToGeminiTools mapping (Read→read_file, Bash→run_shell_command, etc.) - Add convertGeminiToolName() to map tools and filter out mcp__*/Task - Add convertClaudeToGeminiAgent() to convert agent YAML frontmatter - Hook conversion into agent copy logic for Gemini installs - Fixes: tools as YAML array (not string), color field removed, mcp__* excluded * fix: enable experimental.enableAgents for Gemini CLI installs * fix: strip HTML tags for Gemini CLI terminal output * fix: replace ${VAR} with $VAR in agent templates to avoid Gemini CLI template parsing --- agents/gsd-phase-researcher.md | 16 ++-- agents/gsd-plan-checker.md | 10 +-- agents/gsd-planner.md | 26 +++--- agents/gsd-verifier.md | 6 +- bin/install.js | 145 ++++++++++++++++++++++++++++++++- 5 files changed, 172 insertions(+), 31 deletions(-) diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index 4b30b7292..ddb9664b7 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -445,11 +445,11 @@ Orchestrator provides: ```bash # Match both zero-padded (05-*) and unpadded (5-*) folders -PADDED_PHASE=$(printf "%02d" ${PHASE} 2>/dev/null || echo "${PHASE}") -PHASE_DIR=$(ls -d .planning/phases/${PADDED_PHASE}-* .planning/phases/${PHASE}-* 2>/dev/null | head -1) +PADDED_PHASE=$(printf "%02d" $PHASE 2>/dev/null || echo "$PHASE") +PHASE_DIR=$(ls -d .planning/phases/$PADDED_PHASE-* .planning/phases/$PHASE-* 2>/dev/null | head -1) # Read CONTEXT.md if exists (from /gsd:discuss-phase) -cat "${PHASE_DIR}"/*-CONTEXT.md 2>/dev/null +cat "$PHASE_DIR"/*-CONTEXT.md 2>/dev/null # Check if planning docs should be committed (default: true) COMMIT_PLANNING_DOCS=$(cat .planning/config.json 2>/dev/null | grep -o '"commit_docs"[[:space:]]*:[[:space:]]*[^,}]*' | grep -o 'true\|false' || echo "true") @@ -525,7 +525,7 @@ Run through verification protocol checklist: Use the output format template. Populate all sections with verified findings. -Write to: `${PHASE_DIR}/${PADDED_PHASE}-RESEARCH.md` +Write to: `$PHASE_DIR/$PADDED_PHASE-RESEARCH.md` Where `PHASE_DIR` is the full path (e.g., `.planning/phases/01-foundation`) @@ -536,10 +536,10 @@ Where `PHASE_DIR` is the full path (e.g., `.planning/phases/01-foundation`) **If `COMMIT_PLANNING_DOCS=true` (default):** ```bash -git add "${PHASE_DIR}/${PADDED_PHASE}-RESEARCH.md" -git commit -m "docs(${PHASE}): research phase domain +git add "$PHASE_DIR/$PADDED_PHASE-RESEARCH.md" +git commit -m "docs($PHASE): research phase domain -Phase ${PHASE}: ${PHASE_NAME} +Phase $PHASE: $PHASE_NAME - Standard stack identified - Architecture patterns documented - Pitfalls catalogued" @@ -569,7 +569,7 @@ When research finishes successfully: ### File Created -`${PHASE_DIR}/${PADDED_PHASE}-RESEARCH.md` +`$PHASE_DIR/$PADDED_PHASE-RESEARCH.md` ### Confidence Assessment diff --git a/agents/gsd-plan-checker.md b/agents/gsd-plan-checker.md index a180947a3..860597d4e 100644 --- a/agents/gsd-plan-checker.md +++ b/agents/gsd-plan-checker.md @@ -245,14 +245,14 @@ Gather verification context from the phase directory and project state. ```bash # Normalize phase and find directory -PADDED_PHASE=$(printf "%02d" ${PHASE_ARG} 2>/dev/null || echo "${PHASE_ARG}") -PHASE_DIR=$(ls -d .planning/phases/${PADDED_PHASE}-* .planning/phases/${PHASE_ARG}-* 2>/dev/null | head -1) +PADDED_PHASE=$(printf "%02d" $PHASE_ARG 2>/dev/null || echo "$PHASE_ARG") +PHASE_DIR=$(ls -d .planning/phases/$PADDED_PHASE-* .planning/phases/$PHASE_ARG-* 2>/dev/null | head -1) # List all PLAN.md files ls "$PHASE_DIR"/*-PLAN.md 2>/dev/null # Get phase goal from ROADMAP -grep -A 10 "Phase ${PHASE_NUM}" .planning/ROADMAP.md | head -15 +grep -A 10 "Phase $PHASE_NUM" .planning/ROADMAP.md | head -15 # Get phase brief if exists ls "$PHASE_DIR"/*-BRIEF.md 2>/dev/null @@ -384,10 +384,10 @@ Evaluate scope against context budget. **Metrics per plan:** ```bash # Count tasks -grep -c "/dev/null || echo "${PHASE_ARG}") -PHASE_DIR=$(ls -d .planning/phases/${PADDED_PHASE}-* .planning/phases/${PHASE_ARG}-* 2>/dev/null | head -1) +PADDED_PHASE=$(printf "%02d" $PHASE_ARG 2>/dev/null || echo "$PHASE_ARG") +PHASE_DIR=$(ls -d .planning/phases/$PADDED_PHASE-* .planning/phases/$PHASE_ARG-* 2>/dev/null | head -1) # Check for VERIFICATION.md (code verification gaps) ls "$PHASE_DIR"/*-VERIFICATION.md 2>/dev/null @@ -889,7 +889,7 @@ Triggered when orchestrator provides `` with checker issues. Y Read all PLAN.md files in the phase directory: ```bash -cat .planning/phases/${PHASE}-*/*-PLAN.md +cat .planning/phases/$PHASE-*/*-PLAN.md ``` Build mental model of: @@ -958,8 +958,8 @@ After making edits, self-check: **If `COMMIT_PLANNING_DOCS=true` (default):** ```bash -git add .planning/phases/${PHASE}-*/${PHASE}-*-PLAN.md -git commit -m "fix(${PHASE}): revise plans based on checker feedback" +git add .planning/phases/$PHASE-*/$PHASE-*-PLAN.md +git commit -m "fix($PHASE): revise plans based on checker feedback" ``` ### Step 7: Return Revision Summary @@ -1094,17 +1094,17 @@ Understand: ```bash # Match both zero-padded (05-*) and unpadded (5-*) folders -PADDED_PHASE=$(printf "%02d" ${PHASE} 2>/dev/null || echo "${PHASE}") -PHASE_DIR=$(ls -d .planning/phases/${PADDED_PHASE}-* .planning/phases/${PHASE}-* 2>/dev/null | head -1) +PADDED_PHASE=$(printf "%02d" $PHASE 2>/dev/null || echo "$PHASE") +PHASE_DIR=$(ls -d .planning/phases/$PADDED_PHASE-* .planning/phases/$PHASE-* 2>/dev/null | head -1) # Read CONTEXT.md if exists (from /gsd:discuss-phase) -cat "${PHASE_DIR}"/*-CONTEXT.md 2>/dev/null +cat "$PHASE_DIR"/*-CONTEXT.md 2>/dev/null # Read RESEARCH.md if exists (from /gsd:research-phase) -cat "${PHASE_DIR}"/*-RESEARCH.md 2>/dev/null +cat "$PHASE_DIR"/*-RESEARCH.md 2>/dev/null # Read DISCOVERY.md if exists (from mandatory discovery) -cat "${PHASE_DIR}"/*-DISCOVERY.md 2>/dev/null +cat "$PHASE_DIR"/*-DISCOVERY.md 2>/dev/null ``` **If CONTEXT.md exists:** Honor user's vision, prioritize their essential features, respect stated boundaries. These are locked decisions - do not revisit. @@ -1229,10 +1229,10 @@ Commit phase plan(s) and updated roadmap: **If `COMMIT_PLANNING_DOCS=true` (default):** ```bash -git add .planning/phases/${PHASE}-*/${PHASE}-*-PLAN.md .planning/ROADMAP.md -git commit -m "docs(${PHASE}): create phase plan +git add .planning/phases/$PHASE-*/$PHASE-*-PLAN.md .planning/ROADMAP.md +git commit -m "docs($PHASE): create phase plan -Phase ${PHASE}: ${PHASE_NAME} +Phase $PHASE: $PHASE_NAME - [N] plan(s) in [M] wave(s) - [X] parallel, [Y] sequential - Ready for execution" diff --git a/agents/gsd-verifier.md b/agents/gsd-verifier.md index e44701e60..e8bccebca 100644 --- a/agents/gsd-verifier.md +++ b/agents/gsd-verifier.md @@ -61,10 +61,10 @@ ls "$PHASE_DIR"/*-PLAN.md 2>/dev/null ls "$PHASE_DIR"/*-SUMMARY.md 2>/dev/null # Phase goal from ROADMAP -grep -A 5 "Phase ${PHASE_NUM}" .planning/ROADMAP.md +grep -A 5 "Phase $PHASE_NUM" .planning/ROADMAP.md # Requirements mapped to this phase -grep -E "^| ${PHASE_NUM}" .planning/REQUIREMENTS.md 2>/dev/null +grep -E "^| $PHASE_NUM" .planning/REQUIREMENTS.md 2>/dev/null ``` Extract phase goal from ROADMAP.md. This is the outcome to verify, not the tasks. @@ -372,7 +372,7 @@ verify_state_render_link() { If REQUIREMENTS.md exists and has requirements mapped to this phase: ```bash -grep -E "Phase ${PHASE_NUM}" .planning/REQUIREMENTS.md 2>/dev/null +grep -E "Phase $PHASE_NUM" .planning/REQUIREMENTS.md 2>/dev/null ``` For each requirement: diff --git a/bin/install.js b/bin/install.js index f7c9d1cf2..4e59584a4 100755 --- a/bin/install.js +++ b/bin/install.js @@ -227,6 +227,21 @@ const claudeToOpencodeTools = { WebSearch: 'websearch', // Plugin/MCP - keep for compatibility }; +// Tool name mapping from Claude Code to Gemini CLI +// Gemini CLI uses snake_case built-in tool names +const claudeToGeminiTools = { + Read: 'read_file', + Write: 'write_file', + Edit: 'replace', + Bash: 'run_shell_command', + Glob: 'glob', + Grep: 'search_file_content', + WebSearch: 'google_web_search', + WebFetch: 'web_fetch', + TodoWrite: 'write_todos', + AskUserQuestion: 'ask_user', +}; + /** * Convert a Claude Code tool name to OpenCode format * - Applies special mappings (AskUserQuestion -> question, etc.) @@ -245,6 +260,118 @@ function convertToolName(claudeTool) { return claudeTool.toLowerCase(); } +/** + * Convert a Claude Code tool name to Gemini CLI format + * - Applies Claude→Gemini mapping (Read→read_file, Bash→run_shell_command, etc.) + * - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Gemini + * - Filters out Task — agents are auto-registered as tools in Gemini + * @returns {string|null} Gemini tool name, or null if tool should be excluded + */ +function convertGeminiToolName(claudeTool) { + // MCP tools: exclude — auto-discovered from mcpServers config at runtime + if (claudeTool.startsWith('mcp__')) { + return null; + } + // Task: exclude — agents are auto-registered as callable tools + if (claudeTool === 'Task') { + return null; + } + // Check for explicit mapping + if (claudeToGeminiTools[claudeTool]) { + return claudeToGeminiTools[claudeTool]; + } + // Default: lowercase + return claudeTool.toLowerCase(); +} + +/** + * Strip HTML tags for Gemini CLI output + * Terminals don't support subscript — Gemini renders these as raw HTML. + * Converts text to italic *(text)* for readable terminal output. + */ +function stripSubTags(content) { + return content.replace(/(.*?)<\/sub>/g, '*($1)*'); +} + +/** + * Convert Claude Code agent frontmatter to Gemini CLI format + * Gemini agents use .md files with YAML frontmatter, same as Claude, + * but with different field names and formats: + * - tools: must be a YAML array (not comma-separated string) + * - tool names: must use Gemini built-in names (read_file, not Read) + * - color: must be removed (causes validation error) + * - mcp__* tools: must be excluded (auto-discovered at runtime) + */ +function convertClaudeToGeminiAgent(content) { + if (!content.startsWith('---')) return content; + + const endIndex = content.indexOf('---', 3); + if (endIndex === -1) return content; + + const frontmatter = content.substring(3, endIndex).trim(); + const body = content.substring(endIndex + 3); + + const lines = frontmatter.split('\n'); + const newLines = []; + let inAllowedTools = false; + const tools = []; + + for (const line of lines) { + const trimmed = line.trim(); + + // Convert allowed-tools YAML array to tools list + if (trimmed.startsWith('allowed-tools:')) { + inAllowedTools = true; + continue; + } + + // Handle inline tools: field (comma-separated string) + if (trimmed.startsWith('tools:')) { + const toolsValue = trimmed.substring(6).trim(); + if (toolsValue) { + const parsed = toolsValue.split(',').map(t => t.trim()).filter(t => t); + for (const t of parsed) { + const mapped = convertGeminiToolName(t); + if (mapped) tools.push(mapped); + } + } else { + // tools: with no value means YAML array follows + inAllowedTools = true; + } + continue; + } + + // Strip color field (not supported by Gemini CLI, causes validation error) + if (trimmed.startsWith('color:')) continue; + + // Collect allowed-tools/tools array items + if (inAllowedTools) { + if (trimmed.startsWith('- ')) { + const mapped = convertGeminiToolName(trimmed.substring(2).trim()); + if (mapped) tools.push(mapped); + continue; + } else if (trimmed && !trimmed.startsWith('-')) { + inAllowedTools = false; + } + } + + if (!inAllowedTools) { + newLines.push(line); + } + } + + // Add tools as YAML array (Gemini requires array format) + if (tools.length > 0) { + newLines.push('tools:'); + for (const tool of tools) { + newLines.push(` - ${tool}`); + } + } + + const newFrontmatter = newLines.join('\n').trim(); + return `---\n${newFrontmatter}\n---${stripSubTags(body)}`; +} + function convertClaudeToOpencodeFrontmatter(content) { // Replace tool name references in content (applies to all files) let convertedContent = content; @@ -477,7 +604,8 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) { content = convertClaudeToOpencodeFrontmatter(content); fs.writeFileSync(destPath, content); } else if (runtime === 'gemini') { - // Convert to TOML for Gemini + // Convert to TOML for Gemini (strip tags — terminals can't render subscript) + content = stripSubTags(content); const tomlContent = convertClaudeToGeminiToml(content); // Replace extension with .toml const tomlPath = destPath.replace(/\.md$/, '.toml'); @@ -953,9 +1081,11 @@ function install(isGlobal, runtime = 'claude') { // Always replace ~/.claude/ as it is the source of truth in the repo const dirRegex = /~\/\.claude\//g; content = content.replace(dirRegex, pathPrefix); - // Convert frontmatter for opencode compatibility + // Convert frontmatter for runtime compatibility if (isOpencode) { content = convertClaudeToOpencodeFrontmatter(content); + } else if (isGemini) { + content = convertClaudeToGeminiAgent(content); } fs.writeFileSync(path.join(agentsDest, entry.name), content); } @@ -1024,6 +1154,17 @@ function install(isGlobal, runtime = 'claude') { ? buildHookCommand(targetDir, 'gsd-check-update.js') : 'node ' + dirName + '/hooks/gsd-check-update.js'; + // Enable experimental agents for Gemini CLI (required for custom sub-agents) + if (isGemini) { + if (!settings.experimental) { + settings.experimental = {}; + } + if (!settings.experimental.enableAgents) { + settings.experimental.enableAgents = true; + console.log(` ${green}✓${reset} Enabled experimental agents`); + } + } + // Configure SessionStart hook for update checking (skip for opencode) if (!isOpencode) { if (!settings.hooks) {