@sabaiway/agent-workflow-kit 14.19.1 → 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 (50) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +1 -1
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +45 -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 +57 -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/jev.md +3 -2
  30. package/references/modes/recipes.md +1 -1
  31. package/references/modes/recommendations.md +2 -1
  32. package/references/modes/review-state.md +1 -1
  33. package/references/vendor/typesafe-ai/LICENSE +21 -0
  34. package/references/vendor/typesafe-ai/SKILL.md.pinned +149 -0
  35. package/tools/ack-store.mjs +2 -0
  36. package/tools/bridge-catalog.mjs +114 -0
  37. package/tools/bridge-posture.mjs +11 -2
  38. package/tools/bridge-settings-read.mjs +41 -5
  39. package/tools/bridge-settings.mjs +111 -23
  40. package/tools/detect-backends.mjs +6 -6
  41. package/tools/family-registry.mjs +3 -3
  42. package/tools/jev-facts.mjs +101 -4
  43. package/tools/jev-guide.mjs +31 -12
  44. package/tools/jev-skill.mjs +231 -0
  45. package/tools/manifest/schema.md +1 -1
  46. package/tools/manifest/validate.mjs +3 -2
  47. package/tools/procedures.mjs +3 -3
  48. package/tools/recipes.mjs +2 -2
  49. package/tools/recommendations.mjs +49 -4
  50. package/tools/review-state.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,77 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 14.21.0 — the bridge model is one host setting: your choice, else the default, checked against the installed CLI before any run (AD-169)
8
+
9
+ **Each bridge now runs the model you set, and `/agent-workflow-kit bridge-settings` shows and sets it.** The read-out
10
+ names, per bridge, the model (codex: and effort), where it comes from (`default`, `setting` or `environment`) and the
11
+ models the installed CLI offers, in text and `--json`. `--set CODEX_MODEL|CODEX_EFFORT|AGY_MODEL=…` is checked
12
+ against that list before anything is written, on the preview and `--apply` alike; the choice lives in the host file
13
+ `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf`, holds for every later run on that host and
14
+ survives a kit upgrade. A new vendor model needs no kit release.
15
+
16
+ > ### ⚠ BREAKING — inherited from codex-cli-bridge 4.0.0 and antigravity-cli-bridge 6.0.0
17
+ >
18
+ > `codex-exec`, `codex-review` and `agy-review` refuse before any spend (exit 2) a run off the host posture — an
19
+ > environment `CODEX_MODEL`, `CODEX_EFFORT` or `AGY_MODEL` that differs from your setting, else the default — unless
20
+ > it is a probe (`CODEX_PROBE=1`, `AGY_PROBE=1`), whose review receipt never attests; `codex-exec` also refuses a
21
+ > probe one-off that carries a dispatch nonce. The refusal names the settings lines that make the model your posture.
22
+ > All four wrappers, `agy-run` included, refuse (exit 2) a model the installed CLI's catalog does not offer, the
23
+ > default included; an unreadable catalog never refuses. A model or effort flag after `--` is refused by `codex-exec`
24
+ > and `agy-run`. To keep a model you used to pass through the environment, set it once with
25
+ > `bridge-settings --set … --apply`.
26
+
27
+ - **New codex default.** codex runs `gpt-6.1-sol` at effort `high`; agy keeps `Gemini 3.8 Flash (High)`.
28
+ - **Checked before any run.** Each wrapper checks the effective model (codex: and effort) against the installed CLI's
29
+ catalog (`codex debug models`, `agy models`) with every `*_API_KEY` cleared (codex: `OPENAI_BASE_URL` too); a
30
+ model the CLI does not offer is refused with the offered list and one remedy. An unreadable catalog is stated and
31
+ never refuses.
32
+ - **The banners say where the model came from.** Every receipt still names what was handed to the CLI (codex: model,
33
+ effort and tier; agy: model); the posture banners of `codex-exec`, `codex-review` and `agy-review` now name where
34
+ the model and, for codex, the effort came from.
35
+ - **The command runs no model.** It runs only the two catalog commands, concurrently and bounded as the wrappers are
36
+ (codex 15 s, agy 30 s), through the new leaf `tools/bridge-catalog.mjs`; a missing, failing or hung CLI is one line,
37
+ and a set then proceeds stating the value was not checked. The status line and the recipes renders show a set model
38
+ with its source and read no catalog.
39
+ - **The manifests declare the keys.** A new setting kind `posture` in `tools/manifest/validate.mjs` (a non-empty
40
+ string, no control byte, no edge whitespace); `--reconcile` reports the three keys current.
41
+ - **The text names the default only.** The bridges' skill files, references and wrapper headers name each default and
42
+ point at the command; the agy frontier list, its off-frontier warning, its probe advice and the retired 3.5 Flash
43
+ text are gone. The recipes quota note gives no per-call cheapest-model advice.
44
+ - Recorded debt (`docs/debt.md`, twelve rows): among them the posture kind admits line separators and the settings
45
+ reader misses a NUL, the agy catalog lookup ignores `~/.local/bin`, a catalog read whose detached child holds its
46
+ output never settles, and the posture render folds whitespace.
47
+
48
+ ## 14.20.0 — the kit installs the Jev vendor skill: pinned, one verified copy per agent skill root, by one command you run (AD-168)
49
+
50
+ **Once `TYPESAFE_API_KEY` is set, the Recommendations item `jev-skill` offers the vendor's Jev skill for every agent the
51
+ family drives.** Its apply is `HAND-APPLY: node <kit>/tools/jev-skill.mjs --apply --claude-dir <dir>`: after your yes
52
+ in the chat the agent hands the line over and you run it in a terminal of your own. It writes a verified copy of the
53
+ skill into the three agent skill roots: Codex `~/.agents/skills`, Claude Code `<claude dir>/skills`
54
+ (`CLAUDE_CONFIG_DIR` when absolute, else `~/.claude`; the line always names the dir the agent resolved) and
55
+ Antigravity CLI `~/.gemini/config/skills`. Then restart the agent from a new terminal.
56
+
57
+ - **Pinned, never updated silently.** The kit ships the vendor's `SKILL.md` and MIT `LICENSE` at v0.5.7 (commit
58
+ 65a39f39) under `references/vendor/typesafe-ai/`, the skill text as `SKILL.md.pinned` so no agent loads the kit's own
59
+ copy; the command checks both digests before any write and downloads nothing. A copy at an earlier pin is replaced
60
+ only by your own `--apply`.
61
+ - **Your files stay yours.** Each target is judged by content: `current`, `earlier`, `absent` or `foreign`. A foreign
62
+ copy, a link, an extra file or another digest is left untouched and named. The command writes through no link below
63
+ your home or below a Claude dir outside it, makes its temp and aside directories beside a root, never inside one,
64
+ re-checks each target before its rename, and lists a leftover from an interrupted run instead of removing it.
65
+ - **A dry run first.** Without `--apply` the command writes nothing and prints the plan, the leftovers and, when
66
+ something is to install or replace, the apply line; the agent runs only this form, and only when you ask to see the
67
+ plan.
68
+ - **One jev item at a time.** `jev-connect` while the key is not set, `jev-skill` after. A decline is recorded per
69
+ project as `jevSkillAck` in `docs/ai/acks.json` (`ack-write --lane jev-skill`).
70
+ - **The guide.** `/agent-workflow-kit jev` STEP 2 prints the kit route first; the vendor's own routes follow as
71
+ alternatives, then one mark per target.
72
+ - **Delegated runs.** The bundled codex and agy bridge docs say a delegated run reads the skill from its root, never
73
+ inherits the key, and leaves Jev requests to the orchestrator.
74
+ - Recorded debt (Issue-027 (39)-(41)): a cleanup removal that fails after a refused rename ends the run with an
75
+ exception; a home or Claude dir that is not normalized (`/x/claude/.`) is walked at a wrong path; on POSIX a
76
+ backslash in a Claude dir name is split as a separator.
77
+
7
78
  ## 14.19.1 — the ADR navigator stamps a cap it honours (AD-167)
