Story-2985 Walkthrough

Change: model re-points can now be reviewed before a human edits the registry. uv run models what-if <alias> <new-model> finds every runtime seat and eval-path reference naming that alias. uv run models rank compares the committed catalog using tokenizer-normalized cost.

The commands inform; they never re-point or crown. Both Stage 2 verbs read committed YAML only. They do not edit the registry, agent configuration, or pricing catalog, and they never call a vendor.

The action-at-a-distance check

model-registry.yaml: alias → current concrete model
             │
             ├─ model-pricing.yaml → current/proposed effective cost + INT
             └─ agents/*/llm.yaml  → exact alias leaves
                    ├─ models          → runtime seats
                    ├─ judges          → eval-path references
                    └─ eval_candidates → eval-path references

The walker follows mappings and sequences recursively, retaining every index. That covers scalar seats, parallel rosters, fallback chains, list-of-lists, and Blake-registrar's nested primary/fallbacks shape without mistaking cache settings for model references.

The missed compactor is visible

uv run models what-if google-flash google/gemini-3.6-flash

google-flash: google/gemini-3.6-flash → google/gemini-3.6-flash
  INT 50.1 → 50.1   eff in $1.5 → $1.5   eff out $7.5 → $7.5   eff cache_rd $0.15 → $0.15

4 runtime seats move:
  context_judge.compactor    (agents/context_judge/llm.yaml)
  vega.critic                (agents/framer/llm.yaml)
  indigo.proposer[2]         (agents/indigo/llm.yaml)
  mira.proposer[0]           (agents/mira/llm.yaml)

1 eval-path reference:
  indigo.eval_candidates.proposer[2] (agents/indigo/llm.yaml)

The runtime count remains the acceptance-critical four. The later Indigo eval candidate is reported separately, so eval blast radius is visible without being mislabeled as a production seat. The committed registry already points the alias at Gemini 3.6, so this acceptance fixture correctly shows a zero delta while still proving the complete impact scan.

Effective cost, not sticker price

Catalog fieldComparison
Inputinput × tokens_per_text_ratio
Outputoutput × tokens_per_text_ratio
Cache readcache_read × tokens_per_text_ratio

For Claude 4.7+ the ratio is 1.30. A listed $5/$25 Opus row therefore ranks at an effective $6.50/$32.50 for the same text. Missing cache prices render as a dash. The table omits context length because model-pricing.yaml does not carry that field and rank is deliberately forbidden from consulting another source or the network.

uv run models rank --min-int 50

selected rows:
model                               eff $in   eff $out  cache_rd    INT
z-ai/glm-5.2                          0.714      2.244    0.1326   51.1
zai:glm-5.2                           0.826      2.596    0.1534   51.1
openai/gpt-5.6-luna                       1          6       0.1   51.2
...
google/gemini-3.6-flash                 1.5        7.5      0.15   50.1
...
anthropic/claude-sonnet-5               2.6         13      0.26   53.4
...
moonshotai/kimi-k3                        3         15       0.3   57.1
...

Rows sort by effective input cost. Catalog entries below INT 50 are absent; no vendor lookup fills or changes the table.

Fail-closed and read-only proof

Verification

uv run pytest tests/test_model_ops.py -q
→ 23 passed

uv run ruff check agents/ tests/ scripts/
→ All checks passed

uv run pytest tests -q
→ 409 passed

git diff --check
→ passed

Not included