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:
@@ -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
|
||||
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
145
bin/install.js
145
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 <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) {
|
||||
|
||||
Reference in New Issue
Block a user