bug(config): `resolve_model_ids:"omit"` blanks the only routing signal — `tier` exists for this case but 9 of 10 workflow spawn sites never read it
Summary
resolve_model_ids: "omit" blanks the model field by design. tier was added as the replacement signal for exactly that case (#2229). But the workflows were never updated to read it: 10 workflow files surface a *_MODEL variable, and only 1 (explore.md) also surfaces *_TIER.
Under omit, nine of those spawn sites hand the orchestrating model an empty routing signal and no fallback, so agents the profile assigns to a cheap tier get spawned on whatever the session is running.
Sibling of #3696 under the #3473 framing (one owner per invariant): a value with multiple readers where the readers were not updated alongside the producer.
Why the *_MODEL variable is the only routing signal
Two things make the blank load-bearing rather than cosmetic:
The variables are never consumed by shell logic. Across
gsd-core/workflows/*.md, every*_MODELassignment is write-only — a grep for$VAR/${VAR}returns zero references anywhere in the tree. Their whole function is to surface the value into the workflow transcript so the orchestrator spawns with it.GSD agents carry no
model:frontmatter. Checkedgsd-codebase-mapper,gsd-planner,gsd-phase-researcher,gsd-domain-researcher— none declaremodel:. There is no second place the tier could be recovered from.
So when --pick model returns "", nothing downstream knows what to spawn.
Reproduction (gsd-core 1.11.0, Claude runtime, model_profile: "quality")
gsd-tools resolve-model gsd-codebase-mapper # default
# {"model":"sonnet","profile":"quality","effort":"low","tier":"sonnet"}
# add "resolve_model_ids": "omit" to .planning/config.json
gsd-tools resolve-model gsd-codebase-mapper
# {"model":"","profile":"quality","effort":"low","tier":"sonnet"}tier still carries the answer. The consumer does not read it:
$ grep -rhoE '--pick (model|tier)' gsd-core/workflows/ commands/ | sort | uniq -c
7 --pick model
3 --pick tierAffected sites reading --pick model with no tier fallback include gsd-ai-researcher, gsd-domain-researcher, gsd-eval-planner, gsd-eval-auditor, gsd-framework-selector.
The correct pattern already exists in-tree
gsd-core/workflows/explore.md:83-84 does it right:
RESEARCHER_TIER=$(gsd_run query resolve-model gsd-phase-researcher --pick tier 2>/dev/null || true)
RESEARCHER_MODEL=$(gsd_run query resolve-model gsd-phase-researcher --pick model 2>/dev/null || true)Every other site surfaces only the model.
Impact
Silent and cost-shaped rather than loud. gsd-codebase-mapper is profiled sonnet/low; under omit it inherits the session model, so a mapper meant to run cheap runs on the orchestrator's tier. Nothing errors and nothing logs, so the misroute is invisible.
This lands specifically on multi-runtime users — the population omit exists for. Per config.cts (#2840 comment), a concrete model id resolved under one runtime is wrong under another, so omit is the correct global default for anyone running Claude Code + Codex + Gemini side by side. Setting the documented-correct global value is what triggers the degradation.
Suggested
Either:
- Surface
*_TIERalongside*_MODELat every spawn site (mirrorexplore.md), or - Have
--pick modelfall back totierwhenresolve_model_ids: "omit"blanks it, so a single read stays sufficient and no workflow needs to change.
(2) keeps one owner for the invariant, which is the #3473 direction.
Environment
- gsd-core 1.11.0 (npm), Claude Code runtime, local (project) install
model_profile: "quality",.gsd-runtime: claude- macOS 15.6 / darwin 25.6.0, node 24.19.0
Source: open-gsd/gsd-core