8
79
 
9
80
  `references/scripts/archive-decisions.mjs` and its test: byte-identical to memory 8.1.3. `--write-navigator` stamps
package/README.md CHANGED
@@ -255,7 +255,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
255
255
  | `/agent-workflow-kit agents` | Claude Code · opt-in | **subagent vehicles** — places the five bundled subagent definitions into `.claude/agents/`: **four read-only vehicles + one full-tool `executor`**. **No read-only vehicle gets `Bash`**, and that is the load-bearing property: a read-only fan-out on a full-tool subagent shells out for facts it could have read, and every shelled command is an approval prompt you never needed to see. Three of the four ride a **cheap model** (haiku, low effort) for mechanical work — extraction sweeps, changelog fact-skeletons, gate-failure triage; the fourth, **`review-lens`**, is an ADDITIONAL independent read-only review opinion on code your configured backends have already seen (never a replacement for your review recipe, advisory like every review). The fifth, **`executor`** (all tools; model and effort from `docs/ai/vehicles.json`), is the ONE vehicle with a shell and the instrument behind the **Subagent** recipe: dispatched only for a bounded, file-disjoint execution / authoring / routine slice you verify afterwards — never for read-only work, never as a review backend, and it never commits. Its state (`placed` / `customized` / `unusable` / `missing`) is what a `subagent` slot's readiness resolves from. Writing code and running gates stay off the four read-only vehicles; the executor carries a bounded slice you re-verify on your main lane, and every vehicle's output is verified. Preview by default (`--apply` writes); an existing customized read-only vehicle or lens is **preserved, never overwritten**; the executor's body is re-derived from `docs/ai/vehicles.json` (a hand edit is replaced); never touches `settings*.json`, never commits. |
256
256
  | `/agent-workflow-kit hook` | Claude Code · opt-in | **gate-approval hook** — places a self-contained PreToolUse hook (`.claude/hooks/`) and wires it into `.claude/settings.json`: a Bash command **byte-identical** to a gate you declared in `docs/ai/gates.json` (run from the project root) is auto-approved — no prompt, no idle; a seeded read-only command carrying a runtime residual (output redirection, command substitution, `--output` writes) now **asks** even where an allow rule would have silently passed it (proven live — hook `ask` overrides an allow rule). Detection is a conservative string scan, so it over-asks on a byte that is ordinary text — searching for `=>`, or a read wearing `2>/dev/null`, prompts. That is a documented open limit: 4.1.0 built three mechanisms to narrow it and removed all three in review, each counterexample kept as a test (see Mode: hook). Exact matches only, never patterns; never `deny`; a broken `gates.json` only disables auto-approval, never the guard. An opt-in **read-lane** (`--read-lane` → `docs/ai/lanes.json`, a **separate** file from `gates.json`) additionally auto-approves *compounds* of seeded read-only commands carrying no shell metaprogramming; `--apply --read-lane` verifies the placed hook is current first (delete-to-reseed otherwise). Preview by default; never writes `settings.local.json`; never commits. |
257
257
  | `/agent-workflow-kit state-block-guard` | Claude Code · opt-in | **closing-block detector** — a `Stop` hook that reads the turn's final assistant message and warns when the closing state block is defective: the «what I need from you» slot answering *nothing* (a turn that ENDS always needs a resume, so the answer is false, not merely unhelpful), or a first-person promise of imminent work in a turn that is over. Judged on the slot's first clause, so a real ask followed by a clause break and "nothing else" passes, and a promise gated on something named passes; matching is word-bounded and Unicode-aware, quoted and fenced examples are stripped, and the last STARTED block decides — an incomplete tail counts as no block rather than falling back to an earlier one. A message carrying no block at all is silent unless you pass `--require-block` — this kit does not mandate the block, and a hook that runs every turn must not warn every turn. The judgement is lexical and the mode doc names every residual it leaves. **Detection, never prevention** — a `Stop` hook cannot un-send the message it judges; what it buys is that a silent recurrence becomes a visible one. Warns via `systemMessage` and **exits 0 on every path**: never `deny`, never blocks the stop, never re-enters the model. Reads nothing but the message, makes no network call, approves nothing. This mode is **read-only and has no writer** — it ships the self-contained hook and a paste-ready wiring block you merge into `.claude/settings.json` yourself. |
