@sabaiway/agent-workflow-kit 1.49.0 → 2.1.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.
@@ -58,13 +58,17 @@ prompt for `agy-run`. It **mechanizes the grounded-review contract** (see
58
58
  [`review-prompt.md`](./review-prompt.md)) so grounding is the enforced default, not a per-call effort:
59
59
 
60
60
  ```bash
61
- agy-review code [--facts @facts.md] [--decided @decided.md] [--focus "…"] [extra focus…]
62
- agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"]
61
+ agy-review code --facts @facts.md [--decided @decided.md] [--focus "…"] [extra focus…] # facts REQUIRED — refuses pre-spend
62
+ agy-review code --ungrounded # explicit escape: throwaway ungrounded opinion (never attests)
63
+ agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"] # no facts ⇒ loud warning, proceeds
63
64
  agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus "…"]
64
65
  agy-review --continue [--decided @f] [--focus "…"] # round-2 delta — no re-assembly
65
66
  agy-review --conversation <id> [--decided @f] [--focus "…"]
66
67
  ```
67
68
 
69
+ `code` fails CLOSED without a non-empty `--facts` payload — the refusal fires before any run is
70
+ spent; the explicit escapes are `--ungrounded` and `AGY_PROBE=1` (a probe never attests).
71
+
68
72
  What it does for you, and what YOU must supply:
69
73
 
70
74
  - **It** assembles POSTURE + a model/cutoff GUARD + your facts + your already-decided list + your focus
@@ -48,14 +48,15 @@ subscription invariant, and the byte ceiling all apply once. The playbook is in
48
48
  [`driving-agy.md`](./driving-agy.md); the surface:
49
49
 
50
50
  ```bash
51
- agy-review code|plan <file>|diff <file> [--facts @f] [--decided @f] [--focus "…"]
51
+ agy-review code --facts @f [--decided @f] [--focus "…"] # facts REQUIRED — refuses pre-spend (escapes: --ungrounded, AGY_PROBE=1)
52
+ agy-review plan <file>|diff <file> [--facts @f] [--decided @f] [--focus "…"]
52
53
  agy-review --continue | --conversation <id> [--decided @f] [--focus "…"] # round-2 delta
53
54
  ```
54
55
 
55
56
  | Var | Default | Effect |
56
57
  |---|---|---|
57
58
  | `AGY_MODEL` | `Gemini 3.1 Pro (High)` | frontier default; **any** model is allowed — a sub-frontier one earns a silenceable advisory (quality-first, not a gate) |
58
- | `AGY_PROBE` | `0` | `1` silences the off-frontier model advisory (throwaway probe) |
59
+ | `AGY_PROBE` | `0` | `1` silences the off-frontier model advisory AND lets `code` run without `--facts` (an ungrounded probe never attests — its receipt is probe-marked) |
59
60
  | `AGY_REVIEW_ALLOW_ADDDIR` | `0` | `1` lets an oversized `code` review offload ONLY the change set to a private staging dir via `--add-dir` (the grounding stays inline; re-enables the Issue-001 stall risk — prefer splitting into focused reviews) |
60
61
  | `AGY_HARD_TIMEOUT` | `30m` | the review's hard cap (longer default than a probe — reviews are slower) |
61
62
  | `AGY_MAX_PROMPT_BYTES` | `120000` | the same single-argv byte ceiling; oversized → trim/split (or the `--add-dir` escape above) |
@@ -23,7 +23,7 @@ GUARD Do NOT comment on AI model names/versions or your own knowledge cutoff
23
23
  and a known source of false positives. Review ONLY the engineering, AGAINST the facts.
24
24
 
25
25
  ## Grounded facts — review AGAINST these, do NOT guess the code
26
- {{GROUNDED_FACTS}} # from --facts @file (omitted -> an in-band note + a LOUD stderr warning)
26
+ {{GROUNDED_FACTS}} # from --facts @file (code: REQUIRED non-empty, refuses pre-spend — escapes --ungrounded / AGY_PROBE=1; plan/diff omitted -> an in-band note + a LOUD stderr warning)
27
27
 
