@sabaiway/agent-workflow-kit 14.20.0 → 14.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +38 -50
  5. package/bridges/antigravity-cli-bridge/bin/agy-model-posture.test.mjs +257 -0
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +10 -3
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -21
  8. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +4 -4
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +120 -48
  10. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +22 -62
  11. package/bridges/antigravity-cli-bridge/bin/agy.sh +109 -16
  12. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +5 -5
  13. package/bridges/antigravity-cli-bridge/capability.json +14 -7
  14. package/bridges/antigravity-cli-bridge/references/driving-agy.md +16 -20
  15. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +10 -27
  16. package/bridges/codex-cli-bridge/SKILL.md +51 -49
  17. package/bridges/codex-cli-bridge/bin/codex-exec.sh +145 -46
  18. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +28 -55
  19. package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +371 -0
  20. package/bridges/codex-cli-bridge/bin/codex-review.sh +134 -36
  21. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +33 -45
  22. package/bridges/codex-cli-bridge/capability.json +22 -8
  23. package/bridges/codex-cli-bridge/references/driving-codex.md +18 -15
  24. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
  25. package/bridges/codex-cli-bridge/setup/README.md +3 -3
  26. package/capability.json +1 -1
  27. package/package.json +1 -1
  28. package/references/modes/bridge-settings.md +10 -7
  29. package/references/modes/recipes.md +1 -1
  30. package/references/modes/review-state.md +1 -1
  31. package/tools/bridge-catalog.mjs +114 -0
  32. package/tools/bridge-posture.mjs +11 -2
  33. package/tools/bridge-settings-read.mjs +41 -5
  34. package/tools/bridge-settings.mjs +111 -23
  35. package/tools/detect-backends.mjs +6 -6
  36. package/tools/family-registry.mjs +3 -3
  37. package/tools/manifest/schema.md +1 -1
  38. package/tools/manifest/validate.mjs +3 -2
  39. package/tools/procedures.mjs +3 -3
  40. package/tools/recipes.mjs +2 -2
  41. package/tools/review-state.mjs +1 -1
@@ -18,26 +18,29 @@ judgment.
18
18
  4. Let codex run; then **review its diff yourself** and re-run the project's gates.
19
19
  5. **Commit yourself** — codex never commits (a git-write shim enforces it; see below).
20
20
 
21
- ## Quality-first: model & effort are pinned
22
-
23
- Delegated codex work ALWAYS runs on the PINNED model at the pinned effort: `gpt-6-astra` / `high` are **pinned**
24
- and a non-default `CODEX_MODEL`/`CODEX_EFFORT` is **refused** (exit 2). That the pinned id is still the
25
- strongest selectable model is a hand-checked claim with no automated gate and no recorded date — see
26
- SKILL.md. Do not try to "tune down" the
27
- model or effort for a real run — the wrapper will stop you. Quota is metered in **messages** (rolling
28
- 5h + weekly), so economy comes from removing waste (clean capture, the precomputed review diff, resume),
29
- never from a downgrade. The only opt-out is a throwaway, effort-independent probe: `CODEX_PROBE=1`
30
- (loud) — never use its output as real work.
21
+ ## Model & effort: the host posture
22
+
23
+ Delegated codex work runs on the host posture: `CODEX_MODEL` / `CODEX_EFFORT` from the bridge settings
24
+ file, else the built-in default `gpt-6.1-sol` / `high`, checked against the installed CLI's catalog
25
+ before any spend. `/agent-workflow-kit bridge-settings` shows the models the installed codex offers and
26
+ sets the host posture. An environment value off the host posture is a one-off and is **refused** (exit 2)
27
+ with the settings-file remedy unless `CODEX_PROBE=1` — do not "tune down" the model or effort per call;
28
+ a weaker model is the user's choice in the setting. Whether the default is the strongest selectable
29
+ model is a hand-checked claim with no automated gate and no recorded date — see SKILL.md. Quota is
30
+ metered in **messages** (rolling 5h + weekly), so economy comes from removing waste (clean capture, the
31
+ precomputed review diff, resume), never from a per-call downgrade. A probe (`CODEX_PROBE=1`, loud) is
32
+ the one route to a one-off — never use its output as real work; its review receipt never attests.
31
33
 
32
34
  ## Posture banners — quote them verbatim
33
35
 
34
36
  Every dispatch states its ACTUAL posture on ONE stderr line: `codex-exec` emits
35
- `exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=…`
37
+ `exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> source=… timeout=…`
36
38
  (fresh and resume alike, only after the resume id is resolved and validated), `codex-review` emits
37
- `review posture: model=… effort=… tier=… timeout=…`. When you label a dispatch for a user or a
39
+ `review posture: model=… effort=… tier=… source=… timeout=…`. When you label a dispatch for a user or a
38
40
  record, **quote the posture banner verbatim** — the banner is the machine-stated posture; a prose
