@sabaiway/agent-workflow-kit 3.1.0 → 3.2.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 +33 -0
- package/SKILL.md +3 -3
- package/bridges/antigravity-cli-bridge/SKILL.md +6 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +84 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +113 -0
- package/bridges/antigravity-cli-bridge/bin/agy.sh +55 -6
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +18 -0
- package/bridges/antigravity-cli-bridge/capability.json +4 -2
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +5 -0
- package/bridges/codex-cli-bridge/SKILL.md +8 -4
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +129 -11
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +211 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +76 -13
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +47 -0
- package/bridges/codex-cli-bridge/capability.json +19 -4
- package/bridges/codex-cli-bridge/references/driving-codex.md +11 -0
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +3 -2
- package/references/modes/upgrade.md +9 -4
- package/references/modes/velocity.md +2 -0
- package/references/shared/command-shapes.md +22 -0
- package/references/shared/composition-handoff.md +7 -2
- package/references/templates/agent_rules.md +10 -1
- package/tools/bridge-settings-read.mjs +25 -5
- package/tools/delegation.mjs +2 -2
- package/tools/detect-backends.mjs +10 -0
- package/tools/lens-region.mjs +113 -36
- package/tools/release-scan.mjs +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ 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
|
+
## 3.2.0 — plain language + posture as code: the friction cluster shipped as mechanism (AD-061)
|
|
8
|
+
|
|
9
|
+
Carrier release for a friction cluster (kit 3.2.0 bundles the bridges + shared contract + the
|
|
10
|
+
plain-language template bar; memory 3.1.0, codex bridge 3.1.0, agy bridge 4.1.0 ride alongside;
|
|
11
|
+
engine unbumped). The packaged fixes below ship as MECHANISM for every user, never as one agent's
|
|
12
|
+
discipline.
|
|
13
|
+
|
|
14
|
+
- **Shared command-shapes contract.** New `references/shared/command-shapes.md` states the
|
|
15
|
+
promptless bar for improvised reads/probes — the host's file-read tool, else ONE plain
|
|
16
|
+
undecorated command (no compounds, redirects, pipes, or command substitution; improvised writes
|
|
17
|
+
use the host's file-edit tools). Bound via the existing `Requires:` mechanism to exactly the
|
|
18
|
+
probe-instructing modes (bootstrap / upgrade / velocity, closed-world pinned) + a one-sentence
|
|
19
|
+
inline bar on the SKILL.md router stamp-read. The honest residual is stated (a decorated form is
|
|
20
|
+
host/config-dependent) — never a false "always prompts" guarantee.
|
|
21
|
+
- **Plain-language §2.5 Communication bar** in BOTH deployed `agent_rules` templates,
|
|
22
|
+
byte-identically (the kit fallback deploy path communicates under the same bar): user-facing
|
|
23
|
+
narration is short and plain in the dialogue language; transliterated jargon is banned; an
|
|
24
|
+
English term survives only as the NAME of a thing (flag / command / file / test), glossed.
|
|
25
|
+
`lens-region.mjs` now reconciles a SECOND region (Communication, canon from the kit's own bundled
|
|
26
|
+
template) so EXISTING deployments gain the bar on `upgrade`, not only new bootstraps.
|
|
27
|
+
- **Posture as code on the codex-exec dispatch and both review dispatches** (bridges bundled here).
|
|
28
|
+
`codex-exec` gains a D5 exec posture banner (fresh + resume, resolved post-validation values, a
|
|
29
|
+
validated resume-id grammar + raw-byte NUL screen, control-byte refusal on every field). Both
|
|
30
|
+
review banners gain a BANNER-ONLY `timeout` field — outside D5 banner↔receipt parity — from ONE
|
|
31
|
+
shared effective-timeout resolver (byte-identical across the four wrappers) that closes the old
|
|
32
|
+
env-validation bypass and resolves the timeout binary shadow-proof; no capping binary →
|
|
33
|
+
`timeout=uncapped`, never fabricated. (The raw `agy-run` probe lane keeps no posture banner —
|
|
34
|
+
out of scope this release.) The quote-the-banner-verbatim duty rides both bridges' driving
|
|
35
|
+
contracts, capability notes, and the kit registry mirror, drift-guarded by a new cross-file test;
|
|
36
|
+
the threat-model boundary is stated on the codex execute capability and `codex-exec --help`
|
|
37
|
+
surfaces, including the kit mirror. The read-only `bridge-settings` advisor mirrors the wrapper's
|
|
38
|
+
control-byte refusal semantics with escaped output.
|
|
39
|
+
|
|
7
40
|
## 3.1.0 — parallel feature worktrees v1: provision · list · land --prepare · cleanup (AD-060)
|
|
8
41
|
|
|
9
42
|
New `worktrees` mode + `tools/worktrees.mjs` — several features implemented simultaneously in
|
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: '3.
|
|
6
|
+
version: '3.2.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
|
@@ -68,7 +68,7 @@ Pick the mode from the user's invocation — the mapping is pinned by `tools/com
|
|
|
68
68
|
|
|
69
69
|
**Safe-routing rule (which mode did the user invoke?).** Map the invocation token with `tools/commands.mjs` `routeInvocation`: a **known** subcommand → its mode; the **bare/empty** invocation → `bootstrap` — the one writer reachable without a token, and only on an undeployed project (if `docs/ai/` already exists, **ask upgrade-vs-bootstrap**, never overwrite); **any unrecognized/ambiguous** token → `help`, which is **read-only**. The invariant: **no unrecognized/garbage invocation ever triggers a write** (only an explicit known token or the acknowledged bare-bootstrap exception can).
|
|
70
70
|
|
|
71
|
-
Before acting, read `docs/ai/.workflow-version` (the project's stamp) to decide the route — an **internal** routing decision, **not** a line you print (version display: *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Route:
|
|
71
|
+
Before acting, read `docs/ai/.workflow-version` (the project's stamp) — via the host's file-read tool, else ONE plain undecorated command (no compounds/redirects/pipes) — to decide the route — an **internal** routing decision, **not** a line you print (version display: *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Route:
|
|
72
72
|
|
|
73
73
|
- **absent** → bootstrap (a fresh deployment).
|
|
74
74
|
- **stamp < `3.0.0`** (the deployment-lineage head) → `upgrade`.
|
|
@@ -195,7 +195,7 @@ writer — read `${CLAUDE_SKILL_DIR}/references/modes/worktrees.md` before actin
|
|
|
195
195
|
|
|
196
196
|
## References
|
|
197
197
|
|
|
198
|
-
- [`references/modes/`](${CLAUDE_SKILL_DIR}/references/modes/) — one file per mode; [`references/shared/`](${CLAUDE_SKILL_DIR}/references/shared/) — the shared point-of-use contracts (report footer · composition hand-off · deploy tail).
|
|
198
|
+
- [`references/modes/`](${CLAUDE_SKILL_DIR}/references/modes/) — one file per mode; [`references/shared/`](${CLAUDE_SKILL_DIR}/references/shared/) — the shared point-of-use contracts (report footer · composition hand-off · deploy tail · command shapes).
|
|
199
199
|
- [`references/contracts.md`](${CLAUDE_SKILL_DIR}/references/contracts.md) — the three setup contracts.
|
|
200
200
|
- [`references/templates/`](${CLAUDE_SKILL_DIR}/references/templates/) — the `AGENTS.md` + `docs/ai/` templates + seeded configs; [`references/scripts/`](${CLAUDE_SKILL_DIR}/references/scripts/) — the Node enforcement scripts; [`migrations/`](${CLAUDE_SKILL_DIR}/migrations/) — per-version upgrade steps; [`launchers/`](${CLAUDE_SKILL_DIR}/launchers/) — non-Claude agent launchers.
|
|
201
201
|
- [`tools/`](${CLAUDE_SKILL_DIR}/tools/) — the family-wide tooling; each tool's contract lives with its mode file and its own header; bridge mirrors under [`bridges/`](${CLAUDE_SKILL_DIR}/bridges/).
|
|
@@ -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: '4.
|
|
5
|
+
version: '4.1.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# antigravity-cli-bridge
|
|
@@ -134,10 +134,12 @@ agy-review --continue --decided @round1.md --focus "still-open items" # round-
|
|
|
134
134
|
|
|
135
135
|
**Honesty + posture (D4/D5):** a run with no recognized `### Verdict` section — empty output
|
|
136
136
|
included — **exits 4 with NO receipt**: treat it as a *failed review to re-run*, never a fatal
|
|
137
|
-
session error. One stderr banner states the actual posture (`review posture: model=…`)
|
|
138
|
-
receipt records the same `posture {model}`; an attesting review with `AGY_MODEL` explicitly
|
|
137
|
+
session error. One stderr banner states the actual posture (`review posture: model=… timeout=…`)
|
|
138
|
+
and the receipt records the same `posture {model}`; an attesting review with `AGY_MODEL` explicitly
|
|
139
139
|
emptied refuses pre-spend (`AGY_PROBE=1` exempt), and control bytes in a model string refuse
|
|
140
|
-
pre-spend in every mode.
|
|
140
|
+
pre-spend in every mode. The `timeout=` field is **banner-only** (exactly the duration `agy-run`
|
|
141
|
+
hands to `timeout(1)`, or `uncapped`) — informational, never a receipt field. **Quote the posture
|
|
142
|
+
banner verbatim** when labeling a dispatch.
|
|
141
143
|
|
|
142
144
|
Frontier default `Gemini 3.1 Pro (High)`; **any** model is allowed (a sub-frontier one earns a
|
|
143
145
|
silenceable `AGY_PROBE=1` advisory). An oversized `code` review trips the byte ceiling with trim/split
|
|
@@ -77,6 +77,11 @@ Notes:
|
|
|
77
77
|
pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts
|
|
78
78
|
against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE
|
|
79
79
|
dispatching, never fired into a known prompt
|
|
80
|
+
the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the
|
|
81
|
+
duration agy-run hands to timeout(1), uncapped when no timeout/gtimeout binary caps the run;
|
|
82
|
+
INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity
|
|
83
|
+
quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
|
|
84
|
+
posture; a prose re-type drifts
|
|
80
85
|
|
|
81
86
|
Round-2 / resume:
|
|
82
87
|
agy-review --continue [--decided @f] [--focus "…"]
|
|
@@ -115,10 +120,12 @@ Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins ov
|
|
|
115
120
|
Honesty + posture (D4/D5):
|
|
116
121
|
a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4
|
|
117
122
|
with NO receipt: a FAILED review to RE-RUN, never a fatal session error. One stderr banner line
|
|
118
|
-
states the ACTUAL run posture (review posture: model=…) and the receipt records the
|
|
119
|
-
posture {model} (agy has no tier
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
states the ACTUAL run posture (review posture: model=… timeout=…) and the receipt records the
|
|
124
|
+
same posture {model} (agy has no tier; the timeout field is banner-only — never a receipt
|
|
125
|
+
field). Quote the posture banner verbatim when labeling this dispatch. An ATTESTING review with
|
|
126
|
+
AGY_MODEL explicitly emptied refuses pre-spend (the actual model would be unknowable;
|
|
127
|
+
AGY_PROBE=1 is exempt), and a model string carrying control bytes refuses pre-spend in every
|
|
128
|
+
mode.
|
|
122
129
|
|
|
123
130
|
Closed grammar: unknown flags are rejected; no '--' passthrough (the flag escape is --ungrounded; the env escapes are AGY_PROBE=1 and AGY_REVIEW_ALLOW_ADDDIR=1).
|
|
124
131
|
Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
|
|
@@ -235,16 +242,66 @@ aw_apply_settings() {
|
|
|
235
242
|
}
|
|
236
243
|
aw_apply_settings
|
|
237
244
|
|
|
245
|
+
# --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
|
|
246
|
+
# ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
|
|
247
|
+
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim — and
|
|
248
|
+
# `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
|
|
249
|
+
# The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
|
|
250
|
+
# the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
|
|
251
|
+
# invalid value warns + falls back to the built-in default — a typo never silently masquerades
|
|
252
|
+
# as a cap. AGY_TIMEOUT shares AGY_HARD_TIMEOUT's duration rule (it has no settings-file arm).
|
|
253
|
+
aw_effective_timeout() {
|
|
254
|
+
local key="$1" default="$2" value="${!1:-}" rule="$1" intpart
|
|
255
|
+
[[ "$rule" == "AGY_TIMEOUT" ]] && rule="AGY_HARD_TIMEOUT"
|
|
256
|
+
[[ -n "$value" ]] || { printf '%s' "$default"; return 0; }
|
|
257
|
+
intpart="${value%%[!0-9]*}"
|
|
258
|
+
if ! aw_settings_valid "$rule" "$value" || (( ${#intpart} > 7 )); then
|
|
259
|
+
# %q escapes the raw value so a control byte in it can never forge an extra diagnostic line
|
|
260
|
+
# (the direct agy-run lane has no pre-spend screen — the warning itself must be injection-proof).
|
|
261
|
+
printf "warning: invalid value '%q' for %s — using the built-in default %s.\n" "$value" "$key" "$default" >&2
|
|
262
|
+
printf '%s' "$default"
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
printf '%s' "$value"
|
|
266
|
+
}
|
|
267
|
+
aw_timeout_label() {
|
|
268
|
+
local bin="$1" value="$2"
|
|
269
|
+
[[ -n "$bin" ]] || { printf 'uncapped'; return 0; }
|
|
270
|
+
case "$value" in
|
|
271
|
+
*[!0-9]*) printf '%s' "$value" ;;
|
|
272
|
+
*) printf '%ss' "$value" ;;
|
|
273
|
+
esac
|
|
274
|
+
}
|
|
275
|
+
aw_resolve_timeout_bin() {
|
|
276
|
+
local bin dir base
|
|
277
|
+
bin="$(builtin type -P timeout 2>/dev/null || true)"
|
|
278
|
+
[[ -n "$bin" ]] || bin="$(builtin type -P gtimeout 2>/dev/null || true)"
|
|
279
|
+
[[ -n "$bin" ]] || { printf ''; return 0; }
|
|
280
|
+
case "$bin" in
|
|
281
|
+
/*) ;;
|
|
282
|
+
*)
|
|
283
|
+
case "$bin" in
|
|
284
|
+
*/*) dir="${bin%/*}"; base="${bin##*/}" ;;
|
|
285
|
+
*) dir="."; base="$bin" ;;
|
|
286
|
+
esac
|
|
287
|
+
dir="$(builtin cd -- "$dir" 2>/dev/null && builtin pwd -P)" || { printf ''; return 0; }
|
|
288
|
+
bin="$dir/$base"
|
|
289
|
+
;;
|
|
290
|
+
esac
|
|
291
|
+
[[ -f "$bin" && -x "$bin" ]] || { printf ''; return 0; }
|
|
292
|
+
printf '%s' "$bin"
|
|
293
|
+
}
|
|
294
|
+
|
|
238
295
|
DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
|
|
239
296
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
240
297
|
# version (drift-guarded by agy-review.test.mjs against capability.json).
|
|
241
298
|
AW_RECEIPT_BACKEND="agy"
|
|
242
|
-
AW_BRIDGE_VERSION="4.
|
|
299
|
+
AW_BRIDGE_VERSION="4.1.0"
|
|
243
300
|
# `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
|
|
244
301
|
AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
|
|
245
302
|
# D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
|
|
246
303
|
# other interpolation) can echo raw newline/ESC bytes into stderr/the terminal (round-2 fold).
|
|
247
|
-
if [[ "$AGY_MODEL" == *[$'\x01'-$'\x1f']* ]]; then
|
|
304
|
+
if [[ "$AGY_MODEL" == *[$'\x01'-$'\x1f'$'\x7f']* ]]; then
|
|
248
305
|
echo "error: AGY_MODEL contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
|
|
249
306
|
exit 2
|
|
250
307
|
fi
|
|
@@ -254,7 +311,20 @@ FRONTIER_SET=("Gemini 3.1 Pro (High)" "Claude Opus 4.6 (Thinking)" "Claude Sonne
|
|
|
254
311
|
# Duration-string timeouts (NOT codex's bare seconds): agy-run forwards a duration to --print-timeout,
|
|
255
312
|
# and the timeout(1) hard cap is a duration too — never numerically compared, so 30m vs 2h is fine.
|
|
256
313
|
AGY_HARD_TIMEOUT="${AGY_HARD_TIMEOUT:-30m}"
|
|
314
|
+
# D5 banner-field control-byte screen (AD-061) — like AGY_MODEL above, BEFORE any interpolation.
|
|
315
|
+
if [[ "$AGY_HARD_TIMEOUT" == *[$'\x01'-$'\x1f'$'\x7f']* ]]; then
|
|
316
|
+
echo "error: AGY_HARD_TIMEOUT contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
|
|
317
|
+
exit 2
|
|
318
|
+
fi
|
|
319
|
+
AGY_HARD_TIMEOUT="$(aw_effective_timeout AGY_HARD_TIMEOUT 30m)"
|
|
257
320
|
AGY_TIMEOUT="${AGY_TIMEOUT:-$AGY_HARD_TIMEOUT}"
|
|
321
|
+
# D5 banner-field control-byte screen for AGY_TIMEOUT (AD-061): agy-review forwards it to the
|
|
322
|
+
# child agy-run's --print-timeout, so a control byte must refuse HERE, pre-spawn — the child's
|
|
323
|
+
# own diagnostic would otherwise echo it. (agy-run screens its own direct-lane env separately.)
|
|
324
|
+
if [[ "$AGY_TIMEOUT" == *[$'\x01'-$'\x1f'$'\x7f']* ]]; then
|
|
325
|
+
echo "error: AGY_TIMEOUT contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
|
|
326
|
+
exit 2
|
|
327
|
+
fi
|
|
258
328
|
AGY_PROBE="${AGY_PROBE:-0}"
|
|
259
329
|
# A probe run is RECORDED, not just silently allowed: the receipt carries probe:true so the kit's
|
|
260
330
|
# review-state gate rejects it — a guards-relaxed review must never attest a tree.
|
|
@@ -882,7 +952,14 @@ if [[ -z "$AGY_MODEL" && -z "$resume_mode" && "$REVIEW_PROBE" != "true" && "$REV
|
|
|
882
952
|
echo " AGY_MODEL (wrapper default), or set the real model display string; AGY_PROBE=1 is exempt." >&2
|
|
883
953
|
exit 2
|
|
884
954
|
fi
|
|
885
|
-
|
|
955
|
+
# The timeout field is BANNER-ONLY (AD-061): it prints exactly the duration agy-run hands to
|
|
956
|
+
# timeout(1), or `uncapped` without a capping binary, and never enters the receipt posture.
|
|
957
|
+
# aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
|
|
958
|
+
# `type` itself), normalized to an absolute path fail-closed — the banner can never claim a cap
|
|
959
|
+
# the agy-run child would not have.
|
|
960
|
+
aw_banner_timeout_bin="$(aw_resolve_timeout_bin)"
|
|
961
|
+
aw_timeout_banner="$(aw_timeout_label "$aw_banner_timeout_bin" "$AGY_HARD_TIMEOUT")"
|
|
962
|
+
echo "review posture: model=${AGY_MODEL:-<agy settings default>} timeout=$aw_timeout_banner" >&2
|
|
886
963
|
|
|
887
964
|
# --- Execute via agy-run (single home of timeout + subscription + byte ceiling) ---
|
|
888
965
|
# The output is teed into the private staging dir so the mandated '### Verdict' section can be
|
|
@@ -1412,4 +1412,117 @@ describe('agy-review.sh — dispatch-posture labeling (D5)', () => {
|
|
|
1412
1412
|
assert.equal(receipts.length, 0);
|
|
1413
1413
|
assert.match(r.stderr, /control/i);
|
|
1414
1414
|
});
|
|
1415
|
+
|
|
1416
|
+
it('the banner appends the RESOLVED hard timeout verbatim — banner-only, never in the receipt (AD-061)', () => {
|
|
1417
|
+
const sb = makeSandbox();
|
|
1418
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'] });
|
|
1419
|
+
const receipts = readReceipts(sb.repo);
|
|
1420
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1421
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1422
|
+
assert.match(r.stderr, /^review posture: model=Gemini 3\.1 Pro \(High\) timeout=30m$/m);
|
|
1423
|
+
assert.deepEqual(Object.keys(receipts[0].posture), ['model'], 'timeout never enters the receipt posture');
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it('the banner prints the EFFECTIVE hard cap: an env override and a fractional duration ride verbatim', () => {
|
|
1427
|
+
for (const [envValue, want] of [['90s', '90s'], ['1.5m', '1\\.5m']]) {
|
|
1428
|
+
const sb = makeSandbox();
|
|
1429
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_HARD_TIMEOUT: envValue } });
|
|
1430
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1431
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1432
|
+
assert.match(r.stderr, new RegExp(`^review posture: .* timeout=${want}$`, 'm'));
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it('AGY_TIMEOUT (soft print-timeout) alone never moves the banner — the hard cap governs (precedence pin)', () => {
|
|
1437
|
+
const sb = makeSandbox();
|
|
1438
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_TIMEOUT: '5m' } });
|
|
1439
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1440
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1441
|
+
assert.match(r.stderr, /^review posture: .* timeout=30m$/m, 'the soft print-timeout is not the banner value');
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('an INVALID / EMPTY / OVERFLOW effective AGY_HARD_TIMEOUT falls back to the built-in default (loud on invalid)', () => {
|
|
1445
|
+
for (const [bad, wantWarn] of [['10x', true], ['', false], ['99999999m', true]]) {
|
|
1446
|
+
const sb = makeSandbox();
|
|
1447
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_HARD_TIMEOUT: bad } });
|
|
1448
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1449
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1450
|
+
assert.match(r.stderr, /^review posture: .* timeout=30m$/m, `default must stand for ${JSON.stringify(bad)}`);
|
|
1451
|
+
if (wantWarn) assert.match(r.stderr, new RegExp(`invalid value '${bad}' for AGY_HARD_TIMEOUT`));
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
it('a timeout value carrying CONTROL BYTES refuses pre-spend (the banner-field screen)', () => {
|
|
1456
|
+
const sb = makeSandbox();
|
|
1457
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_HARD_TIMEOUT: `30m${String.fromCharCode(1)}` } });
|
|
1458
|
+
const receipts = readReceipts(sb.repo);
|
|
1459
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1460
|
+
assert.notEqual(r.status, 0);
|
|
1461
|
+
assert.equal(r.invoked, false);
|
|
1462
|
+
assert.equal(receipts.length, 0);
|
|
1463
|
+
assert.match(r.stderr, /control/i);
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
it('timeout honesty: no timeout/gtimeout on PATH → timeout=uncapped, never a fabricated number', () => {
|
|
1467
|
+
const sb = makeSandbox();
|
|
1468
|
+
const r = run(sb, {
|
|
1469
|
+
args: ['code', '--facts', 'a tiny fact'],
|
|
1470
|
+
env: { PATH: `${sb.bin}:${farmFor(['agy', 'agy-run', 'timeout', 'gtimeout'])}` },
|
|
1471
|
+
});
|
|
1472
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1473
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1474
|
+
assert.match(r.stderr, /^review posture: .* timeout=uncapped$/m);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it('an EXPORTED shell function shadowing timeout never fools the banner (type -P discipline)', () => {
|
|
1478
|
+
const sb = makeSandbox();
|
|
1479
|
+
const r = run(sb, {
|
|
1480
|
+
args: ['code', '--facts', 'a tiny fact'],
|
|
1481
|
+
env: {
|
|
1482
|
+
PATH: `${sb.bin}:${farmFor(['agy', 'agy-run', 'timeout', 'gtimeout'])}`,
|
|
1483
|
+
'BASH_FUNC_timeout%%': '() { return 0; }',
|
|
1484
|
+
},
|
|
1485
|
+
});
|
|
1486
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1487
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1488
|
+
assert.match(r.stderr, /^review posture: .* timeout=uncapped$/m, 'a shell function is not a capping binary');
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
it('an EXPORTED `type` function faking a path never fools the resolver (builtin type discipline)', () => {
|
|
1492
|
+
const sb = makeSandbox();
|
|
1493
|
+
const r = run(sb, {
|
|
1494
|
+
args: ['code', '--facts', 'a tiny fact'],
|
|
1495
|
+
env: {
|
|
1496
|
+
PATH: `${sb.bin}:${farmFor(['agy', 'agy-run', 'timeout', 'gtimeout'])}`,
|
|
1497
|
+
'BASH_FUNC_type%%': '() { echo /fake/timeout; }',
|
|
1498
|
+
},
|
|
1499
|
+
});
|
|
1500
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1501
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1502
|
+
assert.match(r.stderr, /^review posture: .* timeout=uncapped$/m, 'builtin type bypasses an exported type function');
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it('a DEL (0x7f) byte in a banner field refuses pre-spend like the C0 range', () => {
|
|
1506
|
+
const sb = makeSandbox();
|
|
1507
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: `bad${String.fromCharCode(127)}model` } });
|
|
1508
|
+
const receipts = readReceipts(sb.repo);
|
|
1509
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1510
|
+
assert.notEqual(r.status, 0);
|
|
1511
|
+
assert.equal(r.invoked, false);
|
|
1512
|
+
assert.equal(receipts.length, 0);
|
|
1513
|
+
assert.match(r.stderr, /control/i);
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
it('a control byte in AGY_TIMEOUT refuses pre-spawn — agy-review forwards it to the child agy-run', () => {
|
|
1517
|
+
for (const c of [1, 127]) {
|
|
1518
|
+
const sb = makeSandbox();
|
|
1519
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_TIMEOUT: `30m${String.fromCharCode(c)}` } });
|
|
1520
|
+
const receipts = readReceipts(sb.repo);
|
|
1521
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1522
|
+
assert.notEqual(r.status, 0, `must refuse control byte ${c}`);
|
|
1523
|
+
assert.equal(r.invoked, false, 'refused BEFORE spawning agy-run');
|
|
1524
|
+
assert.equal(receipts.length, 0);
|
|
1525
|
+
assert.match(r.stderr, /AGY_TIMEOUT/, 'names the offending knob');
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1415
1528
|
});
|
|
@@ -180,6 +180,56 @@ aw_apply_settings() {
|
|
|
180
180
|
}
|
|
181
181
|
aw_apply_settings
|
|
182
182
|
|
|
183
|
+
# --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
|
|
184
|
+
# ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
|
|
185
|
+
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim — and
|
|
186
|
+
# `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
|
|
187
|
+
# The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
|
|
188
|
+
# the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
|
|
189
|
+
# invalid value warns + falls back to the built-in default — a typo never silently masquerades
|
|
190
|
+
# as a cap. AGY_TIMEOUT shares AGY_HARD_TIMEOUT's duration rule (it has no settings-file arm).
|
|
191
|
+
aw_effective_timeout() {
|
|
192
|
+
local key="$1" default="$2" value="${!1:-}" rule="$1" intpart
|
|
193
|
+
[[ "$rule" == "AGY_TIMEOUT" ]] && rule="AGY_HARD_TIMEOUT"
|
|
194
|
+
[[ -n "$value" ]] || { printf '%s' "$default"; return 0; }
|
|
195
|
+
intpart="${value%%[!0-9]*}"
|
|
196
|
+
if ! aw_settings_valid "$rule" "$value" || (( ${#intpart} > 7 )); then
|
|
197
|
+
# %q escapes the raw value so a control byte in it can never forge an extra diagnostic line
|
|
198
|
+
# (the direct agy-run lane has no pre-spend screen — the warning itself must be injection-proof).
|
|
199
|
+
printf "warning: invalid value '%q' for %s — using the built-in default %s.\n" "$value" "$key" "$default" >&2
|
|
200
|
+
printf '%s' "$default"
|
|
201
|
+
return 0
|
|
202
|
+
fi
|
|
203
|
+
printf '%s' "$value"
|
|
204
|
+
}
|
|
205
|
+
aw_timeout_label() {
|
|
206
|
+
local bin="$1" value="$2"
|
|
207
|
+
[[ -n "$bin" ]] || { printf 'uncapped'; return 0; }
|
|
208
|
+
case "$value" in
|
|
209
|
+
*[!0-9]*) printf '%s' "$value" ;;
|
|
210
|
+
*) printf '%ss' "$value" ;;
|
|
211
|
+
esac
|
|
212
|
+
}
|
|
213
|
+
aw_resolve_timeout_bin() {
|
|
214
|
+
local bin dir base
|
|
215
|
+
bin="$(builtin type -P timeout 2>/dev/null || true)"
|
|
216
|
+
[[ -n "$bin" ]] || bin="$(builtin type -P gtimeout 2>/dev/null || true)"
|
|
217
|
+
[[ -n "$bin" ]] || { printf ''; return 0; }
|
|
218
|
+
case "$bin" in
|
|
219
|
+
/*) ;;
|
|
220
|
+
*)
|
|
221
|
+
case "$bin" in
|
|
222
|
+
*/*) dir="${bin%/*}"; base="${bin##*/}" ;;
|
|
223
|
+
*) dir="."; base="$bin" ;;
|
|
224
|
+
esac
|
|
225
|
+
dir="$(builtin cd -- "$dir" 2>/dev/null && builtin pwd -P)" || { printf ''; return 0; }
|
|
226
|
+
bin="$dir/$base"
|
|
227
|
+
;;
|
|
228
|
+
esac
|
|
229
|
+
[[ -f "$bin" && -x "$bin" ]] || { printf ''; return 0; }
|
|
230
|
+
printf '%s' "$bin"
|
|
231
|
+
}
|
|
232
|
+
|
|
183
233
|
if ! command -v agy >/dev/null 2>&1; then
|
|
184
234
|
echo "error: 'agy' (Antigravity CLI) not found on PATH. Install it and run 'agy' once to sign in." >&2
|
|
185
235
|
exit 127
|
|
@@ -188,11 +238,13 @@ fi
|
|
|
188
238
|
# `-` (empty) => skip --model and let agy use settings.json; default to Pro.
|
|
189
239
|
AGY_MODEL="${AGY_MODEL-Gemini 3.1 Pro (High)}"
|
|
190
240
|
AGY_TIMEOUT="${AGY_TIMEOUT:-5m}"
|
|
241
|
+
AGY_TIMEOUT="$(aw_effective_timeout AGY_TIMEOUT 5m)"
|
|
191
242
|
# Hard wall-clock cap (defaults to AGY_TIMEOUT). agy's own --print-timeout is NOT a reliable
|
|
192
243
|
# wall-clock kill — a run was observed surviving 32 min past a 10m --print-timeout — so we also wrap
|
|
193
244
|
# agy in timeout(1). A heavy `--add-dir` agentic prompt on the slowest model can otherwise run
|
|
194
245
|
# unbounded, and once a caller backgrounds it nothing kills it. Raise only for a known-healthy run.
|
|
195
246
|
AGY_HARD_TIMEOUT="${AGY_HARD_TIMEOUT:-$AGY_TIMEOUT}"
|
|
247
|
+
AGY_HARD_TIMEOUT="$(aw_effective_timeout AGY_HARD_TIMEOUT "$AGY_TIMEOUT")"
|
|
196
248
|
|
|
197
249
|
if [[ $# -lt 1 ]]; then
|
|
198
250
|
echo "usage: $0 <prompt | - | @file> [-- extra agy flags...]" >&2
|
|
@@ -273,12 +325,9 @@ agy_cmd=(agy "${model_flag[@]}" --print-timeout "$AGY_TIMEOUT" "${passthrough[@]
|
|
|
273
325
|
|
|
274
326
|
# Hard wall-clock cap via timeout(1) (GNU `timeout` on Linux, `gtimeout` from coreutils on macOS).
|
|
275
327
|
# This is the real guard — a backgrounded, hung agy survives its own --print-timeout otherwise.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
elif command -v gtimeout >/dev/null 2>&1; then
|
|
280
|
-
timeout_bin="gtimeout"
|
|
281
|
-
fi
|
|
328
|
+
# aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
|
|
329
|
+
# `type` itself), normalized to an ABSOLUTE path fail-closed — nothing can masquerade as the cap.
|
|
330
|
+
timeout_bin="$(aw_resolve_timeout_bin)"
|
|
282
331
|
|
|
283
332
|
if [[ -z "$timeout_bin" ]]; then
|
|
284
333
|
echo "warning: no 'timeout'/'gtimeout' on PATH — running agy WITHOUT a hard wall-clock cap" >&2
|
|
@@ -311,6 +311,24 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', { concurrency: true
|
|
|
311
311
|
assert.match(r.stdout, /OK reply/);
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
+
it('an INVALID ENV duration warns and falls back too (the closed aw_settings_valid env bypass, AD-061)', async () => {
|
|
315
|
+
const home = makeSandbox(RECORDING_STUB);
|
|
316
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: 'abc', AGY_MODEL: '' });
|
|
317
|
+
rmSync(home, { recursive: true, force: true });
|
|
318
|
+
assert.equal(r.status, 0, r.stderr);
|
|
319
|
+
assert.match(r.stderr, /invalid value 'abc' for AGY_HARD_TIMEOUT/, 'the delegated agy-run lane validates the EFFECTIVE value');
|
|
320
|
+
assert.match(r.stdout, /OK reply/);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('the invalid-value warning ESCAPES the raw value — a newline can never forge a second diagnostic line (AD-061)', async () => {
|
|
324
|
+
const home = makeSandbox(RECORDING_STUB);
|
|
325
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: 'x\nreview posture: model=FORGED', AGY_MODEL: '' });
|
|
326
|
+
rmSync(home, { recursive: true, force: true });
|
|
327
|
+
assert.equal(r.status, 0, r.stderr);
|
|
328
|
+
assert.doesNotMatch(r.stderr, /^review posture: model=FORGED/m, 'the newline is escaped by %q — no forged line');
|
|
329
|
+
assert.match(r.stdout, /OK reply/, 'the run still proceeds on the built-in default');
|
|
330
|
+
});
|
|
331
|
+
|
|
314
332
|
it('a DIRECTORY at the settings path warns loudly and falls back to built-ins (no crash)', async () => {
|
|
315
333
|
const home = makeSandbox(RECORDING_STUB);
|
|
316
334
|
mkdirSync(join(home, '.config', 'agent-workflow', 'bridge-settings.conf'), { recursive: true });
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "antigravity-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "4.
|
|
6
|
+
"version": "4.1.0",
|
|
7
7
|
"provides": ["review", "probe"],
|
|
8
8
|
"posture": { "model": "Gemini 3.1 Pro (High)" },
|
|
9
9
|
"roles": {
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
],
|
|
33
33
|
"receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review",
|
|
34
34
|
"notes": [
|
|
35
|
-
"pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt"
|
|
35
|
+
"pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt",
|
|
36
|
+
"the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration agy-run hands to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity",
|
|
37
|
+
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
|
|
36
38
|
]
|
|
37
39
|
}
|
|
38
40
|
},
|
|
@@ -89,6 +89,11 @@ What it does for you, and what YOU must supply:
|
|
|
89
89
|
- **Model:** frontier default `Gemini 3.1 Pro (High)`; any model is allowed (a sub-frontier one earns a
|
|
90
90
|
silenceable `AGY_PROBE=1` advisory). The service can still **stall on large/substantive prompts**
|
|
91
91
|
(Issue-001) — keep reviews **focused**; the hard timeout is the guard.
|
|
92
|
+
- **Posture banner — quote it verbatim.** Every review states its ACTUAL posture on ONE stderr line
|
|
93
|
+
(`review posture: model=… timeout=…`). When you label a dispatch for a user or a record, **quote
|
|
94
|
+
the posture banner verbatim** — the banner is the machine-stated posture; a prose re-type drifts.
|
|
95
|
+
The `timeout=` field is **banner-only** (exactly the duration `agy-run` hands to `timeout(1)`, or
|
|
96
|
+
`uncapped` when no capping binary is on PATH) — informational, never part of the receipt posture.
|
|
92
97
|
|
|
93
98
|
## Escalation policy (edits, network, git)
|
|
94
99
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: codex-cli-bridge
|
|
3
3
|
description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
|
|
4
4
|
metadata:
|
|
5
|
-
version: '3.
|
|
5
|
+
version: '3.1.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# codex-cli-bridge
|
|
@@ -99,9 +99,13 @@ codex-review code "focus on the new reducer" # review with extra focus
|
|
|
99
99
|
**Honesty + posture (D4/D5):** a run whose final message has no recognized
|
|
100
100
|
`Verdict: <ship|revise|rethink>` line — empty or missing output included — **exits 4 with NO
|
|
101
101
|
receipt**: treat it as a *failed review to re-run*, never a fatal session error. One stderr banner
|
|
102
|
-
states the actual posture (`review posture: model=… effort=… tier=…`) and the receipt
|
|
103
|
-
same `posture {model, effort, tier}` (tier `null` on the standard tier); control bytes
|
|
104
|
-
posture value refuse pre-spend in every mode.
|
|
102
|
+
states the actual posture (`review posture: model=… effort=… tier=… timeout=…`) and the receipt
|
|
103
|
+
records the same `posture {model, effort, tier}` (tier `null` on the standard tier); control bytes
|
|
104
|
+
in a posture value refuse pre-spend in every mode. `codex-exec` states its posture the same way —
|
|
105
|
+
ONE `exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id>
|
|
106
|
+
timeout=…` stderr line before dispatch (the resume id validated pre-spend). The `timeout=` field
|
|
107
|
+
is **banner-only** (exactly the duration handed to `timeout(1)`, or `uncapped`) — informational,
|
|
108
|
+
never a receipt field. **Quote the posture banner verbatim** when labeling a dispatch.
|
|
105
109
|
|
|
106
110
|
`codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
|
|
107
111
|
escalation is refused and reported, never interactively approved. The wrappers capture only codex's
|