The Codex and OpenCode install paths read `model_overrides` only from `~/.gsd/defaults.json` (global). A per-project override set in `.planning/config.json` — the reporter's exact setup for `gsd-codebase-mapper` — was silently dropped, so the child agent inherited the runtime's default model regardless of `model_overrides`. Neither runtime has an inline `model` parameter on its spawn API (Codex `spawn_agent(agent_type, message)`, OpenCode `task(description, prompt, subagent_type, task_id, command)`), so the per-agent model must reach the child via the static config GSD writes at install time. That config was being populated from the wrong source. Fix: add `readGsdEffectiveModelOverrides(targetDir)` which merges `~/.gsd/defaults.json` with per-project `.planning/config.json`, with per-project keys winning on conflict. Both install sites now call it and walk up from the install root to locate `.planning/` — matching the precedence `readGsdRuntimeProfileResolver` already uses for #2517. Also update the Codex Task()->spawn_agent mapping block so it no longer says "omit" without context: it now documents that per-agent overrides are embedded in the agent TOML and notes the restriction that Codex only permits `spawn_agent` when the user explicitly requested sub-agents (do the work inline otherwise). Regression tests (`tests/bug-2256-model-overrides-transport.test.cjs`) cover: global-only, project-only, project-wins-on-conflict, walking up from a nested `targetDir`, Codex TOML `model =` emission, and OpenCode frontmatter `model:` emission. Closes #2256 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -634,6 +634,63 @@ function readGsdGlobalModelOverrides() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective per-agent model_overrides for the Codex / OpenCode install paths.
|
||||
*
|
||||
* Merges `~/.gsd/defaults.json` (global) with per-project
|
||||
* `<project>/.planning/config.json`. Per-project keys win on conflict so a
|
||||
* user can tune a single agent's model in one repo without re-setting the
|
||||
* global defaults for every other repo. Non-conflicting keys from both
|
||||
* sources are preserved.
|
||||
*
|
||||
* This is the fix for #2256: both adapters previously read only the global
|
||||
* file, so a per-project `model_overrides` (the common case the reporter
|
||||
* described — a per-project override for `gsd-codebase-mapper` in
|
||||
* `.planning/config.json`) was silently dropped and child agents inherited
|
||||
* the session default.
|
||||
*
|
||||
* `targetDir` is the consuming runtime's install root (e.g. `~/.codex` for
|
||||
* a global install, or `<project>/.codex` for a local install). We walk up
|
||||
* from there looking for `.planning/` so both cases resolve the correct
|
||||
* project root. When `targetDir` is null/undefined only the global file is
|
||||
* consulted (matches prior behavior for code paths that have no project
|
||||
* context).
|
||||
*
|
||||
* Returns a plain `{ agentName: modelId }` object, or `null` when neither
|
||||
* source defines `model_overrides`.
|
||||
*/
|
||||
function readGsdEffectiveModelOverrides(targetDir = null) {
|
||||
const global = readGsdGlobalModelOverrides();
|
||||
|
||||
let projectOverrides = null;
|
||||
if (targetDir) {
|
||||
let probeDir = path.resolve(targetDir);
|
||||
for (let depth = 0; depth < 8; depth += 1) {
|
||||
const candidate = path.join(probeDir, '.planning', 'config.json');
|
||||
if (fs.existsSync(candidate)) {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
||||
if (parsed && typeof parsed === 'object' && parsed.model_overrides
|
||||
&& typeof parsed.model_overrides === 'object') {
|
||||
projectOverrides = parsed.model_overrides;
|
||||
}
|
||||
} catch {
|
||||
// Malformed config.json — fall back to global; readGsdRuntimeProfileResolver
|
||||
// surfaces a parse warning via _readGsdConfigFile already.
|
||||
}
|
||||
break;
|
||||
}
|
||||
const parent = path.dirname(probeDir);
|
||||
if (parent === probeDir) break;
|
||||
probeDir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
if (!global && !projectOverrides) return null;
|
||||
// Per-project wins on conflict; preserve non-conflicting global keys.
|
||||
return { ...(global || {}), ...(projectOverrides || {}) };
|
||||
}
|
||||
|
||||
/**
|
||||
* #2517 — Read a single GSD config file (defaults.json or per-project
|
||||
* config.json) into a plain object, returning null on missing/empty files
|
||||
@@ -1852,9 +1909,17 @@ GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collabo
|
||||
|
||||
Direct mapping:
|
||||
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
||||
- \`Task(model="...")\` → omit (Codex uses per-role config, not inline model selection)
|
||||
- \`Task(model="...")\` → omit. \`spawn_agent\` has no inline \`model\` parameter;
|
||||
GSD embeds the resolved per-agent model directly into each agent's \`.toml\`
|
||||
at install time so \`model_overrides\` from \`.planning/config.json\` and
|
||||
\`~/.gsd/defaults.json\` are honored automatically by Codex's agent router.
|
||||
- \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
|
||||
|
||||
Spawn restriction:
|
||||
- Codex restricts \`spawn_agent\` to cases where the user has explicitly
|
||||
requested sub-agents. When automatic spawning is not permitted, do the
|
||||
work inline in the current agent rather than attempting to force a spawn.
|
||||
|
||||
Parallel fan-out:
|
||||
- Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
|
||||
|
||||
@@ -3182,12 +3247,15 @@ function installCodexConfig(targetDir, agentsSrc) {
|
||||
|
||||
agents.push({ name, description: toSingleLine(description) });
|
||||
|
||||
// Pass model overrides from ~/.gsd/defaults.json so Codex TOML files
|
||||
// Pass model overrides from both per-project `.planning/config.json` and
|
||||
// `~/.gsd/defaults.json` (project wins on conflict) so Codex TOML files
|
||||
// embed the configured model — Codex cannot receive model inline (#2256).
|
||||
// Previously only the global file was read, which silently dropped the
|
||||
// per-project override the reporter had set for gsd-codebase-mapper.
|
||||
// #2517 — also pass the runtime-aware tier resolver so profile tiers can
|
||||
// resolve to Codex-native model IDs + reasoning_effort when `runtime: "codex"`
|
||||
// is set in defaults.json.
|
||||
const modelOverrides = readGsdGlobalModelOverrides();
|
||||
const modelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
||||
// Pass `targetDir` so per-project .planning/config.json wins over global
|
||||
// ~/.gsd/defaults.json — without this, the PR's headline claim that
|
||||
// setting runtime in the project config reaches the Codex emit path is
|
||||
@@ -5893,9 +5961,13 @@ function install(isGlobal, runtime = 'claude') {
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
// Convert frontmatter for runtime compatibility (agents need different handling)
|
||||
if (isOpencode) {
|
||||
// Resolve per-agent model override from ~/.gsd/defaults.json (#2256)
|
||||
// Resolve per-agent model override from BOTH per-project
|
||||
// `.planning/config.json` and `~/.gsd/defaults.json`, with
|
||||
// per-project winning on conflict (#2256). Without the per-project
|
||||
// probe, an override set in `.planning/config.json` was silently
|
||||
// ignored and the child inherited OpenCode's default model.
|
||||
const _ocAgentName = entry.name.replace(/\.md$/, '');
|
||||
const _ocModelOverrides = readGsdGlobalModelOverrides();
|
||||
const _ocModelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
||||
const _ocModelOverride = _ocModelOverrides?.[_ocAgentName] || null;
|
||||
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride });
|
||||
} else if (isKilo) {
|
||||
@@ -7071,6 +7143,7 @@ if (process.env.GSD_TEST_MODE) {
|
||||
mergeCodexConfig,
|
||||
installCodexConfig,
|
||||
readGsdRuntimeProfileResolver,
|
||||
readGsdEffectiveModelOverrides,
|
||||
install,
|
||||
uninstall,
|
||||
convertClaudeCommandToCodexSkill,
|
||||
|
||||
Reference in New Issue
Block a user