fix: Gemini CLI agent loading errors (#347)

* 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 <sub> tags for Gemini CLI terminal output

* fix: replace ${VAR} with $VAR in agent templates to avoid Gemini CLI template parsing
This commit is contained in:
Cristian Uibar
2026-01-30 03:43:01 +02:00
committed by GitHub
parent beca9faead
commit 5660b6fc0b
5 changed files with 172 additions and 31 deletions

View File

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

View File

@@ -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 "<task" "$PHASE_DIR"/${PHASE}-01-PLAN.md
grep -c "<task" "$PHASE_DIR"/$PHASE-01-PLAN.md
# Count files in files_modified
grep "files_modified:" "$PHASE_DIR"/${PHASE}-01-PLAN.md
grep "files_modified:" "$PHASE_DIR"/$PHASE-01-PLAN.md
```
**Thresholds:**

View File

@@ -809,8 +809,8 @@ Triggered by `--gaps` flag. Creates plans to address verification or UAT failure
```bash
# Match both zero-padded (05-*) and unpadded (5-*) folders
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)
# Check for VERIFICATION.md (code verification gaps)
ls "$PHASE_DIR"/*-VERIFICATION.md 2>/dev/null
@@ -889,7 +889,7 @@ Triggered when orchestrator provides `<revision_context>` 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"

View File

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

View File

@@ -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 <sub> tags for Gemini CLI output
* Terminals don't support subscript — Gemini renders these as raw HTML.
* Converts <sub>text</sub> to italic *(text)* for readable terminal output.
*/
function stripSubTags(content) {
return content.replace(/<sub>(.*?)<\/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 <sub> 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) {