enhance(#3245): report the detected host runtime in init (#3307)

* test(#3245): failing-first coverage for host runtime detection in init

Locks the behavior epic #2313 Phase 5 must produce before any of it exists: init reports the detected host, explicit GSD_RUNTIME and config runtime still outrank detection, non-Codex sessions are untouched, and nothing is ever written to shared defaults (#2297).

* enhance(#3245): report the detected host runtime in init

init reported agent_runtime: claude inside a Codex session, and resolved agents_dir to the Claude agents root with agents_installed: true — a spuriously healthy triple. Runtime identity was only ever read from GSD_RUNTIME or an explicit runtime in .planning/config.json.

Adds a detection rung beneath both explicit sources, in a new pure module. Codex is identified from its own documented session environment (CODEX_SANDBOX / CODEX_SANDBOX_NETWORK_DISABLED), else an explicitly exported CODEX_HOME whose config.toml exists. The default ~/.codex is never probed: that file exists on every machine that has run Codex, so probing it would misreport other runtimes' sessions.

resolveRuntime keeps its exact contract and all 71 dependents, including formatGsdSlash command-style emission; only withProjectRoot consumes the new rung. Nothing is written on any path (#2297). Explicit config still wins (#2517).

* fix(#3245): make the parity guard real and single-source the marker

Four independent review passes found the generative-fix-divergence guard was vacuous: it asserted agreement at the one input where inferPreferredRuntime and detectHostRuntime do not differ, so it could not fail. It now pins the actual divergence point (CODEX_HOME set, config.toml absent) and records that the asymmetry is deliberate.

The config.toml marker is now single-sourced from update-context.cts and imported, rather than carried independently by two surfaces. tests/helpers.cjs now scrubs CODEX_SANDBOX and CODEX_SANDBOX_NETWORK_DISABLED: GSD reads them, so an ambient Codex session would otherwise make the non-codex control test fail non-deterministically.

Also: detection is throw-safe end to end rather than only around the fs probe; the Windows-join test is replaced with one that can actually fail (trailing-separator, catches hand-rolled concatenation); the #2297 no-write proof now wraps resolveReportedRuntime, the function that ships, across all three ladder outcomes.

* chore(#3245): backfill changeset pr number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-10 09:48:26 -04:00
committed by GitHub
parent 57437071e7
commit 96a82bbffb
16 changed files with 926 additions and 12 deletions

View File

@@ -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 `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 |
| `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), model resolution is unchanged from prior versions — but the runtime GSD *reports* (`agent_runtime`) then falls through to [host detection](how-to/control-the-reported-host-runtime.md), which can resolve `codex` from Codex's own session environment. Detection affects reporting and the agent-installation check only; it never feeds tier resolution, which still reads this key alone. Added in v1.39; Codex behavior changed in v1.11; reporting-only host detection added 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)) |

View File

@@ -380,6 +380,7 @@
"hook-bus.cjs",
"host-integration-sdk.cjs",
"host-integration.cjs",
"host-runtime-detection.cjs",
"init-command-router.cjs",
"init.cjs",
"install-effort-resolver.cjs",

View File

@@ -492,6 +492,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `graphify-command-router.cjs` | ADR-959 capability command router for `gsd-tools graphify` — dispatches build/query/status/diff subcommands; first real capability command cutover (phase 4d-impl-2) |
| `gsd2-import.cjs` | External-plan ingest for `/gsd-import --from-gsd2` |
| `host-integration.cjs` | Host-Integration Interface (ADR-1239 Phase A) — negotiated capability contract over the six host-integration points; `negotiateHostCapabilities` fail-closes on undeclared/unknown/`undocumented` values, typed degradation ladder, host-capability profiles; the 8 `runtime.hostIntegration` axes are validated in `capability-validator.cjs` and sourced per-CLI in `docs/reference/host-integration-capability-matrix.md` |
| `host-runtime-detection.cjs` | Host Runtime Detection Module (ADR-2313 Phase 5, #3245) — the detection rung beneath `GSD_RUNTIME` and `.planning/config.json` `runtime` that lets `init` report `agent_runtime: codex` inside a Codex session instead of the hardcoded `claude` default; `detectHostRuntime` returns the typed `{runtime, source, signal}` from citation-backed Codex signals (`CODEX_SANDBOX`/`CODEX_SANDBOX_NETWORK_DISABLED`, else `CODEX_HOME` + `config.toml`), `resolveReportedRuntime` composes the full ladder. Pure, injectable, never writes, never shells out |
| `init-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools init` |
| `init.cjs` | Compound context loading for each workflow type |
| `install-effort-resolver.cjs` | Install-time effort resolution — `readGsdEffectiveEffortConfig` (merges `~/.gsd/defaults.json` + project `.planning/config.json`) + `resolveInstallTimeEffort`, extracted from `bin/install.js` (#2071) so `gsd-tools effort sync` can require it from the shipped runtime instead of the never-copied package-root installer; install.js imports them back (single source) |

View File

@@ -31,6 +31,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
- [Run phases autonomously](how-to/run-phases-autonomously.md) — use autonomous mode for unattended phase execution
- [Handle quick and fast tasks](how-to/handle-quick-and-fast-tasks.md) — use `/gsd-quick` and `/gsd-fast` for ad-hoc work outside the phase loop
- [Configure model profiles](how-to/configure-model-profiles.md) — switch between quality, balanced, and budget model tiers
- [Control which host runtime GSD reports](how-to/control-the-reported-host-runtime.md) — read the `agent_runtime` ladder, understand what host detection looks at, and pin the runtime when detection is not what you want
- [Set up cross-AI review](how-to/set-up-cross-ai-review.md) — configure a second AI to review code produced by the primary agent
- [Work in parallel with workstreams](how-to/work-in-parallel-with-workstreams.md) — run independent lines of work simultaneously using workstreams
- [Isolate work with workspaces](how-to/isolate-work-with-workspaces.md) — use workspaces to sandbox experimental or risky changes

View File

@@ -0,0 +1,83 @@
# How to control which host runtime GSD reports
**Goal:** Make `agent_runtime` — the runtime GSD reports it is running under, and the one it checks for installed agents — say what you actually want, and know *why* it says what it says.
**Prerequisites:** A project with a `.planning/` directory. Read the current answer with:
```bash
node gsd-tools.cjs init plan-phase 1 --raw
```
The JSON carries `agent_runtime`, plus the `agents_dir` / `agents_installed` / `missing_agents` triple derived from it. For the config key itself, see [`runtime`](../CONFIGURATION.md#runtime-aware-profiles-2517).
---
## The ladder, in order
GSD answers "which runtime am I?" from the first of these that produces a value:
| # | Source | Set it by | Wins over |
|---|---|---|---|
| 1 | `GSD_RUNTIME` environment variable | `GSD_RUNTIME=opencode` in the environment | everything below |
| 2 | `runtime` in `.planning/config.json` | `"runtime": "codex"` | detection and the default |
| 3 | **Host detection** (added in v1.11) | nothing — it is automatic | the default only |
| 4 | Default | — | — (`claude`) |
Rungs 1 and 2 are *explicit* — you stated an intent, and GSD does not second-guess it. Rung 3 only ever runs when **both** are unset. This is what preserves the behavior of every existing config: if you have ever set `runtime`, nothing about your setup changes.
---
## What detection actually looks at
Detection answers a narrow question — *is this process running inside a Codex session?* — and only from signals Codex itself documents.
| Signal | Where it comes from | Why it is trusted |
|---|---|---|
| `CODEX_SANDBOX` is set and non-empty | Codex injects it into child processes it spawns via Seatbelt | Documented in Codex's own `AGENTS.md`; present in exactly the processes Codex runs, which is where GSD runs |
| `CODEX_SANDBOX_NETWORK_DISABLED` is set and non-empty | Codex injects it when running the shell tool with the network sandbox on | same source |
| `CODEX_HOME` is set **and** `$CODEX_HOME/config.toml` exists | you exported it | Exporting `CODEX_HOME` is you designating a Codex state root; the `config.toml` check confirms the directory is a real one |
Anything else — no signal — means no detection, and rung 4 applies.
---
## The two questions this page exists for
### "I'm in a Codex session and it still says `claude`"
Work down the ladder:
| Check | What to do |
|---|---|
| Is `GSD_RUNTIME` set to something else? | `echo $GSD_RUNTIME` — it outranks everything. Unset it, or set it to `codex`. |
| Does `.planning/config.json` have a `runtime`? | An explicit `"runtime": "claude"` wins over detection, by design. Change it or remove the key. |
| Is your Codex sandbox off? | With `sandbox_mode = "danger-full-access"`, Codex sets **neither** sandbox variable, so there is nothing for GSD to detect. This is the most common cause. |
| Still nothing? | Set it explicitly. Detection is a convenience, not a contract — `"runtime": "codex"` in `.planning/config.json` is the supported, permanent answer. |
Detection is deliberately conservative: when it cannot tell, it reports the old default rather than guessing. A wrong `agent_runtime` sends GSD looking for agents in the wrong directory, so silence is the safer failure.
### "It says `codex` and I am not using Codex"
One cause, and it is benign:
> `CODEX_HOME` is exported in your shell profile, and `$CODEX_HOME/config.toml` exists.
GSD treats an explicitly-exported `CODEX_HOME` as you designating a Codex root. If you keep it exported globally but work in another runtime, pin the runtime for that project:
```json
{ "runtime": "claude" }
```
in `.planning/config.json`. Rung 2 outranks detection, so this settles it permanently.
Note what is **not** a cause: simply having Codex installed. GSD never probes the default `~/.codex/config.toml`. That file exists on every machine that has ever run Codex, so treating it as a signal would misreport every other runtime's sessions — which is precisely why the check requires you to have exported `CODEX_HOME` yourself.
---
## What this does not change
Detection moves the **reported** runtime and the agent-installation check that hangs off it. It deliberately does not touch:
- **Model resolution.** Runtime-aware tier resolution still reads the explicit `runtime` config key only. A detected-Codex session does not gain or lose model pins — see [Runtime-aware profiles](../CONFIGURATION.md#runtime-aware-profiles-2517) and [ADR-2313](../adr/2313-codex-passive-model-posture.md).
- **Slash-command style.** GSD still emits `/gsd-<cmd>` unless the runtime was set explicitly; a detected-Codex session does not switch to the `$gsd-<cmd>` shell-var form. Set `runtime` explicitly if you want that.
- **Any file.** Detection reads environment variables and checks for one file's existence. It never writes `~/.gsd/defaults.json`, never edits `.planning/config.json`, and never shells out.