@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.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +38 -50
- package/bridges/antigravity-cli-bridge/bin/agy-model-posture.test.mjs +257 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +10 -3
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -21
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +4 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +120 -48
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +22 -62
- package/bridges/antigravity-cli-bridge/bin/agy.sh +109 -16
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +5 -5
- package/bridges/antigravity-cli-bridge/capability.json +14 -7
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +16 -20
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +10 -27
- package/bridges/codex-cli-bridge/SKILL.md +51 -49
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +145 -46
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +28 -55
- package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +371 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +134 -36
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +33 -45
- package/bridges/codex-cli-bridge/capability.json +22 -8
- package/bridges/codex-cli-bridge/references/driving-codex.md +18 -15
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
- package/bridges/codex-cli-bridge/setup/README.md +3 -3
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bridge-settings.md +10 -7
- package/references/modes/recipes.md +1 -1
- package/references/modes/review-state.md +1 -1
- package/tools/bridge-catalog.mjs +114 -0
- package/tools/bridge-posture.mjs +11 -2
- package/tools/bridge-settings-read.mjs +41 -5
- package/tools/bridge-settings.mjs +111 -23
- package/tools/detect-backends.mjs +6 -6
- package/tools/family-registry.mjs +3 -3
- package/tools/manifest/schema.md +1 -1
- package/tools/manifest/validate.mjs +3 -2
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +2 -2
- 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
|
-
##
|
|
22
|
-
|
|
23
|
-
Delegated codex work
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
(
|
|
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 `
|
|
40
|
-
`
|
|
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
|
|
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
|
|
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
|
-
##
|
|
64
|
+
## Host posture (model & effort)
|
|
65
65
|
|
|
66
|
-
The wrappers
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
209
|
-
| `-c model_reasoning_effort=<effort>` | reasoning 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
|
|
74
|
-
|
|
75
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "14.
|
|
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.
|
|
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
|
-
| `
|
|
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
|
|
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** (
|
|
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
|
|
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
|
+
};
|
package/tools/bridge-posture.mjs
CHANGED
|
@@ -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=${
|
|
17
|
-
if (Object.hasOwn(posture, 'effort')) parts.push(`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)
|
|
226
|
-
//
|
|
227
|
-
// bundle or fs error degrades to `{ error }` — the caller renders that
|
|
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
|
};
|