28
28
  ## Decisions already made / already addressed — do NOT re-raise these
29
29
  {{ALREADY_DECIDED}} # from --decided @file (optional — the anti-circling lever; the round-2 payload)
@@ -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.1'
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
@@ -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
@@ -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
@@ -190,7 +193,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
190
193
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
191
194
  # version (drift-guarded by codex-review.test.mjs against capability.json).
192
195
  AW_RECEIPT_BACKEND="codex"
193
- AW_BRIDGE_VERSION="2.7.1"
196
+ AW_BRIDGE_VERSION="2.8.0"
194
197
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
195
198
  CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
196
199
  # Generous hard cap for a slow xhigh review (subscription latency varies).
@@ -217,7 +220,11 @@ fi
217
220
  CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
218
221
 
219
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
220
226
  if [[ "${CODEX_PROBE:-}" == "1" ]]; then
227
+ REVIEW_PROBE=true
221
228
  echo "warning: CODEX_PROBE=1 — THROWAWAY PROBE MODE. Quality guards relaxed; do NOT use this run's" >&2
222
229
  echo " output as a real review (model='$CODEX_MODEL' effort='$CODEX_EFFORT')." >&2
223
230
  else
@@ -414,13 +421,18 @@ receipt_json_scalar() {
414
421
  if [[ -z "${1:-}" ]]; then printf 'null'; else printf '"%s"' "$1"; fi
415
422
  }
416
423
 
417
- # 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]
418
425
  # Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
419
426
  # to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
420
427
  # git dir by construction, so it is never committable). Fail-safe: every failure here warns loudly
421
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.
422
434
  write_review_receipt() {
423
- 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}"
424
436
  local receipts="${AW_REVIEW_RECEIPTS:-}"
425
437
  if [[ -z "$receipts" ]]; then
426
438
  local receipt_git_dir
@@ -430,11 +442,12 @@ write_review_receipt() {
430
442
  fi
431
443
  receipts="$receipt_git_dir/agent-workflow-review-receipts.jsonl"
432
444
  fi
433
- local line
434
- 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}' \
435
448
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
436
449
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
437
- "$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")"
438
451
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
439
452
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
440
453
  echo " the review-state gate will read the current tree as un-receipted." >&2
@@ -687,5 +700,5 @@ fi
687
700
  [[ -z "$verdict" ]] && verdict="unknown"
688
701
  # codex is grounded by construction (AGENTS.md auto-merge + the precomputed change set): grounded
689
702
  # true, factsHash null (native grounding — no separate facts payload exists). Every codex run is a
690
- # full fresh run (one-shot, no resume) → fresh:true.
691
- 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"
@@ -618,6 +618,32 @@ const extractArgCaseArms = (source) => {
618
618
  };
619
619
  const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
620
620
 