258
- | `/agent-workflow-kit bridge-settings` | opt-in, any time | **host-level bridge settings** — read or change the bridges' `KEY=VALUE` config file (`${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf`), the **one place a knob survives kit upgrades** (it lives outside every kit tree; a refresh never touches it, and now states loudly if it ever overwrote a local edit). First knobs: the **codex Fast tier** (`CODEX_SERVICE_TIER=priority` — "2x speed, **increased usage**" in the catalog's own words, quality-neutral, default off — a consented spend), the codex/agy hard timeouts, the codex review byte cap, and `AGY_REVIEW_MAX_TOTAL_BYTES` (the ceiling on what an oversized agy code review may feed before it refuses pre-spend). `AGY_REVIEW_ALLOW_ADDDIR` is a **retired** compatibility key: still recognized so an existing line never warns as unknown, but it arms nothing — the writer refuses to set it and `--unset` clears it. Allowed keys + value rules come from the bundled bridge manifests; **model/effort are never settable** (the quality guard is untouched). **Previews by default**; refuses an unknown key, an out-of-range value, or a duplicate-carrying file; writes atomically (symlink/TOCTOU-safe). Never commits, never runs a subscription CLI. |
258
+ | `/agent-workflow-kit bridge-settings` | opt-in, any time | **host-level bridge settings** — read or change the bridges' `KEY=VALUE` config file (`${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf`), the **one place a knob survives kit upgrades** (it lives outside every kit tree; a refresh never touches it, and now states loudly if it ever overwrote a local edit). The **model each bridge runs**: `CODEX_MODEL` + `CODEX_EFFORT` (default `gpt-6.1-sol` at `high`) and `AGY_MODEL` (default `Gemini 3.8 Flash (High)`) — the read-out shows the models each installed CLI offers, and a set one is checked against that list before anything is written. Other knobs: the **codex Fast tier** (`CODEX_SERVICE_TIER=priority` — "2x speed, **increased usage**" in the catalog's own words, quality-neutral, default off — a consented spend), the codex/agy hard timeouts, the codex review byte cap, and `AGY_REVIEW_MAX_TOTAL_BYTES` (the ceiling on what an oversized agy code review may feed before it refuses pre-spend). `AGY_REVIEW_ALLOW_ADDDIR` is a **retired** compatibility key: still recognized so an existing line never warns as unknown, but it arms nothing — the writer refuses to set it and `--unset` clears it. Allowed keys + value rules come from the bundled bridge manifests. **Previews by default**; refuses an unknown key, an out-of-range value, a model or effort the catalog does not offer, or a duplicate-carrying file; writes atomically (symlink/TOCTOU-safe). Never commits, never runs a model (only the read-only catalog commands). |
259
259
  | `/agent-workflow-kit autonomy-doctor` | opt-in, any time | **guarded sandbox provisioner "doctor"** — can this machine run the Claude sandbox, and (only with your consent) fix it: macOS Seatbelt built-in / Linux+WSL2 needs `bwrap`+`socat` / native Windows → WSL2. Flagless = **FS-only preview** (the diagnosis, the exact absolute-path command it WOULD run, the exact `--apply <pm>:<pkgs>` consent tuple — runs nothing, never claims ready); `--verify` runs the unprivileged proof (a `bwrap` user-namespace smoke + `socat -V`) — the **only** source of a Linux "ready (verified)" claim; `--apply <pm>:<pkgs>` is the consent-gated privileged install (the tuple must equal the previewed plan; everything executed resolves to absolute paths inside `/usr/bin:/bin:/usr/sbin:/sbin`; the exact command is re-printed immediately before execution), then verifies. Honest loud degrades (unknown PM, untrusted location, nested-sandbox INDETERMINATE, root-unproven) and a stated restart step. Sits **outside every velocity auto-approve tier**; never auto-runs, never writes repo files, never commits. |
260
260
 
261
261
  It **never auto-commits** and **never overwrites** an existing `AGENTS.md` without asking.
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '14.19.1'
6
+ version: '14.21.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -2,7 +2,7 @@
2
2
  name: antigravity-cli-bridge
3
3
  description: Delegate work to Google's Antigravity CLI (`agy`) — the successor to Gemini CLI — to reach Gemini, Claude, and GPT-OSS models under a Google AI Pro/Ultra subscription from the terminal. Use when the user wants to run a headless `agy` prompt, hand a focused task or second-opinion review to `agy`, install or authenticate Antigravity CLI, check or economise its quota/models, bridge project context into `agy`, set up a second delegated-execution backend beside Codex, or troubleshoot `agy` flags, models, auth, conversations, or its headless behaviour.
4
4
  metadata:
5
- version: '5.7.0'
5
+ version: '6.0.0'
6
6
  ---
7
7
 
8
8
  # antigravity-cli-bridge
@@ -47,34 +47,19 @@ this skill. The wrapper [`bin/agy.sh`](bin/agy.sh) **unsets every `*_API_KEY`**
47
47
  `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `GOOGLE_GENAI_API_KEY`) before invoking `agy`, so a stray key can
48
48
  never silently switch you to pay-as-you-go billing.
49
49
 
50
- **Caveat:** the subscription has a finite quota. Prefer the cheapest model that fits the task, and
51
- keep probes short (see *How the main agent drives agy*).
50
+ **Caveat:** the subscription has a finite quota. Keep probes short (see *How the main agent drives
51
+ agy*); the model every review and kept run uses is the host posture below, never a per-call pick.
52
52
 
53
53
  ## Models
54
54
 
55
- Pass the **exact display string** to `--model` (or set `AGY_MODEL`). The wrapper defaults to
56
- `Gemini 3.8 Flash (High)`. Run `agy models` for the live list — if it differs from this table, the
57
- live list wins.
58
-
59
- | Model string | Use it for |
60
- |---|---|
61
- | `Gemini 3.8 Flash (Low)` | cheapest; reachability checks, smoke tests, simple transforms |
62
- | `Gemini 3.8 Flash (Medium)` | cheap probes, context-reachability checks, quick summaries |
63
- | `Gemini 3.8 Flash (High)` | wrapper + review default; asserted frontier-grade (AD-136) |
64
- | `Gemini 3.7 Flash (Low)` | previous Flash generation, still served — prefer 3.8 |
65
- | `Gemini 3.7 Flash (Medium)` | previous Flash generation, still served — prefer 3.8 |
66
- | `Gemini 3.7 Flash (High)` | previous Flash generation, still frontier-grade for review (fork (a)) |
67
- | `Gemini 3.6 Flash (Low)` | older Flash generation, still served — prefer 3.8 |
68
- | `Gemini 3.6 Flash (Medium)` | older Flash generation, still served — prefer 3.8 |
69
- | `Gemini 3.6 Flash (High)` | older Flash generation, still served — prefer 3.8 |
70
- | `Gemini 3.5 Flash (Low)` | older Flash generation, still served — prefer 3.8 |
71
- | `Gemini 3.5 Flash (Medium)` | older Flash generation, still served — prefer 3.8 |
72
- | `Gemini 3.5 Flash (High)` | older Flash generation, still served — prefer 3.8 |
73
- | `Gemini 3.1 Pro (Low)` | cheaper Pro pass for medium reasoning |
74
- | `Gemini 3.1 Pro (High)` | hard reasoning, plan critique, architecture review (slower, deeper) |
75
- | `Claude Sonnet 4.6 (Thinking)` | a Claude second opinion through the same subscription |
76
- | `Claude Opus 4.6 (Thinking)` | strongest Claude reasoning available via `agy` |
77
- | `GPT-OSS 120B (Medium)` | an open-weights cross-check / diversity pass |
55
+ The model is **one host setting**: `AGY_MODEL=<exact display string>` in the bridge settings file
56
+ `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf` is the host posture, else the
57
+ built-in default `Gemini 3.8 Flash (High)`. An `AGY_MODEL` environment value off the host posture
58
+ (an explicitly empty one included) is a one-off, which `agy-run` (the probe role) runs and
59
+ `agy-review` refuses pre-spend unless `AGY_PROBE=1`; a `--model`/`--effort` flag after `--` is refused. Before any run the wrappers check the
60
+ model against `agy models` (the live list): a model it does not offer is refused with the offered list
61
+ and the settings-file remedy. **`/agent-workflow-kit bridge-settings`** shows the models the installed
62
+ `agy` offers, in its own order, and sets the host posture (checked against that list before it writes).
78
63
 
79
64
  ## Usage
80
65
 
@@ -92,10 +77,10 @@ Drive `agy` only through the wrapper [`bin/agy.sh`](bin/agy.sh) (installed on `P
92
77
  agy-run "your prompt" # prompt as an argument