39
- re-type drifts. The `timeout=` field is **banner-only** (exactly the duration handed to
40
- `timeout(1)`; on exec `uncapped` when no capping binary is on PATH, while `codex-review` fails
41
+ re-type drifts. The `source=` field (`model:<s>,effort:<s>`, each `default`, `setting` or
42
+ `environment`) and the `timeout=` field are **banner-only** (`timeout=` is exactly the duration handed
43
+ to `timeout(1)`; on exec `uncapped` when no capping binary is on PATH, while `codex-review` fails
41
44
  CLOSED pre-spend there) — informational, never part of a receipt or the banner↔receipt parity.
42
45
 
43
46
  ## Exec vs review
@@ -92,7 +95,7 @@ codex-exec --resume <session-id> <file|-> # explicit session id
92
95
 
93
96
  Resume continues the SAME codex session, so you avoid re-sending the original context. It runs through
94
97
  the wrapper, which **re-establishes every invariant** (subscription-only `*_API_KEY` scrub,
95
- `--ignore-user-config`, the pinned `gpt-6-astra`/`high`) and **restates the full posture via `-c`** —
98
+ `--ignore-user-config`, the host posture's model/effort) and **restates the full posture via `-c`** —
96
99
  because `codex exec resume` resets the sandbox/approval/network posture and rejects `-s`/`--add-dir`/`-C`,
97
100
  the wrapper passes `-c sandbox_mode=workspace-write -c approval_policy=never -c
98
101
  sandbox_workspace_write.network_access=false` explicitly. A resume takes no passthrough flags and an
@@ -47,7 +47,7 @@ carries the session id) into the run trace, with stderr merged into it; `--color
47
47
  `--color`, which stays on the fresh lane), so the resume lane is no longer the odd one out: it used
48
48
  to print its final message straight to stdout with the event stream nowhere, which left the very
49
49
  mode a nested-sandbox incident fired on without a structured evidence surface. Net effect: the wrapper prints just the final answer. **Reasoning still runs at
50
- the pinned effort** — quality is unchanged; only the *noise* is dropped. On success `codex-exec` extracts the
50
+ the run's effort** — quality is unchanged; only the *noise* is dropped. On success `codex-exec` extracts the
51
51
  session id from the trace and records it to `${CODEX_SESSION_FILE:-./.codex-last-session}` (so
52
52
  `--resume-last` can find it) and echoes `session: <id>` to stderr. On a missing/empty final-message
53
53
  file it falls back to the trace tail (loud, never silent).
@@ -61,13 +61,16 @@ well-formed item is simply not evidence, never a parse error. See the dual polic
61
61
  its loose whole-trace rule, and the successful-run arm demands both tokens inside ONE item whose
62
62
  failure is proven.
63
63
 
64
- ## Quality-first guard (pinned model & effort)
64
+ ## Host posture (model & effort)
65
65
 
66
- The wrappers default `CODEX_MODEL=gpt-6-astra` and `CODEX_EFFORT=high` and **refuse** (exit 2, loud) any
67
- non-default — delegated work always uses the PINNED model at the pinned effort; quality is never traded for
68
- quota. (Whether the pinned id is still the strongest selectable model is a hand-checked claim with no
69
- automated gate — see SKILL.md.) `CODEX_PROBE=1` relaxes this for a throwaway, effort-independent probe only (echoed loudly), and
70
- a probe still runs on the subscription, in the sandbox, with clean capture.
66
+ The wrappers run on the host posture: `CODEX_MODEL` / `CODEX_EFFORT` from the bridge settings file, else
67
+ the built-in default `gpt-6.1-sol` / `high`, checked pre-spend against the installed CLI's catalog
68
+ (`codex debug models`); `/agent-workflow-kit bridge-settings` shows the models offered and sets one. A model or effort the catalog does not offer, and an environment value off the
69
+ host posture (a one-off), are **refused** (exit 2, loud) with the settings-file remedy; a weaker model
70
+ is the user's choice in the setting, never a per-call downgrade. (Whether the default is the strongest
71
+ selectable model is a hand-checked claim with no automated gate — see SKILL.md.) `CODEX_PROBE=1` is the
72
+ one route to a one-off, for a throwaway probe only (echoed loudly; a nonced `codex-exec` probe one-off
73
+ is refused), and a probe still runs on the subscription, in the sandbox, with clean capture.
71
74
 
72
75
  ## Passthrough guard (two tiers, after a literal `--`)
73
76
 
@@ -75,7 +78,7 @@ a probe still runs on the subscription, in the sandbox, with clean capture.
75
78
 
76
79
  - **Tier 1 — ALWAYS rejected, even under `CODEX_PROBE=1`:** anything that would defeat the policy or the
77
80
  capture — `-c`/`--config`, `-s`/`--sandbox`, `--full-auto`, `--dangerously-bypass-*`, `--oss`,
78
- `--local-provider`, `-p`/`--profile`, `-m`/`--model` (the model is pinned via `CODEX_MODEL`), and the
81
+ `--local-provider`, `-p`/`--profile`, `-m`/`--model` (the model is the host posture or `CODEX_MODEL`), and the
79
82
  capture flags `-o`/`--output-last-message`/`--json`/`--color`/`--output-schema`/`--ephemeral`.
80
83
  - **Tier 2 — context/discovery knobs, rejected for a real run but allowed under `CODEX_PROBE=1`:**
81
84
  `--add-dir`, `-C`/`--cd`, `--skip-git-repo-check`, `--ignore-rules`, `--enable`/`--disable`.
@@ -155,7 +158,7 @@ run starts, so the failure is loud and costs no quota; the wrapper's own test pi
155
158
  (`RESUME_ACCEPTED_FLAGS`) precisely because the hermetic fake CLI accepts any argv and cannot answer
156
159
  this question. The `codex-exec --resume`/`--resume-last` entrypoint handles the reset: it restates
157
160
  the entire policy via `-c` (`sandbox_mode=workspace-write`, `approval_policy=never`,
158
- `sandbox_workspace_write.network_access=false`) plus the pinned `-m`/effort and
161
+ `sandbox_workspace_write.network_access=false`) plus the host posture's `-m`/effort and
159
162
  `--ignore-user-config`, reads the session id from the sidecar (or an argument), and applies the same
160
163
  EVIDENCE posture as a fresh run — `-o` for the final message, `--json` into the trace. Only a *raw*
161
164
  `codex exec resume` outside the wrapper loses the posture.
@@ -164,7 +167,7 @@ EVIDENCE posture as a fresh run — `-o` for the final message, `--json` into th
164
167
 
165
168
  A backgrounded/hung run survives otherwise, so both wrappers wrap codex in `timeout`/`gtimeout`
166
169
  (`--kill-after=15s`): `CODEX_HARD_TIMEOUT` defaults to **3600s (exec)** / **1800s (review)**, sized for a
167
- slow run at the pinned effort. Exit 124/137 ⇒ "exceeded the hard cap" (raise the cap or narrow the task). If neither
170
+ slow run at the host posture's effort. Exit 124/137 ⇒ "exceeded the hard cap" (raise the cap or narrow the task). If neither
168
171
  `timeout` nor `gtimeout` is on `PATH`, `codex-exec` **warns loudly and runs uncapped** — never a silent
169
172
  no-op — while `codex-review` **refuses pre-spend** (the fail-closed hard-timeout preflight).
170
173
 
@@ -205,8 +208,9 @@ exec` over the precomputed diff instead, keeping every invariant intact.
205
208
  | `-o, --output-last-message <f>` | write ONLY the final message (clean capture) |
206
209
  | `--json` | structured event stream (`thread.started` ⇒ session id) |
207
210
  | `--output-schema <f>` | constrain output to a JSON schema (`CODEX_REVIEW_SCHEMA=1`) |
