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:
@@ -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
38
commands/gsd/inbox.md
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user