* test(#3241): failing-first suite for the codex passive model posture Locks ADR-2313's D1-D5 before any production code exists, so the tests bind to the behavior rather than to whatever the implementation happens to do. Red-first (fail against the current tree): - the resolver path emits no `model` and no `model_reasoning_effort` - a whitespace-only model_overrides value yields no pin - isAnthropicFlavoredModel / CLAUDE_AGENT_ALIASES on model-catalog - the one-time install notice, and its once-per-install dedupe Regression guards (pass today, must keep passing): resolver-null via `inherit` and via absent runtime; a resolver that resolves to nothing; empty-string and non-string overrides; and the light-tier service_tier/model_verbosity fields, which are NOT coupled to the model pin and would silently regress if the implementation coupled them. Classifying each test as red-first or regression guard is deliberate. A test that passes on both sides of the change proves nothing, and this epic has already shipped two such rows before catching them. The whitespace case is a live defect, not a quirk: `' '` is truthy, survives the type guard, is not Anthropic-flavored, and is embedded verbatim as `model = " "` — the same class the #2310 guard exists to stop. Same function, same path, fixed in this phase per CLAUDE.md §3. Two matrix rows were dropped as vacuous rather than shipped green: a 64-char truncation case (the pinned notice interpolates no user-controlled value, so it cannot exhibit truncation) and a newline hazard that the input surface cannot reach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(#3241): omit the codex per-agent model by default Implements ADR-2313 D1-D5. generateCodexAgentToml no longer embeds the runtime resolver's per-tier Codex model, so an agent inherits the always-available session model instead of a pin a ChatGPT-account Codex may not expose. model_reasoning_effort disappears with it via the existing hasPinnedModel coupling (#838) — no logic change needed there. Supersedes #2517's embedding on the default path only. An explicit real-Codex model_overrides pin is still embedded verbatim, and the #2310 Anthropic-flavored guard is retained: the model_overrides route to it is still live even though the resolver route is now unreachable. The shared rule moves down a layer. CLAUDE_AGENT_ALIASES leaves model-resolver for model-catalog — a genuine leaf importing only node:path and its own JSON — with isAnthropicFlavoredModel defined beside it, and is re-exported from model-resolver so every existing importer is untouched. This is what lets Phase 2's install-check and Phase 3's sync consume the rule without taking the config-loader dependency model-resolver would have dragged into a module documented as pure read/verify with 33 dependents. A parity test fails if the two ever fork. Also fixes a live defect surfaced while writing the tests: a whitespace-only model_overrides value was truthy, survived the type guard, was not Anthropic-flavored, and so was embedded verbatim as `model = " "` — the same class the #2310 guard exists to stop, reached by a different route. Trimmed before the truthiness test. It is deliberately not routed through _warnCodexModelOverrideDropped, whose text would misdescribe a blank field as a mis-typed model. Adds the one-time install notice (maintainer direction, recorded as an ADR-2313 amendment): one stderr line naming model_overrides and the session model, deduped per install rather than per agent, and emitted only for the population that actually loses a pin. service_tier and model_verbosity stay decoupled from the model (#774); a regression guard asserts they still emit with nothing pinned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(#3241): amend ADR-2313, add the model-catalog glossary entry ADR-2313 gains two dated amendments rather than edits to its merged text, since ADRs here are append-only. The first records that a deprecation notice IS offered, reversing the Migration section's "no deprecation window" position, and states why that position was wrong rather than just superseding it: the ADR identified the API-key population as losing something real and then declined to warn it, in the same document. Hyrum's guidance was applied to the recourse and not to the notice. The second records the whitespace-only model_overrides defect and notes that D2 always implied the fix — the implementation simply never enforced it and no test covered the case. CONTEXT.md gains a Model Catalog Module entry. The module had none, which is why the glossary gate passed without one: check-glossary-refs verifies that references resolve, not that modules are documented. The entry records why the Anthropic-flavored rule lives there rather than in model-resolver, so a later reader does not "helpfully" move it back. The Model Resolver entry is updated to point at its new home and note the back-compat re-export. docs/CONFIGURATION.md carried a claim that is now false: that the resolved tier ID is embedded into agent frontmatter at install time on codex and opencode. Corrected to name codex as the exception, with the 400 symptom and the model_overrides recourse. Changeset leads with the user-visible change and the migration line rather than the implementation, per the ADR's Hyrum's-Law analysis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#3241): only notice a lost pin when one was actually embeddable Review finding from an isolated reviewer. The deprecation notice gated on whether the runtime resolver would have returned *any* model, but the question that matters is whether that model would have been *embedded*. Those differ. The #2310 safety gate already rejected an Anthropic- flavored model arriving from the resolver path before Phase 1 — so for a mixed-runtime config resolving to a claude-* id against a Codex install target, the user never had that pin. The notice told them they lost something they never got, and pointed them at model_overrides for no reason. The existing #2310 test drives exactly that path but asserts only the emitted `model` line, never stderr, which is why it slipped through. Now covered. Deliberately unchanged: an Anthropic-flavored model_overrides value plus a legal resolver model fires BOTH the override warning and the notice. That is correct — pre-Phase-1 the guard dropped the override, execution fell through to the resolver, and the resolver's model was embedded, so that user did lose a pin. Two messages, two distinct true facts, and the prefixes differ (`gsd: warning — ` vs `gsd: notice — `) so the one-notice-per-install contract holds. A regression test now pins that behavior so it does not get "simplified" away. Of the three tests added, only the first is red-first; the other two pass on both sides by design and are labelled as guards — one against over-correcting the fix into silence, one against removing the intentional double message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#3241): reset the notice dedupe via a seam, not a require.cache bust The remote runner caught a regression I introduced: the #2760 post-write-validation test began failing with the validator override no longer intercepting. Cause, confirmed by trace rather than guessed: the new #3241 review tests deleted require.cache for bin/install.js and re-required it mid suite, to clear the notice's module-level dedupe flag. But runCodexInstall destructures `install` at file load, closing over the ORIGINAL module's exports. After the cache bust a second instance existed, so the test's `installModule.__codexSchemaValidator = ...` mutated the new object while the code under test still called the old one. The override silently stopped intercepting, the real validator ran and passed on GSD-emitted output, and the abort-and-restore path was never exercised. Cache-busting a module mid-suite breaks every later test that assumes a single instance, which every other test in the file is entitled to. So the fix is a seam, not a workaround: bin/install.js exports _resetCodexNoticeDedupeForTests(), and the three tests call it directly instead of reloading the module. The flag is module-level by design — the dedupe is per-install and install() already resets it — so a unit test driving generateCodexAgentToml directly needs an explicit way to reset it. That is now what it has. Swept the rest of the #3241 diff for the same hazard; this flag was the only shared module-level state introduced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#3241): reset both codex dedupe stores, not just the notice flag Second incomplete fix, same class one layer down. bin/install.js keeps TWO module-level dedupe stores and the require.cache bust I removed had been papering over both; my replacement seam cleared only one. _codexModelOverrideDroppedWarned is a Set keyed `${agent}::${value}`. tests/codex-config.test.cjs:558 already emits for `gsd-executor::sonnet`, so by the time the review test using the same agent and value ran, _warnCodexModelOverrideDropped was a silent no-op and the expected warning never appeared. The seam now clears both stores and is renamed to say so. Its comment records that per-install dedupe lives in module scope deliberately and that this is the single sanctioned way for a unit test to clear it. Swept bin/install.js for every other module-scope mutable a test could latch. Two are inert (capability registries assigned once at require time; selectedRuntimes computed once from argv). One is a genuine latent hazard and is deliberately NOT folded in: attributionCache (:1654) memoizes getCommitAttribution by runtime name for process lifetime, so two in-process installs of one runtime with differing attribution config would collide. It is unreachable from any current test and is a different concern from Codex warning dedupe, so it stays out of this PR rather than widening it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(#3241): correct the codex tier-routing how-to The docs gate forced the task-oriented quadrant and found the worst defect in this change's documentation surface. docs/how-to/configure-model-profiles.md carried a section titled "If you want tiered models on Codex" telling users to set runtime:codex + model_profile:balanced, promising "GSD resolves each tier alias to the Codex-native model and reasoning effort defined in the runtime tier map." That is exactly the behavior this PR removes — a how-to page confidently instructing users to do something that no longer works, which is worse than a missing page because it fails at the moment of use. Rewritten to state that Codex does no tier routing, give the model_overrides pin as the supported alternative, and name the two constraints on what may be pinned: it must be a real Codex model id, and the account must actually expose it — GSD cannot verify the second, so the honest advice when unsure is to omit the pin. Carries an upgrade note for both account types, since the change is a no-op for ChatGPT accounts and a real loss for API-key ones. Also tightened the same page's claim that Codex "embeds the resolved model" at install time — now true only of an explicit override. The re-install instruction it supports is still correct and still needed, so only the premise moved. Both the required-docs set (COMMANDS.md + FEATURES.md) and lint-docs-required.cjs would have passed before this commit, since CONFIGURATION.md and the ADR had already moved. Neither checks the quadrant a user in trouble actually opens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#3241): backfill changeset pr number (#3276) --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -169,7 +169,7 @@ project one is reported, since that is the file you are most likely able to fix.
|
||||
| `mode` | enum | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step |
|
||||
| `granularity` | enum | `coarse`, `standard`, `fine` | `standard` | Controls phase count: `coarse` (2-4), `standard` (4-6), `fine` (6-10) |
|
||||
| `model_profile` | enum | `quality`, `balanced`, `budget`, `adaptive`, `inherit` | `balanced` | Model tier for each agent (see [Model Profiles](#model-profiles)). `adaptive` was added per [#1713](https://github.com/open-gsd/gsd-core/issues/1713) / [#1806](https://github.com/open-gsd/gsd-core/issues/1806) and resolves the same way as the other tiers under runtime-aware profiles. |
|
||||
| `runtime` | string | `claude`, `codex`, or any string | (none) | Active runtime for [runtime-aware profile resolution](#runtime-aware-profiles-2517). When set, profile tiers (opus/sonnet/haiku) resolve to runtime-native model IDs. The resolved ID is embedded into each agent's static frontmatter at install time on `codex` and `opencode` (whose `task` / `spawn_agent` interfaces do not accept an inline `model` parameter, so editing `model_overrides` requires re-running `gsd install <runtime>` to take effect — see [Per-Agent Overrides](#per-agent-overrides)); other runtimes consume the resolver at spawn time. When unset (default), behavior is unchanged from prior versions. Added in v1.39 |
|
||||
| `runtime` | string | `claude`, `codex`, or any string | (none) | Active runtime for [runtime-aware profile resolution](#runtime-aware-profiles-2517). When set, profile tiers (opus/sonnet/haiku) resolve to runtime-native model IDs. The resolved ID is embedded into each agent's static frontmatter at install time on `opencode` (whose `spawn_agent` interface does not accept an inline `model` parameter, so editing `model_overrides` requires re-running `gsd install <runtime>` to take effect — see [Per-Agent Overrides](#per-agent-overrides)); other runtimes consume the resolver at spawn time. **`codex` is the exception: it embeds no per-tier model at all.** Codex is a passive / session-only model host ([ADR-2313](adr/2313-codex-passive-model-posture.md)) — a ChatGPT-account session exposes only its own model, so a pinned tier model returns `400 invalid_request_error` and the agent fails to spawn. Codex agents therefore inherit the session model, and only an explicit real-Codex id in `model_overrides` (e.g. `"gpt-5.6-sol"`) is written into the `.toml`. When unset (default), behavior is unchanged from prior versions. Added in v1.39; Codex behavior changed in v1.11 |
|
||||
| `model_profile_overrides.<runtime>.<tier>` | string \| object | per-runtime tier override | (none) | Override the runtime-aware tier mapping for a specific `(runtime, tier)`. Tier is one of `opus`, `sonnet`, `haiku`. Value is either a model ID string (e.g. `"gpt-5-pro"`) or `{ model, reasoning_effort }`. See [Runtime-Aware Profiles](#runtime-aware-profiles-2517). Added in v1.39 |
|
||||
| `model_policy.provider` | string | `openai`, `anthropic`, `anthropic-fable`, `google`, `qwen`, `generic` | (none) | Declares the model provider. Known providers (`openai`, `anthropic`, `anthropic-fable`, `google`, `qwen`) unlock catalog-backed presets. `generic` treats all model IDs as opaque strings — no prefix inference, no reasoning-effort defaults. `model_policy.runtime_tiers` resolves before legacy `model_profile_overrides`. See [Model Policy Presets](#model-policy-presets-model_policy--added-in-v142). Added in v1.42 ([#49](https://github.com/open-gsd/gsd-core/issues/49)) |
|
||||
| `model_policy.budget` | enum | `high`, `medium`, `low` | (none) | Selects a budget tier when using a known provider. GSD materializes the matching catalog preset into explicit tier mappings at resolve time. Ignored when `provider` is `generic` or `custom`. Added in v1.42 ([#49](https://github.com/open-gsd/gsd-core/issues/49)) |
|
||||
|
||||
@@ -279,3 +279,57 @@ to assemble it from Consequences and Scope boundary:
|
||||
ADR does not generalize the posture; extending it to another host would be its own decision.
|
||||
- **The posture is not real until Phase 1 merges.** `Accepted` locks the contract, not the tree —
|
||||
until #3241 lands, `generateCodexAgentToml` still embeds a per-tier model.
|
||||
|
||||
## Amendment (2026-08-09): a deprecation notice IS offered (#3241)
|
||||
|
||||
Recorded as a dated section rather than by editing the Migration section or the Known limits bullet
|
||||
above, since ADRs here are append-only. Both now read as superseded on this one point; the rest of
|
||||
each stands.
|
||||
|
||||
**What changed.** This ADR's Migration section states *"No deprecation window is offered. The default
|
||||
flips in a single release rather than warning first,"* and Known limits repeats it. Phase 1 (#3241)
|
||||
ships a deprecation notice instead, by maintainer direction taken after this ADR merged.
|
||||
|
||||
**Why the original position was wrong, precisely.** The argument for flipping silently was that the
|
||||
behavior being removed *"is one most affected users could never successfully use"* — it 400s on a
|
||||
ChatGPT-account Codex. That is true of the ChatGPT population and false of the API-key population,
|
||||
which is exactly the group the Migration section already identifies as *losing something real*. The
|
||||
ADR named a class of user harmed by the change and then declined to warn them, in the same document.
|
||||
Hyrum's Law's own guidance — break a long-lived observable behavior when you must, but give a
|
||||
migration path — was applied to the *recourse* (`model_overrides` stays) and not to the *notice*.
|
||||
|
||||
**The notice.** One line to stderr per install, emitted only for the population that actually loses a
|
||||
pin: the runtime resolver would have supplied a model, and nothing ends up pinned. It names
|
||||
`model_overrides` as the recovery mechanism and the session model as what the agent gets instead.
|
||||
|
||||
It deliberately names **no agent and no model**. The condition is per-install, not per-agent — every
|
||||
agent hits it simultaneously — so per-agent detail would imply a per-agent decision that was not made,
|
||||
and ~20 identical lines would train the reader to ignore them. It carries no interpolated
|
||||
user-controlled value, which is why it needs none of the length-capping the adjacent
|
||||
`_warnCodexModelOverrideDropped` applies.
|
||||
|
||||
It does **not** fire when the resolver is null (`inherit`, or no configured `runtime`), when the
|
||||
resolver resolves to nothing, or when an explicit real-Codex pin survives. In each of those cases
|
||||
nothing was lost, and a notice would be noise that costs the signal its meaning.
|
||||
|
||||
**Known limit this does not remove.** The notice fires at *install* time. A user who never
|
||||
re-installs never sees it — Phase 2's health-check and Phase 3's sync are what reach them. The
|
||||
Known-limits bullet above is therefore softened, not deleted: there is now a warning, but it is not
|
||||
a full deprecation *release*, and no separate release ships before the flip.
|
||||
|
||||
## Amendment (2026-08-09): whitespace-only `model_overrides` was a live defect (#3241)
|
||||
|
||||
Surfaced while writing Phase 1's failing-first suite, and fixed there rather than filed.
|
||||
|
||||
`model_overrides[<agent>] = " "` is **truthy**, survives the `typeof === 'string'` guard, is not
|
||||
Anthropic-flavored, and was therefore embedded verbatim as `model = " "`. That is the same class
|
||||
the #2310 guard exists to stop — a value that is not a real Codex model id reaching the `.toml` and
|
||||
400-ing the agent — reached by a different route.
|
||||
|
||||
Phase 1 trims before the truthiness test, so a whitespace-only override yields no pin. It is
|
||||
deliberately **not** routed to `_warnCodexModelOverrideDropped`: that message says the value *"is not
|
||||
a valid Codex model (Anthropic alias/id)"*, which misdescribes an empty config field. A blank value
|
||||
is silently no-pin, matching how `""` already behaved.
|
||||
|
||||
This ADR's D2 ("embed a `model` only for an explicit real-Codex pin") always implied this. The
|
||||
implementation simply did not enforce it, and no test covered the case.
|
||||
|
||||
@@ -55,7 +55,7 @@ Valid values: `opus`, `sonnet`, `haiku`, `inherit`, or any fully-qualified model
|
||||
|
||||
`model_overrides` can be set per-project in `.planning/config.json` or globally in `~/.gsd/defaults.json`. Per-project entries win on conflict; non-conflicting global entries are preserved.
|
||||
|
||||
**Important for Codex and OpenCode:** Those runtimes embed the resolved model into each agent's static config at install time. After editing `model_overrides`, re-run the installer for the change to take effect:
|
||||
**Important for Codex and OpenCode:** Those runtimes embed the model into each agent's static config at install time rather than choosing it per spawn, so after editing `model_overrides` you must re-run the installer for the change to take effect:
|
||||
|
||||
```bash
|
||||
npx @opengsd/gsd-core@latest --codex --global # or --opencode, --kilo, etc.
|
||||
@@ -187,16 +187,46 @@ quota / rate-limit failures; other failures keep the tier ladder. Leaving
|
||||
|
||||
If you installed GSD for Codex, OpenCode, Antigravity CLI, or Kilo, the installer already set `resolve_model_ids: "omit"` in your config. This tells GSD to skip Anthropic model ID resolution and let the runtime choose its own default model. No manual setup is needed for the basic case.
|
||||
|
||||
**If you want tiered models on Codex:**
|
||||
### Codex does not do tier routing — pin explicitly instead
|
||||
|
||||
**Codex agents inherit whatever model your Codex session is using.** GSD writes no `model` line into
|
||||
`~/.codex/agents/<agent>.toml`, so setting `model_profile` has no effect on Codex.
|
||||
|
||||
This is deliberate ([ADR-2313](../adr/2313-codex-passive-model-posture.md)). A ChatGPT-account Codex
|
||||
session exposes only its own model, so a pinned tier model fails the request outright —
|
||||
`400 invalid_request_error: "The 'sonnet' model is not supported when using Codex with a ChatGPT
|
||||
account"` — and the agent never spawns.
|
||||
|
||||
**To pin a model on Codex, name a real Codex model id per agent:**
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": "codex",
|
||||
"model_profile": "balanced"
|
||||
"model_overrides": {
|
||||
"gsd-planner": "gpt-5.6-sol",
|
||||
"gsd-executor": "gpt-5.6-terra"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
GSD resolves each tier alias to the Codex-native model and reasoning effort defined in the runtime tier map.
|
||||
Then re-run the installer, as with any `model_overrides` edit on Codex (see above).
|
||||
|
||||
Two rules apply to what you can put there:
|
||||
|
||||
- **It must be a real Codex model id.** A GSD tier alias (`opus`, `sonnet`, `haiku`, `fable`) or a
|
||||
`claude-*` id is dropped with a warning rather than written, because Codex rejects them.
|
||||
- **Your account must actually expose it.** GSD cannot check this — if you pin `gpt-5.6-sol` on an
|
||||
account that does not have it, you get the same 400. When in doubt, omit the pin and let the
|
||||
session model apply.
|
||||
|
||||
`model_reasoning_effort` follows the model: with no pin, GSD writes no effort line either, so the
|
||||
Codex UI drives both rather than one following GSD and the other following your session.
|
||||
|
||||
> **Upgrading from v1.10 or earlier?** Codex installs used to embed a per-tier model
|
||||
> (`opus→gpt-5.6-sol`, `sonnet→gpt-5.6-terra`, `haiku→gpt-5.6-luna`). If you were on an API-key
|
||||
> account where those resolved successfully, add the `model_overrides` block above to keep them.
|
||||
> The installer prints a one-time notice when it drops a pin. If you were on a ChatGPT account, this
|
||||
> is the change that stops the 400s — nothing to do.
|
||||
|
||||
**If you want per-agent model IDs on any non-Claude runtime:**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user