@sabaiway/agent-workflow-kit 1.33.0 → 1.35.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 (45) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +2 -1
  3. package/SKILL.md +6 -2
  4. package/bin/install.mjs +37 -1
  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/launchers/windsurf-workflow.md +3 -1
  19. package/package.json +1 -1
  20. package/references/contracts.md +3 -2
  21. package/references/modes/bootstrap.md +11 -6
  22. package/references/modes/bridge-settings.md +29 -0
  23. package/references/modes/gates.md +4 -2
  24. package/references/modes/review-state.md +1 -1
  25. package/references/modes/status.md +2 -0
  26. package/references/modes/upgrade.md +11 -5
  27. package/references/shared/deploy-tail.md +1 -1
  28. package/references/shared/report-footer.md +11 -4
  29. package/tools/atomic-write.mjs +144 -0
  30. package/tools/bridge-settings-read.mjs +221 -0
  31. package/tools/bridge-settings.mjs +288 -0
  32. package/tools/commands.mjs +22 -1
  33. package/tools/family-registry.mjs +45 -2
  34. package/tools/manifest/schema.md +31 -0
  35. package/tools/manifest/validate.mjs +85 -0
  36. package/tools/orchestration-write.mjs +8 -78
  37. package/tools/presentation.mjs +1 -0
  38. package/tools/procedures.mjs +26 -4
  39. package/tools/recipes.mjs +16 -6
  40. package/tools/renderers.mjs +17 -2
  41. package/tools/review-state.mjs +4 -2
  42. package/tools/seed-gates.mjs +413 -0
  43. package/tools/setup-backends.mjs +107 -9
  44. package/tools/velocity-profile.mjs +4 -0
  45. package/tools/view-model.mjs +20 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,105 @@ 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.35.0 — Host-level bridge settings surface + Codex Fast tier as configuration (AD-043)
