@sabaiway/agent-workflow-kit 1.34.0 → 1.36.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 (42) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +1 -0
  3. package/SKILL.md +23 -13
  4. package/bin/install.mjs +15 -0
  5. package/bridges/antigravity-cli-bridge/SKILL.md +13 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +96 -1
  7. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +204 -1
  8. package/bridges/antigravity-cli-bridge/bin/agy.sh +94 -0
  9. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +245 -1
  10. package/bridges/antigravity-cli-bridge/capability.json +17 -1
  11. package/bridges/codex-cli-bridge/SKILL.md +15 -1
  12. package/bridges/codex-cli-bridge/bin/codex-exec.sh +113 -0
  13. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +288 -0
  14. package/bridges/codex-cli-bridge/bin/codex-review.sh +114 -1
  15. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +229 -1
  16. package/bridges/codex-cli-bridge/capability.json +29 -1
  17. package/capability.json +1 -1
  18. package/package.json +1 -1
  19. package/references/modes/bridge-settings.md +29 -0
  20. package/references/modes/review-ledger.md +28 -0
  21. package/references/modes/set-autonomy.md +29 -0
  22. package/references/modes/upgrade.md +8 -2
  23. package/references/modes/velocity.md +13 -0
  24. package/tools/atomic-write.mjs +59 -21
  25. package/tools/autonomy-config.mjs +306 -0
  26. package/tools/autonomy-write.mjs +27 -0
  27. package/tools/bridge-settings-read.mjs +221 -0
  28. package/tools/bridge-settings.mjs +288 -0
  29. package/tools/commands.mjs +21 -0
  30. package/tools/family-registry.mjs +12 -1
  31. package/tools/manifest/schema.md +31 -0
  32. package/tools/manifest/validate.mjs +85 -0
  33. package/tools/procedures.mjs +26 -4
  34. package/tools/recipes.mjs +16 -6
  35. package/tools/renderers.mjs +7 -0
  36. package/tools/review-ledger-write.mjs +257 -0
  37. package/tools/review-ledger.mjs +508 -0
  38. package/tools/seed-gates.mjs +45 -4
  39. package/tools/set-autonomy.mjs +195 -0
  40. package/tools/setup-backends.mjs +107 -9
  41. package/tools/velocity-profile.mjs +468 -5
  42. package/tools/view-model.mjs +7 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,77 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 1.36.0 — Review-round ledger: the prose crossover-stop becomes a computed signal (AD-045)