93
78
  echo "your prompt" | agy-run - # prompt from stdin
94
79
  agy-run @path/to/prompt.md # prompt from a file
95
- AGY_MODEL="Claude Opus 4.6 (Thinking)" agy-run "..." # pick a model
80
+ AGY_MODEL="<display string>" agy-run "..." # a one-run model (a probe's one-off)
96
81
  AGY_TIMEOUT=10m agy-run "..." # agy's soft --print-timeout
97
82
  AGY_HARD_TIMEOUT=8m agy-run "..." # hard wall-clock cap via timeout(1)
98
- agy-run "..." -- --add-dir . # passthrough agy flags (never a permission widener)
83
+ agy-run "..." -- --add-dir . # passthrough agy flags (never a permission widener; --model*/--effort* refuse)
99
84
  ```
100
85
 
101
86
  `agy` is **headless-only** here (`-p`/`--print`). `agy-run` stays **text**: `--output-format json`
@@ -112,13 +97,14 @@ after `--`. Full detail: [`references/models-and-flags.md`](references/models-an
112
97
  `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf` holds `KEY=VALUE` lines,
113
98
  **parsed, never sourced** — a file line can never execute code. Precedence: explicit env (even
114
99
  empty — `KEY=` disables a knob for one run) > file > built-in default. File-settable keys for this
115
- bridge: `AGY_HARD_TIMEOUT` (duration string, e.g. `5m`/`30m`) and `AGY_REVIEW_MAX_TOTAL_BYTES`
100
+ bridge: `AGY_MODEL` (the host posture — see [§ Models](#models)), `AGY_HARD_TIMEOUT` (duration
101
+ string, e.g. `5m`/`30m`) and `AGY_REVIEW_MAX_TOTAL_BYTES`
116
102
  (integer bytes, default `240000` — the ceiling on the SUM of all outgoing prompt bytes an oversized
117
103
  `code` review may feed). `AGY_REVIEW_ALLOW_ADDDIR` is **RETIRED**: still recognized so an existing
118
104
  line never warns as unknown, but it arms nothing — the writer refuses a new `--set` and `--unset`
119
105
  clears it — exactly the manifest `settings` block (the single source; the wrapper
120
- constants and `--help` are drift-guarded against it). Model keys are **not** file-settable. The
121
- file lives **outside every kit-managed tree**, so a kit refresh/upgrade can never wipe it; edit it
106
+ constants and `--help` are drift-guarded against it, the model key read by the posture reader
107
+ instead). The file lives **outside every kit-managed tree**, so a kit refresh/upgrade can never wipe it; edit it
122
108
  by hand or via `/agent-workflow-kit bridge-settings` (preview-first, consent-gated).
123
109
 
124
110
  ## Review mode (`agy-review`)
@@ -168,12 +154,13 @@ billed turn restating the model's own prose (16,585 → 33,446 tokens on a match
168
154
  included — **exits 4 with NO receipt**: treat it as a *failed review to re-run*, never a fatal
169
155
  session error. The verdict vocabulary is closed (SHIP / SHIP WITH NITS / REWORK), and a
170
156
  ship-class verdict arriving beside a numbered `### Blocking` finding is a verdict-body
171
- contradiction: the same exit 4, NO receipt, both halves named. One stderr banner states the actual posture (`review posture: model=… timeout=…`)
172
- and the receipt records the same `posture {model}`; an attesting review with `AGY_MODEL` explicitly
173
- emptied refuses pre-spend (`AGY_PROBE=1` exempt), and control bytes in a model string refuse
174
- pre-spend in every mode. The `timeout=` field is **banner-only** (exactly the duration `agy-run`
175
- hands to `timeout(1)`; without a capping binary `agy-review` fails CLOSED pre-spend) —
176
- informational, never a receipt field. **Quote the posture banner verbatim** when labeling a
157
+ contradiction: the same exit 4, NO receipt, both halves named. One stderr banner states the actual posture (`review posture: model=… source=… timeout=…`)
158
+ and the receipt records the same `posture {model}`; a one-off model — an `AGY_MODEL` differing from
159
+ the host posture, an explicitly empty one included — refuses pre-spend unless `AGY_PROBE=1`, and
160
+ control bytes in a model string refuse pre-spend in every mode. The `source=` field (`model:<s>`,
161
+ `default`, `setting` or `environment`) and the `timeout=` field are **banner-only** (`timeout=` is
162
+ exactly the duration `agy-run` hands to `timeout(1)`; without a capping binary `agy-review` fails
163
+ CLOSED pre-spend) — informational, never a receipt field. **Quote the posture banner verbatim** when labeling a
177
164
  dispatch.
178
165
 
179
166
  Every successful review receipt carries integer `durationS` and `blocking`; the wrapper prints
@@ -192,8 +179,9 @@ release). `agy-review … --nonce <n>` is the plain-argument equivalent
192
179
  (one seam; flag and a non-empty env must agree, a disagreeing pair refuses pre-spend) — the lane
193
180
  for hosts whose dispatch policy has no env-prefix form.
194
181
 
195
- Frontier default `Gemini 3.8 Flash (High)`; **any** model is allowed (a sub-frontier one earns a
196
- silenceable `AGY_PROBE=1` advisory). An oversized `code` review is **DELIVERED, not refused**: the
182
+ The review runs on the host posture (the `AGY_MODEL` setting, else `Gemini 3.8 Flash (High)`),
183
+ checked once per run against `agy models`; `AGY_PROBE=1` is the one route to a one-off model, and a
184
+ probe review never attests. An oversized `code` review is **DELIVERED, not refused**: the
197
185
  change set is cut into under-cap parts, fed over continuation turns and reviewed in a final turn, and
