#3703·gsd-core

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

Author: lcatlettCreated Aug 20, 2026Updated Sep 21, 2026
Labelsready-for-humanStale

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:

  1. The variables are never consumed by shell logic. Across gsd-core/workflows/*.md, every *_MODEL assignment 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.

  2. GSD agents carry no model: frontmatter. Checked gsd-codebase-mapper, gsd-planner, gsd-phase-researcher, gsd-domain-researcher — none declare model:. 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")

bash
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 tier

Affected 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:

bash
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:

  1. Surface *_TIER alongside *_MODEL at every spawn site (mirror explore.md), or
  2. Have --pick model fall back to tier when resolve_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