8
+
9
+ A **feature** release (deployment-lineage head stays `1.3.0` — no migration). Bridge knobs like the
10
+ Codex Fast tier are now enabled **through a host-level settings file that survives kit upgrades**,
11
+ never re-researched and re-patched per host; the kit surfaces, reconciles, and honestly refreshes
12
+ that surface. Ships **bridges 2.3.0** inside the tarball (the four wrappers' shared settings-reader
13
+ block + the `CODEX_SERVICE_TIER` knob + the typed `settings` manifest schema, Phase 1). The kit
14
+ machinery (Phase 2):
15
+
16
+ - **`bridge-settings` mode — the host-config reader + consent-gated writer.** New
17
+ `tools/bridge-settings.mjs` (+ its read-only core `tools/bridge-settings-read.mjs`) reads/writes
18
+ `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf` — a `KEY=VALUE` file **outside
19
+ every kit tree** (D2, upgrade-survival is structural). Previews by default, `--apply` writes via a
20
+ hardened out-of-tree atomic core (`writeContainedFileAtomic` / `writeHostConfigFileAtomic`,
21
+ factored from `atomic-write.mjs` — symlink/parent/TOCTOU-safe, dir created on first use). The
22
+ allowlist + typed validation come from the **bundled bridge manifests** (`settings` blocks,
23
+ manifest-as-source / D6 — `settingValueValid` is now the single shared predicate). It refuses an
24
+ unknown key, an out-of-range/invalid value, and — loudly, naming the key — a duplicate-carrying
25
+ file; model/effort stay unsettable (the quality guard is untouched, D4). Routed in `SKILL.md`
26
+ (`guarded` kind) with `references/modes/bridge-settings.md`.
27
+ - **Refresh overwrite honesty (D5).** `tools/setup-backends.mjs` now byte-compares a placed bridge
28
+ against the bundle on an equal-version re-sync and **states** the local edits it overwrote (file
29
+ list + the settings-file pointer), instead of the old silent wipe; a version upgrade never cries
30
+ wolf about the version delta, an unreadable placed file degrades honestly, a placed-only extra is
31
+ preserved and never claimed as loss.
32
+ - **Init/upgrade reconcile.** `init` (`bin/install.mjs`) and `Mode: upgrade` run
33
+ `bridge-settings --reconcile` after the bridge refresh: every settings-file key is validated
34
+ against the new bundled manifests; an unknown/retired key is a loud flag, **preserved verbatim**
35
+ (never edited — the lens-region posture).
36
+ - **Status + advisor surfaces (fact-only).** `status`' execution-backends block, the `procedures`
37
+ driving-contract render, and `recipes --status-line` now surface the active knobs (env>file>default)
38
+ and each wrapper's settable knobs — fact-only, no model claim, localized-on-error; the status line
39
+ stays byte-identical unless a knob is active.
40
+ - Parity + budgets: new `test/settings-reader-parity.test.mjs` pins the reader block byte-identical
41
+ across the four wrappers; the always-loaded router stays `≤ 10240 B` (the equal-head reconcile
42
+ enumeration was trimmed to its step-3 pointer to make room for the new mode); the `routerPlusMode`
43
+ budget is re-pinned `29696 → 30720` (documented) for the reconcile paragraph added to `upgrade.md`.
44
+
45
+ ## 1.34.0 — Onboarding UX: one batched setup prompt, honest installer messaging, the visible accelerator funnel, and the consent-gated gates seeder (AD-042)
46
+
47
+ A **feature** release (first-contact flow + a new consent-gated writer; deployment-lineage head
48
+ stays `1.3.0` — no migration). First contact now interrupts once instead of three times, the
49
+ opt-in accelerators are discoverable from every happy path, and a project's own verification
50
+ commands can be seeded into `docs/ai/gates.json` behind an explicit per-entry yes:
51
+
52
+ - **F11 — ONE batched setup prompt.** Bootstrap asks the three setup questions (visibility /
53
+ conversational language / attribution) as one structured multi-question prompt where supported
54
+ (`AskUserQuestion`, up to 4 questions per call), records each answer individually, and writes
55
+ nothing until all are answered (`references/modes/bootstrap.md` preamble + steps 2–4;
56
+ `references/contracts.md` · `references/shared/deploy-tail.md` · `launchers/windsurf-workflow.md`
57
+ reworded to match). Upgrade batches its two migration asks the same way ONLY when BOTH
58
+ `AGENTS.md` blocks are missing (a pre-1.1.0 deployment), collects them in step 6 BEFORE the
59
+ migrations apply, and never re-asks a collected answer (`references/modes/upgrade.md`); the four
60
+ migration files are untouched — their own ask stays the standalone fallback. New
61
+ `test/ask-contract.test.mjs` pins the wording across all 7 files and holds the kit↔memory
62
+ `references/contracts.md` ask paragraph byte-identical (a hand-lockstep pair, deliberately not a
63
+ sync-mirrors family).
64
+ - **F12 — the installer says what a returning user needs.** `bin/install.mjs` prints a restart
65
+ hint on every run over a PRE-existing install ("restart the session so the agent reloads the
66
+ refreshed kit files") — on the verb path, at most once per run, surviving even the fatal
67
+ engine-install abort — and replaces the false "Claude Code / Codex / Devin Desktop all use the
68
+ same /agent-workflow-kit" claim with the real per-agent matrix (Codex invokes via its `/skills`
69
+ menu and may auto-trigger) in `--help` AND the final next-steps block. Pinned in
70
+ `bin/install.test.mjs`.
71
+ - **F10a — the opt-in funnel is visible.** The welcome-mat ladder
72
+ (`references/shared/report-footer.md`) gains caveat-aware rungs: velocity when the allowlist is
73
+ unseeded, `agents` when no cheap-lane vehicle is placed, `hook` when gates are DECLARED
74
+ (non-empty — file presence alone would misfire on the empty seed) but the hook is unwired; the
75
+ two new signals ride the existing status envelope (`tools/family-registry.mjs`
76
+ `surveyCheapAgents` + `surveyGateHook.declaredGates` → `tools/view-model.mjs` →
77
+ `tools/renderers.mjs` — no new helper call). Bootstrap step 11 ends on a compact
78
+ optional-accelerators block (velocity · agents · gates seeding + hook · set-recipe;
79
+ preview-first, nothing runs without a yes), and `help` output gains a matching "Tune" tail
80
+ (`tools/commands.mjs` — no new mode, no new kind; the router SKILL.md is untouched). New
81
+ `test/report-footer-rotation.test.mjs` pins one shorthand ladder literal across
82
+ upgrade + bootstrap; the `gate-approve-hook` fixture is refreshed to the live 10-gate shape.
83
+ - **F10b — the consent-gated `gates.json` seeder (the seeding↔hook trust chain).** New
84
+ `tools/seed-gates.mjs`: dry-run by default (prints the derived `{ id, title, cmd }` entries,
85
+ writes NOTHING; declining leaves the file byte-identical); `--apply [--only <id>]…` appends
86
+ exactly the consented entries — append-only (never modifies or removes an existing entry),
87
+ id-collision refusal, validator-checked (it imports the runner's `validateDeclaration`; the
88
+ runner never imports it), stamp-gated apply, and OUTSIDE every velocity tier (a consent-per-run
89
+ writer is never pre-approved). Offered candidates are terminating verification classes only
90
+ (test / lint / type-check / build) — release/publish/deploy scripts, watch/serve modes, and
91
+ MUTATING variants (`lint:fix`, `test:update`, bodies carrying `--fix`/`--write`/`-w`/`-u`)
92
+ never enter the offer; commands are package-manager-aware (npm/pnpm/yarn). The review-state
93
+ candidate is included ONLY when `docs/ai/orchestration.json` declares reviewed/council on
94
+ `plan-execution.review` (the slot the checker enforces), with the resolved QUOTED path. Every
95
+ preview prints the trust-chain disclosure: the hook auto-approves byte-exact declared commands —
96
+ seeding and hook wiring are two separate consents. The hardened atomic write core is extracted
97
+ into `tools/atomic-write.mjs` (exclusive-create tmp+rename, TOCTOU re-check, symlink STOPs) and
98
+ shared with `tools/orchestration-write.mjs` (public API unchanged). The consent-seed protocol
99
+ lives in `references/modes/gates.md`; `references/modes/review-state.md` step 3 now names the
100
+ seeder path ("by hand OR the explicit-consent seeder — never without consent").
101
+ - **AD-039 amendment (documented, AD-042):** `test/router-contract.test.mjs` `routerPlusMode`
102
+ 28672 → 29696 — the F11 upgrade batching caveat is +422 B of new contract text against 154 B of
103
+ headroom; the router itself is byte-identical. Tarball 116 → 118 files
104
+ (`test/package-content.test.mjs` count + payload pins for the seeder pair).
105
+
7
106
  ## 1.33.0 — The agent-rules lens region: render + reconcile from the engine canon (AD-041)