208
- | `-m <model>` | model (wrapper default `gpt-6-astra`, pinned via `CODEX_MODEL`) |
209
- | `-c model_reasoning_effort=<effort>` | reasoning effort (wrapper default `high`, pinned via `CODEX_EFFORT`) |
211
+ | `-m <model>` | model (the host posture: the `CODEX_MODEL` setting, else `gpt-6.1-sol`) |
212
+ | `-c model_reasoning_effort=<effort>` | reasoning effort (the host posture: the `CODEX_EFFORT` setting, else `high`) |
213
+ | `codex debug models` | the model catalog as JSON `models[]` (`slug`, `visibility`, `priority`, `supported_reasoning_levels`, `default_reasoning_level`) — the wrappers' pre-spend check; reads no stdin (probed on 0.160.0) |
210
214
  | `--ignore-user-config` | do NOT load `$CODEX_HOME/config.toml`; auth still uses `CODEX_HOME` |
211
215
  | `--color never` | disable ANSI colour in output |
212
216
 
@@ -70,9 +70,9 @@ if it reports a missing git work tree or root `AGENTS.md`, run it from a project
70
70
 
71
71
  - The wrappers are **subscription-only** by design and will not use api-key billing.
72
72
  - `codex-exec` runs a **workspace-write** sandbox with **network OFF**; `codex-review` runs
73
- **read-only**. They also pin the model/effort (refusing ANY non-pinned model or effort, a stronger
74
- one included; whether the pinned id is still the strongest selectable model is a hand-checked claim
75
- with no gate — see `../SKILL.md`), enforce a hard
73
+ **read-only**. They also run on the host posture — `CODEX_MODEL` / `CODEX_EFFORT` from the
74
+ bridge settings file, else `gpt-6.1-sol` / `high` — checked against the installed CLI's catalog,
75
+ and refuse a one-off model or effort unless `CODEX_PROBE=1` (see `../SKILL.md`), enforce a hard
76
76
  timeout, capture only codex's final message, and block codex from writing git via a shim — see
77
77
  [`../references/sandbox-and-flags.md`](../references/sandbox-and-flags.md) and the knob table in
