@sabaiway/agent-workflow-kit 2.1.0 → 3.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +6 -5
  3. package/SKILL.md +12 -8
  4. package/bin/install.mjs +1 -1
  5. package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +150 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +95 -4
  10. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  11. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  12. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +143 -0
  13. package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
  14. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +93 -10
  15. package/bridges/codex-cli-bridge/capability.json +3 -2
  16. package/capability.json +1 -1
  17. package/migrations/3.0.0-hardened-core-loop.md +44 -0
  18. package/migrations/README.md +1 -1
  19. package/package.json +2 -2
  20. package/references/hooks/gate-approve.mjs +1 -1
  21. package/references/modes/bootstrap.md +3 -3
  22. package/references/modes/commit-guard.md +19 -0
  23. package/references/modes/core-evidence.md +15 -0
  24. package/references/modes/coverage-check.md +12 -0
  25. package/references/modes/doc-parity.md +7 -6
  26. package/references/modes/gates.md +7 -6
  27. package/references/modes/grounding.md +1 -2
  28. package/references/modes/hook.md +1 -1
  29. package/references/modes/review-state.md +3 -3
  30. package/references/modes/upgrade.md +6 -6
  31. package/references/modes/velocity.md +2 -2
  32. package/references/scripts/archive-decisions.mjs +1 -1
  33. package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
  34. package/references/scripts/install-git-hooks.mjs +90 -18
  35. package/references/scripts/install-git-hooks.test.mjs +102 -0
  36. package/references/scripts/migrate-gates-branches.test.mjs +157 -0
  37. package/references/scripts/migrate-gates.mjs +395 -0
  38. package/references/scripts/migrate-gates.test.mjs +284 -0
  39. package/references/templates/agent_rules.md +2 -2
  40. package/tools/ack-write.mjs +1 -1
  41. package/tools/atomic-write.mjs +2 -2
  42. package/tools/autonomy-config.mjs +1 -1
  43. package/tools/autonomy-doctor.mjs +1 -1
  44. package/tools/autonomy-write.mjs +1 -1
  45. package/tools/bridge-settings-read.mjs +1 -1
  46. package/tools/bridge-settings.mjs +1 -1
  47. package/tools/changed-surface.mjs +8 -69
  48. package/tools/cheap-agents.mjs +2 -2
  49. package/tools/commands.mjs +17 -10
  50. package/tools/commit-guard.mjs +167 -0
  51. package/tools/core-evidence.mjs +914 -0
  52. package/tools/coverage-check.mjs +260 -0
  53. package/tools/delegation.mjs +1 -1
  54. package/tools/detect-backends.mjs +3 -3
  55. package/tools/doc-parity.mjs +11 -27
  56. package/tools/engine-source.mjs +1 -1
  57. package/tools/family-members.mjs +1 -1
  58. package/tools/family-registry.mjs +1 -1
  59. package/tools/fs-safe.mjs +1 -1
  60. package/tools/gate-hook.mjs +1 -1
  61. package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
  62. package/tools/grounding.mjs +6 -83
  63. package/tools/hide-footprint.mjs +1 -1
  64. package/tools/inject-methodology.mjs +1 -1
  65. package/tools/known-footprint.mjs +1 -1
  66. package/tools/labels.mjs +1 -1
  67. package/tools/lcov.mjs +6 -10
  68. package/tools/lens-region.mjs +1 -1
  69. package/tools/manifest/validate.mjs +19 -0
  70. package/tools/migrate-adr-store.mjs +2 -2
  71. package/tools/orchestration-config.mjs +1 -1
  72. package/tools/orchestration-write.mjs +1 -1
  73. package/tools/presentation.mjs +1 -1
  74. package/tools/procedures.mjs +2 -2
  75. package/tools/recipes.mjs +60 -9
  76. package/tools/recommendations.mjs +63 -7
  77. package/tools/release-scan.mjs +1 -1
  78. package/tools/renderers.mjs +1 -1
  79. package/tools/review-state.mjs +288 -340
  80. package/tools/run-gates.mjs +216 -92
  81. package/tools/sandbox-masks.mjs +1 -1
  82. package/tools/semver-lite.mjs +1 -1
  83. package/tools/set-autonomy.mjs +1 -1
  84. package/tools/set-recipe.mjs +1 -1
  85. package/tools/setup-backends.mjs +1 -1
  86. package/tools/surface.mjs +1 -1
  87. package/tools/uninstall.mjs +1 -1
  88. package/tools/velocity-profile.mjs +2 -2
  89. package/tools/view-model.mjs +1 -1
  90. package/references/modes/fold-completeness.md +0 -30
  91. package/references/modes/review-ledger.md +0 -34
  92. package/references/templates/verification-profile.json +0 -10
  93. package/tools/fold-completeness-run.mjs +0 -1120
  94. package/tools/fold-completeness.mjs +0 -672
  95. package/tools/review-ledger-core.mjs +0 -428
  96. package/tools/review-ledger-write.mjs +0 -647
  97. package/tools/review-ledger.mjs +0 -630
  98. package/tools/sarif.mjs +0 -52
  99. package/tools/verification-profile.mjs +0 -219
