fix: embed model_overrides in Codex TOML and OpenCode agent files (#2279)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-15 14:59:56 -04:00
committed by GitHub
parent d9a4e5bf40
commit 262b395879
5 changed files with 164 additions and 11 deletions

View File

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

38
commands/gsd/inbox.md Normal file
View File

@@ -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
---
<objective>
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)
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/inbox.md
</execution_context>
<context>
**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)
</context>
<process>
Execute the inbox workflow from @~/.claude/get-shit-done/workflows/inbox.md end-to-end.
Parse flags from arguments and pass to workflow.
</process>

View File

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

View File

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

View File

@@ -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');
});
});
}
}
// ─────────────────────────────────────────────────────────────────────────────