8
107
 
9
108
  A **feature** release (new shipped tool + wiring; deployment-lineage head stays `1.3.0` — the
package/README.md CHANGED
@@ -234,12 +234,13 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
234
234
  | `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents four named recipes for composing the bridges into plan → execute → review — **Solo / Reviewed / Council / Delegated** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready), and offers the choice. The orchestrator runs it via the bridge skills and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
235
235
  | `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + backend readiness (default Reviewed when a backend is ready, Council on request, slot-aware incl. Delegated) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same). `--override <slot>=<recipe>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
236
236
  | `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<recipe>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend, never commits**; hand-editing stays fully supported. |
237
- | `/agent-workflow-kit review-state` | any time | **read-only review-receipt checker** — makes "reviewed ≠ shipped" detectable: the bridge review wrappers append a receipt per successful review (into a file inside the git dir — never committable); this checks that every backend your configured `plan-execution.review` recipe names holds a **fresh, grounded receipt for the current uncommitted tree** (any later edit moves the fingerprint and stales the receipt; a review continuation never re-attests a folded tree). `--check` gives a gate exit code to declare in `docs/ai/gates.json` **by hand** (never auto-seeded). Never writes, never commits, never runs a subscription CLI; it spawns read-only `git` queries to fingerprint the tree — and `git commit --no-verify` stays possible (discipline, not a sandbox). |
237
+ | `/agent-workflow-kit review-state` | any time | **read-only review-receipt checker** — makes "reviewed ≠ shipped" detectable: the bridge review wrappers append a receipt per successful review (into a file inside the git dir — never committable); this checks that every backend your configured `plan-execution.review` recipe names holds a **fresh, grounded receipt for the current uncommitted tree** (any later edit moves the fingerprint and stales the receipt; a review continuation never re-attests a folded tree). `--check` gives a gate exit code to declare in `docs/ai/gates.json` **by hand or via the consent-gated seeder** (preview → your explicit yes; never without consent). Never writes, never commits, never runs a subscription CLI; it spawns read-only `git` queries to fingerprint the tree — and `git commit --no-verify` stays possible (discipline, not a sandbox). |
238
238
  | `/agent-workflow-kit grounding` | any time | **grounded-review facts assembler** — mechanizes populating `agy-review --facts @f`: slices your entry-point's **Hard Constraints** section verbatim (exactly one match, else a loud stop) and/or a plan's decision-bearing sections (`## Approach` + `## Verification` required, `## Decisions (locked)` when present; duplicates stop), under the same byte budget the agy wrapper enforces (minus `--reserve-bytes` for the artifact share), with a loud tail-trim on overflow. Prints to stdout; `--out` writes **one scratch file only** (gitignored / outside the repo — a tracked or not-ignored in-repo path is refused). Never commits, never runs a subscription CLI. |
239
239
  | `/agent-workflow-kit uninstall` | opt-in, any time | **guarded teardown** — the inverse of `init` / `setup`. Removes only what's **provably ours** (managed skill dirs + bridge wrappers; in a project, the hidden-mode git-ignore block it added + the pre-commit hook it installed); **never deletes** your `docs/ai` / `AGENTS.md` (prints the exact `rm` to run by hand) or your `.claude/settings.json` (prints an **edit** — remove the attribution key, review any velocity `permissions.*` — never an `rm`). Always `--dry-run` first; preflight-then-mutate; never commits. |
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,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".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '1.33.0'
6
+ version: '1.35.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -75,7 +75,7 @@ Before acting, read `docs/ai/.workflow-version` (the project's stamp) to decide
75
75
 
