@sabaiway/agent-workflow-kit 14.20.0 → 14.22.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 (56) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +2 -2
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +38 -50
  5. package/bridges/antigravity-cli-bridge/bin/agy-model-posture.test.mjs +257 -0
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +10 -3
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -21
  8. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +4 -4
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +120 -48
  10. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +22 -62
  11. package/bridges/antigravity-cli-bridge/bin/agy.sh +109 -16
  12. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +5 -5
  13. package/bridges/antigravity-cli-bridge/capability.json +14 -7
  14. package/bridges/antigravity-cli-bridge/references/driving-agy.md +16 -20
  15. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +10 -27
  16. package/bridges/codex-cli-bridge/SKILL.md +51 -49
  17. package/bridges/codex-cli-bridge/bin/codex-exec.sh +145 -46
  18. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +28 -55
  19. package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +371 -0
  20. package/bridges/codex-cli-bridge/bin/codex-review.sh +134 -36
  21. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +33 -45
  22. package/bridges/codex-cli-bridge/capability.json +22 -8
  23. package/bridges/codex-cli-bridge/references/driving-codex.md +18 -15
  24. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
  25. package/bridges/codex-cli-bridge/setup/README.md +3 -3
  26. package/capability.json +1 -1
  27. package/package.json +1 -1
  28. package/references/authoring/TASK_TEMPLATE.md +11 -16
  29. package/references/modes/agents.md +2 -2
  30. package/references/modes/bridge-settings.md +10 -7
  31. package/references/modes/procedures.md +7 -7
  32. package/references/modes/recipes.md +2 -2
  33. package/references/modes/review-state.md +1 -1
  34. package/references/modes/set-recipe.md +1 -1
  35. package/references/modes/tier.md +1 -1
  36. package/references/modes/velocity.md +1 -1
  37. package/references/reference-profile.json +1 -1
  38. package/references/templates/SPEC_TEMPLATE.md +2 -1
  39. package/references/templates/agent_rules.md +7 -7
  40. package/tools/bridge-catalog.mjs +114 -0
  41. package/tools/bridge-posture.mjs +11 -2
  42. package/tools/bridge-settings-read.mjs +41 -5
  43. package/tools/bridge-settings.mjs +111 -23
  44. package/tools/carriers.mjs +6 -4
  45. package/tools/cheap-agents.mjs +1 -1
  46. package/tools/detect-backends.mjs +6 -6
  47. package/tools/family-registry.mjs +3 -3
  48. package/tools/held-session.mjs +1 -33
  49. package/tools/manifest/schema.md +1 -1
  50. package/tools/manifest/validate.mjs +3 -2
  51. package/tools/procedures.mjs +86 -116
  52. package/tools/recipes.mjs +2 -2
  53. package/tools/review-state.mjs +1 -1
  54. package/tools/rules-regions.mjs +2 -1
  55. package/tools/tier-guide-facts.mjs +145 -159
  56. package/tools/tier-guide.mjs +8 -14
