@sabaiway/agent-workflow-kit 1.34.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 +38 -0
- package/README.md +1 -0
- package/SKILL.md +6 -2
- package/bin/install.mjs +15 -0
- 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/package.json +1 -1
- package/references/modes/bridge-settings.md +29 -0
- package/references/modes/upgrade.md +8 -2
- package/tools/atomic-write.mjs +59 -21
- package/tools/bridge-settings-read.mjs +221 -0
- package/tools/bridge-settings.mjs +288 -0
- package/tools/commands.mjs +7 -0
- package/tools/family-registry.mjs +12 -1
- package/tools/manifest/schema.md +31 -0
- package/tools/manifest/validate.mjs +85 -0
- package/tools/procedures.mjs +26 -4
- package/tools/recipes.mjs +16 -6
- package/tools/renderers.mjs +7 -0
- package/tools/setup-backends.mjs +107 -9
- package/tools/view-model.mjs +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,44 @@ 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
|
+
|
|
7
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)
|
|
8
46
|
|
|
9
47
|
A **feature** release (first-contact flow + a new consent-gated writer; deployment-lineage head
|
package/README.md
CHANGED
|
@@ -240,6 +240,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
240
240
|
| `/agent-workflow-kit velocity` | Claude Code · opt-in | **onboarding velocity profile** — seeds a fixed, audited **read-only** allowlist into `.claude/settings.json` so routine read-only commands stop idling on approval prompts while you're away; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes **only** `.claude/settings.json` — **never** allowlists commit/push/publish, never writes `settings.local.json`, never commits. A seeded entry is a **trust posture, not a sandbox** (a runtime residual remains at the settings level — its guard ships as the opt-in `hook` command); a direct commit/push/publish still asks. `--dry-run` first. |
|
|
241
241
|
| `/agent-workflow-kit agents` | Claude Code · opt-in | **cheap-lane subagents** — places bundled subagent definitions (`.claude/agents/`) pinned to a **cheap model** (haiku, low effort, read-only tools) for mechanical work: extraction sweeps, changelog fact-skeletons, gate-failure triage. Judgment, review, and real code stay on your main lane — these vehicles only extract and draft, and their output is verified. Preview by default (`--apply` writes); an existing customized file is **preserved, never overwritten**; never touches `settings*.json`, never commits. |
|
|
242
242
|
| `/agent-workflow-kit hook` | Claude Code · opt-in | **gate-approval hook** — places a self-contained PreToolUse hook (`.claude/hooks/`) and wires it into `.claude/settings.json`: a Bash command **byte-identical** to a gate you declared in `docs/ai/gates.json` (run from the project root) is auto-approved — no prompt, no idle; a seeded read-only command carrying a runtime residual (output redirection, command substitution, `--output` writes) now **asks** even where an allow rule would have silently passed it (proven live — hook `ask` overrides an allow rule). Exact matches only, never patterns; never `deny`; a broken `gates.json` only disables auto-approval, never the guard. Preview by default; never writes `settings.local.json`; never commits. |
|
|
243
|
+
| `/agent-workflow-kit bridge-settings` | opt-in, any time | **host-level bridge settings** — read or change the bridges' `KEY=VALUE` config file (`${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf`), the **one place a knob survives kit upgrades** (it lives outside every kit tree; a refresh never touches it, and now states loudly if it ever overwrote a local edit). First knobs: the **codex Fast tier** (`CODEX_SERVICE_TIER=priority` — ~1.5× speed at a **2.5× credit rate**, quality-neutral, default off — a consented spend), the codex/agy hard timeouts, the review byte cap, the oversized-review add-dir toggle. Allowed keys + value rules come from the bundled bridge manifests; **model/effort are never settable** (the quality guard is untouched). **Previews by default**; refuses an unknown key, an out-of-range value, or a duplicate-carrying file; writes atomically (symlink/TOCTOU-safe). Never commits, never runs a subscription CLI. |
|
|
243
244
|
|
|
244
245
|
It **never auto-commits** and **never overwrites** an existing `AGENTS.md` without asking.
|
|
245
246
|
|
package/SKILL.md
CHANGED
|
@@ -3,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, '..');
|
|
@@ -467,6 +468,20 @@ const main = async () => {
|
|
|
467
468
|
}
|
|
468
469
|
}
|
|
469
470
|
|
|
471
|
+
// Bridge settings reconcile — LOCAL files only, AFTER the bridge refresh, BEFORE the network
|
|
472
|
+
// cascade. Reads the deployed host settings file and validates its keys against the bundled
|
|
473
|
+
// manifests that just landed; it NEVER edits the file (D2 — the file lives outside every kit tree,
|
|
474
|
+
// so upgrade-survival is structural), only STATES unknown/retired keys (preserved verbatim). Rides
|
|
475
|
+
// with the bridge gate: --no-bridges / win32 skip it. Best-effort — any error is a warning + exit 0.
|
|
476
|
+
if (!args.noBridges && process.platform !== 'win32') {
|
|
477
|
+
try {
|
|
478
|
+
const { lines } = reconcileSettings();
|
|
479
|
+
for (const l of lines) console.log(l);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.warn(`[agent-workflow-kit] could not reconcile the bridge settings file (${err.message}) — continuing; the kit itself IS installed and works.`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
470
485
|
// Memory substrate refresh — AFTER the kit + launchers, BEFORE the (fatal) engine and the success
|
|
471
486
|
// block, so a returning `init` leaves no stale memory. Unlike the engine, a memory miss is a
|
|
472
487
|
// DEGRADED success: warn with the exact recovery command + the on-disk version (read crash-proof)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: antigravity-cli-bridge
|
|
3
3
|
description: Delegate work to Google's Antigravity CLI (`agy`) — the successor to Gemini CLI — to reach Gemini, Claude, and GPT-OSS models under a Google AI Pro/Ultra subscription from the terminal. Use when the user wants to run a headless `agy` prompt, hand a focused task or second-opinion review to `agy`, install or authenticate Antigravity CLI, check or economise its quota/models, bridge project context into `agy`, set up a second delegated-execution backend beside Codex, or troubleshoot `agy` flags, models, auth, conversations, or its no-JSON headless behaviour.
|
|
4
4
|
metadata:
|
|
5
|
-
version: '2.
|
|
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
|
+
});
|
|
@@ -50,6 +50,10 @@ Usage:
|
|
|
50
50
|
agy-run @path/to/prompt.md
|
|
51
51
|
agy-run <prompt|-|@file> -- <extra agy flags...>
|
|
52
52
|
|
|
53
|
+
Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
|
|
54
|
+
${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
|
|
55
|
+
AGY_HARD_TIMEOUT — hard wall-clock cap, duration string like 5m/30m/90s (built-in default = AGY_TIMEOUT, 5m)
|
|
56
|
+
|
|
53
57
|
Environment: AGY_MODEL (exact display string from `agy models`; empty ⇒ agy's settings.json), AGY_TIMEOUT / AGY_HARD_TIMEOUT (duration strings), AGY_MAX_PROMPT_BYTES (single-argv byte ceiling; the override only lowers it).
|
|
54
58
|
Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
|
|
55
59
|
HELP
|
|
@@ -68,6 +72,96 @@ while IFS= read -r _api_key_var; do
|
|
|
68
72
|
unset "$_api_key_var" 2>/dev/null || true
|
|
69
73
|
done < <(compgen -v 2>/dev/null | grep '_API_KEY$' || true)
|
|
70
74
|
|
|
75
|
+
# This wrapper's applied settings-file subset (see the shared reader block below).
|
|
76
|
+
AW_SETTINGS_APPLIED="AGY_HARD_TIMEOUT"
|
|
77
|
+
|
|
78
|
+
# --- Bridge settings file (host-level, kit-independent) — byte-identical across the four wrappers ---
|
|
79
|
+
# ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE lines,
|
|
80
|
+
# PARSED (grep/case), NEVER sourced — a file line can never execute code. Precedence: explicit
|
|
81
|
+
# env (even empty: KEY= disables the knob for one run) > file > built-in default. Each wrapper
|
|
82
|
+
# APPLIES only its own subset ($AW_SETTINGS_APPLIED, set above this block) but RECOGNIZES the
|
|
83
|
+
# whole registry: a key belonging to another wrapper or another bridge is skipped silently; only
|
|
84
|
+
# a key unknown to the entire registry warns (once per key), naming this file as the source.
|
|
85
|
+
# A malformed line warns and is ignored; a value failing the key's typed validation warns and
|
|
86
|
+
# falls back to the built-in default (never passed to the binary); duplicate key → the LAST
|
|
87
|
+
# occurrence wins; a missing file is silent; an existing-but-unreadable or non-regular file
|
|
88
|
+
# warns loudly and falls back to built-in defaults (a directory or FIFO is never opened).
|
|
89
|
+
# Diagnostics are emitted once per user-visible run: a delegating wrapper (agy-review →
|
|
90
|
+
# agy-run) exports AW_SETTINGS_NOTIFIED so the child never repeats the same file's warnings.
|
|
91
|
+
# The registry, per-wrapper subsets, and typed constants mirror
|
|
92
|
+
# the bridges' capability.json `settings` blocks (manifest-as-source, drift-guarded by tests).
|
|
93
|
+
aw_settings_file() {
|
|
94
|
+
printf '%s/agent-workflow/bridge-settings.conf' "${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
95
|
+
}
|
|
96
|
+
aw_settings_known() {
|
|
97
|
+
case " CODEX_SERVICE_TIER CODEX_HARD_TIMEOUT CODEX_REVIEW_MAX_TOTAL_BYTES AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR " in
|
|
98
|
+
*" $1 "*) return 0 ;;
|
|
99
|
+
*) return 1 ;;
|
|
100
|
+
esac
|
|
101
|
+
}
|
|
102
|
+
aw_settings_valid() {
|
|
103
|
+
local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
|
|
104
|
+
case "$k" in
|
|
105
|
+
CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
|
|
106
|
+
CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
|
|
107
|
+
CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
|
|
108
|
+
AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
|
|
109
|
+
AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
|
|
110
|
+
*) return 1 ;;
|
|
111
|
+
esac
|
|
112
|
+
}
|
|
113
|
+
aw_apply_settings() {
|
|
114
|
+
local file line key value warned notify
|
|
115
|
+
file="$(aw_settings_file)"
|
|
116
|
+
[[ -e "$file" ]] || return 0
|
|
117
|
+
notify=1
|
|
118
|
+
[[ -n "${AW_SETTINGS_NOTIFIED:-}" ]] && notify=0
|
|
119
|
+
export AW_SETTINGS_NOTIFIED=1
|
|
120
|
+
if [[ ! -f "$file" || ! -r "$file" ]]; then
|
|
121
|
+
if (( notify )); then
|
|
122
|
+
echo "warning: bridge settings file '$file' exists but is unreadable or not a regular file — using built-in defaults." >&2
|
|
123
|
+
fi
|
|
124
|
+
return 0
|
|
125
|
+
fi
|
|
126
|
+
if (( notify )); then
|
|
127
|
+
warned=" "
|
|
128
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
129
|
+
[[ -z "${line//[[:space:]]/}" ]] && continue
|
|
130
|
+
case "${line#"${line%%[![:space:]]*}"}" in "#"*) continue ;; esac
|
|
131
|
+
if [[ ! "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
|
|
132
|
+
echo "warning: malformed line in bridge settings file '$file' (ignored): $line" >&2
|
|
133
|
+
continue
|
|
134
|
+
fi
|
|
135
|
+
key="${line%%=*}"
|
|
136
|
+
if ! aw_settings_known "$key"; then
|
|
137
|
+
case "$warned" in
|
|
138
|
+
*" $key "*) : ;;
|
|
139
|
+
*)
|
|
140
|
+
warned="$warned$key "
|
|
141
|
+
echo "warning: unknown key '$key' in bridge settings file '$file' (ignored)." >&2
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
144
|
+
fi
|
|
145
|
+
done <"$file"
|
|
146
|
+
fi
|
|
147
|
+
for key in $AW_SETTINGS_APPLIED; do
|
|
148
|
+
if [[ -n "${!key+x}" ]]; then continue; fi
|
|
149
|
+
value="$(grep "^${key}=" "$file" 2>/dev/null || true)"
|
|
150
|
+
[[ -n "$value" ]] || continue
|
|
151
|
+
value="${value##*$'\n'}"
|
|
152
|
+
value="${value#*=}"
|
|
153
|
+
if ! aw_settings_valid "$key" "$value"; then
|
|
154
|
+
if (( notify )); then
|
|
155
|
+
echo "warning: invalid value '$value' for $key in bridge settings file '$file' — using the built-in default." >&2
|
|
156
|
+
fi
|
|
157
|
+
continue
|
|
158
|
+
fi
|
|
159
|
+
export "$key=$value"
|
|
160
|
+
done
|
|
161
|
+
return 0
|
|
162
|
+
}
|
|
163
|
+
aw_apply_settings
|
|
164
|
+
|
|
71
165
|
if ! command -v agy >/dev/null 2>&1; then
|
|
72
166
|
echo "error: 'agy' (Antigravity CLI) not found on PATH. Install it and run 'agy' once to sign in." >&2
|
|
73
167
|
exit 127
|