78
78
  [`../SKILL.md`](../SKILL.md#environment-knobs). No setup is needed to enable these — they are on by
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "14.20.0",
6
+ "version": "14.21.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "14.20.0",
3
+ "version": "14.21.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -4,11 +4,14 @@
4
4
 
5
5
  The reader + consent-gated **writer** for the **host-level** bridge settings file — the answer to *"turn on the codex Fast tier (or another bridge knob) once, predictably, so it survives kit upgrades."* The four bridge wrappers read `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf` (`KEY=VALUE` lines, **parsed never sourced**); this is the ONLY writer for it. The file lives **outside every kit-managed tree**, so a kit refresh never writes or clobbers it — upgrade-survival is structural (D2). It **previews by default**; `--apply` writes. Hand-editing the file stays fully supported — this is an offered convenience, never a lock.
6
6
 
7
- **The knobs are the bundled bridges' own `settings` blocks** (manifest-as-source, D6) — the tool never invents a key or a value rule, and what it writes always passes the wrappers' own validation. Model/effort are **NOT** settable here (the wrappers' quality-first guard is untouched, D4). Run **`node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs`** to see the live list; today:
7
+ **The knobs are the bundled bridges' own `settings` blocks** (manifest-as-source, D6) — the tool never invents a key or a value rule, and what it writes always passes the wrappers' own validation. **The model keys set each bridge's host posture** — the model every run of that bridge uses (codex: and its effort); a set one is checked against the models the installed CLI offers **before anything is written**. Run **`/agent-workflow-kit bridge-settings`** (`node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs`) to see the live list and the models on offer; today:
8
8
 
9
9
  | key | bridge | values | note |
10
10
  |---|---|---|---|
11
- | `CODEX_SERVICE_TIER` | codex | `priority` | **SPEND KNOB** — the "Fast" tier: "2x speed, **increased usage**" in the codex catalog's own words for gpt-6-astra (cache fetched 2026-09-05); it states no credit-rate figure; quality-neutral (same model). Default unset ⇒ standard tier. |
11
+ | `CODEX_MODEL` | codex | a model the installed codex offers | the host posture's model; default `gpt-6.1-sol`. |
12
+ | `CODEX_EFFORT` | codex | an effort that model offers | the host posture's reasoning effort; default `high`. |
13
+ | `AGY_MODEL` | agy | a display string the installed agy offers | the host posture's model; default `Gemini 3.8 Flash (High)`. |
14
+ | `CODEX_SERVICE_TIER` | codex | `priority` | **SPEND KNOB** — the "Fast" tier: "2x speed, **increased usage**" in the codex catalog's own words for gpt-6.1-sol (cache fetched 2026-10-03); it states no credit-rate figure; quality-neutral (same model). Default unset ⇒ standard tier. |
12
15
  | `CODEX_HARD_TIMEOUT` | codex | integer `1..86400` | hard wall-clock cap (seconds) via `timeout(1)`. |
13
16
  | `CODEX_REVIEW_MAX_TOTAL_BYTES` | codex | integer `1..100000000` | codex-review payload size above which the diff rides a temp file (never truncated). |
14
17
  | `AGY_HARD_TIMEOUT` | agy | duration `5m`/`30m`/`90s` (unit required, nonzero) | hard wall-clock cap via `timeout(1)`. |
@@ -17,16 +20,16 @@ The reader + consent-gated **writer** for the **host-level** bridge settings fil
17
20
 
18
21
  **Invocations:**
19
22
 
20
- 1. **Read** — `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs [--json]` prints every knob's **effective value + source** (`env` / `file` / `default`) and flags any unknown/duplicate/malformed lines. Read-only.
23
+ 1. **Read** — `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs [--json]` prints every knob's **effective value + source** (`env` / `file` / `default`; a model key's `environment` / `setting` / `default`, as the wrappers' banner names it), each bridge's host posture with the models its installed CLI offers (read with `codex debug models` / `agy models`, never a model run; an unreadable or missing CLI is one stated line, exit 0), and flags any unknown/duplicate/malformed lines. Read-only.
21
24
  2. **Preview a change** — `--set KEY=VALUE` (or `--unset KEY`) prints `before → after` and writes **nothing**. Re-run with **`--apply`** to write. Multiple `--set`/`--unset` ops apply in one atomic write.
22
25
  3. **`--apply`** writes via the hardened out-of-tree atomic writer (creates the dir + file on first use; symlink/parent/TOCTOU-safe; last-writer-wins). It touches **only** the `KEY=` line it owns — every comment / blank / other line is preserved verbatim.
23
26
 
24
- **Precedence at run time:** explicit env (even empty — `KEY=` disables the knob for one run) **>** this file **>** the wrapper's built-in default. So an operator can always override one run without editing the file; the reader shows which source wins.
27
+ **Precedence at run time:** explicit env (even empty — `KEY=` disables the knob for one run) **>** this file **>** the wrapper's built-in default. So an operator can always override one run without editing the file; the reader shows which source wins. A model key is the exception: an environment value off the host posture is a one-off, which `codex-exec`, `codex-review` and `agy-review` refuse unless it is a probe (the bridge-model contract).
25
28
 
26
- **Refusals (the guarded contract):** an **unknown key** or an **invalid / out-of-range value** → exit `2` (nothing read of the file, nothing written). A file that already carries **duplicate keys** → exit `1`, named and left **byte-untouched** (fix the duplicates by hand first — the writer never edits blindly around them). A **symlinked / non-regular / unreadable** settings file → exit `1` (refuses to write through it). `--apply` with `--dry-run`, a duplicate op for one key, or a malformed `--set` → usage exit `2`.
29
+ **Refusals (the guarded contract):** an **unknown key** or an **invalid / out-of-range value** (a model key: empty, padded with whitespace, or carrying a control byte) → exit `2` (nothing read of the file, nothing written). A **model the catalog does not offer**, or a codex **effort that model does not offer**, → exit `2` with the offered list, nothing written; an unreadable catalog leaves the value unchecked and says so. A file that already carries **duplicate keys** → exit `1`, named and left **byte-untouched** (fix the duplicates by hand first — the writer never edits blindly around them). A **symlinked / non-regular / unreadable** settings file → exit `1` (refuses to write through it). `--apply` with `--dry-run`, a duplicate op for one key, or a malformed `--set` → usage exit `2`.
27
30
 
28
- **Spend consent (D4):** enabling `CODEX_SERVICE_TIER=priority` costs **increased usage** (the catalog states no credit-rate figure) — a per-host, consented act, never a default. The preview shows the caveat (from the manifest `effect`); confirm with the user before `--apply`. The tool also warns when an env var currently shadows the key you are writing (the env value wins for that session until unset).
31
+ **Spend consent (D4):** enabling `CODEX_SERVICE_TIER=priority` costs **increased usage** (the catalog states no credit-rate figure) — a per-host, consented act, never a default. The preview shows the caveat (from the manifest `effect`); confirm with the user before `--apply`. The tool also warns when an env var currently shadows the key you are writing (the env value wins for that session until unset; for a model key, an environment value off the host posture is a one-off, which `codex-exec`, `codex-review` and `agy-review` refuse unless it is a probe).
29
32
 
30
33
  Output is **English/structured** — **localize it to the user's conversational language** when you narrate.
31
34
 
32
- **Invariants:** writer (writes only the host settings file — outside any project/kit tree) · never commits · never runs a subscription CLI · previews by default · allowlist + value rules from the bundled manifests · model/effort never settable · the host file survives every kit refresh.
35
+ **Invariants:** writer (writes only the host settings file — outside any project/kit tree) · never commits · never runs a model (only the two read-only catalog commands) · previews by default · allowlist + value rules from the bundled manifests · a model key is judged against its bridge's catalog before any write · the host file survives every kit refresh.
@@ -12,7 +12,7 @@ The five recipes (defined over each bridge's `provides` roles — `codex`: execu
12
12
  - **Delegated** — you hand a **bounded** execution sub-task to a backend (`codex exec`), then review the returned diff and commit.
13
13
  - **Subagent** — a **full-tool frontier subagent**, dispatched from the placed executor vehicle (`.claude/agents/executor.md`), carries a **bounded, file-disjoint** slice — of execution, of plan/contract authoring, of an authoring fold, of routine work, of an epic brief (`epic.author`), of a task brief from one ledger row (`task.author`), or of one stamped task brief run inside its Files (`task.execute`); you verify the returned slice by running its suites yourself and you make the commit. It is **never a review backend**, **never a bridge substitute**, and is **never told to commit**; it **degrades to Solo** when the vehicle is `missing` or `unusable`, with the reason. Readiness here is the **vehicle FILE, never the host** — a **Claude Code lane**, like the `agents` writer itself: on a host that cannot dispatch the vehicle, the rendered dispatch form is an instruction you follow by hand and report as such, never as a subagent dispatch.
14
14
 
15
- 1. Run **`node ${CLAUDE_SKILL_DIR}/tools/recipes.mjs`** (the read-only planner; `--json` for the structured form). It runs the backend detector, lists the five recipes, and prints — for the current environment — a **per-recipe dispatch plan that degrades with a stated reason** when a carrier isn't `ready` (Council → Reviewed → Solo; Delegated → Solo; Subagent → Solo when the executor vehicle is `missing`/`unusable`), plus advisory **quota/health notes** (prefer the cheapest model; Council spends two backends' quota; `agy` may stall on substantive prompts — Issue-001, prefer `codex`).
15
+ 1. Run **`node ${CLAUDE_SKILL_DIR}/tools/recipes.mjs`** (the read-only planner; `--json` for the structured form). It runs the backend detector, lists the five recipes, and prints — for the current environment — a **per-recipe dispatch plan that degrades with a stated reason** when a carrier isn't `ready` (Council → Reviewed → Solo; Delegated → Solo; Subagent → Solo when the executor vehicle is `missing`/`unusable`), plus advisory **quota/health notes** (each bridge runs the host posture the user set — a weaker model is the user's bridge setting, never a per-call choice; Council spends two backends' quota; `agy` may stall on substantive prompts — Issue-001, prefer `codex`).
16
16
  2. **Offer the choice** via **`AskUserQuestion` where your agent supports it** (`AskUserQuestion` in Claude Code) — one option per recipe, the `recommendRecipe` choice listed **first** — otherwise in prose. Then print `planRecipe(chosen, detection)` (the per-stage dispatch + degradation reasons + quota/health notes) so the user sees exactly what running it entails.
17
17
  3. **Availability = `readiness === ready`, full stop.** Every other readiness supplies the human reason (needs-skill → "not installed — `/agent-workflow-kit setup`"; needs-cli → "install the CLI"; needs-credentials → "log in"; degraded → "wrapper not on PATH — `/agent-workflow-kit setup`"). This is set-up state only — **never** a claim that a backend's service is responsive (the detector cannot observe a runtime stall; `agy`'s Issue-001 is a *standing advisory*, not a readiness signal).
18
18
 
@@ -7,7 +7,7 @@ Read-only **review-receipt checker** (AD-038) — it makes *"reviewed ≠ shippe
7
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs [--check] [--json]`:
8
8
 
9
9
  1. Plain run → the human report: resolved recipe + source, plan-in-flight, tree fingerprint, the `held session: <id> — <n> fold(s) rode it` / `none` / substitution line when configured delegated execution arms it (with a task `code` thread of backend `codex` in the commit epoch and no unresolved substitution, `held sessions: <k> chain(s) — <label>: <id> (<n>) · …` is printed instead, one entry per chain, labelled by its brief or `untasked`, with `none` for a chain with no held session), and per-backend receipt state (current / stale / ungrounded / probe / rejected / missing) with verdict + grounding + timestamp.
10
- 2. **`--check`** → the gate exit code. Before the review-recipe early return, a configured `plan-execution.execute` or `task.execute` of `delegated` (each read on its own) with a plan in flight and a dirty, fingerprintable tree audits the delegation ledger and refuses a substituted held session; an absent ledger is inert, while an unreadable, malformed, foreign, audit-refused ledger or failed HEAD read fails closed with its own reason. For a head-base thread, the accepted-replacement escape is a `codex-exec` degrade at the substituted return's tree, standing after every later fold, never the review receipt id `codex`. A checkpoint-based thread's substitution closes only by its own ledger `degrade` (which also withdraws the expectation, so the next fold establishes HELD) — no core-evidence degrade lifts it, and the report's recovery hint says so. The remaining **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a CONFIGURED solo recipe (after the held-session arm above) (a computed readiness-degrade NEVER silently becomes solo — it needs the explicit degrade record below), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`/`TASK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a cwd where git ITSELF answers not-a-repository under both discoveries (the ONLY non-work-tree location that passes — `error`, `redirected`, `no-work-tree` and `env-only` refuse by their state name once a receipt could be required), or every recipe-named backend SATISFIED for the current tree. **A clean-tree PASS under a NON-SOLO review obligation is never silent about a latent arm:** when a `reviewed` or `council` recipe is configured, it NAMES every plan in flight and states that this gate arms as soon as the tree is dirty — the condition is discoverable BEFORE it blocks, instead of surfacing at the worst moment (a pending commit, or the landing of a feature worktree when main first turns dirty). Nothing is announced under a configured `solo` recipe or a not-a-repository cwd, where the gate can never arm. **Satisfaction is ship-class-only on the LATEST NORMAL receipt (D3(b)):** per backend, the latest probe-free current-fingerprint receipt is selected FIRST and THEN verdict-checked — only the recognized ship-class vocabulary (`ship` / `ship with nits`) satisfies; a recognized NEGATIVE (`revise` / `rethink` / `rework`) is an authoritative VETO (an earlier ship never survives a later revise); an UNRECOGNIZED verdict (e.g. `unknown` from a dead run) fails CLOSED unconditionally — a later `unknown` never lets an earlier SHIP stand, and a fresh normal re-run supersedes it. **The ONLY review-backend escape is an explicit degrade RECORD** (`core-evidence.mjs degrade --backend <name> --reason "…"`, fingerprint-bound to the current tree) — and never all backends: ≥1 non-degraded ship-class receipt is required whenever ≥1 backend is configured; a malformed evidence store denies the escape fail-closed but never fails an independently-satisfied tree. Exit 1 otherwise — missing, **stale** (every edit the payload can see moves the fingerprint, and no more), ungrounded, vetoed, or unrecognized. Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green. **Probe receipts never attest either:** a `CODEX_PROBE=1` / `AGY_PROBE=1` review runs with the frontier-model/max-effort guard OFF, so the wrapper stamps `probe:true` and this checker excludes it — per receipt, so a real review at the same fingerprint still satisfies. **Silence is not a declaration:** a malformed *or* absent probe marker is rejected fail-closed and stated in the check line. Honest bound: receipts are **not authenticated** (a forger could write `probe:false` as easily as any other field) — like the rest of the receipt this is a self-discipline mechanism, not a security boundary.
10
+ 2. **`--check`** → the gate exit code. Before the review-recipe early return, a configured `plan-execution.execute` or `task.execute` of `delegated` (each read on its own) with a plan in flight and a dirty, fingerprintable tree audits the delegation ledger and refuses a substituted held session; an absent ledger is inert, while an unreadable, malformed, foreign, audit-refused ledger or failed HEAD read fails closed with its own reason. For a head-base thread, the accepted-replacement escape is a `codex-exec` degrade at the substituted return's tree, standing after every later fold, never the review receipt id `codex`. A checkpoint-based thread's substitution closes only by its own ledger `degrade` (which also withdraws the expectation, so the next fold establishes HELD) — no core-evidence degrade lifts it, and the report's recovery hint says so. The remaining **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a CONFIGURED solo recipe (after the held-session arm above) (a computed readiness-degrade NEVER silently becomes solo — it needs the explicit degrade record below), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`/`TASK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a cwd where git ITSELF answers not-a-repository under both discoveries (the ONLY non-work-tree location that passes — `error`, `redirected`, `no-work-tree` and `env-only` refuse by their state name once a receipt could be required), or every recipe-named backend SATISFIED for the current tree. **A clean-tree PASS under a NON-SOLO review obligation is never silent about a latent arm:** when a `reviewed` or `council` recipe is configured, it NAMES every plan in flight and states that this gate arms as soon as the tree is dirty — the condition is discoverable BEFORE it blocks, instead of surfacing at the worst moment (a pending commit, or the landing of a feature worktree when main first turns dirty). Nothing is announced under a configured `solo` recipe or a not-a-repository cwd, where the gate can never arm. **Satisfaction is ship-class-only on the LATEST NORMAL receipt (D3(b)):** per backend, the latest probe-free current-fingerprint receipt is selected FIRST and THEN verdict-checked — only the recognized ship-class vocabulary (`ship` / `ship with nits`) satisfies; a recognized NEGATIVE (`revise` / `rethink` / `rework`) is an authoritative VETO (an earlier ship never survives a later revise); an UNRECOGNIZED verdict (e.g. `unknown` from a dead run) fails CLOSED unconditionally — a later `unknown` never lets an earlier SHIP stand, and a fresh normal re-run supersedes it. **The ONLY review-backend escape is an explicit degrade RECORD** (`core-evidence.mjs degrade --backend <name> --reason "…"`, fingerprint-bound to the current tree) — and never all backends: ≥1 non-degraded ship-class receipt is required whenever ≥1 backend is configured; a malformed evidence store denies the escape fail-closed but never fails an independently-satisfied tree. Exit 1 otherwise — missing, **stale** (every edit the payload can see moves the fingerprint, and no more), ungrounded, vetoed, or unrecognized. Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green. **Probe receipts never attest either:** a `CODEX_PROBE=1` / `AGY_PROBE=1` review is the one route to a one-off model or effort, so the wrapper stamps `probe:true` and this checker excludes it — per receipt, so a real review at the same fingerprint still satisfies. **Silence is not a declaration:** a malformed *or* absent probe marker is rejected fail-closed and stated in the check line. Honest bound: receipts are **not authenticated** (a forger could write `probe:false` as easily as any other field) — like the rest of the receipt this is a self-discipline mechanism, not a security boundary.
11
11
  3. **Wire it as a gate by hand OR via the explicit-consent init preview — never without consent (AD-021/D9).** The candidate line for your own `docs/ai/gates.json`: `{ "id": "review-state", "title": "Review receipts current for the uncommitted tree", "cmd": "node <path-to-this-skill>/tools/review-state.mjs --check" }` — with the path your project actually reaches the kit by, QUOTED so a path with spaces survives, executable from the project root. The consent-gated gates-init preview — `node ${CLAUDE_SKILL_DIR}/tools/gates-init.mjs --cwd <project>` (dry-run: it prints the derived entries and writes NOTHING; `--apply --only review-state` writes exactly this one on your explicit yes) — offers exactly this entry, path resolved and quoted, ONLY when your `docs/ai/orchestration.json` declares `reviewed`/`council` on `plan-execution.review` (the slot this checker enforces). The full consent-fill contract is in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`. Once declared, the opt-in `${CLAUDE_SKILL_DIR}/references/modes/hook.md` auto-approves it like any other declared gate — and `run-gates --final` requires this check among its canonical core gates.
12
12
  4. **`--await [--timeout <s>]`** (BUGFREE-3 / AD-049) → BLOCK until every recipe-named backend is SATISFIED for the current tree (i.e. until `--check` would PASS), or the bounded timeout elapses (a loud exit 1; default 900s). Run it after dispatching the review bridges to WAIT for their receipts to land instead of hand-polling a pid: the durable completion signal is the **receipt**, never a process event (a harness "completed" notification fires early; a bridge's output late-flushes). It inherits the `--check` decision whole — a backend with a current-tree degrade RECORD stops being waited on (the shared decideCheck), so you never hand-`--await` around a known degrade. A held-session refusal and an **AUTHORITATIVE veto terminate the wait loudly BEFORE the deadline**: neither can be moved by waiting; only the held-session retry/degrade lane or a fresh review can move its corresponding refusal. Still read-only (it re-reads the receipts + the evidence store); solo / no-plan / clean-tree resolve instantly. For waiting on **one dispatch's arrival** (never obligation satisfaction) use the receipt-arrival deadline runner, `${CLAUDE_SKILL_DIR}/references/modes/receipt-deadline.md`.
13
13
 
@@ -0,0 +1,114 @@
1
+ // bridge-catalog.mjs — reads the models each installed bridge CLI offers, exactly as the bridge
2
+ // wrappers read them before a run (spec bridge-model: Catalog check; spec bridge-settings): codex
3
+ // `codex debug models -c model_provider=openai`, the `visibility: list` slugs each with its efforts;
4
+ // agy `agy models`, the display string after the TAB of each line carrying one. Each read runs on the
5
+ // caller's PATH with every *_API_KEY (and, for codex, OPENAI_BASE_URL) removed, so the list is the
6
+ // subscription catalog the runs use. A read is bounded by the wrapper's own constant, then killed with
7
+ // its whole process group after the wrapper's kill grace. Read-only: it runs no model, writes nothing.
8
+ //
9
+ // Dependency-free, Node >= 22. No side effects on import.
10
+
11
+ import { spawn } from 'node:child_process';
12
+ import { accessSync, statSync, constants } from 'node:fs';
13
+ import { delimiter, isAbsolute, join } from 'node:path';
14
+
15
+ // The wrappers' constants: codex-exec.sh CODEX_CATALOG_TIMEOUT_S=15, agy.sh AGY_CATALOG_TIMEOUT=30s,
16
+ // both under `timeout --kill-after=5s`.
17
+ export const CATALOG_BOUNDS_MS = Object.freeze({ 'codex-cli-bridge': 15000, 'antigravity-cli-bridge': 30000 });
18
+ export const CATALOG_KILL_GRACE_MS = 5000;
19
+
20
+ const CONTROL = /[\u0000-\u001f\u007f]/u;
21
+ const unreadable = (cause) => ({ state: 'unreadable', cause, entries: [] });
22
+
23
+ export const parseCodexCatalog = (text) => {
24
+ let doc;
25
+ try { doc = JSON.parse(text); } catch { return unreadable('its output is not JSON'); }
26
+ if (doc === null || typeof doc !== 'object' || !Array.isArray(doc.models)) return unreadable('it carries no models[]');
27
+ const offered = doc.models.filter((m) => m !== null && typeof m === 'object' && m.visibility === 'list');
28
+ if (offered.some((m) => typeof m.slug !== 'string' || m.slug === '' || CONTROL.test(m.slug))) return unreadable('an offered entry carries no usable slug');
29
+ if (offered.length === 0) return unreadable('it offers no model');
30
+ const effortsOf = (m) => (Array.isArray(m.supported_reasoning_levels)
31
+ && m.supported_reasoning_levels.every((l) => l !== null && typeof l === 'object' && typeof l.effort === 'string' && l.effort !== '' && !CONTROL.test(l.effort))
32
+ ? m.supported_reasoning_levels.map((l) => l.effort) : null);
33
+ return { state: 'offered', cause: null, entries: offered.map((m) => ({ model: m.slug, efforts: effortsOf(m) })) };
34
+ };
35
+
36
+ export const parseAgyCatalog = (text) => {
37
+ const entries = text.split('\n')
38
+ .map((line) => line.replace(/\r$/u, ''))
39
+ .filter((line) => line.includes('\t'))
40
+ .map((line) => line.slice(line.indexOf('\t') + 1))
41
+ .filter((model) => model !== '' && !CONTROL.test(model))
42
+ .map((model) => ({ model }));
43
+ return entries.length ? { state: 'offered', cause: null, entries } : unreadable('it offers no model');
44
+ };
45
+
46
+ const CLIS = {
47
+ 'codex-cli-bridge': { cli: 'codex', args: ['debug', 'models', '-c', 'model_provider=openai'], parse: parseCodexCatalog, clears: ['OPENAI_BASE_URL'] },
48
+ 'antigravity-cli-bridge': { cli: 'agy', args: ['models'], parse: parseAgyCatalog, clears: [] },
49
+ };
50
+
51
+ // The environment a catalog read runs in: the caller's, minus every *_API_KEY and the bridge's own
52
+ // base-URL variable — the variables the wrapper's subscription guard clears before its own read.
53
+ export const catalogEnv = (name, getenv) => Object.fromEntries(Object.entries(getenv)
54
+ .filter(([key]) => !/_API_KEY$/u.test(key) && !(CLIS[name]?.clears ?? []).includes(key)));
55
+
56
+ const resolveOnPath = (cli, env) => {
57
+ for (const dir of String(env.PATH ?? '').split(delimiter)) {
58
+ if (!dir || !isAbsolute(dir)) continue;
59
+ const candidate = join(dir, cli);
60
+ try {
61
+ if (!statSync(candidate).isFile()) continue;
62
+ accessSync(candidate, constants.X_OK);
63
+ return candidate;
64
+ } catch { /* not this directory */ }
65
+ }
66
+ return null;
67
+ };
68
+
69
+ const killGroup = (child, signal) => {
70
+ try { process.kill(-child.pid, signal); } catch { try { child.kill(signal); } catch { /* already gone */ } }
71
+ };
72
+
73
+ const readOne = (name, ctx) => new Promise((done) => {
74
+ const spec = CLIS[name];
75
+ if (!spec) return done(unreadable(`no catalog reader for ${name}`));
76
+ const env = catalogEnv(name, ctx.getenv ?? process.env);
77
+ const bin = resolveOnPath(spec.cli, env);
78
+ if (bin === null) return done({ state: 'not-installed', cause: `${spec.cli} is not on PATH`, entries: [] });
79
+ const boundMs = ctx.catalogBoundsMs?.[name] ?? CATALOG_BOUNDS_MS[name];
80
+ const graceMs = ctx.catalogKillGraceMs ?? CATALOG_KILL_GRACE_MS;
81
+ const label = `${spec.cli} ${spec.args.slice(0, name === 'codex-cli-bridge' ? 2 : 1).join(' ')}`;
82
+ const chunks = [];
83
+ let timedOut = false;
84
+ let settled = false;
85
+ let bound;
86
+ let kill;
87
+ const finish = (result) => { if (!settled) { settled = true; clearTimeout(bound); clearTimeout(kill); done(result); } };
88
+ // spawn THROWS for an execve errno it does not emit (ETXTBSY: the binary is being replaced).
89
+ let child;
90
+ try {
91
+ child = spawn(bin, spec.args, { env, stdio: ['ignore', 'pipe', 'ignore'], detached: process.platform !== 'win32' });
92
+ } catch (err) {
93
+ return finish(unreadable(`${label} could not start (${err.code ?? err.message})`));
94
+ }
95
+ bound = setTimeout(() => {
96
+ timedOut = true;
97
+ killGroup(child, 'SIGTERM');
98
+ kill = setTimeout(() => killGroup(child, 'SIGKILL'), graceMs);
99
+ }, boundMs);
100
+ child.stdout?.on('data', (chunk) => chunks.push(chunk));
101
+ child.on('error', (err) => finish(unreadable(`${label} could not start (${err.code ?? err.message})`)));
102
+ child.on('close', (code, signal) => {
103
+ if (timedOut) return finish(unreadable(`it exceeded ${boundMs / 1000}s`));
104
+ if (code !== 0) return finish(unreadable(`${label} exited ${code ?? signal}`));
105
+ return finish(spec.parse(Buffer.concat(chunks).toString('utf8')));
106
+ });
107
+ });
108
+
109
+ // names: bridge names (the family-members `name`). Resolves to { [name]: { state, cause, entries } },
110
+ // state `offered` | `unreadable` | `not-installed`; the reads run concurrently and never reject.
111
+ export const readCatalogs = async (names, ctx = {}) => {
112
+ const results = await Promise.all(names.map((name) => readOne(name, ctx)));
113
+ return Object.fromEntries(names.map((name, i) => [name, results[i]]));
114
+ };
@@ -12,9 +12,18 @@ const validPosture = (posture) => posture !== null && typeof posture === 'object
12
12
  && (!Object.hasOwn(posture, 'tier') || posture.tier === null || (typeof posture.tier === 'string' && posture.tier.length > 0))
13
13
  && Object.keys(posture).every((key) => ['model', 'effort', 'tier'].includes(key));
14
14
 
15
+ // A posture key the settings file or the environment sets replaces the manifest default and names its
16
+ // source; an explicitly empty AGY_MODEL (value null) renders as no model.
17
+ const overlaid = (slot, fallback, backend, settings) => {
18
+ const row = (settings?.posture ?? []).find((r) => r.bridge === backend.name && r.source !== 'default'
19
+ && (r.key.endsWith('_EFFORT') ? 'effort' : 'model') === slot);
20
+ if (!row) return oneLine(fallback);
21
+ return `${row.value == null ? 'none' : oneLine(row.value)} (${row.source})`;
22
+ };
23
+
15
24
  const postureString = (posture, backend, settings) => {
16
- const parts = [`model=${oneLine(posture.model)}`];
17
- if (Object.hasOwn(posture, 'effort')) parts.push(`effort=${oneLine(posture.effort)}`);
25
+ const parts = [`model=${overlaid('model', posture.model, backend, settings)}`];
26
+ if (Object.hasOwn(posture, 'effort')) parts.push(`effort=${overlaid('effort', posture.effort, backend, settings)}`);
18
27
  if (Object.hasOwn(posture, 'tier')) {
19
28
  const knob = (settings?.active ?? []).find((row) => row.key === 'CODEX_SERVICE_TIER' && row.bridge === backend.name);
20
29
  parts.push(knob ? `tier=${oneLine(knob.value)} (bridge-settings)` : `tier=${posture.tier ?? 'standard'}`);
@@ -75,6 +75,7 @@ export const allowedLabel = (entry) => {
75
75
  case 'integer': return `integer ${entry.min}..${entry.max}`;
76
76
  case 'duration': return 'duration (e.g. 5m, 30m, 90s — a unit is required, nonzero)';
77
77
  case 'boolean': return '"0" | "1"';
78
+ case 'posture': return 'a value the installed CLI offers (checked when set)';
78
79
  default: return entry.kind;
79
80
  }
80
81
  };
@@ -172,8 +173,34 @@ const REFUSED = (key, v) => ({ value: null, source: 'default', configuredIn: 'en
172
173
  // built-in default (`source: 'default'`) while still being an explicit line the operator wrote — and
173
174
  // for a RETIRED knob that is exactly the line the retirement asks them to clear, so it must not
174
175
  // vanish from the surfaces (council fold: `AGY_REVIEW_ALLOW_ADDDIR=2` and `=` both disappeared).
176
+ // A posture key (the model and effort, spec bridge-model) follows the wrappers' aw_read_posture, not
177
+ // the applied-settings lane: an env key set at all wins, its empty value meaning the key's default
178
+ // (AGY_MODEL: no model, agy's own settings decide); else a non-empty, control-free file value; else the
179
+ // default. Its sources are the wrappers' banner words: default, setting, environment.
180
+ const EMPTY_ENV_DROPS_MODEL = new Set(['AGY_MODEL']);
181
+ const postureOf = (entry, parsed, getenv) => {
182
+ const key = entry.key;
183
+ if (Object.prototype.hasOwnProperty.call(getenv, key)) {
184
+ const v = getenv[key];
185
+ if (v === '' && EMPTY_ENV_DROPS_MODEL.has(key)) return { value: null, source: 'environment', configuredIn: 'env', note: `the env ${key}= drops the model — agy's own settings decide` };
186
+ if (v === '') return { value: entry.default, source: 'environment', configuredIn: 'env', note: `the env ${key}= selects the built-in default` };
187
+ if (hasControlByte(v)) return { value: null, source: 'environment', configuredIn: 'env', note: `env value "${safeShow(v)}" carries control bytes for ${key} — ${entry.bridge === 'antigravity-cli-bridge'
188
+ ? 'agy-review refuses the run pre-spend; agy-run has no control-byte screen and refuses it only through its catalog check (it runs it when the catalog is unreadable)'
189
+ : 'the codex wrappers refuse the run pre-spend'}` };
190
+ return { value: v, source: 'environment', configuredIn: 'env' };
191
+ }
192
+ const fileEntries = parsed.byKey.get(key);
193
+ if (fileEntries && fileEntries.length) {
194
+ const v = fileEntries[fileEntries.length - 1].value;
195
+ if (v !== '' && !hasControlByte(v)) return { value: v, source: 'setting', configuredIn: 'file' };
196
+ return { value: entry.default, source: 'default', configuredIn: 'file', note: `file value "${safeShow(v)}" is empty or carries a control byte — falls back to the built-in default` };
197
+ }
198
+ return { value: entry.default, source: 'default', configuredIn: null };
199
+ };
200
+
175
201
  export const effectiveOf = (entry, parsed, getenv) => {
176
202
  const key = entry.key;
203
+ if (entry.kind === 'posture') return postureOf(entry, parsed, getenv);
177
204
  if (Object.prototype.hasOwnProperty.call(getenv, key)) {
178
205
  const v = getenv[key];
179
206
  // An EXPLICITLY-EMPTY env (`KEY=`) suppresses the FILE override for this run (the wrapper skips a
@@ -222,21 +249,29 @@ export const displayValue = (v) => (v == null ? '(unset — wrapper built-in app
222
249
  // ── the fact-only snapshot the read-only status/advisor surfaces consume ────────────────
223
250
 
224
251
  // Best-effort, never throws: the ACTIVE knobs (a non-default value is in play — source env/file,
225
- // non-null) plus file presence + any unknown/duplicate keys. Model/effort are structurally excluded
226
- // from the registry, so `active` can NEVER carry a model claim (the fact-only guarantee). A corrupt
227
- // bundle or fs error degrades to `{ error }` — the caller renders that localized-on-error, never crashes.
252
+ // non-null), every posture key's effective value and source (`posture`), plus file presence + any
253
+ // unknown/duplicate keys. A posture key's sources are default/setting/environment, so it never enters
254
+ // `active`. A corrupt bundle or fs error degrades to `{ error }` — the caller renders that
255
+ // localized-on-error, never crashes.
256
+ const postureRows = (registry, parsed, getenv) => [...registry.values()]
257
+ .filter((entry) => entry.kind === 'posture')
258
+ .map((entry) => {
259
+ const eff = effectiveOf(entry, parsed, getenv);
260
+ return { key: entry.key, bridge: entry.bridge, value: eff.value, source: eff.source, retired: entry.retired ?? null };
261
+ });
262
+
228
263
  export const settingsSnapshot = (ctx = {}) => {
229
264
  try {
230
265
  const registry = loadRegistry(ctx);
231
266
  const path = settingsPath(ctx);
232
267
  const fileState = readFileState(path, ctx);
268
+ const getenv = ctx.getenv ?? process.env;
233
269
  // A symlink / non-regular / unreadable file is IGNORED by the wrappers (built-in defaults apply) —
234
270
  // surface that honestly instead of silently omitting it (the wrappers warn; the status must too).
235
271
  if (fileState.state === 'unusable') {
236
- return { path, fileState: 'unusable', active: [], unknown: [], duplicates: [], error: 'the settings file is a symlink / not a regular file — ignored, built-in defaults apply' };
272
+ return { path, fileState: 'unusable', active: [], posture: postureRows(registry, parseSettings(''), getenv), unknown: [], duplicates: [], error: 'the settings file is a symlink / not a regular file — ignored, built-in defaults apply' };
237
273
  }
238
274
  const parsed = parseSettings(fileState.text ?? '');
239
- const getenv = ctx.getenv ?? process.env;
240
275
  const active = [];
241
276
  for (const entry of registry.values()) {
242
277
  const eff = effectiveOf(entry, parsed, getenv);
@@ -262,6 +297,7 @@ export const settingsSnapshot = (ctx = {}) => {
262
297
  path,
263
298
  fileState: fileState.state,
264
299
  active,
300
+ posture: postureRows(registry, parsed, getenv),
265
301
  unknown: [...parsed.byKey.keys()].filter((k) => !registry.has(k)),
266
302
  duplicates: duplicateKeys(parsed),
267
303
  };