package/CHANGELOG.md CHANGED
@@ -4,6 +4,93 @@ 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.22.0 — a story runs in four sessions, each step with one checked result; the advisor renders the new review loop (AD-170)
8
+
9
+ **Your project's rules now state the story flow the engine's canon (5.12.0) defines: four sessions — spec and plan,
10
+ tests, code, then diff review, release and record — with review on the spec with the plan's ledger and on the staged
11
+ diff.** The canon adds the epic's review by the review lens alone and the one commit after the diff review;
12
+ `/agent-workflow-kit procedures` renders both. Both rules templates' §2.6 (the lens) and §2.7
13
+ (Story sessions) carry the new bodies; the outgoing ones are priors, so an upgrade refreshes an unedited region and
14
+ keeps a custom edit, flagged.
15
+
16
+ - **`/agent-workflow-kit procedures` renders the loop the canon states.** One fold ask per member per round, carrying
17
+ all of its findings; round two with the folded artifact, the decided register and the fold diffs; a fold of the
18
+ last round re-read by its raiser; for `plan-execution`, the re-check of a staged tree that moves after a member's
19
+ last ship-class verdict. The round render, the cap triage and the red-proof instrument are gone from the
20
+ plan-authoring and plan-execution renders; `feedback-triage` keeps its round table. plan-authoring's agy review
21
+ reads the spec review file, and the unarmed flow line names no arming command. On a project that armed the flow
22
+ chain itself, `plan-execution` still renders the armed pre-fold line, a stated residual.
23
+ - **The epic by the lens.** `procedures epic` renders the lens review and, whatever `epic.review` resolves to, no
24
+ bridge wrapper, driving contract or grounding pre-step; the slot keys and the Slots line are unchanged.
25
+ `references/modes/agents.md` and the README say the lens is the whole review of an epic and never replaces the
26
+ review recipe of a plan or a diff; the `agents` report and `dispatch advise` say it never replaces a plan or diff
27
+ review recipe.
28
+ - **Cleanup after the commit.** The rules template's §1.3 Task Completion asks before committing first and runs the
29
+ plan's Phase: Cleanup after the commit.
30
+ - **One fold line.** Under a delegated execute slot the fold lane is one line: resume the run's session with
31
+ `codex-exec --resume <session id> --nonce <n> <prompt file>`, the id read from the run's exec receipt. The advisor
32
+ no longer reads the delegation ledger; `tools/held-session.mjs` drops `foldLaneLines`, which nothing else read.
33
+ - **`task.author` dispatches no slice.** `tools/carriers.mjs`: the orchestrator writes each row's prompt, so under
34
+ `subagent` `dispatchForm` prints the slot's sentence alone, without the executor dispatch lines; under `delegated`
35
+ the slot still renders `codex-exec` and its driving contract, which no step reads (display only, recorded debt).
36
+ `task.execute`'s slice is one prompt the orchestrator wrote from one ledger row; `references/modes/{agents,recipes,
37
+ set-recipe,procedures}.md` say the same.
38
+ - `tools/tier-guide.mjs`, `references/modes/tier.md` and the reference profile say four story sessions;
39
+ `references/templates/SPEC_TEMPLATE.md` says a draft exists at the spec review.
40
+ - Recorded debt (`docs/debt.md`): `recipes --active-line` (`→ codex-review`), the orchestration README default
41
+ sentence, the tier offer and `status` still render `epic.review` as a bridge review (`reviewed`), and `recipes
42
+ --active-line`, `procedures task` and the tier offer still name `codex-exec` for a delegated `task.author`; no
43
+ step reads either slot; display only.
44
+ - **The tier guide walks a story on this flow.** `tools/tier-guide.mjs` says a task is one plan row handed out in
45
+ one prompt from the task template, and the story commits once, after its diff review.
46
+ `references/authoring/TASK_TEMPLATE.md` is that prompt's form: Row, Files, Reads, Run and Do not run, seven keys;
47
+ a bridge prompt adds the contract block with a fresh nonce that the engine's *The task* names. The
48
+ guide's story stages are S1-S4: it reads each row's prompt at its canonical name, and a landed story's leftover
49
+ (read the staged diff, check the review state, the one commit, remove the plan) holds every other story and
50
+ every other epic's close until it is done, so the story commits before its epic closes. The guide no longer judges the plan, mints a checkpoint, stamps
51
+ or checks a brief, or commits per task; it prints `plan-shape-cli --check` as a step.
52
+
53
+ ## 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)
54
+
55
+ **Each bridge now runs the model you set, and `/agent-workflow-kit bridge-settings` shows and sets it.** The read-out
56
+ names, per bridge, the model (codex: and effort), where it comes from (`default`, `setting` or `environment`) and the
57
+ models the installed CLI offers, in text and `--json`. `--set CODEX_MODEL|CODEX_EFFORT|AGY_MODEL=…` is checked
58
+ against that list before anything is written, on the preview and `--apply` alike; the choice lives in the host file
59
+ `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf`, holds for every later run on that host and
60
+ survives a kit upgrade. A new vendor model needs no kit release.
61
+
62
+ > ### ⚠ BREAKING — inherited from codex-cli-bridge 4.0.0 and antigravity-cli-bridge 6.0.0
63
+ >
64
+ > `codex-exec`, `codex-review` and `agy-review` refuse before any spend (exit 2) a run off the host posture — an
65
+ > environment `CODEX_MODEL`, `CODEX_EFFORT` or `AGY_MODEL` that differs from your setting, else the default — unless
66
+ > it is a probe (`CODEX_PROBE=1`, `AGY_PROBE=1`), whose review receipt never attests; `codex-exec` also refuses a
67
+ > probe one-off that carries a dispatch nonce. The refusal names the settings lines that make the model your posture.
68
+ > All four wrappers, `agy-run` included, refuse (exit 2) a model the installed CLI's catalog does not offer, the
69
+ > default included; an unreadable catalog never refuses. A model or effort flag after `--` is refused by `codex-exec`
70
+ > and `agy-run`. To keep a model you used to pass through the environment, set it once with
71
+ > `bridge-settings --set … --apply`.
72
+
73
+ - **New codex default.** codex runs `gpt-6.1-sol` at effort `high`; agy keeps `Gemini 3.8 Flash (High)`.
74
+ - **Checked before any run.** Each wrapper checks the effective model (codex: and effort) against the installed CLI's
75
+ catalog (`codex debug models`, `agy models`) with every `*_API_KEY` cleared (codex: `OPENAI_BASE_URL` too); a
76
+ model the CLI does not offer is refused with the offered list and one remedy. An unreadable catalog is stated and
77
+ never refuses.
78
+ - **The banners say where the model came from.** Every receipt still names what was handed to the CLI (codex: model,
79
+ effort and tier; agy: model); the posture banners of `codex-exec`, `codex-review` and `agy-review` now name where
80
+ the model and, for codex, the effort came from.
81
+ - **The command runs no model.** It runs only the two catalog commands, concurrently and bounded as the wrappers are
82
+ (codex 15 s, agy 30 s), through the new leaf `tools/bridge-catalog.mjs`; a missing, failing or hung CLI is one line,
83
+ and a set then proceeds stating the value was not checked. The status line and the recipes renders show a set model
84
+ with its source and read no catalog.
85
+ - **The manifests declare the keys.** A new setting kind `posture` in `tools/manifest/validate.mjs` (a non-empty
86
+ string, no control byte, no edge whitespace); `--reconcile` reports the three keys current.
87
+ - **The text names the default only.** The bridges' skill files, references and wrapper headers name each default and
88
+ point at the command; the agy frontier list, its off-frontier warning, its probe advice and the retired 3.5 Flash
89
+ text are gone. The recipes quota note gives no per-call cheapest-model advice.
90
+ - Recorded debt (`docs/debt.md`, twelve rows): among them the posture kind admits line separators and the settings
91
+ reader misses a NUL, the agy catalog lookup ignores `~/.local/bin`, a catalog read whose detached child holds its
92
+ output never settles, and the posture render folds whitespace.
93
+
7
94
  ## 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)
8
95
 
9
96
  **Once `TYPESAFE_API_KEY` is set, the Recommendations item `jev-skill` offers the vendor's Jev skill for every agent the
package/README.md CHANGED
@@ -252,10 +252,10 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
252
252
  | `/agent-workflow-kit worktrees` | any time | **parallel feature worktrees** — run several features in DIFFERENT agent sessions on one repo, zero interference on working-tree files (the ONE exception is the dependency cache, below): `provision <slug> --plan <file>` creates a sibling git worktree on branch `aw/<slug>` and populates it (registry-derived footprint copy-if-missing — a tracked file is never overwritten; EXACTLY ONE seeded feature plan; the `handoff-<slug>.md` record from minute zero; `node_modules` symlinked where the link stays ignored — a shared MUTABLE dependency cache: writes through it hit MAIN's node_modules; for isolation run the printed isolated-install command (`--install` only PRINTS it; on `--resume` run the printed unlink-first recovery first — printed only for a link whose RAW TARGET is MAIN's `node_modules`, since a foreign or unreadable link is reported as such with no removal advised); absolute root-pinned gate commands rebased on untracked copies only, and only while their bytes equal the MAIN source or its rebased form — user-modified copies stay untouched); `list` is read-only (slug, branch, base, dirty, handoff); `prompt <slug>` is read-only too and re-prints the satellite's cold-start prompt — the same text `provision` ends its report with: where the worktree is, its ONE seeded plan, the handoff as the one channel back, MAIN's orientation (shared series index, landing) and, under its own heading because it is probed on the satellite, that checkout's install posture — every value derived LIVE rather than replayed from the frozen provision record, a recorded value that no longer matches NAMED as a divergence (with the cause its source makes likely) instead of printed as the runnable one, every offered command marked with WHO runs it (`MAIN $ …` for the landing, `HERE $ …` for this checkout's own install, a posture with nothing to run staying prose), and a control character in any rendered value a typed STOP rather than a forged prompt line; `land <slug> --prepare` locks the common git dir, fail-closes on divergence or incomplete satellite state, transfers the complete accepted satellite diff onto a CLEAN main, runs sync plus the declared gates, and reports HEAD/TRANSFER/PREPARED OIDs — the commit ALWAYS stays a dialogue ask; `cleanup <slug>` takes the same lock and removes a LANDED worktree only after live landed-verification against main HEAD, while `--abandon` is the ONE destructive arm (destroys unlanded work; **no preview step** on any writer). The parent dir is the `docs/ai/worktrees.json` `parentDir` setting (default: the repo's sibling parent); an unwritable parent degrades to printed maintainer-pasted commands, and the one-time host consent that makes it promptless surfaces via `recommendations`. Never commits, never pushes, never runs a subscription CLI. |
253
253
  | `/agent-workflow-kit uninstall` | opt-in, any time | **guarded teardown** — the inverse of `init` / `setup`. Removes only what's **provably ours** (managed skill dirs + bridge wrappers; in a project, the hidden-mode git-ignore block it added + the pre-commit hook it installed); **never deletes** your `docs/ai` / `AGENTS.md` (prints the exact `rm` to run by hand) or your `.claude/settings.json` (prints an **edit** — remove the attribution key, review any velocity `permissions.*` — never an `rm`). Always `--dry-run` first; preflight-then-mutate; never commits. |
254
254
  | `/agent-workflow-kit velocity` | Claude Code · opt-in | **onboarding velocity profile** — seeds a fixed, audited **read-only** allowlist into `.claude/settings.json` so routine read-only commands stop idling on approval prompts while you're away; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes **only** `.claude/settings.json` — **never** allowlists commit/push/publish, never writes `settings.local.json`, never commits. A seeded entry is a **trust posture, not a sandbox** (a runtime residual remains at the settings level — its guard ships as the opt-in `hook` command); a direct commit/push/publish still asks. `--dry-run` first. |
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. |
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 plan or diff review recipe, advisory like every review; an epic is reviewed by the lens alone, whatever `epic.review` resolves to). 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.20.0'
6
+ version: '14.22.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.1'
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,12 +213,12 @@ 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
 
@@ -246,10 +234,10 @@ host is governed by the user's agy permissions and sandbox settings, not by the
246
234
  See [`references/driving-agy.md`](references/driving-agy.md) for the full playbook (delegation
247
235
  checklist, prompt templates, output handling). Essentials:
248
236
 
249
- - **Pick the cheapest model that fits.** Flash (Low/Medium) for reachability/probes; Pro (High) for
250
- reasoning; `Claude Sonnet 4.6 (Thinking)`, `Claude Opus 4.6 (Thinking)`, or `GPT-OSS 120B (Medium)`
251
- for a different engine's opinion (exact strings — see the Models table) — all on the same
252
- 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).
253
241
  - **Hand `agy` a self-contained prompt.** It cannot see your conversation — embed the goal,
254
242
  constraints, relevant excerpts, and the expected output shape; nothing more.
255
243
  - **Continue a thread** with `-- --continue` (most recent) or `-- --conversation <id>` (by id) instead
@@ -291,13 +279,13 @@ checklist, prompt templates, output handling). Essentials:
291
279
  parses the envelope; only `stream-json` stays deferred there.) And there is **no `agy inspect`**:
292
280
  no machine-readable introspection.
293
281
  - Model names must match the `agy models` display strings **exactly**.
294
- - **Quota is finite.** Heavy use of Pro/Claude models can exhaust the subscription; prefer Flash for
295
- 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.
296
284
  - **A run can't hang forever.** The wrapper caps `agy` with `timeout(1)` (`AGY_HARD_TIMEOUT`,
297
285
  default = `AGY_TIMEOUT`) because `agy`'s own `--print-timeout` is **not** a reliable wall-clock
298
286
  kill (a run was seen surviving 32 min past a 10m `--print-timeout`). A heavy `--add-dir` agentic
299
- prompt on a slow model (e.g. `Gemini 3.1 Pro (High)`) can run unbounded — prefer a faster model or
300
- 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.
301
289
  - `agy` output may be incomplete or out of date — treat it as advisory until the main agent verifies
302
290
  it. (Its FORMAT is not one thing: see the transport section for which wrapper prints text, when,
303
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.