@sabaiway/agent-workflow-kit 14.20.0 → 14.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +38 -50
- package/bridges/antigravity-cli-bridge/bin/agy-model-posture.test.mjs +257 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +10 -3
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -21
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +4 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +120 -48
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +22 -62
- package/bridges/antigravity-cli-bridge/bin/agy.sh +109 -16
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +5 -5
- package/bridges/antigravity-cli-bridge/capability.json +14 -7
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +16 -20
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +10 -27
- package/bridges/codex-cli-bridge/SKILL.md +51 -49
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +145 -46
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +28 -55
- package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +371 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +134 -36
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +33 -45
- package/bridges/codex-cli-bridge/capability.json +22 -8
- package/bridges/codex-cli-bridge/references/driving-codex.md +18 -15
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
- package/bridges/codex-cli-bridge/setup/README.md +3 -3
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bridge-settings.md +10 -7
- package/references/modes/recipes.md +1 -1
- package/references/modes/review-state.md +1 -1
- package/tools/bridge-catalog.mjs +114 -0
- package/tools/bridge-posture.mjs +11 -2
- package/tools/bridge-settings-read.mjs +41 -5
- package/tools/bridge-settings.mjs +111 -23
- package/tools/detect-backends.mjs +6 -6
- package/tools/family-registry.mjs +3 -3
- package/tools/manifest/schema.md +1 -1
- package/tools/manifest/validate.mjs +3 -2
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +2 -2
- package/tools/review-state.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ 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
|
+
|
|
7
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)
|
|
8
49
|
|
|
9
50
|
**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
|
@@ -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).
|
|
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.
|
|
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
|
+
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.
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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="
|
|
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: `
|
|
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
|
|
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}`;
|
|
173
|
-
|
|
174
|
-
pre-spend in every mode. The `
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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 (
|
|
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="
|
|
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="
|
|
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
|
-
- **
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
subscription
|
|
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
|
|
295
|
-
|
|
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.
|
|
300
|
-
|
|
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.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// agy-review-honesty.test.mjs — the round-1 hardening pins for the D4/D5 arms (strip Phase 4):
|
|
2
2
|
// the verdict parse is EXACT (an inexact heading or a token buried mid-line never attests —
|
|
3
|
-
// `NOT SHIP` must not read as SHIP), and
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// `NOT SHIP` must not read as SHIP), and an explicitly empty AGY_MODEL is a one-off on every
|
|
4
|
+
// branch: refused pre-spend unless AGY_PROBE=1, whose receipt records posture.model null.
|
|
5
|
+
// Colocated separately from agy-review.test.mjs — that file is
|
|
6
6
|
// red-proof-frozen; this one carries its own minimal standalone harness (the family idiom).
|
|
7
7
|
|
|
8
8
|
import { describe, it, after } from 'node:test';
|
|
@@ -156,30 +156,29 @@ describe('agy-review — the verdict parse is EXACT (M1): a buried token never a
|
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
describe('agy-review —
|
|
160
|
-
it('plan and
|
|
159
|
+
describe('agy-review — an explicitly empty AGY_MODEL is a one-off on every branch (M3)', () => {
|
|
160
|
+
it('plan, diff, --ungrounded and grounded code with AGY_MODEL= refuse pre-spend naming unset AGY_MODEL', () => {
|
|
161
|
+
for (const args of [['plan', 'plan.md'], ['diff', 'plan.md'], ['code', '--ungrounded'], ['code', '--facts', 'a tiny fact']]) {
|
|
162
|
+
const sb = makeSandbox();
|
|
163
|
+
const r = run(sb, { args, env: { AGY_MODEL: '' } });
|
|
164
|
+
const receipts = readReceipts(sb.repo);
|
|
165
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
166
|
+
assert.equal(r.status, 2, `${args.join(' ')}: ${r.stderr}`);
|
|
167
|
+
assert.equal(r.invoked, false, 'refused BEFORE any spend');
|
|
168
|
+
assert.equal(receipts.length, 0);
|
|
169
|
+
assert.match(r.stderr, /unset AGY_MODEL/, 'the remedy names unsetting it');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('under AGY_PROBE=1 plan and diff modes with AGY_MODEL= run and record posture.model null', () => {
|
|
161
174
|
for (const args of [['plan', 'plan.md'], ['diff', 'plan.md']]) {
|
|
162
175
|
const sb = makeSandbox();
|
|
163
|
-
const r = run(sb, { args, env: { AGY_MODEL: '', AGY_FAKE_OUTPUT: '### Verdict\nSHIP — fine.' } });
|
|
176
|
+
const r = run(sb, { args, env: { AGY_MODEL: '', AGY_PROBE: '1', AGY_FAKE_OUTPUT: '### Verdict\nSHIP — fine.' } });
|
|
164
177
|
const receipts = readReceipts(sb.repo);
|
|
165
178
|
rmSync(sb.home, { recursive: true, force: true });
|
|
166
|
-
assert.equal(r.status, 0, `${args[0]} must run
|
|
179
|
+
assert.equal(r.status, 0, `${args[0]} must run as a probe: ${r.stderr}`);
|
|
167
180
|
assert.equal(r.invoked, true);
|
|
168
181
|
assert.deepEqual(receipts[0].posture, { model: null }, 'an unknowable model is recorded null');
|
|
169
182
|
}
|
|
170
183
|
});
|
|
171
|
-
|
|
172
|
-
it('code --ungrounded with AGY_MODEL= RUNS (its receipt never attests); grounded code still refuses', () => {
|
|
173
|
-
const sb = makeSandbox();
|
|
174
|
-
const un = run(sb, { args: ['code', '--ungrounded'], env: { AGY_MODEL: '', AGY_FAKE_OUTPUT: '### Verdict\nSHIP — throwaway.' } });
|
|
175
|
-
const unReceipts = readReceipts(sb.repo);
|
|
176
|
-
rmSync(sb.home, { recursive: true, force: true });
|
|
177
|
-
assert.equal(un.status, 0, un.stderr);
|
|
178
|
-
assert.deepEqual(unReceipts[0].posture, { model: null });
|
|
179
|
-
const sb2 = makeSandbox();
|
|
180
|
-
const grounded = run(sb2, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: '' } });
|
|
181
|
-
rmSync(sb2.home, { recursive: true, force: true });
|
|
182
|
-
assert.notEqual(grounded.status, 0, 'the ATTESTING branch still refuses pre-spend');
|
|
183
|
-
assert.equal(grounded.invoked, false);
|
|
184
|
-
});
|
|
185
184
|
});
|