198
186
  the answer must reproduce a line the wrapper picked from each part — a missing or wrong echo is a
199
187
  FAILED review (`exit 4`, no receipt). `plan`/`diff` still refuse over the cap (their artifact is a
@@ -225,24 +213,31 @@ being pointed at any file** (auto-discovery), but that is `agy`'s own mechanism,
225
213
  Claude-style description-dispatch engine; don't promise more than the probe shows in a given repo.
226
214
  **A review, though, must never *depend* on this** — `agy` does not read your repo code or a diff
227
215
  without an explicit `--add-dir`, so ground a review **self-contained** via `agy-review --facts` (above)
228
- rather than relying on `agy` to read the change set. Re-runnable from a project root (use a cheap model):
216
+ rather than relying on `agy` to read the change set. Re-runnable from a project root (an `agy-run` probe, so a one-run model is fine):
229
217
 
230
218
  ```bash
231
- AGY_MODEL="Gemini 3.7 Flash (Low)" agy-run \
219
+ AGY_MODEL="<an offered display string>" agy-run \
232
220
  "Read the cwd context file and state the dialogue language plus one Hard Constraint, in two lines."
233
- AGY_MODEL="Gemini 3.7 Flash (Low)" agy-run \
221
+ AGY_MODEL="<an offered display string>" agy-run \
234
222
  "Without me pointing you at any file, name a project-specific skill under .agents/skills/ here and cite its path."
235
223
  ```
236
224
 
225
+ **The Jev skill in a delegated run.** A headless `agy-run` loads the vendor's Jev skill from the user root the kit's
226
+ `jev-skill` command fills, `~/.gemini/config/skills` — measured 2026-10-02 on agy 1.2.14: a probe skill under that
227
+ root was listed, and none under `~/.gemini/antigravity-cli/skills`. The run never inherits `TYPESAFE_API_KEY`: the
228
+ wrapper unsets every `*_API_KEY` before agy starts. Jev requests are the orchestrator's own, made in the user's
229
+ environment, and their answers are passed into a brief as text; whether a headless agy command reaches a network
230
+ host is governed by the user's agy permissions and sandbox settings, not by the bridge.
231
+
237
232
  ## How the main agent drives `agy` efficiently
238
233
 
239
234
  See [`references/driving-agy.md`](references/driving-agy.md) for the full playbook (delegation
240
235
  checklist, prompt templates, output handling). Essentials:
241
236
 
242
- - **Pick the cheapest model that fits.** Flash (Low/Medium) for reachability/probes; Pro (High) for
243
- reasoning; `Claude Sonnet 4.6 (Thinking)`, `Claude Opus 4.6 (Thinking)`, or `GPT-OSS 120B (Medium)`
244
- for a different engine's opinion (exact strings — see the Models table) — all on the same
245
- subscription. Quota is finite, so don't reach for Pro by reflex.
237
+ - **The model is the host posture.** Reviews and kept runs use the user's `AGY_MODEL` setting (else
238
+ the default), never a per-call pick. An `agy-run` probe may set `AGY_MODEL` for one run — a
239
+ lighter model for reachability, or another engine for a different opinion, all on the same
240
+ subscription (exact display strings: `/agent-workflow-kit bridge-settings` lists what is offered).
246
241
  - **Hand `agy` a self-contained prompt.** It cannot see your conversation — embed the goal,
247
242
  constraints, relevant excerpts, and the expected output shape; nothing more.
248
243
  - **Continue a thread** with `-- --continue` (most recent) or `-- --conversation <id>` (by id) instead
@@ -284,13 +279,13 @@ checklist, prompt templates, output handling). Essentials:
284
279
  parses the envelope; only `stream-json` stays deferred there.) And there is **no `agy inspect`**:
285
280
  no machine-readable introspection.
286
281
  - Model names must match the `agy models` display strings **exactly**.
287
- - **Quota is finite.** Heavy use of Pro/Claude models can exhaust the subscription; prefer Flash for
288
- cheap work.
282
+ - **Quota is finite.** Heavy use of Pro/Claude models can exhaust the subscription — weigh that when
283
+ choosing the `AGY_MODEL` setting.
289
284
  - **A run can't hang forever.** The wrapper caps `agy` with `timeout(1)` (`AGY_HARD_TIMEOUT`,
290
285
  default = `AGY_TIMEOUT`) because `agy`'s own `--print-timeout` is **not** a reliable wall-clock
291
286
  kill (a run was seen surviving 32 min past a 10m `--print-timeout`). A heavy `--add-dir` agentic
292
- prompt on a slow model (e.g. `Gemini 3.1 Pro (High)`) can run unbounded — prefer a faster model or
293
- a **self-contained prompt** (no `--add-dir`); an "exceeded the hard cap" error is the guard firing.
287
+ prompt on a slow model (e.g. a Pro-class one) can run unbounded — prefer a
288
+ **self-contained prompt** (no `--add-dir`); an "exceeded the hard cap" error is the guard firing.
294
289
  - `agy` output may be incomplete or out of date — treat it as advisory until the main agent verifies
295
290
  it. (Its FORMAT is not one thing: see the transport section for which wrapper prints text, when,
296
291
  and what a failing run prints instead.)
