@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.
- package/CHANGELOG.md +99 -0
- package/README.md +2 -1
- package/SKILL.md +6 -2
- package/bin/install.mjs +37 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +13 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +96 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +204 -1
- package/bridges/antigravity-cli-bridge/bin/agy.sh +94 -0
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +245 -1
- package/bridges/antigravity-cli-bridge/capability.json +17 -1
- package/bridges/codex-cli-bridge/SKILL.md +15 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +113 -0
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +288 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +114 -1
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +229 -1
- package/bridges/codex-cli-bridge/capability.json +29 -1
- package/capability.json +1 -1
- package/launchers/windsurf-workflow.md +3 -1
- package/package.json +1 -1
- package/references/contracts.md +3 -2
- package/references/modes/bootstrap.md +11 -6
- package/references/modes/bridge-settings.md +29 -0
- package/references/modes/gates.md +4 -2
- package/references/modes/review-state.md +1 -1
- package/references/modes/status.md +2 -0
- package/references/modes/upgrade.md +11 -5
- package/references/shared/deploy-tail.md +1 -1
- package/references/shared/report-footer.md +11 -4
- package/tools/atomic-write.mjs +144 -0
- package/tools/bridge-settings-read.mjs +221 -0
- package/tools/bridge-settings.mjs +288 -0
- package/tools/commands.mjs +22 -1
- package/tools/family-registry.mjs +45 -2
- package/tools/manifest/schema.md +31 -0
- package/tools/manifest/validate.mjs +85 -0
- package/tools/orchestration-write.mjs +8 -78
- package/tools/presentation.mjs +1 -0
- package/tools/procedures.mjs +26 -4
- package/tools/recipes.mjs +16 -6
- package/tools/renderers.mjs +17 -2
- package/tools/review-state.mjs +4 -2
- package/tools/seed-gates.mjs +413 -0
- package/tools/setup-backends.mjs +107 -9
- package/tools/velocity-profile.mjs +4 -0
- 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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|