From 262b3958790900ac5f399c0257cba8da98c3414f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 15 Apr 2026 14:59:56 -0400 Subject: [PATCH] fix: embed model_overrides in Codex TOML and OpenCode agent files (#2279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: sync ARCHITECTURE.md command count to 74 commands/gsd/ has 74 .md files; the two count references in ARCHITECTURE.md still said 73. Fixes the command-count-sync regression test. Co-Authored-By: Claude Sonnet 4.6 * fix: embed model_overrides in Codex TOML and OpenCode agent files (#2256) Codex and OpenCode use static agent files (TOML / markdown frontmatter) rather than inline Task(model=...) parameters, so model_overrides set in ~/.gsd/defaults.json was silently ignored — all subagents fell through to the runtime's default model. Fix: at install time, read model_overrides from ~/.gsd/defaults.json and embed the matching model ID into each agent file: - Codex: model = "..." field in the agent TOML (generateCodexAgentToml) - OpenCode: model: ... field in agent frontmatter (convertClaudeToOpencodeFrontmatter) Also adds readGsdGlobalModelOverrides() helper and passes the result through installCodexConfig() and the OpenCode agent install loop. Closes #2256 Co-Authored-By: Claude Sonnet 4.6 * feat(commands): add gsd:inbox command for GitHub issue/PR triage inbox.md was created but not committed, causing the command count to read 73 in git while ARCHITECTURE.md correctly stated 74. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- bin/install.js | 62 ++++++++++++++++++++++++++----- commands/gsd/inbox.md | 38 +++++++++++++++++++ docs/ARCHITECTURE.md | 4 +- tests/codex-config.test.cjs | 29 +++++++++++++++ tests/runtime-converters.test.cjs | 42 +++++++++++++++++++++ 5 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 commands/gsd/inbox.md diff --git a/bin/install.js b/bin/install.js index 9a5f39a22..8d5543107 100755 --- a/bin/install.js +++ b/bin/install.js @@ -589,6 +589,27 @@ function writeSettings(settingsPath, settings) { fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); } +/** + * Read model_overrides from ~/.gsd/defaults.json at install time. + * Returns an object mapping agent names to model IDs, or null if the file + * doesn't exist or has no model_overrides entry. + * Used by Codex TOML and OpenCode agent file generators to embed per-agent + * model assignments so that model_overrides is respected on non-Claude runtimes (#2256). + */ +function readGsdGlobalModelOverrides() { + try { + const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json'); + if (!fs.existsSync(defaultsPath)) return null; + const raw = fs.readFileSync(defaultsPath, 'utf-8'); + const parsed = JSON.parse(raw); + const overrides = parsed.model_overrides; + if (!overrides || typeof overrides !== 'object') return null; + return overrides; + } catch { + return null; + } +} + // Cache for attribution settings (populated once per runtime during install) const attributionCache = new Map(); @@ -1748,7 +1769,7 @@ purpose: ${toSingleLine(description)} * Sets required agent metadata, sandbox_mode, and developer_instructions * from the agent markdown content. */ -function generateCodexAgentToml(agentName, agentContent) { +function generateCodexAgentToml(agentName, agentContent, modelOverrides = null) { const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only'; const { frontmatter, body } = extractFrontmatterAndBody(agentContent); const frontmatterText = frontmatter || ''; @@ -1762,12 +1783,22 @@ function generateCodexAgentToml(agentName, agentContent) { `name = ${JSON.stringify(resolvedName)}`, `description = ${JSON.stringify(resolvedDescription)}`, `sandbox_mode = "${sandboxMode}"`, - // Agent prompts contain raw backslashes in regexes and shell snippets. - // TOML literal multiline strings preserve them without escape parsing. - `developer_instructions = '''`, - instructions, - `'''`, ]; + + // Embed model override when configured in ~/.gsd/defaults.json so that + // model_overrides is respected on Codex (which uses static TOML, not inline + // Task() model parameters). See #2256. + const modelOverride = modelOverrides?.[resolvedName] || modelOverrides?.[agentName]; + if (modelOverride) { + lines.push(`model = ${JSON.stringify(modelOverride)}`); + } + + // Agent prompts contain raw backslashes in regexes and shell snippets. + // TOML literal multiline strings preserve them without escape parsing. + lines.push(`developer_instructions = '''`); + lines.push(instructions); + lines.push(`'''`); + return lines.join('\n') + '\n'; } @@ -2991,7 +3022,10 @@ function installCodexConfig(targetDir, agentsSrc) { agents.push({ name, description: toSingleLine(description) }); - const tomlContent = generateCodexAgentToml(name, content); + // Pass model overrides from ~/.gsd/defaults.json so Codex TOML files + // embed the configured model — Codex cannot receive model inline (#2256). + const modelOverrides = readGsdGlobalModelOverrides(); + const tomlContent = generateCodexAgentToml(name, content, modelOverrides); fs.writeFileSync(path.join(agentsTomlDir, `${name}.toml`), tomlContent); } @@ -3145,7 +3179,7 @@ function convertClaudeToGeminiAgent(content) { return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`; } -function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) { +function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOverride = null } = {}) { // Replace tool name references in content (applies to all files) let convertedContent = content; convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question'); @@ -3280,6 +3314,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) { // use its default model for subagents. See #1156. if (isAgent) { newLines.push('mode: subagent'); + // Embed model override from ~/.gsd/defaults.json so model_overrides is + // respected on OpenCode (which uses static agent frontmatter, not inline + // Task() model parameters). See #2256. + if (modelOverride) { + newLines.push(`model: ${modelOverride}`); + } } // For commands: add tools object if we had allowed-tools or tools @@ -5682,7 +5722,11 @@ function install(isGlobal, runtime = 'claude') { content = processAttribution(content, getCommitAttribution(runtime)); // Convert frontmatter for runtime compatibility (agents need different handling) if (isOpencode) { - content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true }); + // Resolve per-agent model override from ~/.gsd/defaults.json (#2256) + const _ocAgentName = entry.name.replace(/\.md$/, ''); + const _ocModelOverrides = readGsdGlobalModelOverrides(); + const _ocModelOverride = _ocModelOverrides?.[_ocAgentName] || null; + content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride }); } else if (isKilo) { content = convertClaudeToKiloFrontmatter(content, { isAgent: true }); } else if (isGemini) { diff --git a/commands/gsd/inbox.md b/commands/gsd/inbox.md new file mode 100644 index 000000000..a57684fe2 --- /dev/null +++ b/commands/gsd/inbox.md @@ -0,0 +1,38 @@ +--- +name: gsd:inbox +description: Triage and review all open GitHub issues and PRs against project templates and contribution guidelines +argument-hint: "[--issues] [--prs] [--label] [--close-incomplete] [--repo owner/repo]" +allowed-tools: + - Read + - Bash + - Write + - Grep + - Glob + - AskUserQuestion +--- + +One-command triage of the project's GitHub inbox. Fetches all open issues and PRs, +reviews each against the corresponding template requirements (feature, enhancement, +bug, chore, fix PR, enhancement PR, feature PR), reports completeness and compliance, +and optionally applies labels or closes non-compliant submissions. + +**Flow:** Detect repo → Fetch open issues + PRs → Classify each by type → Review against template → Report findings → Optionally act (label, comment, close) + + + +@~/.claude/get-shit-done/workflows/inbox.md + + + +**Flags:** +- `--issues` — Review only issues (skip PRs) +- `--prs` — Review only PRs (skip issues) +- `--label` — Auto-apply recommended labels after review +- `--close-incomplete` — Close issues/PRs that fail template compliance (with comment explaining why) +- `--repo owner/repo` — Override auto-detected repository (defaults to current git remote) + + + +Execute the inbox workflow from @~/.claude/get-shit-done/workflows/inbox.md end-to-end. +Parse flags from arguments and pass to workflow. + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6f04657dd..5d7c03d48 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -113,7 +113,7 @@ User-facing entry points. Each file contains YAML frontmatter (name, description - **Copilot:** Slash commands (`/gsd-command-name`) - **Antigravity:** Skills -**Total commands:** 73 +**Total commands:** 74 ### Workflows (`get-shit-done/workflows/*.md`) @@ -409,7 +409,7 @@ UI-SPEC.md (per phase) ─────────────────── ``` ~/.claude/ # Claude Code (global install) -├── commands/gsd/*.md # 73 slash commands +├── commands/gsd/*.md # 74 slash commands ├── get-shit-done/ │ ├── bin/gsd-tools.cjs # CLI utility │ ├── bin/lib/*.cjs # 19 domain modules diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index f02db36f5..60f863dea 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -320,6 +320,35 @@ tools: Read, Grep, Glob const result = generateCodexAgentToml('gsd-unknown', sampleAgent); assert.ok(result.includes('sandbox_mode = "read-only"'), 'defaults to read-only'); }); + + // ─── #2256: model_overrides support ─────────────────────────────────────── + + test('emits model field when modelOverrides contains an entry for the agent (#2256)', () => { + const overrides = { 'gsd-executor': 'gpt-5.3-codex' }; + const result = generateCodexAgentToml('gsd-executor', sampleAgent, overrides); + assert.ok(result.includes('model = "gpt-5.3-codex"'), 'model field must be present in TOML'); + }); + + test('does not emit model field when modelOverrides is null (#2256)', () => { + const result = generateCodexAgentToml('gsd-executor', sampleAgent, null); + assert.ok(!result.includes('model ='), 'model field must be absent when no override'); + }); + + test('does not emit model field when modelOverrides has no entry for this agent (#2256)', () => { + const overrides = { 'gsd-planner': 'gpt-5.4' }; + const result = generateCodexAgentToml('gsd-executor', sampleAgent, overrides); + assert.ok(!result.includes('model ='), 'model field must be absent for agents not in overrides'); + }); + + test('model field appears before developer_instructions (#2256)', () => { + const overrides = { 'gsd-executor': 'gpt-5.3-codex' }; + const result = generateCodexAgentToml('gsd-executor', sampleAgent, overrides); + const modelIdx = result.indexOf('model = "gpt-5.3-codex"'); + const instrIdx = result.indexOf("developer_instructions = '''"); + assert.ok(modelIdx !== -1, 'model field present'); + assert.ok(instrIdx !== -1, 'developer_instructions present'); + assert.ok(modelIdx < instrIdx, 'model field must appear before developer_instructions'); + }); }); // ─── CODEX_AGENT_SANDBOX mapping ──────────────────────────────────────────────── diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs index a8803c598..dcffa9924 100644 --- a/tests/runtime-converters.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -184,6 +184,48 @@ Fallback skills live in .agents/skills/.`; assert.ok(frontmatter.includes('description:'), 'description should be kept'); }); }); + + // ─── #2256: model_overrides support for OpenCode/Kilo agents ──────────────── + // Only test OpenCode — Kilo uses the same converter but model override injection + // is wired only for OpenCode at the call site in install(). + if (label === 'OpenCode') { + describe('OpenCode agent model override (modelOverride option) (#2256)', () => { + test('adds model: field when modelOverride is provided', () => { + const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: 'gpt-5.3-codex' }); + const frontmatter = result.split('---')[1]; + assert.ok(frontmatter.includes('model: gpt-5.3-codex'), 'model: field must be added with override value'); + }); + + test('does not add model: field when modelOverride is null', () => { + const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: null }); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('model:'), 'model: field must be absent when no override'); + }); + + test('does not add model: field when modelOverride is omitted', () => { + const result = convert(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('model:'), 'model: field must be absent when option omitted'); + }); + + test('model: field appears after mode: subagent', () => { + const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: 'o4-mini' }); + const frontmatter = result.split('---')[1]; + const modeIdx = frontmatter.indexOf('mode: subagent'); + const modelIdx = frontmatter.indexOf('model: o4-mini'); + assert.ok(modeIdx !== -1, 'mode: subagent must be present'); + assert.ok(modelIdx !== -1, 'model: field must be present'); + assert.ok(modelIdx > modeIdx, 'model: must appear after mode: subagent'); + }); + + test('model override does not affect command conversion', () => { + // modelOverride has no effect when isAgent is false (commands) + const result = convert(SAMPLE_COMMAND, { modelOverride: 'gpt-5.4' }); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('model:'), 'model: must not appear in command output'); + }); + }); + } } // ─────────────────────────────────────────────────────────────────────────────