@@ -0,0 +1,257 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { makeSandbox, farmFor, readReceipts, FED_CAP, seedFedChangeSet } from './agy-review-harness.test.mjs';
8
+
9
+ const HERE = dirname(fileURLToPath(import.meta.url));
10
+ const BRIDGE = join(HERE, '..');
11
+ const REVIEW = join(HERE, 'agy-review.sh');
12
+ const AGY_RUN = join(HERE, 'agy.sh');
13
+ const MANIFEST = JSON.parse(readFileSync(join(BRIDGE, 'capability.json'), 'utf8'));
14
+ const REAL_TIMEOUT = spawnSync('bash', ['-c', 'type -P timeout'], { encoding: 'utf8' }).stdout.trim();
15
+ const ESC = String.fromCharCode(27);
16
+ const DEFAULT = 'Gemini 3.8 Flash (High)';
17
+ const SET = 'Claude Opus 4.6 (Thinking)';
18
+ const SECOND = 'Gemini 3.7 Flash (Low)';
19
+ const FACTS = ['code', '--facts', 'a verified fact'];
20
+
21
+ const FAKE_TIMEOUT = [
22
+ '#!/usr/bin/env bash',
23
+ 'if [[ " $* " == *" agy models "* ]]; then',
24
+ ' while [[ $# -gt 0 && "$1" != "agy" ]]; do shift; done',
25
+ ` exec "${REAL_TIMEOUT}" 1 "$@"`,
26
+ 'fi',
27
+ `exec "${REAL_TIMEOUT}" "$@"`,
28
+ '',
29
+ ].join('\n');
30
+
31
+ const catalogOf = (...displays) => displays.map((display, index) => `id-${index}\t${display}\n`).join('');
32
+
33
+ const sandbox = ({ settings, fakeTimeout = false } = {}) => {
34
+ const sb = makeSandbox();
35
+ sb.settingsFile = join(sb.home, '.config', 'agent-workflow', 'bridge-settings.conf');
36
+ if (settings !== undefined) {
37
+ mkdirSync(dirname(sb.settingsFile), { recursive: true });
38
+ writeFileSync(sb.settingsFile, settings);
39
+ }
40
+ if (fakeTimeout) writeFileSync(join(sb.bin, 'timeout'), FAKE_TIMEOUT, { mode: 0o755 });
41
+ writeFileSync(join(sb.repo, 'plan.md'), '# a plan under review\n');
42
+ writeFileSync(join(sb.repo, 'change.diff'), '--- a/x\n+++ b/x\n@@ -1 +1 @@\n-a\n+b\n');
43
+ return sb;
44
+ };
45
+
46
+ let sequence = 0;
47
+ const drive = (sb, args, env = {}, wrapper = REVIEW) => {
48
+ const tag = join(sb.home, `posture-${++sequence}`);
49
+ const r = spawnSync('bash', [wrapper, ...args], {
50
+ cwd: sb.repo, input: '', encoding: 'utf8', timeout: 60000,
51
+ env: {
52
+ HOME: sb.home, PATH: `${sb.bin}:${farmFor(['agy', 'agy-run'])}`, TMPDIR: process.env.TMPDIR ?? '/tmp',
53
+ AGY_FAKE_MODELS_LOG: `${tag}-models`, AGY_FAKE_SENTINEL: `${tag}-sentinel`, AGY_FAKE_ARGV: `${tag}-argv`,
54
+ AGY_FAKE_PROMPT: `${tag}-prompt`, AGY_FAKE_TURNS: `${tag}-turns`, ...env,
55
+ },
56
+ });
57
+ const read = (path) => (existsSync(path) ? readFileSync(path, 'utf8') : '');
58
+ const modelsLog = read(`${tag}-models`);
59
+ return {
60
+ status: r.status, stderr: r.stderr, invoked: existsSync(`${tag}-sentinel`), argv: read(`${tag}-argv`),
61
+ turns: Number(read(`${tag}-turns`) || 0), modelsLog, modelsCalls: modelsLog.split('\n').filter(Boolean).length,
62
+ };
63
+ };
64
+ const runDirect = (sb, env = {}, extra = []) => drive(sb, ['a probe prompt', ...extra], env, AGY_RUN);
65
+
66
+ const quote = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
67
+ const remedyRe = (sb, model) => (model === ''
68
+ ? /remedy: unset AGY_MODEL in the environment/
69
+ : new RegExp(`remedy: set this line in ${quote(sb.settingsFile)}:\\s+AGY_MODEL=${quote(model)}\\s+and unset AGY_MODEL in the environment`));
70
+ const bannerOf = (stderr) => stderr.split('\n').find((line) => line.startsWith('review posture: ')) ?? '';
71
+ const modelArg = (r) => (r.argv.match(/(?:^|\n)--model\n(.*)\n/) ?? [])[1] ?? null;
72
+ const assertRan = (r, model) => {
73
+ assert.equal(r.status, 0, r.stderr);
74
+ assert.equal(r.invoked, true, 'the CLI ran');
75
+ assert.equal(modelArg(r), model, r.argv);
76
+ };
77
+ const assertRefusedUnspent = (r) => {
78
+ assert.equal(r.status, 2, r.stderr);
79
+ assert.equal(r.invoked, false, 'refused before any run is spent');
80
+ };
81
+
82
+ const PARENTS = {
83
+ 'review.code': FACTS,
84
+ 'review.plan': ['plan', 'plan.md'],
85
+ 'review.diff': ['diff', 'change.diff'],
86
+ 'review.continue': ['--continue'],
87
+ 'review.conversation': ['--conversation', 'conv-1'],
88
+ };
89
+
90
+ describe('agy runs the host posture on both wrappers, with no advisory', () => {
91
+ it('agy-review runs the set model without a probe flag; the banner names source setting', () => {
92
+ const r = drive(sandbox({ settings: `AGY_MODEL=${SET}\n` }), FACTS);
93
+ assertRan(r, SET);
94
+ assert.equal(bannerOf(r.stderr), `review posture: model=${SET} source=model:setting timeout=30m`);
95
+ assert.doesNotMatch(r.stderr, /unknown key|frontier/i);
96
+ });
97
+ it('agy-run runs the set model and prints no posture banner', () => {
98
+ const r = runDirect(sandbox({ settings: `AGY_MODEL=${SET}\n` }));
99
+ assertRan(r, SET);
100
+ assert.doesNotMatch(r.stderr, /posture/);
101
+ });
102
+ for (const [label, settings, env, model] of [
103
+ ['the setting restated in the env', `AGY_MODEL=${SET}\n`, { AGY_MODEL: SET }, SET],
104
+ ['env Gemini 3.8 Flash (High) on a fresh host', undefined, { AGY_MODEL: DEFAULT }, DEFAULT],
105
+ ]) {
106
+ it(`${label} is no one-off: agy-review runs it, source environment`, () => {
107
+ const r = drive(sandbox({ settings }), FACTS, env);
108
+ assertRan(r, model);
109
+ assert.equal(bannerOf(r.stderr), `review posture: model=${model} source=model:environment timeout=30m`);
110
+ });
111
+ }
112
+ });
113
+
114
+ describe('an agy one-off is refused pre-spend unless AGY_PROBE=1 — spec:bridge-model/S5', () => {
115
+ it('the probe hook names exactly the parents driven here', () => {
116
+ const hook = MANIFEST.modeCatalog.find((mode) => mode.key === 'AGY_PROBE');
117
+ assert.deepEqual([...hook.parents].sort(), Object.keys(PARENTS).sort());
118
+ });
119
+ const forms = [['env Gemini 3.8 Flash (High)', DEFAULT], ['a second offered non-default model', SECOND], ['an explicitly empty AGY_MODEL=', '']];
120
+ for (const [label, model] of forms) {
121
+ for (const [parent, args] of Object.entries(PARENTS)) {
122
+ it(`${label} on ${parent}: refused with the remedy, run under AGY_PROBE=1`, () => {
123
+ const refusedSb = sandbox({ settings: `AGY_MODEL=${SET}\n` });
124
+ const refused = drive(refusedSb, args, { AGY_MODEL: model });
125
+ assertRefusedUnspent(refused);
126
+ assert.match(refused.stderr, /is a one-off/);
127
+ assert.match(refused.stderr, remedyRe(refusedSb, model));
128
+ const probed = drive(sandbox({ settings: `AGY_MODEL=${SET}\n` }), args, { AGY_MODEL: model, AGY_PROBE: '1' });
129
+ assertRan(probed, model === '' ? null : model);
130
+ });
131
+ }
132
+ it(`${label}: agy-run, the probe role, runs it`, () => {
133
+ assertRan(runDirect(sandbox({ settings: `AGY_MODEL=${SET}\n` }), { AGY_MODEL: model }), model === '' ? null : model);
134
+ });
135
+ }
136
+ });
137
+
138
+ describe('agy-run refuses a model or effort flag after -- on the host posture', () => {
139
+ for (const flag of [['--model', SET], [`--model=${SET}`], ['--effort', 'high'], ['--effort=low']]) {
140
+ it(`refuses ${flag[0]}`, () => {
141
+ const r = runDirect(sandbox(), {}, ['--', ...flag]);
142
+ assertRefusedUnspent(r);
143
+ assert.match(r.stderr, /is not allowed/);
144
+ });
145
+ }
146
+ });
147
+
148
+ describe('an agy model the catalog does not offer is refused with the offered list', () => {
149
+ const laterDefault = catalogOf(SECOND, DEFAULT);
150
+ for (const [label, settings, env] of [['the setting', 'AGY_MODEL=Gemini 9 Ultra\n', {}], ['the environment', undefined, { AGY_MODEL: 'Gemini 9 Ultra', AGY_PROBE: '1' }]]) {
151
+ for (const [wrapper, args] of [[REVIEW, FACTS], [AGY_RUN, ['a probe prompt']]]) {
152
+ it(`a model from ${label} on ${wrapper === REVIEW ? 'agy-review' : 'agy-run'}: the default offered on a later line is set`, () => {
153
+ const sb = sandbox({ settings });
154
+ const r = drive(sb, args, { AGY_FAKE_MODELS: laterDefault, ...env }, wrapper);
155
+ assertRefusedUnspent(r);
156
+ assert.match(r.stderr, /AGY_MODEL='Gemini 9 Ultra' .*not a model the installed agy offers/);
157
+ assert.ok(r.stderr.includes(`offered: ${SECOND}, ${DEFAULT}`), r.stderr);
158
+ assert.match(r.stderr, remedyRe(sb, DEFAULT));
159
+ });
160
+ }
161
+ }
162
+ it('the default itself is refused when not offered; the remedy names the first offered line', () => {
163
+ const sb = sandbox();
164
+ const r = drive(sb, FACTS, { AGY_FAKE_MODELS: `Fetching...\n${catalogOf(SET, SECOND)}` });
165
+ assertRefusedUnspent(r);
166
+ assert.match(r.stderr, new RegExp(`AGY_MODEL='${quote(DEFAULT)}' \\(default\\)`));
167
+ assert.match(r.stderr, remedyRe(sb, SET));
168
+ });
169
+ });
170
+
171
+ describe('an unreadable agy catalog is one stderr line, one read, and the run proceeds', () => {
172
+ const unreadable = [
173
+ ['a failing read', {}, { AGY_FAKE_MODELS_STATUS: '1' }],
174
+ ['a timed-out read', { fakeTimeout: true }, { AGY_FAKE_MODELS_SLEEP: '5' }],
175
+ ['output without a TAB line', {}, { AGY_FAKE_MODELS: 'Fetching available models...\nno model here\n' }],
176
+ ['an empty output', {}, { AGY_FAKE_MODELS: '' }],
177
+ ];
178
+ for (const [label, options, env] of unreadable) {
179
+ for (const [wrapper, args] of [[REVIEW, FACTS], [AGY_RUN, ['a probe prompt']]]) {
180
+ it(`${label} on ${wrapper === REVIEW ? 'agy-review' : 'agy-run'}`, () => {
181
+ const r = drive(sandbox(options), args, env, wrapper);
182
+ assertRan(r, DEFAULT);
183
+ assert.equal(r.modelsCalls, 1);
184
+ assert.equal(r.stderr.split('\n').filter((line) => /catalog/.test(line)).length, 1, r.stderr);
185
+ assert.match(r.stderr, /^warning: the agy model catalog is unreadable \(.+\) — the run proceeds/m);
186
+ });
187
+ }
188
+ }
189
+ it('agy-review calls agy models without any of the caller\'s *_API_KEY values', () => {
190
+ const r = drive(sandbox(), FACTS, { FOO_API_KEY: 'foo-key', ANTIGRAVITY_API_KEY: 'agy-key' });
191
+ assertRan(r, DEFAULT);
192
+ assert.equal(r.modelsLog, 'models unset unset\n');
193
+ });
194
+ });
195
+
196
+ describe('agy-review reads the catalog once and its children skip the read — spec:bridge-model/S12', () => {
197
+ for (const [label, env] of [['a readable catalog', {}], ['a failing catalog read', { AGY_FAKE_MODELS_STATUS: '1' }]]) {
198
+ it(`a fed review over ${label} makes exactly one agy models call`, () => {
199
+ const sb = sandbox();
200
+ seedFedChangeSet(sb);
201
+ const r = drive(sb, FACTS, { AGY_MAX_PROMPT_BYTES: String(FED_CAP), ...env });
202
+ assert.equal(r.status, 0, r.stderr);
203
+ assert.ok(r.turns > 1, 'the change set was fed over several agy-run turns');
204
+ assert.equal(r.modelsCalls, 1);
205
+ });
206
+ }
207
+ });
208
+
209
+ describe('an explicitly empty AGY_MODEL reads no catalog and drops --model — spec:bridge-model/S13', () => {
210
+ it('a probe agy-review runs without --model, records model null, and makes no agy models call', () => {
211
+ const sb = sandbox({ settings: `AGY_MODEL=${SET}\n` });
212
+ const r = drive(sb, FACTS, { AGY_MODEL: '', AGY_PROBE: '1' });
213
+ assertRan(r, null);
214
+ assert.equal(r.modelsCalls, 0);
215
+ assert.equal(bannerOf(r.stderr), 'review posture: model=<agy settings default> source=model:environment timeout=30m');
216
+ assert.deepEqual(readReceipts(sb.repo).at(-1).posture, { model: null });
217
+ });
218
+ it('a direct agy-run runs without --model and makes no agy models call', () => {
219
+ const r = runDirect(sandbox(), { AGY_MODEL: '' });
220
+ assertRan(r, null);
221
+ assert.equal(r.modelsCalls, 0);
222
+ });
223
+ });
224
+
225
+ describe('no frontier list and no off-frontier warning for any offered model — spec:bridge-model/S14', () => {
226
+ it('agy-review.sh carries no frontier list', () => {
227
+ assert.doesNotMatch(readFileSync(REVIEW, 'utf8'), /frontier/i);
228
+ });
229
+ for (const model of [SECOND, SET]) {
230
+ it(`a host posture of ${model} reviews without a warning`, () => {
231
+ const r = drive(sandbox({ settings: `AGY_MODEL=${model}\n` }), FACTS);
232
+ assertRan(r, model);
233
+ assert.doesNotMatch(r.stderr, /warning/i);
234
+ });
235
+ }
236
+ });
237
+
238
+ describe('the shipped agy text names no 3.5 Flash model or family — spec:bridge-model/S15', () => {
239
+ for (const rel of ['SKILL.md', 'references/models-and-flags.md', 'references/driving-agy.md', 'bin/agy.sh']) {
240
+ it(rel, () => {
241
+ assert.doesNotMatch(readFileSync(join(BRIDGE, rel), 'utf8'), /3[ ._-]?5[\s_-]*flash|flash[\s_-]*3[ ._-]?5/i);
242
+ });
243
+ }
244
+ });
245
+
246
+ describe('a settings-file AGY_MODEL with a control byte or an empty value falls back to the default', () => {
247
+ for (const [label, settings] of [['a control byte', `AGY_MODEL=${SET}${ESC}[0m\n`], ['an empty value', 'AGY_MODEL=\n']]) {
248
+ for (const [wrapper, args] of [[REVIEW, FACTS], [AGY_RUN, ['a probe prompt']]]) {
249
+ it(`${label} on ${wrapper === REVIEW ? 'agy-review' : 'agy-run'} warns without the raw byte`, () => {
250
+ const r = drive(sandbox({ settings }), args, {}, wrapper);
251
+ assertRan(r, DEFAULT);
252
+ assert.equal(r.stderr.includes(ESC), false, 'no raw control byte reaches stderr');
253
+ assert.match(r.stderr, /warning: AGY_MODEL in bridge settings file .* built-in default/);
254
+ });
255
+ }
256
+ }
257
+ });
@@ -38,6 +38,13 @@ export const FAKE_AGY = [
38
38
  ' if [[ -n "${AGY_FAKE_HELP_EXTRA:-}" ]]; then printf "%s\\n" "$AGY_FAKE_HELP_EXTRA"; fi',
39
39
  ' exit 0 ;;',
40
40
  ' --version) printf "%s\\n" "${AGY_FAKE_VERSION:-1.1.13}"; exit 0 ;;',
41
+ ' models)',
42
+ ' printf "models %s %s\\n" "${FOO_API_KEY:-unset}" "${ANTIGRAVITY_API_KEY:-unset}" >> "${AGY_FAKE_MODELS_LOG:-/dev/null}"',
43
+ ' if [[ -n "${AGY_FAKE_MODELS_SLEEP:-}" ]]; then sleep "$AGY_FAKE_MODELS_SLEEP"; fi',
44
+ ' if [[ -n "${AGY_FAKE_MODELS+x}" ]]; then printf "%s" "$AGY_FAKE_MODELS"; else',
45
+ ' printf "%s\\t%s\\n" gemini-3.8-flash-high "Gemini 3.8 Flash (High)" gemini-3.7-flash-low "Gemini 3.7 Flash (Low)" claude-opus-4-6-thinking "Claude Opus 4.6 (Thinking)"',
46
+ ' fi',
47
+ ' exit "${AGY_FAKE_MODELS_STATUS:-0}" ;;',
41
48
  'esac',
42
49
  ': "${AGY_FAKE_ARGV:=/dev/null}"',
43
50
  ': "${AGY_FAKE_ENV:=/dev/null}"',
@@ -200,7 +207,7 @@ export const run = (sb, { args, env = {}, cwd, wrapper } = {}) => new Promise((s
200
207
  sentinel: join(home, `${tag}-sentinel`), adddir: join(home, `${tag}-adddir`),
201
208
  adddirMode: join(home, `${tag}-adddir-mode`), artifactMode: join(home, `${tag}-artifact-mode`),
202
209
  artifactCopy: join(home, `${tag}-artifact-copy`), turns: join(home, `${tag}-turns`),
203
- dispatchCwd: join(home, `${tag}-dispatch-cwd`),
210
+ dispatchCwd: join(home, `${tag}-dispatch-cwd`), models: join(home, `${tag}-models`),
204
211
  };
205
212
  const child = execFile('bash', [wrapper || WRAPPER, ...args], {
206
213
  cwd: cwd || repo,
@@ -214,7 +221,7 @@ export const run = (sb, { args, env = {}, cwd, wrapper } = {}) => new Promise((s
214
221
  AGY_FAKE_ARGV: cap.argv, AGY_FAKE_ENV: cap.env, AGY_FAKE_PROMPT: cap.prompt,
215
222
  AGY_FAKE_SENTINEL: cap.sentinel, AGY_FAKE_ADDDIR: cap.adddir, AGY_FAKE_ADDDIR_MODE: cap.adddirMode,
216
223
  AGY_FAKE_ARTIFACT_MODE: cap.artifactMode, AGY_FAKE_ARTIFACT_COPY: cap.artifactCopy,
217
- AGY_FAKE_TURNS: cap.turns, AGY_FAKE_CWD: cap.dispatchCwd,
224
+ AGY_FAKE_TURNS: cap.turns, AGY_FAKE_CWD: cap.dispatchCwd, AGY_FAKE_MODELS_LOG: cap.models,
218
225
  ...env,
219
226
  },
220
227
  }, (error, stdout, stderr) => {
@@ -232,7 +239,7 @@ export const run = (sb, { args, env = {}, cwd, wrapper } = {}) => new Promise((s
232
239
  argv: readIf(cap.argv), capEnv: readIf(cap.env), prompt: readIf(cap.prompt),
233
240
  adddir: readIf(cap.adddir).trim(), adddirMode: readIf(cap.adddirMode).trim(),
234
241
  artifactMode: readIf(cap.artifactMode).trim(), artifactCopy: readIf(cap.artifactCopy),
235
- dispatchCwd: readIf(cap.dispatchCwd).trim(), turns, prompts, argvs,
242
+ dispatchCwd: readIf(cap.dispatchCwd).trim(), turns, prompts, argvs, modelsLog: readIf(cap.models),
236
243
  });
237
244
  });
238
245
  // Only EPIPE is a refusal working — any other stdin write failure must reach the test.