8
+
9
+ A **feature** release (deployment-lineage head stays `1.3.0` — no migration). The review-loop
10
+ crossover-stop that `planning.md` §9 and `procedures.md` describe in prose — "cap ≤2 rounds", the
11
+ crossover, "fold-at-altitude vs residual", "{round N · finding-origin tally · per-backend verdict} …
12
+ a computed signal, not a remembered rule" — was **prose with no checker** and broke under load. This
13
+ ships it as a **mechanism** (DEBT-REVIEW-CAP): a review-round **LEDGER** that protects every consumer
14
+ project's `plan-execution` review loop.
15
+
16
+ - **`tools/review-ledger.mjs`** (read-only) — the record schema (two kinds, `round` / `triage`,
17
+ internally-consistency-validated), a tolerant reader (malformed lines counted + surfaced, never
18
+ dropped), the pure **`decideStop`** truth table returning exactly one state under a fixed precedence
19
+ (**converged > resolved-residual > triage-required > continue**) from machine fields only, the
20
+ integrity receipt cross-check, and the **`--check`** gate — **fail-CLOSED** on every unknown state
21
+ (detector failure · unreadable / malformed ledger · a corrupt round sequence · more than one plan
22
+ in flight).
23
+ - **`tools/review-ledger-write.mjs`** (the sole writer, over the hardened `atomic-write` core) —
24
+ `record` / `classify` with **the teeth**: it refuses a round while triage is required, beyond the
25
+ hard-max ceiling of 3, or without a grounded review receipt, and enforces round-sequence integrity +
26
+ round-bound classifications. The read/write split is pinned by an import-split test; the ledger lives
27
+ in the git dir (uncommittable by construction, mirroring the receipts precedent).
28
+ - **Command surface** — a `review-ledger` catalog entry + `### Mode: review-ledger` + its mode
29
+ reference; the conditional `seed-gates` candidate (offered only when `plan-execution.review` is
30
+ reviewed / council).
31
+ - **Honest residual** (stated in the tool header, like `review-state`'s): the ledger attests a review
32
+ occurred and its ship-class is consistent; it does not prove the recorded counts are truthful nor
33
+ that a self-reported `degraded` is real — a self-discipline mechanism, not a security boundary.
34
+
35
+ Self-arming dogfood: this release's own review loop was recorded through the ledger it builds
36
+ (`--check` exits 0 via the `converged` branch). The optional per-fold `testId` slot exists but stays
37
+ **unenforced** — enforcement + a fold-completeness signal + the canon-pointer mechanization are the
38
+ next plan (DEBT-TEST-COMPLETENESS).
39
+
40
+ ## 1.35.0 — Host-level bridge settings surface + Codex Fast tier as configuration (AD-043)
41
+
42
+ A **feature** release (deployment-lineage head stays `1.3.0` — no migration). Bridge knobs like the
43
+ Codex Fast tier are now enabled **through a host-level settings file that survives kit upgrades**,
44
+ never re-researched and re-patched per host; the kit surfaces, reconciles, and honestly refreshes
45
+ that surface. Ships **bridges 2.3.0** inside the tarball (the four wrappers' shared settings-reader
46
+ block + the `CODEX_SERVICE_TIER` knob + the typed `settings` manifest schema, Phase 1). The kit
47
+ machinery (Phase 2):
48
+
49
+ - **`bridge-settings` mode — the host-config reader + consent-gated writer.** New
50
+ `tools/bridge-settings.mjs` (+ its read-only core `tools/bridge-settings-read.mjs`) reads/writes
51
+ `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf` — a `KEY=VALUE` file **outside
52
+ every kit tree** (D2, upgrade-survival is structural). Previews by default, `--apply` writes via a
53
+ hardened out-of-tree atomic core (`writeContainedFileAtomic` / `writeHostConfigFileAtomic`,
54
+ factored from `atomic-write.mjs` — symlink/parent/TOCTOU-safe, dir created on first use). The
55
+ allowlist + typed validation come from the **bundled bridge manifests** (`settings` blocks,
56
+ manifest-as-source / D6 — `settingValueValid` is now the single shared predicate). It refuses an
57
+ unknown key, an out-of-range/invalid value, and — loudly, naming the key — a duplicate-carrying
58
+ file; model/effort stay unsettable (the quality guard is untouched, D4). Routed in `SKILL.md`
59
+ (`guarded` kind) with `references/modes/bridge-settings.md`.
60
+ - **Refresh overwrite honesty (D5).** `tools/setup-backends.mjs` now byte-compares a placed bridge
61
+ against the bundle on an equal-version re-sync and **states** the local edits it overwrote (file
62
+ list + the settings-file pointer), instead of the old silent wipe; a version upgrade never cries
63
+ wolf about the version delta, an unreadable placed file degrades honestly, a placed-only extra is
64
+ preserved and never claimed as loss.
65
+ - **Init/upgrade reconcile.** `init` (`bin/install.mjs`) and `Mode: upgrade` run
66
+ `bridge-settings --reconcile` after the bridge refresh: every settings-file key is validated
67
+ against the new bundled manifests; an unknown/retired key is a loud flag, **preserved verbatim**
68
+ (never edited — the lens-region posture).
69
+ - **Status + advisor surfaces (fact-only).** `status`' execution-backends block, the `procedures`
70
+ driving-contract render, and `recipes --status-line` now surface the active knobs (env>file>default)
71
+ and each wrapper's settable knobs — fact-only, no model claim, localized-on-error; the status line
72
+ stays byte-identical unless a knob is active.
73
+ - Parity + budgets: new `test/settings-reader-parity.test.mjs` pins the reader block byte-identical
74
+ across the four wrappers; the always-loaded router stays `≤ 10240 B` (the equal-head reconcile
75
+ enumeration was trimmed to its step-3 pointer to make room for the new mode); the `routerPlusMode`
76
+ budget is re-pinned `29696 → 30720` (documented) for the reconcile paragraph added to `upgrade.md`.
77
+
7
78
  ## 1.34.0 — Onboarding UX: one batched setup prompt, honest installer messaging, the visible accelerator funnel, and the consent-gated gates seeder (AD-042)
8
79
 
9
80
  A **feature** release (first-contact flow + a new consent-gated writer; deployment-lineage head
package/README.md CHANGED
@@ -240,6 +240,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
240
240
  | `/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. |
241
241
  | `/agent-workflow-kit agents` | Claude Code · opt-in | **cheap-lane subagents** — places bundled subagent definitions (`.claude/agents/`) pinned to a **cheap model** (haiku, low effort, read-only tools) for mechanical work: extraction sweeps, changelog fact-skeletons, gate-failure triage. Judgment, review, and real code stay on your main lane — these vehicles only extract and draft, and their output is verified. Preview by default (`--apply` writes); an existing customized file is **preserved, never overwritten**; never touches `settings*.json`, never commits. |
242
242
  | `/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). Exact matches only, never patterns; never `deny`; a broken `gates.json` only disables auto-approval, never the guard. Preview by default; never writes `settings.local.json`; never commits. |
243
+ | `/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` — ~1.5× speed at a **2.5× credit rate**, quality-neutral, default off — a consented spend), the codex/agy hard timeouts, the review byte cap, the oversized-review add-dir toggle. 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. |
243
244
 
244
245
  It **never auto-commits** and **never overwrites** an existing `AGENTS.md` without asking.
245
246
 
package/SKILL.md CHANGED
@@ -3,12 +3,12 @@ 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".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '1.34.0'
6
+ version: '1.36.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
10
10
 