@@ -38,7 +38,9 @@ const FAKE_AGY = [
38
38
  ' fi; prev="$a"',
39
39
  'done',
40
40
  'if [[ -n "${AGY_FAKE_SLEEP:-}" ]]; then sleep "$AGY_FAKE_SLEEP"; fi',
41
- 'printf "%s\\n" "${AGY_FAKE_OUTPUT:-FAKE_AGY_REVIEW_OUTPUT}"',
41
+ // Unset AGY_FAKE_OUTPUT → a verdict-carrying default (D4: a verdict-less run is a FAILURE, so
42
+ // the success-path tests need one); an EXPLICIT empty value exercises the empty-output failure.
43
+ 'if [[ -z "${AGY_FAKE_OUTPUT+x}" ]]; then printf "FAKE_AGY_REVIEW_OUTPUT\\n### Verdict\\nSHIP\\n"; else printf "%s\\n" "$AGY_FAKE_OUTPUT"; fi',
42
44
  'exit "${AGY_FAKE_EXIT:-0}"',
43
45
  '',
44
46
  ].join('\n');
@@ -870,7 +872,7 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
870
872
  // The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (backend/verdict here
871
873
  // carry this bridge's vocabulary; dynamic values are asserted by shape):
872
874
  const RECEIPT_FIXTURE = JSON.parse(
873
- '{"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}',
875
+ '{"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,"posture":{"model":"<display>"}}',
874
876
  );
875
877
  const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
876
878
  const readReceipts = (repo) => {
@@ -962,11 +964,10 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
962
964
  assert.equal(receipts.length, 0, 'no run — no receipt');
963
965
  });
964
966
 
965
- it('parses REWORK and plain SHIP; records "unknown" when the mandated section is absent', () => {
967
+ it('parses REWORK and plain SHIP verbatim (an absent section is a FAILED run — the D4 describe owns that arm)', () => {
966
968
  for (const [output, want] of [
967
969
  ['### Verdict\nREWORK — the contract is violated.', 'REWORK'],
968
970
  ['### Verdict\nSHIP — clean.', 'SHIP'],
969
- ['free-form text with no verdict heading', 'unknown'],
970
971
  ]) {
971
972
  const sb = makeSandbox();
972
973
  const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_OUTPUT: output } });
@@ -1264,3 +1265,93 @@ describe('agy-review.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
1264
1265
  }
1265
1266
  });
1266
1267
  });
1268
+
1269
+ // ── strip-the-kit Phase 4: wrapper honesty (D4) + dispatch-posture labeling (D5) ────────────────
1270
+ describe('agy-review.sh — wrapper honesty: a verdict-less run is a FAILED review (D4)', () => {
1271
+ it('a VERDICT-LESS review output: non-zero exit, NO receipt, the stated re-run recovery', () => {
1272
+ const sb = makeSandbox();
1273
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: 'prose without the mandated section' } });
1274
+ const receipts = readReceipts(sb.repo);
1275
+ rmSync(sb.home, { recursive: true, force: true });
1276
+ assert.notEqual(r.status, 0, 'a verdict-less review never exits 0');
1277
+ assert.equal(receipts.length, 0, 'NO receipt is minted for a failed review');
1278
+ assert.match(r.stderr, /### Verdict/, 'the missing section is named');
1279
+ assert.match(r.stderr, /re-run/i, 'documented as a failed review — re-run, never fatal');
1280
+ });
1281
+
1282
+ it('EMPTY review output is the same failed run (non-zero, no receipt)', () => {
1283
+ const sb = makeSandbox();
1284
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: '' } });
1285
+ const receipts = readReceipts(sb.repo);
1286
+ rmSync(sb.home, { recursive: true, force: true });
1287
+ assert.notEqual(r.status, 0);
1288
+ assert.equal(receipts.length, 0);
1289
+ });
1290
+
1291
+ it('the closed vocabulary still parses (SHIP WITH NITS before SHIP; REWORK) and a recognized run exits 0', () => {
1292
+ for (const [out, want] of [[VERDICT_OUTPUT, 'SHIP WITH NITS'], ['### Verdict\nREWORK — reasons.\n', 'REWORK']]) {
1293
+ const sb = makeSandbox();
1294
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: out } });
1295
+ const receipts = readReceipts(sb.repo);
1296
+ rmSync(sb.home, { recursive: true, force: true });
1297
+ assert.equal(r.status, 0, r.stderr);
1298
+ assert.equal(receipts[0].verdict, want);
1299
+ }
1300
+ });
1301
+ });
1302
+
1303
+ describe('agy-review.sh — dispatch-posture labeling (D5)', () => {
1304
+ it('ONE banner line carries the ACTUAL model and the receipt carries the SAME posture (agy has no tier)', () => {
1305
+ const sb = makeSandbox();
1306
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'] });
1307
+ const receipts = readReceipts(sb.repo);
1308
+ rmSync(sb.home, { recursive: true, force: true });
1309
+ assert.equal(r.status, 0, r.stderr);
1310
+ assert.match(r.stderr, /review posture: model=Gemini 3\.1 Pro \(High\)/, 'the banner states the actual run posture');
1311
+ assert.deepEqual(receipts[0].posture, { model: 'Gemini 3.1 Pro (High)' }, 'banner ↔ receipt parity');
1312
+ assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
1313
+ });
1314
+
1315
+ it('an ATTESTING review with AGY_MODEL explicitly emptied REFUSES pre-spend naming the fix', () => {
1316
+ const sb = makeSandbox();
1317
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: '' } });
1318
+ const receipts = readReceipts(sb.repo);
1319
+ rmSync(sb.home, { recursive: true, force: true });
1320
+ assert.notEqual(r.status, 0);
1321
+ assert.equal(r.invoked, false, 'refused BEFORE any spend');
1322
+ assert.equal(receipts.length, 0);
1323
+ assert.match(r.stderr, /AGY_MODEL/, 'the fix is named');
1324
+ });
1325
+
1326
+ it('AGY_PROBE=1 with AGY_MODEL emptied still runs (probe exempt; posture model null on the probe receipt)', () => {
1327
+ const sb = makeSandbox();
1328
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: '', AGY_PROBE: '1' } });
1329
+ const receipts = readReceipts(sb.repo);
1330
+ rmSync(sb.home, { recursive: true, force: true });
1331
+ assert.equal(r.status, 0, r.stderr);
1332
+ assert.equal(receipts[0].probe, true);
1333
+ assert.deepEqual(receipts[0].posture, { model: null }, 'an unknowable model is recorded null, never guessed');
1334
+ });
1335
+
1336
+ it('a HOSTILE model string (quotes + backslash) rides the receipt strictly JSON-encoded', () => {
1337
+ const hostile = 'we"ird \\ mo"del';
1338
+ const sb = makeSandbox();
1339
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: hostile } });
1340
+ const receipts = readReceipts(sb.repo); // JSON.parse throwing here IS the encoding failure
1341
+ rmSync(sb.home, { recursive: true, force: true });
1342
+ assert.equal(r.status, 0, r.stderr);
1343
+ assert.equal(receipts[0].posture.model, hostile, 'the exact bytes round-trip through strict encoding');
1344
+ assert.match(r.stderr, /review posture: /, 'the banner still renders');
1345
+ });
1346
+
1347
+ it('a model string carrying CONTROL BYTES refuses pre-spend (never a broken banner or receipt)', () => {
1348
+ const sb = makeSandbox();
1349
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: `bad${String.fromCharCode(1)}model` } });
1350
+ const receipts = readReceipts(sb.repo);
1351
+ rmSync(sb.home, { recursive: true, force: true });
1352
+ assert.notEqual(r.status, 0);
1353
+ assert.equal(r.invoked, false);
1354
+ assert.equal(receipts.length, 0);
1355
+ assert.match(r.stderr, /control/i);
1356
+ });
1357
+ });
@@ -3,8 +3,9 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "3.0.0",
6
+ "version": "4.0.0",
7
7
  "provides": ["review", "probe"],
