@sabaiway/agent-workflow-kit 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -1
- package/README.md +7 -5
- package/SKILL.md +16 -8
- package/bin/install.mjs +11 -2
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -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 +171 -22
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
- 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-exec.test.mjs +67 -22
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
- 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/recommendations.md +1 -0
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +3 -3
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- 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 +2 -2
- 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 +16 -77
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +26 -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 +19 -96
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +72 -41
- 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 +5 -5
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +62 -11
- package/tools/recommendations.mjs +97 -14
- package/tools/release-scan.mjs +10 -3
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +291 -343
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +14 -14
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +2 -2
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +9 -9
- package/tools/view-model.mjs +1 -1
- package/tools/worktrees.mjs +2292 -0
- 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
|
@@ -0,0 +1,156 @@
|
|
|
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, after } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync, existsSync, readdirSync, symlinkSync, cpSync } 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
|
+
// Farm + sandbox base are READ-ONLY per invocation — built ONCE, shared (a per-run farm rebuild
|
|
50
|
+
// plus a per-test `git init`+commit dominate the wall otherwise).
|
|
51
|
+
const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'codex-honesty-shared-'));
|
|
52
|
+
after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
|
|
53
|
+
const FARM = makePathWithout(SHARED_ROOT, ['codex']);
|
|
54
|
+
|
|
55
|
+
const TEMPLATE_ROOT = (() => {
|
|
56
|
+
const root = join(SHARED_ROOT, 'template-root');
|
|
57
|
+
const bin = join(root, 'bin');
|
|
58
|
+
mkdirSync(bin, { recursive: true });
|
|
59
|
+
writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
|
|
60
|
+
const repo = join(root, 'repo');
|
|
61
|
+
mkdirSync(repo);
|
|
62
|
+
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
63
|
+
g('init', '-q');
|
|
64
|
+
g('config', 'user.email', 'probe@example.com');
|
|
65
|
+
g('config', 'user.name', 'probe');
|
|
66
|
+
writeFileSync(join(repo, 'AGENTS.md'), '# Hard Constraints\n');
|
|
67
|
+
g('add', '-A');
|
|
68
|
+
g('commit', '-qm', 'base');
|
|
69
|
+
writeFileSync(join(repo, 'pending.txt'), 'PENDING\n');
|
|
70
|
+
return root;
|
|
71
|
+
})();
|
|
72
|
+
|
|
73
|
+
const makeSandbox = () => {
|
|
74
|
+
const root = mkdtempSync(join(tmpdir(), 'codex-honesty-'));
|
|
75
|
+
cpSync(TEMPLATE_ROOT, root, { recursive: true });
|
|
76
|
+
const bin = join(root, 'bin');
|
|
77
|
+
chmodSync(join(bin, 'codex'), 0o755);
|
|
78
|
+
return { root, bin, repo: join(root, 'repo') };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const run = (sb, { args = ['code'], env = {} } = {}) => {
|
|
82
|
+
const sentinel = join(sb.root, 'cap-sentinel');
|
|
83
|
+
const r = spawnSync('bash', [WRAPPER, ...args], {
|
|
84
|
+
cwd: sb.repo,
|
|
85
|
+
encoding: 'utf8',
|
|
86
|
+
timeout: 30000,
|
|
87
|
+
env: {
|
|
88
|
+
HOME: sb.repo,
|
|
89
|
+
PATH: `${sb.bin}:${FARM}`,
|
|
90
|
+
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
91
|
+
CODEX_HOME: join(sb.root, 'codex-home'),
|
|
92
|
+
CODEX_FAKE_SENTINEL: sentinel,
|
|
93
|
+
...env,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
return { ...r, invoked: existsSync(sentinel) };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const readReceipts = (repo) => {
|
|
100
|
+
const p = join(repo, '.git', 'agent-workflow-review-receipts.jsonl');
|
|
101
|
+
if (!existsSync(p)) return [];
|
|
102
|
+
return readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
describe('codex-review — schema-mode verdict is parsed STRUCTURALLY (M2)', () => {
|
|
106
|
+
it('a LEGAL multiline JSON final message (key/value split across lines) parses and attests', () => {
|
|
107
|
+
const sb = makeSandbox();
|
|
108
|
+
const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{\n "verdict":\n "ship",\n "findings": []\n}' } });
|
|
109
|
+
const receipts = readReceipts(sb.repo);
|
|
110
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
111
|
+
assert.equal(r.status, 0, r.stderr);
|
|
112
|
+
assert.equal(receipts[0].verdict, 'ship', 'a formatting choice never fails a legal schema payload');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('a DECOY "verdict" inside a findings STRING never substitutes the top-level field', () => {
|
|
116
|
+
const sb = makeSandbox();
|
|
117
|
+
const decoy = '{"verdict":"revise","findings":["the text \\"verdict\\": \\"ship\\" appeared in a doc line"]}';
|
|
118
|
+
const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: decoy } });
|
|
119
|
+
const receipts = readReceipts(sb.repo);
|
|
120
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
121
|
+
assert.equal(r.status, 0, r.stderr);
|
|
122
|
+
assert.equal(receipts[0].verdict, 'revise', 'the TOP-LEVEL field is authoritative — a quoted decoy never wins');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('an OUT-OF-ENUM top-level verdict dies on the D4 arm (no receipt)', () => {
|
|
126
|
+
const sb = makeSandbox();
|
|
127
|
+
const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{"verdict":"maybe","findings":[]}' } });
|
|
128
|
+
const receipts = readReceipts(sb.repo);
|
|
129
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
130
|
+
assert.notEqual(r.status, 0);
|
|
131
|
+
assert.equal(receipts.length, 0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('MALFORMED JSON in schema mode dies on the D4 arm (no receipt)', () => {
|
|
135
|
+
const sb = makeSandbox();
|
|
136
|
+
const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{not json' } });
|
|
137
|
+
const receipts = readReceipts(sb.repo);
|
|
138
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
139
|
+
assert.notEqual(r.status, 0);
|
|
140
|
+
assert.equal(receipts.length, 0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('codex-review — the RAW tier is control-byte-screened BEFORE validation (M4)', () => {
|
|
145
|
+
it('a CODEX_SERVICE_TIER carrying a control byte refuses pre-spend — codex never runs, no warning echo', () => {
|
|
146
|
+
const sb = makeSandbox();
|
|
147
|
+
const r = run(sb, { env: { CODEX_SERVICE_TIER: `priority${String.fromCharCode(1)}` } });
|
|
148
|
+
const receipts = readReceipts(sb.repo);
|
|
149
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
150
|
+
assert.notEqual(r.status, 0);
|
|
151
|
+
assert.equal(r.invoked, false, 'refused BEFORE any spend');
|
|
152
|
+
assert.equal(receipts.length, 0);
|
|
153
|
+
assert.match(r.stderr, /control/i, 'named as the control-byte class');
|
|
154
|
+
assert.doesNotMatch(r.stderr, /not a supported service tier/, 'the validity warning never echoes a hostile value');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -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.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it, after } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {
|
|
4
4
|
mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
|
|
5
|
-
existsSync, readdirSync, symlinkSync,
|
|
5
|
+
existsSync, readdirSync, symlinkSync, cpSync,
|
|
6
6
|
} from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join, dirname, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { spawnSync, execFile } from 'node:child_process';
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
12
|
|
|
13
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -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}"}',
|
|
@@ -52,16 +54,23 @@ const FAKE_CODEX = [
|
|
|
52
54
|
'',
|
|
53
55
|
].join('\n');
|
|
54
56
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
57
|
+
// The PATH farms and the sandbox base are READ-ONLY per invocation, so both are built ONCE and
|
|
58
|
+
// shared (a per-call farm rebuild is thousands of symlinks; a per-test `git init`+commit
|
|
59
|
+
// dominates the sandbox cost).
|
|
60
|
+
const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'codex-review-shared-'));
|
|
61
|
+
after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
|
|
62
|
+
const farms = new Map();
|
|
63
|
+
const farmFor = (exclude) => {
|
|
64
|
+
const key = exclude.join('|');
|
|
65
|
+
if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
|
|
66
|
+
return farms.get(key);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const TEMPLATE_ROOT = (() => {
|
|
70
|
+
const root = join(SHARED_ROOT, 'template-root');
|
|
60
71
|
const bin = join(root, 'bin');
|
|
61
72
|
mkdirSync(bin, { recursive: true });
|
|
62
|
-
|
|
63
|
-
writeFileSync(stub, FAKE_CODEX, { mode: 0o755 });
|
|
64
|
-
chmodSync(stub, 0o755);
|
|
73
|
+
writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
|
|
65
74
|
const repo = join(root, 'repo');
|
|
66
75
|
mkdirSync(repo);
|
|
67
76
|
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
@@ -72,6 +81,18 @@ const makeSandbox = ({ clean = false } = {}) => {
|
|
|
72
81
|
writeFileSync(join(repo, 'plan.md'), '# Plan\n\nDo a thing in two steps.\n');
|
|
73
82
|
g('add', '-A');
|
|
74
83
|
g('commit', '-qm', 'base');
|
|
84
|
+
return root;
|
|
85
|
+
})();
|
|
86
|
+
|
|
87
|
+
// `clean: true` leaves a committed, pristine tree (for the no-diff preflight);
|
|
88
|
+
// the default leaves one uncommitted untracked file so `code` mode has a diff to
|
|
89
|
+
// review (otherwise the new no-diff preflight short-circuits before codex runs).
|
|
90
|
+
const makeSandbox = ({ clean = false } = {}) => {
|
|
91
|
+
const root = mkdtempSync(join(tmpdir(), 'codex-review-test-'));
|
|
92
|
+
cpSync(TEMPLATE_ROOT, root, { recursive: true });
|
|
93
|
+
const bin = join(root, 'bin');
|
|
94
|
+
chmodSync(join(bin, 'codex'), 0o755);
|
|
95
|
+
const repo = join(root, 'repo');
|
|
75
96
|
if (!clean) writeFileSync(join(repo, 'pending.txt'), 'an uncommitted change to review\n');
|
|
76
97
|
return { root, bin, repo };
|
|
77
98
|
};
|
|
@@ -121,6 +142,35 @@ const run = ({ repo, bin }, { args = ['code'], env = {}, path, cwd } = {}) => {
|
|
|
121
142
|
return { ...r, codexHome, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) };
|
|
122
143
|
};
|
|
123
144
|
|
|
145
|
+
// Async twin of run() for the sleep-bound timeout test: spawnSync blocks the event loop for
|
|
146
|
+
// the whole deliberate wait, so a concurrent describe could not overlap it. Same contract.
|
|
147
|
+
const runAsync = ({ repo, bin }, { args = ['code'], env = {}, path, cwd } = {}) =>
|
|
148
|
+
new Promise((done) => {
|
|
149
|
+
const argvFile = join(repo, '.cap-argv');
|
|
150
|
+
const envFile = join(repo, '.cap-env');
|
|
151
|
+
const stdinFile = join(repo, '.cap-stdin');
|
|
152
|
+
const codexHome = join(repo, '..', 'codex-home');
|
|
153
|
+
const child = execFile('bash', [WRAPPER, ...args], {
|
|
154
|
+
cwd: cwd || repo,
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
timeout: 30000,
|
|
157
|
+
env: {
|
|
158
|
+
PATH: path || `${bin}:${process.env.PATH}`,
|
|
159
|
+
HOME: repo,
|
|
160
|
+
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
161
|
+
CODEX_HOME: codexHome,
|
|
162
|
+
CODEX_FAKE_ARGV: argvFile,
|
|
163
|
+
CODEX_FAKE_ENV: envFile,
|
|
164
|
+
CODEX_FAKE_STDIN: stdinFile,
|
|
165
|
+
...env,
|
|
166
|
+
},
|
|
167
|
+
}, (error, stdout, stderr) => {
|
|
168
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
|
|
169
|
+
done({ status: error ? (error.code ?? 1) : 0, stdout, stderr, codexHome, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) });
|
|
170
|
+
});
|
|
171
|
+
child.stdin.end();
|
|
172
|
+
});
|
|
173
|
+
|
|
124
174
|
describe('codex-review.sh — quality-first model/effort guard (1.1)', () => {
|
|
125
175
|
it('refuses a non-default CODEX_MODEL', () => {
|
|
126
176
|
const sb = makeSandbox();
|
|
@@ -253,11 +303,11 @@ describe('codex-review.sh — subscription / config isolation (invariant)', () =
|
|
|
253
303
|
});
|
|
254
304
|
});
|
|
255
305
|
|
|
256
|
-
describe('codex-review.sh — hard timeout (1.3)', () => {
|
|
257
|
-
it('kills a hung review at CODEX_HARD_TIMEOUT and reports it', () => {
|
|
306
|
+
describe('codex-review.sh — hard timeout (1.3)', { concurrency: true }, () => {
|
|
307
|
+
it('kills a hung review at CODEX_HARD_TIMEOUT and reports it', async () => {
|
|
258
308
|
const sb = makeSandbox();
|
|
259
309
|
const started = Date.now();
|
|
260
|
-
const r =
|
|
310
|
+
const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '30', CODEX_HARD_TIMEOUT: '2' } });
|
|
261
311
|
const elapsed = Date.now() - started;
|
|
262
312
|
rmSync(sb.root, { recursive: true, force: true });
|
|
263
313
|
assert.ok(elapsed < 18000, `must return well under the kill-after window, took ${elapsed}ms`);
|
|
@@ -267,7 +317,7 @@ describe('codex-review.sh — hard timeout (1.3)', () => {
|
|
|
267
317
|
|
|
268
318
|
it('warns and runs uncapped when neither timeout nor gtimeout is on PATH', () => {
|
|
269
319
|
const sb = makeSandbox();
|
|
270
|
-
const path = `${sb.bin}:${
|
|
320
|
+
const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
|
|
271
321
|
const r = run(sb, { path });
|
|
272
322
|
rmSync(sb.root, { recursive: true, force: true });
|
|
273
323
|
assert.equal(r.status, 0, r.stderr);
|
|
@@ -366,7 +416,8 @@ describe('codex-review.sh — precomputed diff for code mode (2.1)', () => {
|
|
|
366
416
|
describe('codex-review.sh — optional structured findings (2.2)', () => {
|
|
367
417
|
it('CODEX_REVIEW_SCHEMA=1 passes --output-schema to codex', () => {
|
|
368
418
|
const sb = makeSandbox();
|
|
369
|
-
|
|
419
|
+
// Schema mode parses the schema's verdict FIELD (D4) — the fixture output must carry it.
|
|
420
|
+
const r = run(sb, { args: ['code'], env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{"verdict":"ship","findings":[]}' } });
|
|
370
421
|
rmSync(sb.root, { recursive: true, force: true });
|
|
371
422
|
assert.equal(r.status, 0, r.stderr);
|
|
372
423
|
assert.match(r.argv, /(^|\n)--output-schema(\n|$)/);
|
|
@@ -379,8 +430,11 @@ describe('codex-review.sh — optional structured findings (2.2)', () => {
|
|
|
379
430
|
assert.doesNotMatch(r.argv, /--output-schema/);
|
|
380
431
|
});
|
|
381
432
|
|
|
382
|
-
it('falls back to a raw-text run when the schema run fails (loud; exit 0)', () => {
|
|
433
|
+
it('falls back to a raw-text run when the schema run fails (loud; exit 0) — and parses the TEXT verdict', () => {
|
|
383
434
|
const sb = makeSandbox();
|
|
435
|
+
// No CODEX_FAKE_FINAL: the fallback run emits the TEXT default (FAKE_FINAL_MESSAGE +
|
|
436
|
+
// Verdict: ship) — the wrapper must parse the mode of the run that actually SUCCEEDED,
|
|
437
|
+
// or every fallback would read verdict-less and die on the D4 arm.
|
|
384
438
|
const r = run(sb, { args: ['code'], env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FAIL_ON_SCHEMA: '1' } });
|
|
385
439
|
rmSync(sb.root, { recursive: true, force: true });
|
|
386
440
|
assert.equal(r.status, 0, r.stderr);
|
|
@@ -408,7 +462,7 @@ describe('codex-review.sh — optional structured findings (2.2)', () => {
|
|
|
408
462
|
describe('codex-review.sh — environment preflight (fail fast, before a run)', () => {
|
|
409
463
|
it('STOPs with 127 when codex is not on PATH', () => {
|
|
410
464
|
const sb = makeSandbox();
|
|
411
|
-
const path =
|
|
465
|
+
const path = farmFor(['codex']); // no fake codex, no real codex
|
|
412
466
|
const r = run(sb, { args: ['code'], path });
|
|
413
467
|
rmSync(sb.root, { recursive: true, force: true });
|
|
414
468
|
assert.equal(r.status, 127);
|
|
@@ -527,13 +581,13 @@ describe('codex-review.sh — assemble & output edge cases', () => {
|
|
|
527
581
|
assert.match(r.capStdin, /Extra focus: watch the parser/);
|
|
528
582
|
});
|
|
529
583
|
|
|
530
|
-
it('warns and prints the trace tail when codex writes no final-message file', () => {
|
|
584
|
+
it('warns and prints the trace tail when codex writes no final-message file (then fails the D4 arm)', () => {
|
|
531
585
|
const sb = makeSandbox();
|
|
532
586
|
const r = run(sb, { args: ['code'], env: { CODEX_FAKE_NO_OUT: '1' } });
|
|
533
587
|
rmSync(sb.root, { recursive: true, force: true });
|
|
534
|
-
assert.
|
|
588
|
+
assert.notEqual(r.status, 0, 'no final message ⇒ no verdict ⇒ a FAILED review (D4)');
|
|
535
589
|
assert.match(r.stderr, /no final-message file/);
|
|
536
|
-
assert.match(r.stdout, /turn\.completed/, 'the trace tail carries the event stream');
|
|
590
|
+
assert.match(r.stdout, /turn\.completed/, 'the trace tail still carries the event stream');
|
|
537
591
|
});
|
|
538
592
|
|
|
539
593
|
it('prints no session line when codex emits no thread id', () => {
|
|
@@ -564,7 +618,7 @@ const runHelp = (arg) => {
|
|
|
564
618
|
const root = mkdtempSync(join(tmpdir(), 'codex-review-help-'));
|
|
565
619
|
const nongit = join(root, 'nongit');
|
|
566
620
|
mkdirSync(nongit, { recursive: true });
|
|
567
|
-
const path =
|
|
621
|
+
const path = farmFor(['codex', 'agy', 'git']);
|
|
568
622
|
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
569
623
|
cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
|
|
570
624
|
});
|
|
@@ -812,7 +866,7 @@ describe('codex-review.sh — mode catalog ⟷ wrapper reality (manifest-pinned)
|
|
|
812
866
|
// The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (field VALUES with
|
|
813
867
|
// dynamic content are asserted by shape):
|
|
814
868
|
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}',
|
|
869
|
+
'{"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
870
|
);
|
|
817
871
|
const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
|
|
818
872
|
const readReceipts = (repo) => {
|
|
@@ -905,13 +959,13 @@ describe('codex-review.sh — review receipts (AD-038)', () => {
|
|
|
905
959
|
assert.equal(receipts[0].verdict, 'ship');
|
|
906
960
|
});
|
|
907
961
|
|
|
908
|
-
it('no parseable verdict
|
|
962
|
+
it('no parseable verdict is a FAILED run — never recorded as "unknown" (D4 owns the arm)', () => {
|
|
909
963
|
const sb = makeSandbox();
|
|
910
964
|
const r = run(sb, { env: { CODEX_FAKE_FINAL: 'looks fine to me overall' } });
|
|
911
965
|
const receipts = readReceipts(sb.repo);
|
|
912
966
|
rmSync(sb.root, { recursive: true, force: true });
|
|
913
|
-
assert.
|
|
914
|
-
assert.equal(receipts
|
|
967
|
+
assert.notEqual(r.status, 0);
|
|
968
|
+
assert.equal(receipts.length, 0, 'an unknown verdict never reaches the receipt store');
|
|
915
969
|
});
|
|
916
970
|
|
|
917
971
|
it('plan mode: artifact "plan", fingerprint = the artifact-file sha256', () => {
|
|
@@ -1195,3 +1249,80 @@ describe('codex-review.sh — settings surface ⟷ manifest (D6, manifest-pinned
|
|
|
1195
1249
|
}
|
|
1196
1250
|
});
|
|
1197
1251
|
});
|
|
1252
|
+
|
|
1253
|
+
// ── strip-the-kit Phase 4: wrapper honesty (D4) + dispatch-posture labeling (D5) ────────────────
|
|
1254
|
+
describe('codex-review.sh — wrapper honesty: a verdict-less run is a FAILED review (D4)', () => {
|
|
1255
|
+
it('a VERDICT-LESS final message: non-zero exit, NO receipt, the stated re-run recovery', () => {
|
|
1256
|
+
const sb = makeSandbox();
|
|
1257
|
+
const r = run(sb, { env: { CODEX_FAKE_FINAL: 'prose without the mandated verdict line' } });
|
|
1258
|
+
const receipts = readReceipts(sb.repo);
|
|
1259
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1260
|
+
assert.notEqual(r.status, 0, 'a verdict-less review never exits 0');
|
|
1261
|
+
assert.equal(receipts.length, 0, 'NO receipt is minted for a failed review');
|
|
1262
|
+
assert.match(r.stderr, /Verdict/, 'the missing line is named');
|
|
1263
|
+
assert.match(r.stderr, /re-run/i, 'documented as a failed review — re-run, never fatal');
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
it('an EMPTY final message is the same failed run (non-zero, no receipt)', () => {
|
|
1267
|
+
const sb = makeSandbox();
|
|
1268
|
+
const r = run(sb, { env: { CODEX_FAKE_FINAL: '' } });
|
|
1269
|
+
const receipts = readReceipts(sb.repo);
|
|
1270
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1271
|
+
assert.notEqual(r.status, 0);
|
|
1272
|
+
assert.equal(receipts.length, 0);
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
it('a MISSING final-message file is the same failed run', () => {
|
|
1276
|
+
const sb = makeSandbox();
|
|
1277
|
+
const r = run(sb, { env: { CODEX_FAKE_NO_OUT: '1' } });
|
|
1278
|
+
const receipts = readReceipts(sb.repo);
|
|
1279
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1280
|
+
assert.notEqual(r.status, 0);
|
|
1281
|
+
assert.equal(receipts.length, 0);
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
describe('codex-review.sh — dispatch-posture labeling (D5)', () => {
|
|
1286
|
+
it('ONE banner line carries the ACTUAL {model, effort, tier} and the receipt carries the SAME posture', () => {
|
|
1287
|
+
const sb = makeSandbox();
|
|
1288
|
+
const r = run(sb, {});
|
|
1289
|
+
const receipts = readReceipts(sb.repo);
|
|
1290
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1291
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1292
|
+
assert.match(r.stderr, /review posture: model=gpt-5\.6-sol effort=xhigh tier=standard/, 'the banner states the actual run posture');
|
|
1293
|
+
assert.deepEqual(receipts[0].posture, { model: 'gpt-5.6-sol', effort: 'xhigh', tier: null }, 'banner ↔ receipt parity (standard tier = null)');
|
|
1294
|
+
assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it('an ARMED Fast tier rides both surfaces (banner tier=priority; receipt tier "priority")', () => {
|
|
1298
|
+
const sb = makeSandbox();
|
|
1299
|
+
const r = run(sb, { env: { CODEX_SERVICE_TIER: 'priority' } });
|
|
1300
|
+
const receipts = readReceipts(sb.repo);
|
|
1301
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1302
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1303
|
+
assert.match(r.stderr, /review posture: model=gpt-5\.6-sol effort=xhigh tier=priority/);
|
|
1304
|
+
assert.equal(receipts[0].posture.tier, 'priority');
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
it('a HOSTILE model string (quotes + backslash) rides the receipt strictly JSON-encoded (probe lane)', () => {
|
|
1308
|
+
const hostile = 'we"ird \\ mo"del';
|
|
1309
|
+
const sb = makeSandbox();
|
|
1310
|
+
const r = run(sb, { env: { CODEX_PROBE: '1', CODEX_MODEL: hostile } });
|
|
1311
|
+
const receipts = readReceipts(sb.repo); // JSON.parse throwing here IS the encoding failure
|
|
1312
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1313
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1314
|
+
assert.equal(receipts[0].posture.model, hostile, 'the exact bytes round-trip through strict encoding');
|
|
1315
|
+
assert.equal(receipts[0].probe, true, 'an off-pinned model runs only on the probe lane');
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it('a posture value carrying CONTROL BYTES refuses pre-spend, BEFORE the frontier guard', () => {
|
|
1319
|
+
const sb = makeSandbox();
|
|
1320
|
+
const r = run(sb, { env: { CODEX_MODEL: `gpt-5.6-sol${String.fromCharCode(1)}` } });
|
|
1321
|
+
const receipts = readReceipts(sb.repo);
|
|
1322
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1323
|
+
assert.notEqual(r.status, 0);
|
|
1324
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1325
|
+
assert.equal(receipts.length, 0);
|
|
1326
|
+
assert.match(r.stderr, /control/i, 'named as the control-byte class, not a policy refusal');
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "codex-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "3.0.0",
|
|
7
|
+
"posture": { "model": "gpt-5.6-sol", "effort": "xhigh", "tier": null },
|
|
7
8
|
"provides": ["execute", "review"],
|
|
8
9
|
"roles": {
|
|
9
10
|
"execute": {
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
],
|
|
43
44
|
"grounding": "automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags",
|
|
44
45
|
"continue": [],
|
|
45
|
-
"receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); a write failure warns, never fails the review"
|
|
46
|
+
"receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard 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, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing 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"
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
},
|