11
- Deploys a **portable AI-agent memory-and-workflow system** into a project, and upgrades it as the kernel evolves — so any future agent can reconstruct project context in ~60 seconds and avoid repeating past mistakes.
11
+ Deploys a **portable AI-agent memory-and-workflow system** into a project, and upgrades it as the kernel evolves — so any future agent can reconstruct project context fast and avoid repeating past mistakes.
12
12
 
13
13
  **Posture:** never leak kit internals — translate tool outcomes to plain language; Gotchas: `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`.
14
14
 
@@ -23,14 +23,13 @@ when it is present and healthy, and otherwise uses its **own bundled copy** (`re
23
23
  `migrations/`) — so the existing one-command install keeps working with **no new dependency on the
24
24
  memory substrate**. (The methodology slot is a separate axis: its fragment is read **live from the
25
25
  installed `agent-workflow-engine`**, which `npx @sabaiway/agent-workflow-kit@latest init` installs — a
26
- runtime dependency placed by `init` and read live; see `${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md`.)
26
+ runtime dependency read live; see `${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md`.)
27
27
  `init` also **refreshes the installed memory substrate** (best-effort — a miss is a loud DEGRADED
28
- success: a warning with the exact recovery command + exit 0, never silent, never the engine's hard
29
- STOP; `--no-memory` skips it), so a returning `init` leaves **no stale core member**. The
28
+ success: warn + the exact recovery command + exit 0, never silent, never the engine's hard STOP;
29
+ `--no-memory` skips it), so a returning `init` leaves **no stale core member**. The
30
30
  execution-backend bridges are still never **placed** by `init` (placed on demand by `setup`, opt-in);
31
- **once placed**, `init` **refreshes** them from the kit's own bundled copies (refresh-only, local
32
- files, never a downgrade; `--no-bridges` skips it) — so a returning `init` leaves no stale placed
33
- bridge either.
31
+ **once placed**, `init` **refreshes** them from the kit's own bundled copies (refresh-only, never a
32
+ downgrade; `--no-bridges` skips it) — so a returning `init` leaves no stale placed bridge either.
34
33
 
35
34
  **Detection (kit-owned, decided BEFORE any project write).** Run the kit's **own shipped**
36
35
  validator — `node ${CLAUDE_SKILL_DIR}/tools/manifest/validate.mjs <memory-skill-dir>` — never a
@@ -38,9 +37,8 @@ validator shipped by the candidate (which could itself be broken). Delegate only
38
37
  hold:
39
38
  - result is **valid** and `kind` is `memory-substrate`;
40
39
  - **every required asset is present** in the candidate, at its real path:
41
- `references/templates/`, **`references/templates/orchestration.json`** (the orchestration-config
42
- template a memory too old to ship it, pre-`1.2.0`, can't seed `docs/ai/orchestration.json`, so it
43
- falls back to the kit's own bundled substrate, which does), `references/contracts.md`,
40
+ `references/templates/`, **`references/templates/orchestration.json`** (a pre-`1.2.0` memory can't
41
+ ship it, so it falls back to the kit's bundled substrate), `references/contracts.md`,
44
42
  `references/scripts/`, `scripts/stamp-takeover.mjs`, `migrations/`, `capability.json`. A partial
45
43
  install (manifest + `SKILL.md` only) is treated as **invalid**.
46
44
 
@@ -75,13 +73,13 @@ Before acting, read `docs/ai/.workflow-version` (the project's stamp) to decide
75
73
 
76
74
  - **absent** → bootstrap (a fresh deployment).
77
75
  - **stamp < `1.3.0`** (the deployment-lineage head) → `upgrade`.
78
- - **stamp == `1.3.0`** → already current; only the stamp-independent reconciles may run — the FULL set lives in `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 3 (pointer slots · hidden-mode footprint · both `.json` config ensures · the enforcement-script ensure · the placed-bridge refresh · the agent-rules lens refresh); run step 3 rather than enumerating from memory.
76
+ - **stamp == `1.3.0`** → already current; only the stamp-independent reconciles may run — the FULL set lives in `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 3; run step 3 (never enumerate the reconciles from memory).
79
77
  - **stamp > head / unparseable** → STOP — never-downgrade gate (see `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 2).
80
78
 
81
79
  **Two independent version axes — never conflate them:**
82
80
 
83
81
  1. **Project deployment** — `docs/ai/.workflow-version` vs the lineage head (`1.3.0`). This is the **only** axis this skill compares.
84
- 2. **Kit freshness** — this skill's own files vs the published npm package. That is the **npx installer's** job: `npx @sabaiway/agent-workflow-kit@latest init` (it refuses a stale-cache downgrade by comparing the version on disk — **no network**). This skill never checks npm, and the package version (e.g. `1.x`) is **not** the lineage head.
82
+ 2. **Kit freshness** — this skill's own files vs the published npm package. That is the **npx installer's** job: `npx @sabaiway/agent-workflow-kit@latest init` (it refuses a stale-cache downgrade by comparing the version on disk — **no network**). This skill never checks npm, and the package version is **not** the lineage head.
85
83
 
86
84
  **Refreshed the kit but nothing changed?** The skill you are running is whatever was on disk when the session started. After `npx @sabaiway/agent-workflow-kit@latest init` updates `~/.claude/skills/agent-workflow-kit/`, **restart the session** so the agent reloads the new skill files (the slash command + this `SKILL.md`).
87
85
 
@@ -125,6 +123,10 @@ read-only — read `${CLAUDE_SKILL_DIR}/references/modes/procedures.md` before a
125
123
 
126
124
  writer — read `${CLAUDE_SKILL_DIR}/references/modes/set-recipe.md` before acting.
127
125
 
126
+ ### Mode: set-autonomy
127
+
128
+ writer — read `${CLAUDE_SKILL_DIR}/references/modes/set-autonomy.md` before acting.
129
+
128
130
  ### Mode: uninstall
129
131
 
130
132
  guarded — read `${CLAUDE_SKILL_DIR}/references/modes/uninstall.md` before acting.
@@ -141,6 +143,10 @@ writer — read `${CLAUDE_SKILL_DIR}/references/modes/agents.md` before acting.
141
143
 
142
144
  writer — read `${CLAUDE_SKILL_DIR}/references/modes/hook.md` before acting.
143
145
 
146
+ ### Mode: bridge-settings
147
+
148
+ guarded — read `${CLAUDE_SKILL_DIR}/references/modes/bridge-settings.md` before acting.
149
+
144
150
  ### Mode: review-state
145
151
 
146
152
  read-only — read `${CLAUDE_SKILL_DIR}/references/modes/review-state.md` before acting.
@@ -149,6 +155,10 @@ read-only — read `${CLAUDE_SKILL_DIR}/references/modes/review-state.md` before
149
155
 
150
156
  writer — read `${CLAUDE_SKILL_DIR}/references/modes/grounding.md` before acting.
151
157
 
158
+ ### Mode: review-ledger
159
+
160
+ writer — read `${CLAUDE_SKILL_DIR}/references/modes/review-ledger.md` before acting.
161
+
152
162
  ---
153
163
 
154
164
  ## References
package/bin/install.mjs CHANGED
@@ -53,6 +53,7 @@ import { FAMILY_MEMBERS } from '../tools/family-members.mjs';
53
53
  // skip (never a first placement, AD-009/AD-011) and a placed bridge newer than this kit's bundled
54
54
  // mirror is a stated skip too (never a downgrade). Every line it returns is tool-composed.
55
55
  import { refreshPlacedBridges } from '../tools/setup-backends.mjs';
56
+ import { reconcileSettings } from '../tools/bridge-settings.mjs';
56
57
 
57
58
  const __dirname = dirname(fileURLToPath(import.meta.url));
58
59
  const PKG_ROOT = resolve(__dirname, '..');
@@ -467,6 +468,20 @@ const main = async () => {
467
468
  }
468
469
  }
469
470
 
471
+ // Bridge settings reconcile — LOCAL files only, AFTER the bridge refresh, BEFORE the network
472
+ // cascade. Reads the deployed host settings file and validates its keys against the bundled
473
+ // manifests that just landed; it NEVER edits the file (D2 — the file lives outside every kit tree,
474
+ // so upgrade-survival is structural), only STATES unknown/retired keys (preserved verbatim). Rides
475
+ // with the bridge gate: --no-bridges / win32 skip it. Best-effort — any error is a warning + exit 0.
476
+ if (!args.noBridges && process.platform !== 'win32') {
477
+ try {
478
+ const { lines } = reconcileSettings();
479
+ for (const l of lines) console.log(l);
480
+ } catch (err) {
481
+ console.warn(`[agent-workflow-kit] could not reconcile the bridge settings file (${err.message}) — continuing; the kit itself IS installed and works.`);
482
+ }
483
+ }
484
+
470
485
  // Memory substrate refresh — AFTER the kit + launchers, BEFORE the (fatal) engine and the success
471
486
  // block, so a returning `init` leaves no stale memory. Unlike the engine, a memory miss is a
472
487
  // DEGRADED success: warn with the exact recovery command + the on-disk version (read crash-proof)
@@ -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 no-JSON headless behaviour.
4
4
  metadata:
5
- version: '2.2.0'
5
+ version: '2.3.0'
6
6
  ---
7
7
 
8
8
  # antigravity-cli-bridge
@@ -88,6 +88,18 @@ yourself. Wrapper inputs: first argument is the prompt (`text`, `-` for stdin, o
88
88
  `AGY_HARD_TIMEOUT` → hard `timeout(1)` wall-clock cap (default = `AGY_TIMEOUT`); extra `agy` flags
89
89
  after `--`. Full detail: [`references/models-and-flags.md`](references/models-and-flags.md).
90
90
 
91
+ ## Settings file (host-level, survives kit upgrades)
92
+
93
+ `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf` holds `KEY=VALUE` lines,
94
+ **parsed, never sourced** — a file line can never execute code. Precedence: explicit env (even
95
+ empty — `KEY=` disables a knob for one run) > file > built-in default. File-settable keys for this
96
+ bridge: `AGY_HARD_TIMEOUT` (duration string, e.g. `5m`/`30m`) and `AGY_REVIEW_ALLOW_ADDDIR`
97
+ (`0`/`1`; arming the oversized `--add-dir` escape re-enables the Issue-001 stall risk — the hard
98
+ timeout bounds it) — exactly the manifest `settings` block (the single source; the wrapper
99
+ constants and `--help` are drift-guarded against it). Model keys are **not** file-settable. The
100
+ file lives **outside every kit-managed tree**, so a kit refresh/upgrade can never wipe it; edit it
101
+ by hand or via `/agent-workflow-kit bridge-settings` (preview-first, consent-gated).
102
+
91
103
  ## Review mode (`agy-review`)
92
104
 
93
105
  For a **code / plan / diff review**, drive the dedicated **`agy-review`** wrapper
@@ -86,6 +86,11 @@ Receipt:
86
86
  payload; a continuation receipt is fresh:false (informational-only — it cannot attest the
87
87
  folded tree); a write failure warns, never fails the review
88
88
 
89
+ Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
90
+ ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
91
+ AGY_HARD_TIMEOUT — hard wall-clock cap, duration string like 5m/30m/90s (built-in default 30m)
92
+ AGY_REVIEW_ALLOW_ADDDIR — boolean 0/1: 1 arms the oversized --add-dir escape (re-enables the Issue-001 stall risk; default 0)
93
+
89
94
  Closed grammar: unknown flags are rejected; no '--' passthrough (the only escapes are AGY_PROBE=1 and AGY_REVIEW_ALLOW_ADDDIR=1).
90
95
  Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
91
96
  HELP
@@ -93,11 +98,101 @@ HELP
93
98
  ;;
94
99
  esac
95
100
 
101
+ # This wrapper's applied settings-file subset (see the shared reader block below).
102
+ AW_SETTINGS_APPLIED="AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR"
103
+
104
+ # --- Bridge settings file (host-level, kit-independent) — byte-identical across the four wrappers ---
105
+ # ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE lines,
106
+ # PARSED (grep/case), NEVER sourced — a file line can never execute code. Precedence: explicit
107
+ # env (even empty: KEY= disables the knob for one run) > file > built-in default. Each wrapper
108
+ # APPLIES only its own subset ($AW_SETTINGS_APPLIED, set above this block) but RECOGNIZES the
109
+ # whole registry: a key belonging to another wrapper or another bridge is skipped silently; only
110
+ # a key unknown to the entire registry warns (once per key), naming this file as the source.
111
+ # A malformed line warns and is ignored; a value failing the key's typed validation warns and
112
+ # falls back to the built-in default (never passed to the binary); duplicate key → the LAST
113
+ # occurrence wins; a missing file is silent; an existing-but-unreadable or non-regular file
114
+ # warns loudly and falls back to built-in defaults (a directory or FIFO is never opened).
115
+ # Diagnostics are emitted once per user-visible run: a delegating wrapper (agy-review →
116
+ # agy-run) exports AW_SETTINGS_NOTIFIED so the child never repeats the same file's warnings.
117
+ # The registry, per-wrapper subsets, and typed constants mirror
118
+ # the bridges' capability.json `settings` blocks (manifest-as-source, drift-guarded by tests).
119
+ aw_settings_file() {
120
+ printf '%s/agent-workflow/bridge-settings.conf' "${XDG_CONFIG_HOME:-$HOME/.config}"
121
+ }
122
+ aw_settings_known() {
123
+ case " CODEX_SERVICE_TIER CODEX_HARD_TIMEOUT CODEX_REVIEW_MAX_TOTAL_BYTES AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR " in
124
+ *" $1 "*) return 0 ;;
125
+ *) return 1 ;;
126
+ esac
127
+ }
128
+ aw_settings_valid() {
129
+ local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
130
+ case "$k" in
131
+ CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
132
+ CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
133
+ CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
134
+ AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
135
+ AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
136
+ *) return 1 ;;
137
+ esac
138
+ }
139
+ aw_apply_settings() {
140
+ local file line key value warned notify
141
+ file="$(aw_settings_file)"
142
+ [[ -e "$file" ]] || return 0
143
+ notify=1
144
+ [[ -n "${AW_SETTINGS_NOTIFIED:-}" ]] && notify=0
145
+ export AW_SETTINGS_NOTIFIED=1
146
+ if [[ ! -f "$file" || ! -r "$file" ]]; then
147
+ if (( notify )); then
148
+ echo "warning: bridge settings file '$file' exists but is unreadable or not a regular file — using built-in defaults." >&2
149
+ fi
150
+ return 0
151
+ fi
152
+ if (( notify )); then
153
+ warned=" "
154
+ while IFS= read -r line || [[ -n "$line" ]]; do
155
+ [[ -z "${line//[[:space:]]/}" ]] && continue
156
+ case "${line#"${line%%[![:space:]]*}"}" in "#"*) continue ;; esac
157
+ if [[ ! "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
158
+ echo "warning: malformed line in bridge settings file '$file' (ignored): $line" >&2
159
+ continue
160
+ fi
161
+ key="${line%%=*}"
162
+ if ! aw_settings_known "$key"; then
163
+ case "$warned" in
164
+ *" $key "*) : ;;
165
+ *)
166
+ warned="$warned$key "
167
+ echo "warning: unknown key '$key' in bridge settings file '$file' (ignored)." >&2
168
+ ;;
169
+ esac
170
+ fi
171
+ done <"$file"
172
+ fi
173
+ for key in $AW_SETTINGS_APPLIED; do
174
+ if [[ -n "${!key+x}" ]]; then continue; fi
175
+ value="$(grep "^${key}=" "$file" 2>/dev/null || true)"
176
+ [[ -n "$value" ]] || continue
177
+ value="${value##*$'\n'}"
178
+ value="${value#*=}"
179
+ if ! aw_settings_valid "$key" "$value"; then
180
+ if (( notify )); then
181
+ echo "warning: invalid value '$value' for $key in bridge settings file '$file' — using the built-in default." >&2
182
+ fi
183
+ continue
184
+ fi
185
+ export "$key=$value"
186
+ done
187
+ return 0
188
+ }
189
+ aw_apply_settings
190
+
96
191
  DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