8
+ "posture": { "model": "Gemini 3.1 Pro (High)" },
8
9
  "roles": {
9
10
  "review": {
10
11
  "cmd": "agy-review",
@@ -29,7 +30,7 @@
29
30
  "agy-review --continue [--decided @f] [--focus \"…\"]",
30
31
  "agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
31
32
  ],
32
- "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); a write failure warns, never fails the review",
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",
33
34
  "notes": [
34
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
36
  ]
@@ -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.8.0'
5
+ version: '3.0.0'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -96,6 +96,13 @@ codex-review code # review the current working-tr
96
96
  codex-review code "focus on the new reducer" # review with extra focus
97
97
  ```
98
98
 
99
+ **Honesty + posture (D4/D5):** a run whose final message has no recognized
100
+ `Verdict: <ship|revise|rethink>` line — empty or missing output included — **exits 4 with NO
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 records the
103
+ same `posture {model, effort, tier}` (tier `null` on the standard tier); control bytes in a
104
+ posture value refuse pre-spend in every mode.
105
+
99
106
  `codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
100
107
  escalation is refused and reported, never interactively approved. The wrappers capture only codex's
101
108
  **final message** (`-o`; the JSON event stream + reasoning go to a discarded trace), so output is
@@ -0,0 +1,143 @@
1
+ // codex-review-honesty.test.mjs — the round-1 hardening pins for the D4/D5 arms (strip Phase 4):
2
+ // the schema-mode verdict is parsed STRUCTURALLY (a legal multiline JSON parses; a decoy
3
+ // "verdict" inside a findings string never substitutes the top-level field; out-of-enum and
4
+ // malformed JSON die on the D4 arm), and the control-byte pre-spend screen covers the RAW
5
+ // CODEX_SERVICE_TIER BEFORE tier validation (no multiline warning echo, no silent standard-tier
6
+ // run). Colocated separately — codex-review.test.mjs is red-proof-frozen; standalone harness.
7
+
8
+ import { describe, it } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync, existsSync, readdirSync, symlinkSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { join, dirname, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { spawnSync } from 'node:child_process';
15
+
16
+ const HERE = dirname(fileURLToPath(import.meta.url));
17
+ const WRAPPER = join(HERE, 'codex-review.sh');
18
+
19
+ const FAKE_CODEX = [
20
+ '#!/usr/bin/env bash',
21
+ 'set -u',
22
+ 'if [[ "${1:-}" == "login" ]]; then echo "Logged in using ChatGPT"; exit 0; fi',
23
+ 'printf invoked > "${CODEX_FAKE_SENTINEL:-/dev/null}"',
24
+ 'cat >/dev/null',
25
+ 'out=""; prev=""',
26
+ 'for a in "$@"; do if [[ "$prev" == "-o" || "$prev" == "--output-last-message" ]]; then out="$a"; fi; prev="$a"; done',
27
+ 'if [[ -n "$out" ]]; then if [[ -z "${CODEX_FAKE_FINAL+x}" ]]; then printf "Verdict: ship\\n" >"$out"; else printf "%s\\n" "$CODEX_FAKE_FINAL" >"$out"; fi; fi',
28
+ 'echo \'{"type":"thread.started","thread_id":"honesty-fake"}\'',
29
+ 'exit 0',
30
+ '',
31
+ ].join('\n');
32
+
33
+ const makePathWithout = (root, exclude = []) => {
34
+ const skip = new Set(exclude);
35
+ const dir = mkdtempSync(join(root, 'nobin-'));
36
+ for (const d of (process.env.PATH || '').split(':').filter(Boolean)) {
37
+ let names;
38
+ try { names = readdirSync(d); } catch { continue; }
39
+ for (const name of names) {
40
+ if (skip.has(name)) continue;
41
+ const link = join(dir, name);
42
+ if (existsSync(link)) continue;
43
+ try { symlinkSync(resolve(d, name), link); } catch { /* dup / race — ignore */ }
44
+ }
45
+ }
46
+ return dir;
47
+ };
48
+
49
+ const makeSandbox = () => {
50
+ const root = mkdtempSync(join(tmpdir(), 'codex-honesty-'));
51
+ const bin = join(root, 'bin');
52
+ mkdirSync(bin, { recursive: true });
53
+ writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
54
+ chmodSync(join(bin, 'codex'), 0o755);
55
+ const repo = join(root, 'repo');
56
+ mkdirSync(repo);
57
+ const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
58
+ g('init', '-q');
59
+ g('config', 'user.email', 'probe@example.com');
60
+ g('config', 'user.name', 'probe');
61
+ writeFileSync(join(repo, 'AGENTS.md'), '# Hard Constraints\n');
62
+ g('add', '-A');
63
+ g('commit', '-qm', 'base');
64
+ writeFileSync(join(repo, 'pending.txt'), 'PENDING\n');
65
+ return { root, bin, repo };
66
+ };
67
+
68
+ const run = (sb, { args = ['code'], env = {} } = {}) => {
69
+ const sentinel = join(sb.root, 'cap-sentinel');
70
+ const r = spawnSync('bash', [WRAPPER, ...args], {
71
+ cwd: sb.repo,
72
+ encoding: 'utf8',
73
+ timeout: 30000,
74
+ env: {
75
+ HOME: sb.repo,
76
+ PATH: `${sb.bin}:${makePathWithout(sb.root, ['codex'])}`,
77
+ TMPDIR: process.env.TMPDIR ?? '/tmp',
78
+ CODEX_HOME: join(sb.root, 'codex-home'),
79
+ CODEX_FAKE_SENTINEL: sentinel,
80
+ ...env,
81
+ },
82
+ });
83
+ return { ...r, invoked: existsSync(sentinel) };
84
+ };
85
+
86
+ const readReceipts = (repo) => {
87
+ const p = join(repo, '.git', 'agent-workflow-review-receipts.jsonl');
88
+ if (!existsSync(p)) return [];
89
+ return readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
90
+ };
91
+
92
+ describe('codex-review — schema-mode verdict is parsed STRUCTURALLY (M2)', () => {
93
+ it('a LEGAL multiline JSON final message (key/value split across lines) parses and attests', () => {
94
+ const sb = makeSandbox();
95
+ const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{\n "verdict":\n "ship",\n "findings": []\n}' } });
96
+ const receipts = readReceipts(sb.repo);
97
+ rmSync(sb.root, { recursive: true, force: true });
98
+ assert.equal(r.status, 0, r.stderr);
99
+ assert.equal(receipts[0].verdict, 'ship', 'a formatting choice never fails a legal schema payload');
100
+ });
101
+
102
+ it('a DECOY "verdict" inside a findings STRING never substitutes the top-level field', () => {
103
+ const sb = makeSandbox();
104
+ const decoy = '{"verdict":"revise","findings":["the text \\"verdict\\": \\"ship\\" appeared in a doc line"]}';
105
+ const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: decoy } });
106
+ const receipts = readReceipts(sb.repo);
107
+ rmSync(sb.root, { recursive: true, force: true });
108
+ assert.equal(r.status, 0, r.stderr);
109
+ assert.equal(receipts[0].verdict, 'revise', 'the TOP-LEVEL field is authoritative — a quoted decoy never wins');
110
+ });
111
+
112
+ it('an OUT-OF-ENUM top-level verdict dies on the D4 arm (no receipt)', () => {
113
+ const sb = makeSandbox();
114
+ const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{"verdict":"maybe","findings":[]}' } });
115
+ const receipts = readReceipts(sb.repo);
116
+ rmSync(sb.root, { recursive: true, force: true });
117
+ assert.notEqual(r.status, 0);
118
+ assert.equal(receipts.length, 0);
119
+ });
120
+
121
+ it('MALFORMED JSON in schema mode dies on the D4 arm (no receipt)', () => {
122
+ const sb = makeSandbox();
123
+ const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{not json' } });
124
+ const receipts = readReceipts(sb.repo);
125
+ rmSync(sb.root, { recursive: true, force: true });
126
+ assert.notEqual(r.status, 0);
127
+ assert.equal(receipts.length, 0);
128
+ });
129
+ });
130
+
131
+ describe('codex-review — the RAW tier is control-byte-screened BEFORE validation (M4)', () => {
132
+ it('a CODEX_SERVICE_TIER carrying a control byte refuses pre-spend — codex never runs, no warning echo', () => {
133
+ const sb = makeSandbox();
134
+ const r = run(sb, { env: { CODEX_SERVICE_TIER: `priority${String.fromCharCode(1)}` } });
135
+ const receipts = readReceipts(sb.repo);
136
+ rmSync(sb.root, { recursive: true, force: true });
137
+ assert.notEqual(r.status, 0);
138
+ assert.equal(r.invoked, false, 'refused BEFORE any spend');
139
+ assert.equal(receipts.length, 0);
140
+ assert.match(r.stderr, /control/i, 'named as the control-byte class');
141
+ assert.doesNotMatch(r.stderr, /not a supported service tier/, 'the validity warning never echoes a hostile value');
142
+ });
143
+ });
@@ -64,8 +64,14 @@ Receipt:
64
64
  verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge,
65
65
  factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on
66
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
67
+ probe review never attests) and equally rejects an unmarked one (silence is not a declaration);
68
+ posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written
69
+ on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5
70
+ wrapper minted it; re-run the review), one stderr banner line states the same posture, and a
71
+ posture value carrying control bytes refuses pre-spend in every mode; a run whose final message
72
+ carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing output included —
73
+ exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write
74
+ failure warns, never fails the review
69
75
 
70
76
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
71
77
  ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
@@ -73,6 +79,13 @@ Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins ov
73
79
  CODEX_HARD_TIMEOUT — hard wall-clock cap, integer seconds 1..86400 (built-in default 1800)
74
80
  CODEX_REVIEW_MAX_TOTAL_BYTES — inline-payload cap, integer bytes 1..100000000 (default 1500000); above it the diff rides via a git-dir temp file
75
81
 
82
+ Honesty + posture (D4/D5):
83
+ a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or
84
+ missing output included — exits 4 with NO receipt: a FAILED review to RE-RUN, never a fatal
85
+ session error. One stderr banner line states the ACTUAL run posture (review posture: model=…
86
+ effort=… tier=…) and the receipt records the same posture {model, effort, tier} (tier null on
87
+ the standard tier). A posture value carrying control bytes refuses pre-spend in every mode.
88
+
76
89
  Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only), AW_REVIEW_RECEIPTS (receipt file override).
77
90
  Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
78
91
  HELP
@@ -193,7 +206,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
193
206
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
194
207
  # version (drift-guarded by codex-review.test.mjs against capability.json).
195
208
  AW_RECEIPT_BACKEND="codex"
196
- AW_BRIDGE_VERSION="2.8.0"
209
+ AW_BRIDGE_VERSION="3.0.0"
197
210
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
198
211
  CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
199
212
  # Generous hard cap for a slow xhigh review (subscription latency varies).
@@ -209,6 +222,15 @@ CODEX_REVIEW_MAX_TOTAL_BYTES="${CODEX_REVIEW_MAX_TOTAL_BYTES:-1500000}"
209
222
  # the wrapper validates the effective value: an unsupported one warns and runs on the standard
210
223
  # tier — a typo can never silently masquerade as Fast.
211
224
  CODEX_SERVICE_TIER="${CODEX_SERVICE_TIER:-}"
225
+ # D5 pre-spend control-byte screen — BEFORE tier validation (a malformed value is not a policy
226
+ # question; validating first would echo the hostile value into a multiline warning and then run
227
+ # on the standard tier, defeating the stated refusal). Screens the RAW model/effort/tier.
228
+ for _posture_pair in "CODEX_MODEL=$CODEX_MODEL" "CODEX_EFFORT=$CODEX_EFFORT" "CODEX_SERVICE_TIER=$CODEX_SERVICE_TIER"; do
229
+ if [[ "${_posture_pair#*=}" == *[$'\x01'-$'\x1f']* ]]; then
230
+ echo "error: ${_posture_pair%%=*} contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
231
+ exit 2
232
+ fi
233
+ done
212
234
  if [[ -n "$CODEX_SERVICE_TIER" ]] && ! aw_settings_valid CODEX_SERVICE_TIER "$CODEX_SERVICE_TIER"; then
213
235
  echo "warning: CODEX_SERVICE_TIER='$CODEX_SERVICE_TIER' is not a supported service tier ('priority') — running on the standard tier." >&2
214
236
  CODEX_SERVICE_TIER=""
@@ -219,6 +241,11 @@ if [[ -n "$CODEX_SERVICE_TIER" ]]; then
219
241
  fi
220
242
  CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
221
243
 
244
+ # --- D5 banner (one line, the ACTUAL run posture) -------------------------------------------
245
+ # The control-byte screen already ran above (BEFORE tier validation); the banner states the
246
+ # EFFECTIVE posture (post tier validation) and the receipt records the same fields.
247
+ echo "review posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard}" >&2
248
+
222
249
  # --- Quality-first guard: refuse a silent model/effort downgrade ---------------
223
250
  # A relaxed run is RECORDED, not just warned about: the receipt carries probe:true so the kit's
224
251
  # review-state gate rejects it — "do NOT use this as a real review" becomes a mechanism, not a plea.
@@ -421,6 +448,24 @@ receipt_json_scalar() {
421
448
  if [[ -z "${1:-}" ]]; then printf 'null'; else printf '"%s"' "$1"; fi
422
449
  }
423
450
 
451
+ # STRICT JSON string encoding for the free-form posture fields (model/effort ride env/settings):
452
+ # backslash then double-quote escaped. Control bytes never reach here — the D5 pre-spend gate
453
+ # refuses them, so these two escapes make the encoding total.
454
+ json_string_escape() {
455
+ local s="${1//\\/\\\\}"
456
+ s="${s//\"/\\\"}"
457
+ printf '%s' "$s"
458
+ }
459
+
460
+ # The D5 posture object this wrapper writes into EVERY receipt: the ACTUAL {model, effort, tier}
461
+ # of the run (tier null on the standard tier — the validated CODEX_SERVICE_TIER is the one source).
462
+ posture_json() {
463
+ local tier_json='null'
464
+ if [[ -n "${CODEX_SERVICE_TIER:-}" ]]; then tier_json="\"$(json_string_escape "$CODEX_SERVICE_TIER")\""; fi
465
+ printf '{"model":"%s","effort":"%s","tier":%s}' \
466
+ "$(json_string_escape "$CODEX_MODEL")" "$(json_string_escape "$CODEX_EFFORT")" "$tier_json"
467
+ }
468
+
424
469
  # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false]
425
470
  # Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
426
471
  # to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
@@ -444,10 +489,10 @@ write_review_receipt() {
444
489
  fi
445
490
  local line probe_field=',"probe":false'
446
491
  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}' \
492
+ line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"%s,"posture":%s}' \
448
493
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
449
494
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
450
- "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field")"
495
+ "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)")"
451
496
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
452
497
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
453
498
  echo " the review-state gate will read the current tree as un-receipted." >&2
@@ -648,7 +693,11 @@ codex_cmd=("${codex_flags[@]}" -)
648
693
  invoke_codex
649
694
 
650
695
  # Raw-text fallback: if the structured-findings run failed (not a timeout), retry
651
- # once WITHOUT the schema rather than lose the review — loud, never silent.
696
+ # once WITHOUT the schema rather than lose the review — loud, never silent. The verdict parse
697
+ # tracks the run that actually SUCCEEDED: a fallback run emits the TEXT verdict line, so parsing
698
+ # it in schema mode would read every fallback as verdict-less (a guaranteed D4 failure).
699
+ verdict_parse_mode="text"
700
+ if [[ -n "$schema_file" ]]; then verdict_parse_mode="schema"; fi
652
701
  if [[ -n "$schema_file" && $rc -ne 0 && $rc -ne 124 && $rc -ne 137 ]]; then
653
702
  echo "warning: the --output-schema run failed (exit $rc) — retrying once without the schema constraint." >&2
654
703
  codex_cmd=()
@@ -657,6 +706,7 @@ if [[ -n "$schema_file" && $rc -ne 0 && $rc -ne 124 && $rc -ne 137 ]]; then
657
706
  codex_cmd+=("$_f")
658
707
  done
659
708
  codex_cmd+=(-)
709
+ verdict_parse_mode="text"
660
710
  invoke_codex
661
711
  fi
662
712
 
@@ -688,16 +738,27 @@ fi
688
738
 
689
739
  # --- Review receipt (AD-038): parse the verdict, append one receipt line ------
690
740
  # Text mode parses the mandated literal 'Verdict: <ship|revise|rethink>' line; CODEX_REVIEW_SCHEMA=1
691
- # reads the schema's "verdict" field instead; absent either way "unknown" (recorded, never guessed).
741
+ # reads the schema's "verdict" field instead. D4 (wrapper honesty): NO recognized verdict
742
+ # empty/missing final message included — is a FAILED review: non-zero exit, NO receipt; a failed
743
+ # review to RE-RUN, never a fatal session error (documented in --help).
692
744
  verdict=""
693
745
  if [[ -f "$out" && -s "$out" ]]; then
694
- if [[ "${CODEX_REVIEW_SCHEMA:-}" == "1" ]]; then
695
- verdict="$(sed -nE 's/.*"verdict"[[:space:]]*:[[:space:]]*"(ship|revise|rethink)".*/\1/p' "$out" | tail -n1)"
746
+ if [[ "$verdict_parse_mode" == "schema" ]]; then
747
+ # STRUCTURAL parse (round-1 fold): the TOP-LEVEL `verdict` field of the schema payload,
748
+ # accepted only inside the closed enum — a legal multiline layout parses, a decoy "verdict"
749
+ # inside a findings string never substitutes, malformed/out-of-enum stays empty (the D4
750
+ # failed-run arm). Node ≥22 is a family floor, so the one-liner adds no dependency.
751
+ verdict="$(node -e 'try{const v=JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8")).verdict;if(["ship","revise","rethink"].includes(v))process.stdout.write(v);}catch{}' "$out" 2>/dev/null || true)"
696
752
  else
697
753
  verdict="$(sed -nE 's/^Verdict: (ship|revise|rethink)[[:space:]]*$/\1/p' "$out" | tail -n1)"
698
754
  fi
699
755
  fi
700
- [[ -z "$verdict" ]] && verdict="unknown"
756
+ if [[ -z "$verdict" ]]; then
757
+ echo "error: the review produced no recognized 'Verdict: <ship|revise|rethink>' line (empty or" >&2
758
+ echo " verdict-less final message) — a FAILED review; NO receipt was written. Re-run the" >&2
759
+ echo " review; if it recurs, inspect the captured output for what the model produced." >&2
760
+ exit 4
761
+ fi
701
762
  # codex is grounded by construction (AGENTS.md auto-merge + the precomputed change set): grounded
702
763
  # true, factsHash null (native grounding — no separate facts payload exists). Every codex run is a
703
764
  # full fresh run (one-shot, no resume) → fresh:true. $REVIEW_PROBE marks a guards-relaxed run.
