@sabaiway/agent-workflow-kit 1.48.0 → 2.0.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.
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.6.0",
6
+ "version": "2.7.0",
7
7
  "provides": ["review", "probe"],
8
8
  "roles": {
9
9
  "review": {
@@ -28,7 +28,7 @@
28
28
  "agy-review --continue [--decided @f] [--focus \"…\"]",
29
29
  "agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
30
30
  ],
31
- "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 (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); a write failure warns, never fails the review",
31
+ "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 (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); a write failure warns, never fails the review",
32
32
  "notes": [
33
33
  "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"
34
34
  ]
@@ -36,6 +36,164 @@
36
36
  },
37
37
  "probe": { "cmd": "agy-run", "source": "bin/agy.sh", "output": "advisory" }
38
38
  },
39
+ "modeCatalog": [
40
+ {
41
+ "key": "review.code",
42
+ "kind": "primary",
43
+ "role": "review",
44
+ "submode": "code",
45
+ "purpose": "Review the uncommitted working-tree change set against facts you supply.",
46
+ "whenToUse": [
47
+ "a finished segment heading into its review round",
48
+ "a second opinion on real code before the commit ask"
49
+ ],
50
+ "whenNotTo": ["a clean tree — the wrapper exits before spending a run"],
51
+ "invocationRefs": [{ "contractField": "invocations", "index": 0 }],
52
+ "operands": [
53
+ { "slot": "[--facts @f]", "required": false, "description": "the verified facts to review AGAINST; omitting it warns loudly and records the review ungrounded" },
54
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
55
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" },
56
+ { "slot": "[extra focus…]", "required": false, "description": "extra focus words appended to the review directive" }
57
+ ],
58
+ "guardrails": [
59
+ { "value": "read-only posture — the prompt forbids edits, commands and git writes", "enforcement": "advisory", "source": "bin/agy-review.sh" },
60
+ { "value": "receives no ambient repo/file context by default — the wrapper passes no --add-dir", "enforcement": "enforced", "source": "bin/agy-review.sh" },
61
+ { "value": "an ungrounded review guesses — stale-model and partial-diff false positives", "enforcement": "advisory", "source": "bin/agy-review.sh" },
62
+ { "value": "an ungrounded run records grounded:false and the review-state gate rejects it", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" },
63
+ { "value": "an oversized prompt refuses rather than truncate", "enforcement": "enforced", "condition": "unless AGY_REVIEW_ALLOW_ADDDIR=1 offloads it to a private --add-dir staging dir", "source": "capability.json settings.AGY_REVIEW_ALLOW_ADDDIR" }
64
+ ],
65
+ "customHooks": ["AGY_PROBE"]
66
+ },
67
+ {
68
+ "key": "review.plan",
69
+ "kind": "primary",
70
+ "role": "review",
71
+ "submode": "plan",
72
+ "purpose": "Critique an implementation plan before any of its code exists.",
73
+ "whenToUse": [
74
+ "a plan draft heading into its review council",
75
+ "checking whether a cold executor could really run the plan"
76
+ ],
77
+ "whenNotTo": ["a working-tree change set — that is review.code"],
78
+ "invocationRefs": [{ "contractField": "invocations", "index": 1 }],
79
+ "operands": [
80
+ { "slot": "<plan-file>", "required": true, "description": "the plan file under review" },
81
+ { "slot": "[--facts @f]", "required": false, "description": "the verified facts to review AGAINST; omitting it warns loudly and records the review ungrounded" },
82
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
83
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" }
84
+ ],
85
+ "guardrails": [
86
+ { "value": "read-only posture — the prompt forbids edits, commands and git writes", "enforcement": "advisory", "source": "bin/agy-review.sh" },
87
+ { "value": "outside a git tree the receipt is skipped with a warning", "enforcement": "enforced", "condition": "unless AW_REVIEW_RECEIPTS names a destination", "source": "capability.json roles.review.contract.receipt" }
88
+ ],
89
+ "customHooks": ["AGY_PROBE"]
90
+ },
91
+ {
92
+ "key": "review.diff",
93
+ "kind": "primary",
94
+ "role": "review",
95
+ "submode": "diff",
96
+ "purpose": "Review a diff file you assembled yourself, rather than the live working tree.",
97
+ "whenToUse": [
98
+ "reviewing a diff that is not the current tree — a patch, a range, another branch",
99
+ "reviewing a change set trimmed to the hunks that matter"
100
+ ],
101
+ "whenNotTo": ["the current uncommitted tree — review.code assembles that for you"],
102
+ "invocationRefs": [{ "contractField": "invocations", "index": 2 }],
103
+ "operands": [
104
+ { "slot": "<diff-file>", "required": true, "description": "the diff file under review" },
105
+ { "slot": "[--facts @f]", "required": false, "description": "the verified facts to review AGAINST; omitting it warns loudly and records the review ungrounded" },
106
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
107
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" }
108
+ ],
109
+ "guardrails": [
110
+ { "value": "read-only posture — the prompt forbids edits, commands and git writes", "enforcement": "advisory", "source": "bin/agy-review.sh" },
111
+ { "value": "the receipt fingerprints the DIFF FILE, never the tree — it cannot attest the tree", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" }
112
+ ],
113
+ "customHooks": ["AGY_PROBE"]
114
+ },
115
+ {
116
+ "key": "review.continue",
117
+ "kind": "continuation",
118
+ "role": "review",
119
+ "purpose": "Send a round-2 delta into the review already open in this conversation.",
120
+ "whenToUse": [
121
+ "asking the same reviewer whether a fold really solves its own finding",
122
+ "a follow-up round that must not re-read the whole artifact"
123
+ ],
124
+ "whenNotTo": ["attesting a folded tree — a continuation records fresh:false and never satisfies the gate"],
125
+ "invocationRefs": [{ "contractField": "continue", "index": 0 }],
126
+ "operands": [
127
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
128
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this round must look at (repeatable)" }
129
+ ],
130
+ "guardrails": [
131
+ { "value": "--facts is rejected on a continuation — the facts are already in the conversation", "enforcement": "enforced", "source": "bin/agy-review.sh" },
132
+ { "value": "a continuation receipt is fresh:false — informational only, never a gate pass", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" }
133
+ ],
134
+ "customHooks": ["AGY_PROBE"]
135
+ },
136
+ {
137
+ "key": "review.conversation",
138
+ "kind": "continuation",
139
+ "role": "review",
140
+ "purpose": "Resume a NAMED review conversation by its id, rather than the most recent one.",
141
+ "whenToUse": ["returning to a specific earlier review after other agy runs happened in between"],
142
+ "invocationRefs": [{ "contractField": "continue", "index": 1 }],
143
+ "operands": [
144
+ { "slot": "<id>", "required": true, "description": "the conversation id of the review to resume" },
145
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
146
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this round must look at (repeatable)" }
147
+ ],
148
+ "guardrails": [
149
+ { "value": "a continuation receipt is fresh:false — informational only, never a gate pass", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" }
150
+ ],
151
+ "customHooks": ["AGY_PROBE"]
152
+ },
153
+ {
154
+ "key": "run",
155
+ "kind": "primary",
156
+ "role": "probe",
157
+ "purpose": "Send a raw advisory prompt to a selectable model and print the reply.",
158
+ "whenToUse": [
159
+ "a one-off question that needs no repo grounding",
160
+ "reaching a specific model (Gemini, Claude, GPT-OSS) from the terminal",
161
+ "a throwaway probe of the CLI, a model, or the wrapper plumbing"
162
+ ],
163
+ "whenNotTo": ["a code or plan review — agy-review owns the grounded posture and the receipt"],
164
+ "descriptor": "agy-run <prompt|-|@file> -- <extra agy flags...>",
165
+ "operands": [
166
+ { "slot": "<prompt|-|@file>", "required": true, "description": "the prompt text, - to read it from stdin, or @path to read it from a file" },
167
+ { "slot": "<extra agy flags...>", "required": false, "description": "raw agy flags passed straight through after --" }
168
+ ],
169
+ "guardrails": [
170
+ { "value": "subscription-only — every *_API_KEY env var is unset before the run", "enforcement": "enforced", "source": "bin/agy.sh" },
171
+ { "value": "any model is selectable via AGY_MODEL; the default is Gemini 3.1 Pro (High)", "enforcement": "advisory", "source": "bin/agy.sh" },
172
+ { "value": "hard wall-clock cap AGY_HARD_TIMEOUT (built-in default 5m)", "enforcement": "enforced", "condition": "only while timeout(1)/gtimeout is on PATH — otherwise the wrapper warns and runs uncapped", "source": "capability.json settings.AGY_HARD_TIMEOUT" },
173
+ { "value": "the prompt rides a single argv — over AGY_MAX_PROMPT_BYTES (120000) the run refuses", "enforcement": "enforced", "source": "bin/agy.sh" },
174
+ { "value": "no review posture, no grounding, no receipt — the reply is raw model output", "enforcement": "advisory", "source": "bin/agy.sh" }
175
+ ],
176
+ "customHooks": ["run"]
177
+ },
178
+ {
179
+ "key": "AGY_PROBE",
180
+ "kind": "env-hook",
181
+ "parents": ["review.code", "review.plan", "review.diff", "review.continue", "review.conversation"],
182
+ "purpose": "Silence the off-frontier model advisory for a THROWAWAY probe review.",
183
+ "whenToUse": [
184
+ "a probe whose answer cannot depend on the model",
185
+ "deliberately reviewing with a cheaper non-frontier model"
186
+ ],
187
+ "whenNotTo": ["any run whose output informs what ships — a probe never attests"],
188
+ "descriptor": "AGY_PROBE=1 agy-review code --facts @<facts-file>",
189
+ "operands": [
190
+ { "slot": "@<facts-file>", "required": true, "description": "the verified facts the review runs AGAINST" }
191
+ ],
192
+ "guardrails": [
193
+ { "value": "a probe review mints a probe-marked receipt the review-state gate rejects", "enforcement": "enforced", "source": "agent-workflow-kit tools/review-state.mjs" }
194
+ ]
195
+ }
196
+ ],
39
197
  "settings": [
40
198
  {
41
199
  "key": "AGY_HARD_TIMEOUT",
@@ -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: '2.7.0'
5
+ version: '2.8.0'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -71,6 +71,13 @@ waste, never lowering quality. Full knob list: [§ Environment knobs](#environme
71
71
 
72
72
  ## Usage
73
73
 
74
+ > **The machine-readable mode catalog lives in [`capability.json`](capability.json) `modeCatalog`** —
75
+ > every documented mode with its purpose, when to use it (and when not), the exact invocation form
76
+ > with its operand slots, and the guardrails that really apply. The catalog tracks **the documented
77
+ > wrapper mode set** (never "the CLI's modes"): an upstream Codex CLI change reaches it through a
78
+ > bridge release, where the source-level drift tests fail loudly until the catalog is updated.
79
+ > Nothing probes a live CLI. The prose below stays the human tour.
80
+
74
81
  Drive codex only through the two wrappers (installed on `PATH`), run from the target project root:
75
82
 
76
83
  ```bash
@@ -112,12 +112,22 @@ aw_settings_known() {
112
112
  *) return 1 ;;
113
113
  esac
114
114
  }
115
+ aw_int_in_range() {
116
+ # All-digits $1 vs [min,max] WITHOUT 64-bit wrap (Issue-012): strip leading zeros, then a longer
117
+ # digit count than max's is unconditionally out of range — never do the arithmetic on a huge string.
118
+ # A leading-zero in-range value still passes (its stripped count is small); all-zeros collapses to
119
+ # "0" (below min>=1). Mirrors the JS settingValueValid safe-integer bound, verified by parity test.
120
+ local n="${1#"${1%%[!0]*}"}" min="$2" max="$3"
121
+ n="${n:-0}"
122
+ (( ${#n} > ${#max} )) && return 1
123
+ (( n >= min && n <= max ))
124
+ }
115
125
  aw_settings_valid() {
116
126
  local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
117
127
  case "$k" in
118
128
  CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
119
- CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
120
- CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
129
+ CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 86400 ;;
130
+ CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 100000000 ;;
121
131
  AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
122
132
  AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
123
133
  *) return 1 ;;
@@ -169,6 +179,14 @@ aw_apply_settings() {
169
179
  fi
170
180
  continue
171
181
  fi
182
+ # Normalize an all-digit (integer) value to DECIMAL before export: a leading-zero value the integer
183
+ # arms legitimately accept (000…086400 == 86400) would otherwise read as OCTAL in downstream Bash
184
+ # arithmetic ("value too great for base"). Strip leading zeros, floor "0"; enum/duration (non-digit)
185
+ # and boolean 0/1 are unaffected.
186
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
187
+ value="${value#"${value%%[!0]*}"}"
188
+ value="${value:-0}"
189
+ fi
172
190
  export "$key=$value"
173
191
  done
174
192
  return 0
@@ -821,6 +821,139 @@ describe('codex-exec.sh — source-level reverse guard (parser arms ⟷ manifest
821
821
  });
822
822
  });
823
823
 
824
+ // ── mode catalog ⟷ wrapper reality (BRIDGE-MODES-CATALOG) ─────────────────────────
825
+ // The kit validator owns the catalog's INTERNAL shape; these arms pin the half only this wrapper's
826
+ // source can settle — the execute role's cataloged modes really exist here, every declared contract
827
+ // invocation is cataloged, and the env-hook the catalog aims at exec really changes the run.
828
+
829
+ // The source lines that really EXECUTE: a heredoc body (the --help text) and a comment both carry
830
+ // names without carrying logic, so a bare name-grep stays green after the logic is deleted.
831
+ const executableLines = (source) => {
832
+ const out = [];
833
+ let heredoc = null;
834
+ for (const raw of source.split('\n')) {
835
+ if (heredoc) {
836
+ if (raw.trim() === heredoc) heredoc = null;
837
+ continue;
838
+ }
839
+ const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
840
+ if (hd) { heredoc = hd[1]; continue; }
841
+ if (raw.trimStart().startsWith('#')) continue;
842
+ out.push(raw);
843
+ }
844
+ return out;
845
+ };
846
+ const consultsEnv = (source, name) =>
847
+ executableLines(source).some((l) => new RegExp(`\\[\\[[^\\]]*\\$\\{?${name}\\b[^\\]]*(==|!=)`).test(l));
848
+ // The optional `@` prefix rides WITH the slot — the catalog declares the whole token a user types.
849
+ const SLOT_RE = /@?<[^<>]+>|\[[^[\]]*\]/g;
850
+
851
+ describe('codex-exec.sh — mode catalog ⟷ wrapper reality (manifest-pinned)', () => {
852
+ const source = readFileSync(WRAPPER, 'utf8');
853
+ const catalog = MANIFEST.modeCatalog ?? [];
854
+ const execEntries = catalog.filter((e) => e.role === 'execute');
855
+
856
+ it('the execute role is cataloged: one primary plus a continuation per resume flag', () => {
857
+ const primaries = execEntries.filter((e) => e.kind === 'primary');
858
+ const continuations = execEntries.filter((e) => e.kind === 'continuation');
859
+ assert.equal(primaries.length, 1, 'codex-exec has exactly one primary drive form');
860
+ assert.equal(continuations.length, EXEC_CONTRACT.continue.length, 'one continuation entry per declared resume descriptor');
861
+ });
862
+
863
+ it('every execute entry composes BY REFERENCE and every reference resolves', () => {
864
+ for (const entry of execEntries) {
865
+ assert.ok(
866
+ Array.isArray(entry.invocationRefs) && entry.invocationRefs.length > 0,
867
+ `${entry.key}: a contract-backed entry references at least one contract descriptor`,
868
+ );
869
+ assert.ok(!Object.hasOwn(entry, 'descriptor'), `${entry.key}: a contract-backed entry never restates a literal descriptor`);
870
+ for (const ref of entry.invocationRefs) {
871
+ assert.equal(
872
+ typeof EXEC_CONTRACT[ref.contractField]?.[ref.index], 'string',
873
+ `${entry.key}: ref ${ref.contractField}[${ref.index}] does not resolve into the manifest contract`,
874
+ );
875
+ }
876
+ }
877
+ });
878
+
879
+ it('every execute contract invocation is claimed by exactly ONE catalog entry (no uncataloged form)', () => {
880
+ const claims = execEntries.flatMap((e) => e.invocationRefs.map((r) => `${r.contractField}[${r.index}]`));
881
+ assert.equal(new Set(claims).size, claims.length, 'a contract invocation is claimed at most once');
882
+ const declared = [
883
+ ...EXEC_CONTRACT.invocations.map((_, i) => `invocations[${i}]`),
884
+ ...EXEC_CONTRACT.continue.map((_, i) => `continue[${i}]`),
885
+ ];
886
+ setEq(new Set(claims), declared, 'catalog claims ⟷ declared contract invocations');
887
+ });
888
+
889
+ it('every env-hook the catalog aims at an execute mode is a real EXECUTABLE guard, not a mention', () => {
890
+ const hooks = catalog.filter((e) => e.kind === 'env-hook' && e.parents.some((p) => execEntries.some((x) => x.key === p)));
891
+ assert.ok(hooks.length > 0, 'CODEX_PROBE must be cataloged as an env-hook over codex-exec');
892
+ for (const hook of hooks) {
893
+ assert.ok(
894
+ consultsEnv(source, hook.key),
895
+ `env-hook ${hook.key} is named in the source but never TESTED in an executable condition — a help/comment mention would keep a name-grep green after the logic is deleted`,
896
+ );
897
+ }
898
+ });
899
+
900
+ it('the catalog operand slots set-EQUAL the slots its rendered forms really carry (both directions)', () => {
901
+ for (const entry of execEntries) {
902
+ const forms = entry.invocationRefs.map((r) => EXEC_CONTRACT[r.contractField][r.index]);
903
+ // The DEDUPLICATED UNION over every resolved form: `exec` legitimately spreads its slots across
904
+ // two descriptors, so per-form equality would false-fail a correct catalog.
905
+ const realSlots = new Set(forms.flatMap((f) => f.match(SLOT_RE) ?? []));
906
+ setEq(new Set((entry.operands ?? []).map((o) => o.slot)), realSlots, `${entry.key}: catalog operands ⟷ the slots its forms really carry`);
907
+ }
908
+ });
909
+
910
+ it('the catalog claims CODEX_PROBE over the resume modes because the guard really precedes resume parsing', () => {
911
+ // Verified in source: the quality guard runs BEFORE the resume dispatch, so a resume run is
912
+ // relaxed too — the catalog must say so, and this pins the ORDER the claim rests on.
913
+ const lines = executableLines(source);
914
+ const guardAt = lines.findIndex((l) => /\$\{?CODEX_PROBE/.test(l));
915
+ const resumeAt = lines.findIndex((l) => /^\s*--resume-last\)/.test(l));
916
+ assert.ok(guardAt !== -1 && resumeAt !== -1, 'both the probe guard and the resume arm must exist');
917
+ assert.ok(guardAt < resumeAt, 'the probe guard must precede resume parsing — else the resume parents are a false claim');
918
+ const hook = catalog.find((e) => e.key === 'CODEX_PROBE');
919
+ for (const key of ['exec.resume-last', 'exec.resume']) {
920
+ assert.ok(hook.parents.includes(key), `${key} is relaxed by the hook, so the catalog must declare it a parent`);
921
+ }
922
+ });
923
+
924
+ it('CODEX_PROBE really relaxes the guard on EVERY execute parent the catalog claims (behavioural)', () => {
925
+ // Source ORDER alone is not the claim: a branch bug after resume parsing would keep it green.
926
+ // Drive each claimed parent for real — the guard must stop the dispatch without the hook, and
927
+ // codex must really be reached with it (r.argv is non-empty only on a real invocation).
928
+ const hook = catalog.find((e) => e.key === 'CODEX_PROBE');
929
+ const drive = {
930
+ exec: (sb) => ({ args: ['-'], input: 'do a thing' }),
931
+ 'exec.resume-last': (sb) => {
932
+ writeFileSync(join(sb.repo, '.codex-last-session'), 'sess-from-sidecar\n');
933
+ return { args: ['--resume-last', '-'], input: 'continue' };
934
+ },
935
+ 'exec.resume': () => ({ args: ['--resume', 'sess-xyz', '-'], input: 'continue' }),
936
+ };
937
+ const execParents = hook.parents.filter((p) => execEntries.some((x) => x.key === p));
938
+ assert.ok(execParents.length > 0, 'CODEX_PROBE must claim at least one execute parent');
939
+ for (const parent of execParents) {
940
+ assert.ok(drive[parent], `no behavioural drive for claimed parent "${parent}" — add one`);
941
+
942
+ const guarded = makeSandbox();
943
+ const off = run(guarded, { ...drive[parent](guarded), env: { CODEX_MODEL: 'not-the-pinned-model' } });
944
+ rmSync(guarded.root, { recursive: true, force: true });
945
+ assert.equal(off.status, 2, `${parent}: the quality guard must refuse an off-pin model without the hook`);
946
+ assert.equal(off.argv, '', `${parent}: the guard must refuse BEFORE spending a run`);
947
+
948
+ const probed = makeSandbox();
949
+ const on = run(probed, { ...drive[parent](probed), env: { CODEX_MODEL: 'not-the-pinned-model', CODEX_PROBE: '1' } });
950
+ rmSync(probed.root, { recursive: true, force: true });
951
+ assert.equal(on.status, 0, `${parent}: CODEX_PROBE=1 must really relax the guard — the catalog claims it does`);
952
+ assert.notEqual(on.argv, '', `${parent}: CODEX_PROBE=1 must really reach codex, not merely exit 0`);
953
+ }
954
+ });
955
+ });
956
+
824
957
  // ── bridge settings file + service tier knob (bridges 2.3.0) ─────────────────────
825
958
  // ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE
826
959
  // lines, PARSED (never sourced). Precedence: explicit env (even empty: KEY= disables the
@@ -1080,8 +1213,9 @@ describe('codex-exec.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
1080
1213
  assert.ok(arm, `no validation arm for ${s.key}`);
1081
1214
  if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
1082
1215
  if (s.kind === 'integer') {
1083
- assert.match(arm[1], new RegExp(`>= ${s.min}\\b`), `${s.key}: min ${s.min} not pinned`);
1084
- assert.match(arm[1], new RegExp(`<= ${s.max}\\b`), `${s.key}: max ${s.max} not pinned`);
1216
+ // Issue-012 refactor: min/max are pinned as the aw_int_in_range helper's positional bounds
1217
+ // (`aw_int_in_range "$v" <min> <max>`) the overflow-safe range check replaced raw arithmetic.
1218
+ assert.match(arm[1], new RegExp(`aw_int_in_range "\\$v" ${s.min} ${s.max}\\b`), `${s.key}: min/max ${s.min}/${s.max} not pinned as the aw_int_in_range bounds`);
1085
1219
  }
1086
1220
  if (s.kind === 'boolean') assert.ok(arm[1].includes('"0"') && arm[1].includes('"1"'), `${s.key}: boolean 0/1 not pinned`);
1087
1221
  if (s.kind === 'duration') {
@@ -62,7 +62,10 @@ Receipt:
62
62
  symlinks/directories ride as name-only notes) in code mode, the artifact-file
63
63
  sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the
64
64
  verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge,
65
- factsHash null); a write failure warns, never fails the review
65
+ factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on
66
+ EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a
67
+ probe review never attests) and equally rejects an unmarked one (silence is not a declaration); a
68
+ write failure warns, never fails the review
66
69
 
67
70
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
68
71
  ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
@@ -104,12 +107,22 @@ aw_settings_known() {
104
107
  *) return 1 ;;
105
108
  esac
106
109
  }
110
+ aw_int_in_range() {
111
+ # All-digits $1 vs [min,max] WITHOUT 64-bit wrap (Issue-012): strip leading zeros, then a longer
112
+ # digit count than max's is unconditionally out of range — never do the arithmetic on a huge string.
113
+ # A leading-zero in-range value still passes (its stripped count is small); all-zeros collapses to
114
+ # "0" (below min>=1). Mirrors the JS settingValueValid safe-integer bound, verified by parity test.
115
+ local n="${1#"${1%%[!0]*}"}" min="$2" max="$3"
116
+ n="${n:-0}"
117
+ (( ${#n} > ${#max} )) && return 1
118
+ (( n >= min && n <= max ))
119
+ }
107
120
  aw_settings_valid() {
108
121
  local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
109
122
  case "$k" in
110
123
  CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
111
- CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
112
- CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
124
+ CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 86400 ;;
125
+ CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 100000000 ;;
113
126
  AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
114
127
  AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
115
128
  *) return 1 ;;
@@ -161,6 +174,14 @@ aw_apply_settings() {
161
174
  fi
162
175
  continue
163
176
  fi
177
+ # Normalize an all-digit (integer) value to DECIMAL before export: a leading-zero value the integer
178
+ # arms legitimately accept (000…086400 == 86400) would otherwise read as OCTAL in downstream Bash
179
+ # arithmetic ("value too great for base"). Strip leading zeros, floor "0"; enum/duration (non-digit)
180
+ # and boolean 0/1 are unaffected.
181
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
182
+ value="${value#"${value%%[!0]*}"}"
183
+ value="${value:-0}"
184
+ fi
164
185
  export "$key=$value"
165
186
  done
166
187
  return 0
@@ -172,7 +193,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
172
193
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
173
194
  # version (drift-guarded by codex-review.test.mjs against capability.json).
174
195
  AW_RECEIPT_BACKEND="codex"
175
- AW_BRIDGE_VERSION="2.7.0"
196
+ AW_BRIDGE_VERSION="2.8.0"
176
197
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
177
198
  CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
178
199
  # Generous hard cap for a slow xhigh review (subscription latency varies).
@@ -199,7 +220,11 @@ fi
199
220
  CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
200
221
 
201
222
  # --- Quality-first guard: refuse a silent model/effort downgrade ---------------
223
+ # A relaxed run is RECORDED, not just warned about: the receipt carries probe:true so the kit's
224
+ # review-state gate rejects it — "do NOT use this as a real review" becomes a mechanism, not a plea.
225
+ REVIEW_PROBE=false
202
226
  if [[ "${CODEX_PROBE:-}" == "1" ]]; then
227
+ REVIEW_PROBE=true
203
228
  echo "warning: CODEX_PROBE=1 — THROWAWAY PROBE MODE. Quality guards relaxed; do NOT use this run's" >&2
204
229
  echo " output as a real review (model='$CODEX_MODEL' effort='$CODEX_EFFORT')." >&2
205
230
  else
@@ -396,13 +421,18 @@ receipt_json_scalar() {
396
421
  if [[ -z "${1:-}" ]]; then printf 'null'; else printf '"%s"' "$1"; fi
397
422
  }
398
423
 
399
- # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|"">
424
+ # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false]
400
425
  # Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
401
426
  # to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
402
427
  # git dir by construction, so it is never committable). Fail-safe: every failure here warns loudly
403
428
  # and returns 0 — a missing receipt fails the kit's review-state CHECKER, never the review run.
429
+ # The optional 7th argument marks a PROBE run (CODEX_PROBE=1 / AGY_PROBE=1 — the quality guards
430
+ # relaxed), which the kit's review-state gate rejects: a probe review must never attest a tree. The
431
+ # marker is written ALWAYS, true or false: the receipt SELF-DECLARES, so the gate reads the fact
432
+ # itself instead of inferring it from this wrapper's version (which bumps in a different release
433
+ # phase). Silence is not a declaration — an unmarked receipt is untrustworthy and the gate rejects it.
404
434
  write_review_receipt() {
405
- local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6"
435
+ local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6" probe="${7:-false}"
406
436
  local receipts="${AW_REVIEW_RECEIPTS:-}"
407
437
  if [[ -z "$receipts" ]]; then
408
438
  local receipt_git_dir
@@ -412,11 +442,12 @@ write_review_receipt() {
412
442
  fi
413
443
  receipts="$receipt_git_dir/agent-workflow-review-receipts.jsonl"
414
444
  fi
415
- local line
416
- line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"}' \
445
+ local line probe_field=',"probe":false'
446
+ if [[ "$probe" == "true" ]]; then probe_field=',"probe":true'; fi
447
+ line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"%s}' \
417
448
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
418
449
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
419
- "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)")"
450
+ "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field")"
420
451
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
421
452
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
422
453
  echo " the review-state gate will read the current tree as un-receipted." >&2
@@ -669,5 +700,5 @@ fi
669
700
  [[ -z "$verdict" ]] && verdict="unknown"
670
701
  # codex is grounded by construction (AGENTS.md auto-merge + the precomputed change set): grounded
671
702
  # true, factsHash null (native grounding — no separate facts payload exists). Every codex run is a
672
- # full fresh run (one-shot, no resume) → fresh:true.
673
- write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" true ""
703
+ # full fresh run (one-shot, no resume) → fresh:true. $REVIEW_PROBE marks a guards-relaxed run.
704
+ write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" true "" "$REVIEW_PROBE"