76
76
  - **absent** → bootstrap (a fresh deployment).
77
77
  - **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.
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; run step 3 (never enumerate the reconciles from memory).
79
79
  - **stamp > head / unparseable** → STOP — never-downgrade gate (see `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 2).
80
80
 
81
81
  **Two independent version axes — never conflate them:**
@@ -141,6 +141,10 @@ writer — read `${CLAUDE_SKILL_DIR}/references/modes/agents.md` before acting.
141
141
 
142
142
  writer — read `${CLAUDE_SKILL_DIR}/references/modes/hook.md` before acting.
143
143
 
144
+ ### Mode: bridge-settings
145
+
146
+ guarded — read `${CLAUDE_SKILL_DIR}/references/modes/bridge-settings.md` before acting.
147
+
144
148
  ### Mode: review-state
145
149
 
146
150
  read-only — read `${CLAUDE_SKILL_DIR}/references/modes/review-state.md` before acting.
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, '..');
@@ -303,7 +304,8 @@ Installs/refreshes the kit at ~/.claude/skills/agent-workflow-kit
303
304
  After install, invoke the skill in your agent, inside a project:
304
305
  first time in the project -> /agent-workflow-kit
305
306
  project already has it -> /agent-workflow-kit upgrade
306
- (Claude Code / Codex / Devin Desktop all use the same /agent-workflow-kit.)
307
+ Per agent: Claude Code -> /agent-workflow-kit; Devin Local -> /agent-workflow-kit;
308
+ Codex -> its /skills menu -> agent-workflow-kit (Codex may also auto-trigger it).
307
309
 
308
310
  Re-running this npx command updates the kit's own files; /agent-workflow-kit
309
311
  upgrade then migrates a project's deployment to that version.`);
@@ -388,6 +390,23 @@ const main = async () => {
388
390
  : 'installed';
389
391
  console.log(`[agent-workflow-kit] ${verb} v${version} -> ${tildify(target)}`);
390
392
 
393
+ // F12 restart hint — any run over a PRE-EXISTING install (every cmp outcome, incl. the legacy
394
+ // unstamped one) leaves an already-open agent session still holding the PREVIOUS kit files in
395
+ // memory (mirrors the SKILL.md "restart the session" note the memory-miss paths already carry).
396
+ // Printed AT MOST ONCE per run: here on the verb path — so it survives the fatal engine abort
397
+ // below — with the final next-steps block as the carrier only if this line did not print.
398
+ const restartHintState = { pending: wasPresent };
399
+ const printRestartHint = () => {
400
+ if (!restartHintState.pending) return;
401
+ restartHintState.pending = false;
402
+ console.log(
403
+ `[agent-workflow-kit] the kit was already installed here — an agent session that was ` +
404
+ `already open is still running the previous kit files; restart the session so the agent ` +
405
+ `reloads the refreshed kit files.`,
406
+ );
407
+ };
408
+ printRestartHint();
409
+
391
410
  // Same-version re-run: state observable facts only. The copy DID run (repair-on-rerun is a feature —
392
411
  // it restores locally modified/deleted files), and whether npx served a cached build is NOT
393
412
  // observable here (no network), so the note never claims it; the @latest hint is conditional.
@@ -449,6 +468,20 @@ const main = async () => {
449
468
  }
450
469
  }
451
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
+
452
485
  // Memory substrate refresh — AFTER the kit + launchers, BEFORE the (fatal) engine and the success
453
486
  // block, so a returning `init` leaves no stale memory. Unlike the engine, a memory miss is a
454
487
  // DEGRADED success: warn with the exact recovery command + the on-disk version (read crash-proof)
@@ -506,10 +539,13 @@ const main = async () => {
506
539
  // This command (de)installed the *kit* globally. Deploying it into a project is a
507
540
  // separate, in-agent step — and which sub-command depends on whether that project
508
541
  // already has the kit. Spell both out so it's unambiguous (see README "Use").
542
+ printRestartHint(); // carrier fallback — a no-op when the verb path already printed it
509
543
  console.log(`
510
544
  Next — open your agent inside a project and run the skill:
511
545
  • first time in this project -> /agent-workflow-kit
512
546
  • project already has the kit -> /agent-workflow-kit upgrade
547
+ • per agent: Claude Code -> /agent-workflow-kit · Devin Local -> /agent-workflow-kit ·
548
+ Codex -> its /skills menu -> agent-workflow-kit (Codex may also auto-trigger it)
513
549
 
514
550
  This command only installs/updates the kit itself (in ${tildify(target)}).
515
551
  To update the kit later, re-run: npx @sabaiway/agent-workflow-kit@latest init`);
@@ -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
+ });