97
192
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
98
193
  # version (drift-guarded by agy-review.test.mjs against capability.json).
99
194
  AW_RECEIPT_BACKEND="agy"
100
- AW_BRIDGE_VERSION="2.2.0"
195
+ AW_BRIDGE_VERSION="2.3.0"
101
196
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
102
197
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
103
198
  # Frontier review models. ANY model is allowed; a sub-frontier one only earns a soft, silenceable warning.
@@ -637,7 +637,7 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
637
637
  // The normative fixture (docs: the AD-038 plan Decisions — copied verbatim; backend/verdict here
638
638
  // carry this bridge's vocabulary; dynamic values are asserted by shape):
639
639
  const RECEIPT_FIXTURE = JSON.parse(
640
- '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.2.0","timestamp":"2026-07-03T12:00:00Z"}',
640
+ '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z"}',
641
641
  );
642
642
  const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
643
643
  const readReceipts = (repo) => {
@@ -793,3 +793,206 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
793
793
  assert.equal(receipts.length, 0, 'no review ran — no receipt');
794
794
  });
795
795
  });
796
+
797
+ // ── bridge settings file (bridges 2.3.0) ─────────────────────────────────────────
798
+ // ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE
799
+ // lines, PARSED (never sourced). Precedence: explicit env (even empty: KEY= disables the
800
+ // knob) > file > built-in default. agy-review APPLIES AGY_HARD_TIMEOUT +
801
+ // AGY_REVIEW_ALLOW_ADDDIR and RECOGNIZES the whole registry. HOME is the sandbox home,
802
+ // so the default path is hermetic per test.
803
+
804
+ const writeSettings = (sb, text) => {
805
+ const dir = join(sb.home, '.config', 'agent-workflow');
806
+ mkdirSync(dir, { recursive: true });
807
+ const file = join(dir, 'bridge-settings.conf');
808
+ writeFileSync(file, text);
809
+ return file;
810
+ };
811
+ const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
812
+
813
+ describe('agy-review.sh — bridge settings file (bridges 2.3.0)', () => {
814
+ it('a file-set AGY_REVIEW_ALLOW_ADDDIR=1 arms the oversized --add-dir escape', () => {
815
+ const sb = makeSandbox();
816
+ writeFileSync(join(sb.repo, 'unique.txt'), `OVERSIZE_UNIQUE_MARKER\n${'x'.repeat(8000)}\n`);
817
+ writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=1\n');
818
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000' } });
819
+ rmSync(sb.home, { recursive: true, force: true });
820
+ assert.equal(r.status, 0, r.stderr);
821
+ assert.equal(r.invoked, true, 'the file-armed escape lets the run proceed');
822
+ assert.match(r.argv, /--add-dir/);
823
+ assert.match(r.stderr, /RE-ENABLES the Issue-001 stall risk/);
824
+ });
825
+
826
+ it('env overrides file: AGY_REVIEW_ALLOW_ADDDIR env=0 file=1 → the refusal stands', () => {
827
+ const sb = makeSandbox();
828
+ writeFileSync(join(sb.repo, 'unique.txt'), `MARKER\n${'x'.repeat(8000)}\n`);
829
+ writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=1\n');
830
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000', AGY_REVIEW_ALLOW_ADDDIR: '0' } });
831
+ rmSync(sb.home, { recursive: true, force: true });
832
+ assert.equal(r.status, 2, r.stderr);
833
+ assert.match(r.stderr, /over AGY_MAX_PROMPT_BYTES=2000/);
834
+ assert.equal(r.invoked, false);
835
+ });
836
+
837
+ it('an EXPLICITLY EMPTY env (AGY_REVIEW_ALLOW_ADDDIR=) disables the file knob', () => {
838
+ const sb = makeSandbox();
839
+ writeFileSync(join(sb.repo, 'unique.txt'), `MARKER\n${'x'.repeat(8000)}\n`);
840
+ writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=1\n');
841
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000', AGY_REVIEW_ALLOW_ADDDIR: '' } });
842
+ rmSync(sb.home, { recursive: true, force: true });
843
+ assert.equal(r.status, 2, 'env wins over file — empty means knob off (built-in default 0)');
844
+ assert.equal(r.invoked, false);
845
+ });
846
+
847
+ it('an invalid boolean warns and falls back to the built-in default (refusal stands)', () => {
848
+ const sb = makeSandbox();
849
+ writeFileSync(join(sb.repo, 'unique.txt'), `MARKER\n${'x'.repeat(8000)}\n`);
850
+ writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=yes\n');
851
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000' } });
852
+ rmSync(sb.home, { recursive: true, force: true });
853
+ assert.equal(r.status, 2, r.stderr);
854
+ assert.match(r.stderr, /invalid value 'yes'/);
855
+ assert.equal(r.invoked, false);
856
+ });
857
+
858
+ it('a file-set AGY_HARD_TIMEOUT flows through the agy-run delegation (killed at the file cap)', () => {
859
+ const sb = makeSandbox();
860
+ writeSettings(sb, 'AGY_HARD_TIMEOUT=2s\n');
861
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_SLEEP: '5' } });
862
+ rmSync(sb.home, { recursive: true, force: true });
863
+ assert.notEqual(r.status, 0, 'the file cap must apply end-to-end (reader → agy-run → timeout)');
864
+ assert.match(r.stderr, /exceeded the hard cap AGY_HARD_TIMEOUT=2s/);
865
+ });
866
+
867
+ it("another bridge's valid key is skipped silently", () => {
868
+ const sb = makeSandbox();
869
+ writeSettings(sb, 'CODEX_SERVICE_TIER=priority\nCODEX_HARD_TIMEOUT=2\nCODEX_REVIEW_MAX_TOTAL_BYTES=100\n');
870
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
871
+ rmSync(sb.home, { recursive: true, force: true });
872
+ assert.equal(r.status, 0, r.stderr);
873
+ assert.doesNotMatch(r.stderr, /bridge settings/, 'a recognized non-applied key earns NO warning');
874
+ assert.equal(r.invoked, true);
875
+ });
876
+
877
+ it('a truly unknown key warns ONCE naming the file; the review is unaffected', () => {
878
+ const sb = makeSandbox();
879
+ writeSettings(sb, 'TOTALLY_UNKNOWN=1\nTOTALLY_UNKNOWN=2\n');
880
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
881
+ rmSync(sb.home, { recursive: true, force: true });
882
+ assert.equal(r.status, 0, r.stderr);
883
+ const warns = r.stderr.match(/unknown key 'TOTALLY_UNKNOWN'/g) ?? [];
884
+ assert.equal(warns.length, 1, `exactly one warning per unknown key, got ${warns.length}`);
885
+ assert.match(r.stderr, /bridge-settings\.conf/, 'the warning must name the settings file');
886
+ assert.equal(r.invoked, true);
887
+ });
888
+
889
+ it('malformed lines warn and are ignored; comments and blank lines are silent', () => {
890
+ const sb = makeSandbox();
891
+ writeSettings(sb, '# a comment\n\nNOT A KEY VALUE LINE\n');
892
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
893
+ rmSync(sb.home, { recursive: true, force: true });
894
+ assert.equal(r.status, 0, r.stderr);
895
+ const malformed = r.stderr.match(/malformed line/g) ?? [];
896
+ assert.equal(malformed.length, 1, 'comments/blank lines must NOT count as malformed');
897
+ assert.equal(r.invoked, true);
898
+ });
899
+
900
+ it('an existing-but-unreadable file warns loudly and falls back to built-ins', { skip: isRoot }, () => {
901
+ const sb = makeSandbox();
902
+ const file = writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=1\n');
903
+ chmodSync(file, 0o000);
904
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
905
+ rmSync(sb.home, { recursive: true, force: true });
906
+ assert.equal(r.status, 0, r.stderr);
907
+ assert.match(r.stderr, /unreadable/);
908
+ assert.equal(r.invoked, true);
909
+ });
910
+
911
+ it('a settings line can NEVER execute code (command-substitution payload inert)', () => {
912
+ const sb = makeSandbox();
913
+ const pwned = join(sb.home, 'pwned');
914
+ writeSettings(sb, `AGY_HARD_TIMEOUT=$(touch ${pwned})\nEVIL_KEY=\`touch ${pwned}2\`\n`);
915
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
916
+ const executed = existsSync(pwned) || existsSync(`${pwned}2`);
917
+ rmSync(sb.home, { recursive: true, force: true });
918
+ assert.equal(r.status, 0, r.stderr);
919
+ assert.equal(executed, false, 'file content must be parsed, never evaluated');
920
+ assert.equal(r.invoked, true);
921
+ });
922
+
923
+ it('no file → byte-identical behaviour to today (no settings chatter)', () => {
924
+ const sb = makeSandbox();
925
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
926
+ rmSync(sb.home, { recursive: true, force: true });
927
+ assert.equal(r.status, 0, r.stderr);
928
+ assert.doesNotMatch(r.stderr, /bridge settings/);
929
+ assert.equal(r.invoked, true);
930
+ });
931
+
932
+ it('a DIRECTORY at the settings path warns loudly and falls back to built-ins (no crash)', () => {
933
+ const sb = makeSandbox();
934
+ mkdirSync(join(sb.home, '.config', 'agent-workflow', 'bridge-settings.conf'), { recursive: true });
935
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
936
+ rmSync(sb.home, { recursive: true, force: true });
937
+ assert.equal(r.status, 0, `a directory must degrade honestly, not kill the run: ${r.stderr}`);
938
+ assert.match(r.stderr, /unreadable or not a regular file/);
939
+ assert.doesNotMatch(r.stderr, /Is a directory/, 'no raw bash error may leak');
940
+ assert.equal(r.invoked, true);
941
+ });
942
+ });
943
+
944
+ // ── settings surface ⟷ manifest (drift guard, D6) — same contract as the codex bridge ──
945
+ const SETTINGS_HEADER = 'Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):';
946
+ const SIBLING_MANIFEST = JSON.parse(readFileSync(join(HERE, '..', '..', 'codex-cli-bridge', 'capability.json'), 'utf8'));
947
+ const ALL_SETTINGS = [...(MANIFEST.settings ?? []), ...(SIBLING_MANIFEST.settings ?? [])];
948
+ const SETTINGS_CMD = 'agy-review';
949
+
950
+ describe('agy-review.sh — settings surface ⟷ manifest (D6, manifest-pinned)', () => {
951
+ it('--help Settings section keys set-EQUAL the manifest appliesTo subset', () => {
952
+ const help = runHelp('--help').stdout;
953
+ const section = helpSection(help, SETTINGS_HEADER);
954
+ const got = section.filter((l) => /^[A-Z][A-Z0-9_]+ —/.test(l)).map((l) => l.split(' ')[0]);
955
+ const want = (MANIFEST.settings ?? []).filter((s) => s.appliesTo.includes(SETTINGS_CMD)).map((s) => s.key);
956
+ assert.ok(want.length > 0, 'the manifest must declare settings for this wrapper');
957
+ setEq(got, want, 'help Settings keys ⟷ manifest settings.appliesTo');
958
+ assert.ok(section.some((l) => l.includes('agent-workflow/bridge-settings.conf')), 'the section names the settings file');
959
+ });
960
+
961
+ const source = readFileSync(WRAPPER, 'utf8');
962
+
963
+ it('aw_settings_known carries exactly the UNION of both bridges settings keys', () => {
964
+ const m = source.match(/aw_settings_known\(\) \{\n case " ([^"]+) " in/);
965
+ assert.ok(m, 'aw_settings_known registry case not found');
966
+ assert.ok(ALL_SETTINGS.length >= 5, 'both manifests must contribute settings');
967
+ setEq(m[1].trim().split(/\s+/), ALL_SETTINGS.map((s) => s.key), 'shell registry ⟷ manifest union');
968
+ });
969
+
970
+ it('AW_SETTINGS_APPLIED equals the manifest appliesTo subset for this wrapper', () => {
971
+ const m = source.match(/^AW_SETTINGS_APPLIED="([^"]*)"$/m);
972
+ assert.ok(m, 'AW_SETTINGS_APPLIED not found');
973
+ const want = ALL_SETTINGS.filter((s) => s.appliesTo.includes(SETTINGS_CMD)).map((s) => s.key);
974
+ assert.ok(want.length > 0);
975
+ setEq(m[1].trim().split(/\s+/), want, 'applied subset ⟷ manifest appliesTo');
976
+ });
977
+
978
+ it('aw_settings_valid arms carry the manifest typed constants per key', () => {
979
+ const body = source.match(/aw_settings_valid\(\) \{[\s\S]*?\n\}/);
980
+ assert.ok(body, 'aw_settings_valid not found');
981
+ const armKeys = [...body[0].matchAll(/^ ([A-Z][A-Z0-9_]*)\)/gm)].map((x) => x[1]);
982
+ setEq(armKeys, ALL_SETTINGS.map((s) => s.key), 'validation arms ⟷ manifest keys');
983
+ for (const s of ALL_SETTINGS) {
984
+ const arm = body[0].match(new RegExp(`^ ${s.key}\\) (.*) ;;$`, 'm'));
985
+ assert.ok(arm, `no validation arm for ${s.key}`);
986
+ if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
987
+ if (s.kind === 'integer') {
988
+ assert.match(arm[1], new RegExp(`>= ${s.min}\\b`), `${s.key}: min ${s.min} not pinned`);
989
+ assert.match(arm[1], new RegExp(`<= ${s.max}\\b`), `${s.key}: max ${s.max} not pinned`);
990
+ }
991
+ if (s.kind === 'boolean') assert.ok(arm[1].includes('"0"') && arm[1].includes('"1"'), `${s.key}: boolean 0/1 not pinned`);
992
+ if (s.kind === 'duration') {
993
+ assert.ok(arm[1].includes('$dur_re'), `${s.key}: duration grammar not pinned`);
994
+ assert.ok(arm[1].includes('$zero_re'), `${s.key}: zero-duration rejection not pinned (timeout 0 disables the cap)`);
995
+ }
996
+ }
997
+ });
998
+ });