621
+ // The source lines that really EXECUTE: a heredoc body (the --help text) and a comment both carry
622
+ // names without carrying logic, so a bare name-grep over the whole source stays green after the
623
+ // logic is deleted. Reuses the same heredoc discipline as extractArgCaseArms.
624
+ const executableLines = (source) => {
625
+ const out = [];
626
+ let heredoc = null;
627
+ for (const raw of source.split('\n')) {
628
+ if (heredoc) {
629
+ if (raw.trim() === heredoc) heredoc = null;
630
+ continue;
631
+ }
632
+ const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
633
+ if (hd) { heredoc = hd[1]; continue; }
634
+ if (raw.trimStart().startsWith('#')) continue;
635
+ out.push(raw);
636
+ }
637
+ return out;
638
+ };
639
+ // An env var is really CONSULTED when an executable test compares it: [[ "${NAME:-}" == … ]].
640
+ const consultsEnv = (source, name) =>
641
+ executableLines(source).some((l) => new RegExp(`\\[\\[[^\\]]*\\$\\{?${name}\\b[^\\]]*(==|!=)`).test(l));
642
+ // The operand slots a rendered invocation form really carries: <angle> and [bracket] placeholders.
643
+ // The optional `@` prefix rides WITH the slot (`@<facts-file>` is one operand, not a bare
644
+ // `<facts-file>` behind a stray character) — the catalog declares the whole token a user types.
645
+ const SLOT_RE = /@?<[^<>]+>|\[[^[\]]*\]/g;
646
+
621
647
  describe('codex-review.sh — --help contract (manifest-pinned)', () => {
622
648
  it('--help and -h exit 0 pre-preflight (no codex, no git, no AGENTS.md)', () => {
623
649
  for (const arg of ['--help', '-h']) {
@@ -679,11 +705,114 @@ describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manife
679
705
  });
680
706
  });
681
707
 
708
+ // ── mode catalog ⟷ wrapper reality (BRIDGE-MODES-CATALOG) ─────────────────────────
709
+ // The kit validator owns the catalog's INTERNAL shape; these arms pin what only the wrapper source
710
+ // can settle — the catalog documents THIS wrapper's real modes and real escape hatches, and every
711
+ // contract invocation the wrapper honours is cataloged (adding a mode without one fails here).
712
+ describe('codex-review.sh — mode catalog ⟷ wrapper reality (manifest-pinned)', () => {
713
+ const source = readFileSync(WRAPPER, 'utf8');
714
+ const arms = extractArgCaseArms(source);
715
+ const catalog = MANIFEST.modeCatalog ?? [];
716
+ const reviewEntries = catalog.filter((e) => e.role === 'review');
717
+ const reviewPrimaries = reviewEntries.filter((e) => e.kind === 'primary');
718
+
719
+ it('the catalog submodes ARE the wrapper\'s real parser mode arms (both directions)', () => {
720
+ const modes = splitArms(arms.get('"$mode"')).filter((a) => a !== '*');
721
+ assert.ok(reviewPrimaries.length > 0, 'the manifest must catalog its review modes');
722
+ setEq(reviewPrimaries.map((e) => e.submode), modes, 'catalog submodes ⟷ real parser mode arms');
723
+ });
724
+
725
+ it('every review entry composes BY REFERENCE and every reference resolves', () => {
726
+ for (const entry of reviewEntries) {
727
+ assert.ok(
728
+ Array.isArray(entry.invocationRefs) && entry.invocationRefs.length > 0,
729
+ `${entry.key}: a contract-backed entry references at least one contract descriptor`,
730
+ );
731
+ assert.ok(!Object.hasOwn(entry, 'descriptor'), `${entry.key}: a contract-backed entry never restates a literal descriptor`);
732
+ for (const ref of entry.invocationRefs) {
733
+ assert.equal(
734
+ typeof REVIEW_CONTRACT[ref.contractField]?.[ref.index], 'string',
735
+ `${entry.key}: ref ${ref.contractField}[${ref.index}] does not resolve into the manifest contract`,
736
+ );
737
+ }
738
+ }
739
+ });
740
+
741
+ it('every review contract invocation is claimed by exactly ONE catalog entry (no uncataloged mode)', () => {
742
+ const claims = reviewEntries.flatMap((e) => e.invocationRefs.map((r) => `${r.contractField}[${r.index}]`));
743
+ assert.equal(new Set(claims).size, claims.length, 'a contract invocation is claimed at most once');
744
+ const declared = [
745
+ ...REVIEW_CONTRACT.invocations.map((_, i) => `invocations[${i}]`),
746
+ ...(REVIEW_CONTRACT.continue ?? []).map((_, i) => `continue[${i}]`),
747
+ ];
748
+ setEq(new Set(claims), declared, 'catalog claims ⟷ declared contract invocations');
749
+ });
750
+
751
+ it('every env-hook the catalog aims at a review mode is a real EXECUTABLE guard, not a mention', () => {
752
+ const hooks = catalog.filter((e) => e.kind === 'env-hook' && e.parents.some((p) => reviewPrimaries.some((r) => r.key === p)));
753
+ assert.ok(hooks.length > 0, 'CODEX_PROBE must be cataloged as an env-hook over the review modes');
754
+ for (const hook of hooks) {
755
+ assert.ok(
756
+ consultsEnv(source, hook.key),
757
+ `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`,
758
+ );
759
+ }
760
+ });
761
+
762
+ it('the catalog operand slots set-EQUAL the slots its rendered forms really carry (both directions)', () => {
763
+ for (const entry of reviewEntries) {
764
+ const forms = entry.invocationRefs.map((r) => REVIEW_CONTRACT[r.contractField][r.index]);
765
+ // The DEDUPLICATED UNION over every resolved form: a plural-ref entry legitimately spreads its
766
+ // slots across forms, so per-form equality would false-fail a correct catalog.
767
+ const realSlots = new Set(forms.flatMap((f) => f.match(SLOT_RE) ?? []));
768
+ setEq(new Set((entry.operands ?? []).map((o) => o.slot)), realSlots, `${entry.key}: catalog operands ⟷ the slots its forms really carry`);
769
+ }
770
+ });
771
+
772
+ it('an entry rendering a LITERAL descriptor is slot-checked too (env-hooks have no role to filter on)', () => {
773
+ // The contract-backed arm above filters by role — and an env-hook HAS no role, so its descriptor
774
+ // would never be slot-checked. That is exactly how a hardcoded dead path can reach the discovery
775
+ // surface looking ready-to-run. Every literal-descriptor kind is covered here: env-hooks and
776
+ // contract-free primaries.
777
+ const literalEntries = catalog.filter((e) => typeof e.descriptor === 'string');
778
+ assert.ok(literalEntries.length > 0, 'CODEX_PROBE must be cataloged with a literal descriptor');
779
+ for (const entry of literalEntries) {
780
+ const realSlots = new Set(entry.descriptor.match(SLOT_RE) ?? []);
781
+ setEq(new Set((entry.operands ?? []).map((o) => o.slot)), realSlots, `${entry.key}: catalog operands ⟷ the slots its descriptor really carries`);
782
+ }
783
+ });
784
+
785
+ it('CODEX_PROBE really relaxes the guard on EVERY review parent the catalog claims (behavioural)', () => {
786
+ // The catalog CLAIMS these modes are modified by the hook; prove it per parent rather than
787
+ // trusting a source scan: an off-pin model is exit 2 normally, exit 0 under probe.
788
+ const hook = catalog.find((e) => e.key === 'CODEX_PROBE');
789
+ const drive = { 'review.plan': ['plan', 'plan.md'], 'review.code': ['code'] };
790
+ const reviewParents = hook.parents.filter((p) => reviewPrimaries.some((r) => r.key === p));
791
+ assert.ok(reviewParents.length > 0, 'CODEX_PROBE must claim at least one review parent');
792
+ for (const parent of reviewParents) {
793
+ assert.ok(drive[parent], `no behavioural drive for claimed parent "${parent}" — add one`);
794
+ // The exit code alone is weak evidence: pin the real DISPATCH too (capStdin is non-empty only
795
+ // when codex was actually invoked), so a run that dies early can never pass the probe-on branch.
796
+ const guarded = makeSandbox();
797
+ const off = run(guarded, { args: drive[parent], env: { CODEX_MODEL: 'not-the-pinned-model' } });
798
+ rmSync(guarded.root, { recursive: true, force: true });
799
+ assert.equal(off.status, 2, `${parent}: the quality guard must refuse an off-pin model without the hook`);
800
+ assert.equal(off.capStdin, '', `${parent}: the guard must refuse BEFORE spending a run`);
801
+
802
+ const probed = makeSandbox();
803
+ const on = run(probed, { args: drive[parent], env: { CODEX_MODEL: 'not-the-pinned-model', CODEX_PROBE: '1' } });
804
+ rmSync(probed.root, { recursive: true, force: true });
805
+ assert.equal(on.status, 0, `${parent}: CODEX_PROBE=1 must really relax the guard — the catalog claims it does`);
806
+ assert.notEqual(on.capStdin, '', `${parent}: CODEX_PROBE=1 must really reach codex, not merely exit 0`);
807
+ }
808
+ });
809
+ });
810
+
682
811
  // ── review receipts (AD-038) ─────────────────────────────────────────────────────
683
- // The normative fixture (docs: the AD-038 plan Decisions copied verbatim; field VALUES with
812
+ // The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (field VALUES with
684
813
  // dynamic content are asserted by shape):
685
814
  const RECEIPT_FIXTURE = JSON.parse(
686
- '{"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"}',
815
+ '{"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","probe":false}',
687
816
  );
688
817
  const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
689
818
  const readReceipts = (repo) => {
@@ -715,6 +844,40 @@ describe('codex-review.sh — review receipts (AD-038)', () => {
715
844
  assert.match(receipt.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
716
845
  });
717
846
 
847
+ // The probe marker (BRIDGE-MODES-CATALOG, D3): a CODEX_PROBE=1 review runs with the
848
+ // frontier-model/max-effort guard OFF, so its receipt must be distinguishable — the kit's
849
+ // review-state gate rejects a probe-marked receipt. EVERY receipt carries the marker (true or
850
+ // false): it self-declares, so the gate reads the fact rather than inferring it from a version
851
+ // string that bumps in a different release phase. Silence is not a declaration.
852
+ it('CODEX_PROBE=1 stamps probe:true — a throwaway probe can never attest a tree (D3)', () => {
853
+ const sb = makeSandbox();
854
+ const r = run(sb, { env: { CODEX_PROBE: '1', CODEX_FAKE_FINAL: 'Verdict: ship' } });
855
+ const receipts = readReceipts(sb.repo);
856
+ rmSync(sb.root, { recursive: true, force: true });
857
+ assert.equal(r.status, 0, r.stderr);
858
+ assert.equal(receipts[0].probe, true, 'a probe-relaxed run marks its own receipt');
859
+ assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
860
+ });
861
+
862
+ // Every receipt SELF-DECLARES: the kit's gate reads the marker, never the wrapper version — so
863
+ // the marker must not depend on a version bump landing in the same release phase.
864
+ it('a normal review self-declares probe:false — the receipt states the fact, not a version', () => {
865
+ const sb = makeSandbox();
866
+ run(sb, { env: { CODEX_FAKE_FINAL: 'Verdict: ship' } });
867
+ const receipts = readReceipts(sb.repo);
868
+ rmSync(sb.root, { recursive: true, force: true });
869
+ assert.equal(receipts[0].probe, false, 'silence is not a declaration — the gate rejects an unmarked receipt');
870
+ });
871
+
872
+ it('the marker tracks the RELAXED GUARD, not the model — a probe on an off-pinned model still marks', () => {
873
+ const sb = makeSandbox();
874
+ const r = run(sb, { env: { CODEX_PROBE: '1', CODEX_MODEL: 'gpt-5-mini', CODEX_EFFORT: 'low', CODEX_FAKE_FINAL: 'Verdict: ship' } });
875
+ const receipts = readReceipts(sb.repo);
876
+ rmSync(sb.root, { recursive: true, force: true });
877
+ assert.equal(r.status, 0, r.stderr);
878
+ assert.equal(receipts[0].probe, true, 'exactly the runs the guard let through unpinned are marked');
879
+ });
880
+
718
881
  it('the code-mode fingerprint tracks the uncommitted state (same tree → same hash; edit → different)', () => {
719
882
  const sb = makeSandbox();
720
883
  // Route the fake-codex capture files to /dev/null so the runs themselves leave the repo
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "codex-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.7.1",
6
+ "version": "2.8.0",
7
7
  "provides": ["execute", "review"],
8
8
  "roles": {
9
9
  "execute": {
@@ -42,10 +42,126 @@
42
42
  ],
43
43
  "grounding": "automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags",
44
44
  "continue": [],
45
- "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): 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 mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); a write failure warns, never fails the review"
45
+ "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): 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 mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_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"
46
46
  }
47
47
  }
48
48
  },
49
+ "modeCatalog": [
50
+ {
51
+ "key": "exec",
52
+ "kind": "primary",
53
+ "role": "execute",
54
+ "purpose": "Execute a bounded plan in a sandboxed workspace and hand back a diff to review.",
55
+ "whenToUse": [
56
+ "a bounded sub-task whose acceptance criteria are already pinned",
57
+ "work you will review and commit yourself — the backend never commits"
58
+ ],
59
+ "whenNotTo": [
60
+ "work whose design is still open — decide it first, then delegate",
61
+ "anything you cannot check: the diff is advisory until you review it"
62
+ ],
63
+ "invocationRefs": [
64
+ { "contractField": "invocations", "index": 0 },
65
+ { "contractField": "invocations", "index": 1 }
66
+ ],
67
+ "operands": [
68
+ { "slot": "<plan-file|->", "required": true, "description": "the plan or instruction file, or - to read it from stdin" },
69
+ { "slot": "<extra codex flags...>", "required": false, "description": "extra codex flags, filtered by the guarded passthrough tiers" }
70
+ ],
71
+ "guardrails": [
72
+ { "value": "runs under codex's OWN OS sandbox (workspace-write)", "enforcement": "enforced", "condition": "it cannot nest inside another sandbox — the FS turns read-only; route it outside on the OBSERVED failure", "source": "capability.json roles.execute.contract.notes" },
73
+ { "value": "the guarded passthrough blocks model / sandbox / approval overrides", "enforcement": "enforced", "source": "bin/codex-exec.sh" },
74
+ { "value": "hard wall-clock cap CODEX_HARD_TIMEOUT (built-in default 3600s)", "enforcement": "enforced", "condition": "only while timeout(1)/gtimeout is on PATH — otherwise the wrapper warns and runs uncapped", "source": "capability.json settings.CODEX_HARD_TIMEOUT" }
75
+ ],
76
+ "customHooks": ["CODEX_PROBE"]
77
+ },
78
+ {
79
+ "key": "exec.resume-last",
80
+ "kind": "continuation",
81
+ "role": "execute",
82
+ "purpose": "Continue the most recent codex-exec session with a follow-up instruction.",
83
+ "whenToUse": ["a small delta on work the same session already holds in context"],
84
+ "whenNotTo": ["a fresh task — a stale session carries stale assumptions"],
85
+ "invocationRefs": [{ "contractField": "continue", "index": 0 }],
86
+ "operands": [
87
+ { "slot": "<plan-file|->", "required": true, "description": "the follow-up instruction file, or - to read it from stdin" }
88
+ ],
89
+ "customHooks": ["CODEX_PROBE"]
90
+ },
91
+ {
92
+ "key": "exec.resume",
93
+ "kind": "continuation",
94
+ "role": "execute",
95
+ "purpose": "Continue a NAMED codex-exec session by the session id it printed.",
96
+ "whenToUse": ["resuming a specific earlier session after other runs happened in between"],
97
+ "invocationRefs": [{ "contractField": "continue", "index": 1 }],
98
+ "operands": [
99
+ { "slot": "<session-id>", "required": true, "description": "the session id the original run printed on stderr" },
100
+ { "slot": "<plan-file|->", "required": true, "description": "the follow-up instruction file, or - to read it from stdin" }
101
+ ],
102
+ "customHooks": ["CODEX_PROBE"]
103
+ },
104
+ {
105
+ "key": "review.plan",
106
+ "kind": "primary",
107
+ "role": "review",
108
+ "submode": "plan",
109
+ "purpose": "Critique an implementation plan before any of its code exists.",
110
+ "whenToUse": [
111
+ "a plan draft heading into its review council",
112
+ "checking whether a cold executor could really run the plan"
113
+ ],
114
+ "whenNotTo": ["a working-tree change set — that is review.code"],
115
+ "invocationRefs": [{ "contractField": "invocations", "index": 0 }],
116
+ "operands": [
117
+ { "slot": "<plan-file>", "required": true, "description": "the plan file under review" }
118
+ ],
119
+ "guardrails": [
120
+ { "value": "read-only sandbox — codex cannot edit, create or delete a file", "enforcement": "enforced", "source": "bin/codex-review.sh" },
121
+ { "value": "runs on the pinned frontier model at max effort", "enforcement": "enforced", "condition": "unless CODEX_PROBE=1 relaxes the guard for a throwaway probe", "source": "bin/codex-review.sh" },
122
+ { "value": "a successful review appends one receipt line the review-state gate reads", "enforcement": "enforced", "condition": "a receipt write failure warns and the review still succeeds — the tree then reads un-receipted", "source": "capability.json roles.review.contract.receipt" }
123
+ ],
124
+ "customHooks": ["CODEX_PROBE"]
125
+ },
126
+ {
127
+ "key": "review.code",
128
+ "kind": "primary",
129
+ "role": "review",
130
+ "submode": "code",
131
+ "purpose": "Review the uncommitted working-tree change set, precomputed in full for codex.",
132
+ "whenToUse": [
133
+ "a finished segment heading into its review round",
134
+ "a second opinion on real code before the commit ask"
135
+ ],
136
+ "whenNotTo": ["a clean tree — the wrapper exits before spending a run"],
137
+ "invocationRefs": [{ "contractField": "invocations", "index": 1 }],
138
+ "operands": [
139
+ { "slot": "[extra focus...]", "required": false, "description": "extra focus words appended to the review directive" }
140
+ ],
141
+ "guardrails": [
142
+ { "value": "read-only sandbox — codex cannot edit, create or delete a file", "enforcement": "enforced", "source": "bin/codex-review.sh" },
143
+ { "value": "an oversized change set rides via a git-dir temp file, never truncated", "enforcement": "enforced", "source": "bin/codex-review.sh" },
144
+ { "value": "a successful review appends one receipt line the review-state gate reads", "enforcement": "enforced", "condition": "a receipt write failure warns and the review still succeeds — the tree then reads un-receipted", "source": "capability.json roles.review.contract.receipt" }
145
+ ],
146
+ "customHooks": ["CODEX_PROBE"]
147
+ },
148
+ {
149
+ "key": "CODEX_PROBE",
150
+ "kind": "env-hook",
151
+ "parents": ["exec", "exec.resume-last", "exec.resume", "review.plan", "review.code"],
152
+ "purpose": "Relax the quality guards for a THROWAWAY probe run.",
153
+ "whenToUse": [
154
+ "a probe whose answer cannot depend on model or effort",
155
+ "checking that the wrapper plumbing works at all"
156
+ ],
157
+ "whenNotTo": ["any run whose output informs what ships — a probe never attests"],
158
+ "descriptor": "CODEX_PROBE=1 codex-review code",
159
+ "guardrails": [
160
+ { "value": "a probe review mints a probe-marked receipt the review-state gate rejects", "enforcement": "enforced", "source": "agent-workflow-kit tools/review-state.mjs" },
161
+ { "value": "on codex-exec it also relaxes the passthrough to the probeRelaxed tier", "enforcement": "enforced", "source": "bin/codex-exec.sh" }
162
+ ]
163
+ }
164
+ ],
49
165
  "settings": [
50
166
  {
51
167
  "key": "CODEX_SERVICE_TIER",
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "1.49.0",
6
+ "version": "2.1.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {