@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.
- package/CHANGELOG.md +45 -0
- package/README.md +6 -5
- package/SKILL.md +12 -8
- package/bin/install.mjs +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +150 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +95 -4
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +143 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +93 -10
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +44 -0
- package/migrations/README.md +1 -1
- package/package.json +2 -2
- package/references/hooks/gate-approve.mjs +1 -1
- package/references/modes/bootstrap.md +3 -3
- package/references/modes/commit-guard.md +19 -0
- package/references/modes/core-evidence.md +15 -0
- package/references/modes/coverage-check.md +12 -0
- package/references/modes/doc-parity.md +7 -6
- package/references/modes/gates.md +7 -6
- package/references/modes/grounding.md +1 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/scripts/archive-decisions.mjs +1 -1
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/tools/ack-write.mjs +1 -1
- package/tools/atomic-write.mjs +2 -2
- package/tools/autonomy-config.mjs +1 -1
- package/tools/autonomy-doctor.mjs +1 -1
- package/tools/autonomy-write.mjs +1 -1
- package/tools/bridge-settings-read.mjs +1 -1
- package/tools/bridge-settings.mjs +1 -1
- package/tools/changed-surface.mjs +8 -69
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +17 -10
- package/tools/commit-guard.mjs +167 -0
- package/tools/core-evidence.mjs +914 -0
- package/tools/coverage-check.mjs +260 -0
- package/tools/delegation.mjs +1 -1
- package/tools/detect-backends.mjs +3 -3
- package/tools/doc-parity.mjs +11 -27
- package/tools/engine-source.mjs +1 -1
- package/tools/family-members.mjs +1 -1
- package/tools/family-registry.mjs +1 -1
- package/tools/fs-safe.mjs +1 -1
- package/tools/gate-hook.mjs +1 -1
- package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
- package/tools/grounding.mjs +6 -83
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +1 -1
- package/tools/known-footprint.mjs +1 -1
- package/tools/labels.mjs +1 -1
- package/tools/lcov.mjs +6 -10
- package/tools/lens-region.mjs +1 -1
- package/tools/manifest/validate.mjs +19 -0
- package/tools/migrate-adr-store.mjs +2 -2
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +2 -2
- package/tools/recipes.mjs +60 -9
- package/tools/recommendations.mjs +63 -7
- package/tools/release-scan.mjs +1 -1
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +288 -340
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +1 -1
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +1 -1
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +2 -2
- package/tools/view-model.mjs +1 -1
- package/references/modes/fold-completeness.md +0 -30
- package/references/modes/review-ledger.md +0 -34
- package/references/templates/verification-profile.json +0 -10
- package/tools/fold-completeness-run.mjs +0 -1120
- package/tools/fold-completeness.mjs +0 -672
- package/tools/review-ledger-core.mjs +0 -428
- package/tools/review-ledger-write.mjs +0 -647
- package/tools/review-ledger.mjs +0 -630
- package/tools/sarif.mjs +0 -52
- 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
|
-
|
|
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
|
|
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": "
|
|
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: '
|
|
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);
|
|
68
|
-
|
|
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="
|
|
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
|
|
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 [[ "$
|
|
695
|
-
|
|
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" ]]
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
914
|
-
assert.equal(receipts
|
|
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
|
+
});
|