@@ -37,7 +37,9 @@ const FAKE_CODEX = [
37
37
  ' if [[ "$prev" == "-o" || "$prev" == "--output-last-message" ]]; then out="$a"; fi',
38
38
  ' prev="$a"',
39
39
  'done',
40
- 'if [[ -n "$out" && "${CODEX_FAKE_NO_OUT:-}" != "1" ]]; then echo "${CODEX_FAKE_FINAL:-FAKE_FINAL_MESSAGE}" >"$out"; fi',
40
+ // Unset CODEX_FAKE_FINAL a verdict-carrying default (D4: a verdict-less run is a FAILURE, so
41
+ // the success-path tests need one); an EXPLICIT empty value exercises the empty-output failure.
42
+ 'if [[ -n "$out" && "${CODEX_FAKE_NO_OUT:-}" != "1" ]]; then if [[ -z "${CODEX_FAKE_FINAL+x}" ]]; then printf "FAKE_FINAL_MESSAGE\\nVerdict: ship\\n" >"$out"; else printf "%s\\n" "$CODEX_FAKE_FINAL" >"$out"; fi; fi',
41
43
  'if [[ "${CODEX_FAKE_NO_THREAD:-}" != "1" ]]; then',
42
44
  'cat <<EOF',
43
45
  '{"type":"thread.started","thread_id":"${CODEX_FAKE_THREAD_ID:-fake-thread-123}"}',
@@ -366,7 +368,8 @@ describe('codex-review.sh — precomputed diff for code mode (2.1)', () => {
366
368
  describe('codex-review.sh — optional structured findings (2.2)', () => {
367
369
  it('CODEX_REVIEW_SCHEMA=1 passes --output-schema to codex', () => {
368
370
  const sb = makeSandbox();
369
- const r = run(sb, { args: ['code'], env: { CODEX_REVIEW_SCHEMA: '1' } });
371
+ // Schema mode parses the schema's verdict FIELD (D4) the fixture output must carry it.
372
+ const r = run(sb, { args: ['code'], env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{"verdict":"ship","findings":[]}' } });
370
373
  rmSync(sb.root, { recursive: true, force: true });
371
374
  assert.equal(r.status, 0, r.stderr);
372
375
  assert.match(r.argv, /(^|\n)--output-schema(\n|$)/);
@@ -379,8 +382,11 @@ describe('codex-review.sh — optional structured findings (2.2)', () => {
379
382
  assert.doesNotMatch(r.argv, /--output-schema/);
380
383
  });
381
384
 
382
- it('falls back to a raw-text run when the schema run fails (loud; exit 0)', () => {
385
+ it('falls back to a raw-text run when the schema run fails (loud; exit 0) — and parses the TEXT verdict', () => {
383
386
  const sb = makeSandbox();
387
+ // No CODEX_FAKE_FINAL: the fallback run emits the TEXT default (FAKE_FINAL_MESSAGE +
388
+ // Verdict: ship) — the wrapper must parse the mode of the run that actually SUCCEEDED,
389
+ // or every fallback would read verdict-less and die on the D4 arm.
384
390
  const r = run(sb, { args: ['code'], env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FAIL_ON_SCHEMA: '1' } });
385
391
  rmSync(sb.root, { recursive: true, force: true });
386
392
  assert.equal(r.status, 0, r.stderr);
@@ -527,13 +533,13 @@ describe('codex-review.sh — assemble & output edge cases', () => {
527
533
  assert.match(r.capStdin, /Extra focus: watch the parser/);
528
534
  });
529
535
 
530
- it('warns and prints the trace tail when codex writes no final-message file', () => {
536
+ it('warns and prints the trace tail when codex writes no final-message file (then fails the D4 arm)', () => {
531
537
  const sb = makeSandbox();
532
538
  const r = run(sb, { args: ['code'], env: { CODEX_FAKE_NO_OUT: '1' } });
533
539
  rmSync(sb.root, { recursive: true, force: true });
534
- assert.equal(r.status, 0, r.stderr);
540
+ assert.notEqual(r.status, 0, 'no final message ⇒ no verdict ⇒ a FAILED review (D4)');
535
541
  assert.match(r.stderr, /no final-message file/);
536
- assert.match(r.stdout, /turn\.completed/, 'the trace tail carries the event stream');
542
+ assert.match(r.stdout, /turn\.completed/, 'the trace tail still carries the event stream');
537
543
  });
538
544
 
539
545
  it('prints no session line when codex emits no thread id', () => {
@@ -812,7 +818,7 @@ describe('codex-review.sh — mode catalog ⟷ wrapper reality (manifest-pinned)
812
818
  // The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (field VALUES with
813
819
  // dynamic content are asserted by shape):
814
820
  const RECEIPT_FIXTURE = JSON.parse(
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}',
821
+ '{"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,"posture":{"model":"<m>","effort":"<e>","tier":null}}',
816
822
  );
817
823
  const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
818
824
  const readReceipts = (repo) => {
@@ -905,13 +911,13 @@ describe('codex-review.sh — review receipts (AD-038)', () => {
905
911
  assert.equal(receipts[0].verdict, 'ship');
906
912
  });
907
913
 
908
- it('no parseable verdict recorded as "unknown", never guessed', () => {
914
+ it('no parseable verdict is a FAILED run — never recorded as "unknown" (D4 owns the arm)', () => {
909
915
  const sb = makeSandbox();
910
916
  const r = run(sb, { env: { CODEX_FAKE_FINAL: 'looks fine to me overall' } });
911
917
  const receipts = readReceipts(sb.repo);
912
918
  rmSync(sb.root, { recursive: true, force: true });
913
- assert.equal(r.status, 0, r.stderr);
914
- assert.equal(receipts[0].verdict, 'unknown');
919
+ assert.notEqual(r.status, 0);
920
+ assert.equal(receipts.length, 0, 'an unknown verdict never reaches the receipt store');
915
921
  });
916
922
 
917
923
  it('plan mode: artifact "plan", fingerprint = the artifact-file sha256', () => {
@@ -1195,3 +1201,80 @@ describe('codex-review.sh — settings surface ⟷ manifest (D6, manifest-pinned
1195
1201
  }
1196
1202
  });
1197
1203
  });
1204
+
1205
+ // ── strip-the-kit Phase 4: wrapper honesty (D4) + dispatch-posture labeling (D5) ────────────────
1206
+ describe('codex-review.sh — wrapper honesty: a verdict-less run is a FAILED review (D4)', () => {
1207
+ it('a VERDICT-LESS final message: non-zero exit, NO receipt, the stated re-run recovery', () => {
1208
+ const sb = makeSandbox();
1209
+ const r = run(sb, { env: { CODEX_FAKE_FINAL: 'prose without the mandated verdict line' } });
1210
+ const receipts = readReceipts(sb.repo);
1211
+ rmSync(sb.root, { recursive: true, force: true });
1212
+ assert.notEqual(r.status, 0, 'a verdict-less review never exits 0');
1213
+ assert.equal(receipts.length, 0, 'NO receipt is minted for a failed review');
1214
+ assert.match(r.stderr, /Verdict/, 'the missing line is named');
1215
+ assert.match(r.stderr, /re-run/i, 'documented as a failed review — re-run, never fatal');
1216
+ });
1217
+
1218
+ it('an EMPTY final message is the same failed run (non-zero, no receipt)', () => {
1219
+ const sb = makeSandbox();
1220
+ const r = run(sb, { env: { CODEX_FAKE_FINAL: '' } });
1221
+ const receipts = readReceipts(sb.repo);
1222
+ rmSync(sb.root, { recursive: true, force: true });
1223
+ assert.notEqual(r.status, 0);
1224
+ assert.equal(receipts.length, 0);
1225
+ });
1226
+
1227
+ it('a MISSING final-message file is the same failed run', () => {
1228
+ const sb = makeSandbox();
1229
+ const r = run(sb, { env: { CODEX_FAKE_NO_OUT: '1' } });
1230
+ const receipts = readReceipts(sb.repo);
1231
+ rmSync(sb.root, { recursive: true, force: true });
1232
+ assert.notEqual(r.status, 0);
1233
+ assert.equal(receipts.length, 0);
1234
+ });
1235
+ });
1236
+
1237
+ describe('codex-review.sh — dispatch-posture labeling (D5)', () => {
1238
+ it('ONE banner line carries the ACTUAL {model, effort, tier} and the receipt carries the SAME posture', () => {
1239
+ const sb = makeSandbox();
1240
+ const r = run(sb, {});
1241
+ const receipts = readReceipts(sb.repo);
1242
+ rmSync(sb.root, { recursive: true, force: true });
1243
+ assert.equal(r.status, 0, r.stderr);
1244
+ assert.match(r.stderr, /review posture: model=gpt-5\.6-sol effort=xhigh tier=standard/, 'the banner states the actual run posture');
1245
+ assert.deepEqual(receipts[0].posture, { model: 'gpt-5.6-sol', effort: 'xhigh', tier: null }, 'banner ↔ receipt parity (standard tier = null)');
1246
+ assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
1247
+ });
1248
+
1249
+ it('an ARMED Fast tier rides both surfaces (banner tier=priority; receipt tier "priority")', () => {
1250
+ const sb = makeSandbox();
1251
+ const r = run(sb, { env: { CODEX_SERVICE_TIER: 'priority' } });
1252
+ const receipts = readReceipts(sb.repo);
1253
+ rmSync(sb.root, { recursive: true, force: true });
1254
+ assert.equal(r.status, 0, r.stderr);
1255
+ assert.match(r.stderr, /review posture: model=gpt-5\.6-sol effort=xhigh tier=priority/);
1256
+ assert.equal(receipts[0].posture.tier, 'priority');
1257
+ });
1258
+
1259
+ it('a HOSTILE model string (quotes + backslash) rides the receipt strictly JSON-encoded (probe lane)', () => {
1260
+ const hostile = 'we"ird \\ mo"del';
1261
+ const sb = makeSandbox();
1262
+ const r = run(sb, { env: { CODEX_PROBE: '1', CODEX_MODEL: hostile } });
1263
+ const receipts = readReceipts(sb.repo); // JSON.parse throwing here IS the encoding failure
1264
+ rmSync(sb.root, { recursive: true, force: true });
1265
+ assert.equal(r.status, 0, r.stderr);
1266
+ assert.equal(receipts[0].posture.model, hostile, 'the exact bytes round-trip through strict encoding');
1267
+ assert.equal(receipts[0].probe, true, 'an off-pinned model runs only on the probe lane');
1268
+ });
1269
+
1270
+ it('a posture value carrying CONTROL BYTES refuses pre-spend, BEFORE the frontier guard', () => {
1271
+ const sb = makeSandbox();
1272
+ const r = run(sb, { env: { CODEX_MODEL: `gpt-5.6-sol${String.fromCharCode(1)}` } });
1273
+ const receipts = readReceipts(sb.repo);
1274
+ rmSync(sb.root, { recursive: true, force: true });
1275
+ assert.notEqual(r.status, 0);
1276
+ assert.equal(r.capStdin, '', 'codex is never invoked');
1277
+ assert.equal(receipts.length, 0);
1278
+ assert.match(r.stderr, /control/i, 'named as the control-byte class, not a policy refusal');
1279
+ });
1280
+ });