@sabaiway/agent-workflow-kit 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +6 -5
  3. package/SKILL.md +12 -8
  4. package/bin/install.mjs +1 -1
  5. package/bridges/antigravity-cli-bridge/SKILL.md +16 -4
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +150 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +131 -23
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +193 -15
  10. package/bridges/antigravity-cli-bridge/capability.json +12 -8
  11. package/bridges/antigravity-cli-bridge/references/driving-agy.md +6 -2
  12. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +3 -2
  13. package/bridges/antigravity-cli-bridge/references/review-prompt.md +1 -1
  14. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  15. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +143 -0
  16. package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
  17. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +93 -10
  18. package/bridges/codex-cli-bridge/capability.json +3 -2
  19. package/capability.json +1 -1
  20. package/migrations/3.0.0-hardened-core-loop.md +44 -0
  21. package/migrations/README.md +1 -1
  22. package/package.json +2 -2
  23. package/references/hooks/gate-approve.mjs +1 -1
  24. package/references/modes/bootstrap.md +3 -3
  25. package/references/modes/commit-guard.md +19 -0
  26. package/references/modes/core-evidence.md +15 -0
  27. package/references/modes/coverage-check.md +12 -0
  28. package/references/modes/doc-parity.md +7 -6
  29. package/references/modes/gates.md +7 -6
  30. package/references/modes/grounding.md +1 -2
  31. package/references/modes/hook.md +1 -1
  32. package/references/modes/review-state.md +3 -3
  33. package/references/modes/upgrade.md +6 -6
  34. package/references/modes/velocity.md +2 -2
  35. package/references/scripts/archive-decisions.mjs +1 -1
  36. package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
  37. package/references/scripts/install-git-hooks.mjs +90 -18
  38. package/references/scripts/install-git-hooks.test.mjs +102 -0
  39. package/references/scripts/migrate-gates-branches.test.mjs +157 -0
  40. package/references/scripts/migrate-gates.mjs +395 -0
  41. package/references/scripts/migrate-gates.test.mjs +284 -0
  42. package/references/templates/agent_rules.md +2 -2
  43. package/tools/ack-write.mjs +1 -1
  44. package/tools/atomic-write.mjs +2 -2
  45. package/tools/autonomy-config.mjs +1 -1
  46. package/tools/autonomy-doctor.mjs +1 -1
  47. package/tools/autonomy-write.mjs +1 -1
  48. package/tools/bridge-settings-read.mjs +1 -1
  49. package/tools/bridge-settings.mjs +1 -1
  50. package/tools/changed-surface.mjs +8 -69
  51. package/tools/cheap-agents.mjs +2 -2
  52. package/tools/commands.mjs +17 -10
  53. package/tools/commit-guard.mjs +167 -0
  54. package/tools/core-evidence.mjs +914 -0
  55. package/tools/coverage-check.mjs +260 -0
  56. package/tools/delegation.mjs +1 -1
  57. package/tools/detect-backends.mjs +7 -6
  58. package/tools/doc-parity.mjs +11 -27
  59. package/tools/engine-source.mjs +1 -1
  60. package/tools/family-members.mjs +1 -1
  61. package/tools/family-registry.mjs +1 -1
  62. package/tools/fs-safe.mjs +1 -1
  63. package/tools/gate-hook.mjs +1 -1
  64. package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
  65. package/tools/grounding.mjs +6 -83
  66. package/tools/hide-footprint.mjs +1 -1
  67. package/tools/inject-methodology.mjs +1 -1
  68. package/tools/known-footprint.mjs +1 -1
  69. package/tools/labels.mjs +1 -1
  70. package/tools/lcov.mjs +6 -10
  71. package/tools/lens-region.mjs +1 -1
  72. package/tools/manifest/validate.mjs +19 -0
  73. package/tools/migrate-adr-store.mjs +2 -2
  74. package/tools/orchestration-config.mjs +1 -1
  75. package/tools/orchestration-write.mjs +1 -1
  76. package/tools/presentation.mjs +1 -1
  77. package/tools/procedures.mjs +2 -2
  78. package/tools/recipes.mjs +60 -9
  79. package/tools/recommendations.mjs +63 -7
  80. package/tools/release-scan.mjs +1 -1
  81. package/tools/renderers.mjs +1 -1
  82. package/tools/review-state.mjs +288 -340
  83. package/tools/run-gates.mjs +216 -92
  84. package/tools/sandbox-masks.mjs +1 -1
  85. package/tools/semver-lite.mjs +1 -1
  86. package/tools/set-autonomy.mjs +1 -1
  87. package/tools/set-recipe.mjs +1 -1
  88. package/tools/setup-backends.mjs +1 -1
  89. package/tools/surface.mjs +1 -1
  90. package/tools/uninstall.mjs +1 -1
  91. package/tools/velocity-profile.mjs +2 -2
  92. package/tools/view-model.mjs +1 -1
  93. package/references/modes/fold-completeness.md +0 -30
  94. package/references/modes/review-ledger.md +0 -34
  95. package/references/templates/verification-profile.json +0 -10
  96. package/tools/fold-completeness-run.mjs +0 -1120
  97. package/tools/fold-completeness.mjs +0 -672
  98. package/tools/review-ledger-core.mjs +0 -428
  99. package/tools/review-ledger-write.mjs +0 -647
  100. package/tools/review-ledger.mjs +0 -630
  101. package/tools/sarif.mjs +0 -52
  102. package/tools/verification-profile.mjs +0 -219
@@ -58,13 +58,17 @@ prompt for `agy-run`. It **mechanizes the grounded-review contract** (see
58
58
  [`review-prompt.md`](./review-prompt.md)) so grounding is the enforced default, not a per-call effort:
59
59
 
60
60
  ```bash
61
- agy-review code [--facts @facts.md] [--decided @decided.md] [--focus "…"] [extra focus…]
62
- agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"]
61
+ agy-review code --facts @facts.md [--decided @decided.md] [--focus "…"] [extra focus…] # facts REQUIRED — refuses pre-spend
62
+ agy-review code --ungrounded # explicit escape: throwaway ungrounded opinion (never attests)
63
+ agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"] # no facts ⇒ loud warning, proceeds
63
64
  agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus "…"]
64
65
  agy-review --continue [--decided @f] [--focus "…"] # round-2 delta — no re-assembly
65
66
  agy-review --conversation <id> [--decided @f] [--focus "…"]
66
67
  ```
67
68
 
69
+ `code` fails CLOSED without a non-empty `--facts` payload — the refusal fires before any run is
70
+ spent; the explicit escapes are `--ungrounded` and `AGY_PROBE=1` (a probe never attests).
71
+
68
72
  What it does for you, and what YOU must supply:
69
73
 
70
74
  - **It** assembles POSTURE + a model/cutoff GUARD + your facts + your already-decided list + your focus
@@ -48,14 +48,15 @@ subscription invariant, and the byte ceiling all apply once. The playbook is in
48
48
  [`driving-agy.md`](./driving-agy.md); the surface:
49
49
 
50
50
  ```bash
51
- agy-review code|plan <file>|diff <file> [--facts @f] [--decided @f] [--focus "…"]
51
+ agy-review code --facts @f [--decided @f] [--focus "…"] # facts REQUIRED — refuses pre-spend (escapes: --ungrounded, AGY_PROBE=1)
52
+ agy-review plan <file>|diff <file> [--facts @f] [--decided @f] [--focus "…"]
52
53
  agy-review --continue | --conversation <id> [--decided @f] [--focus "…"] # round-2 delta
53
54
  ```
54
55
 
55
56
  | Var | Default | Effect |
56
57
  |---|---|---|
57
58
  | `AGY_MODEL` | `Gemini 3.1 Pro (High)` | frontier default; **any** model is allowed — a sub-frontier one earns a silenceable advisory (quality-first, not a gate) |
58
- | `AGY_PROBE` | `0` | `1` silences the off-frontier model advisory (throwaway probe) |
59
+ | `AGY_PROBE` | `0` | `1` silences the off-frontier model advisory AND lets `code` run without `--facts` (an ungrounded probe never attests — its receipt is probe-marked) |
59
60
  | `AGY_REVIEW_ALLOW_ADDDIR` | `0` | `1` lets an oversized `code` review offload ONLY the change set to a private staging dir via `--add-dir` (the grounding stays inline; re-enables the Issue-001 stall risk — prefer splitting into focused reviews) |
60
61
  | `AGY_HARD_TIMEOUT` | `30m` | the review's hard cap (longer default than a probe — reviews are slower) |
61
62
  | `AGY_MAX_PROMPT_BYTES` | `120000` | the same single-argv byte ceiling; oversized → trim/split (or the `--add-dir` escape above) |
@@ -23,7 +23,7 @@ GUARD Do NOT comment on AI model names/versions or your own knowledge cutoff
23
23
  and a known source of false positives. Review ONLY the engineering, AGAINST the facts.
24
24
 
25
25
  ## Grounded facts — review AGAINST these, do NOT guess the code
26
- {{GROUNDED_FACTS}} # from --facts @file (omitted -> an in-band note + a LOUD stderr warning)
26
+ {{GROUNDED_FACTS}} # from --facts @file (code: REQUIRED non-empty, refuses pre-spend — escapes --ungrounded / AGY_PROBE=1; plan/diff omitted -> an in-band note + a LOUD stderr warning)
27
27
 
28
28
  ## Decisions already made / already addressed — do NOT re-raise these
29
29
  {{ALREADY_DECIDED}} # from --decided @file (optional — the anti-circling lever; the round-2 payload)
@@ -2,7 +2,7 @@
2
2
  name: codex-cli-bridge
3
3
  description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
4
4
  metadata:
5
- version: '2.8.0'
5
+ version: '3.0.0'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -96,6 +96,13 @@ codex-review code # review the current working-tr
96
96
  codex-review code "focus on the new reducer" # review with extra focus
97
97
  ```
98
98
 
99
+ **Honesty + posture (D4/D5):** a run whose final message has no recognized
100
+ `Verdict: <ship|revise|rethink>` line — empty or missing output included — **exits 4 with NO
101
+ receipt**: treat it as a *failed review to re-run*, never a fatal session error. One stderr banner
102
+ states the actual posture (`review posture: model=… effort=… tier=…`) and the receipt records the
103
+ same `posture {model, effort, tier}` (tier `null` on the standard tier); control bytes in a
104
+ posture value refuse pre-spend in every mode.
105
+
99
106
  `codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
100
107
  escalation is refused and reported, never interactively approved. The wrappers capture only codex's
101
108
  **final message** (`-o`; the JSON event stream + reasoning go to a discarded trace), so output is
@@ -0,0 +1,143 @@
1
+ // codex-review-honesty.test.mjs — the round-1 hardening pins for the D4/D5 arms (strip Phase 4):
2
+ // the schema-mode verdict is parsed STRUCTURALLY (a legal multiline JSON parses; a decoy
3
+ // "verdict" inside a findings string never substitutes the top-level field; out-of-enum and
4
+ // malformed JSON die on the D4 arm), and the control-byte pre-spend screen covers the RAW
5
+ // CODEX_SERVICE_TIER BEFORE tier validation (no multiline warning echo, no silent standard-tier
6
+ // run). Colocated separately — codex-review.test.mjs is red-proof-frozen; standalone harness.
7
+
8
+ import { describe, it } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync, existsSync, readdirSync, symlinkSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { join, dirname, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { spawnSync } from 'node:child_process';
15
+
16
+ const HERE = dirname(fileURLToPath(import.meta.url));
17
+ const WRAPPER = join(HERE, 'codex-review.sh');
18
+
19
+ const FAKE_CODEX = [
20
+ '#!/usr/bin/env bash',
21
+ 'set -u',
22
+ 'if [[ "${1:-}" == "login" ]]; then echo "Logged in using ChatGPT"; exit 0; fi',
23
+ 'printf invoked > "${CODEX_FAKE_SENTINEL:-/dev/null}"',
24
+ 'cat >/dev/null',
25
+ 'out=""; prev=""',
26
+ 'for a in "$@"; do if [[ "$prev" == "-o" || "$prev" == "--output-last-message" ]]; then out="$a"; fi; prev="$a"; done',
27
+ 'if [[ -n "$out" ]]; then if [[ -z "${CODEX_FAKE_FINAL+x}" ]]; then printf "Verdict: ship\\n" >"$out"; else printf "%s\\n" "$CODEX_FAKE_FINAL" >"$out"; fi; fi',
28
+ 'echo \'{"type":"thread.started","thread_id":"honesty-fake"}\'',
29
+ 'exit 0',
30
+ '',
31
+ ].join('\n');
32
+
33
+ const makePathWithout = (root, exclude = []) => {
34
+ const skip = new Set(exclude);
35
+ const dir = mkdtempSync(join(root, 'nobin-'));
36
+ for (const d of (process.env.PATH || '').split(':').filter(Boolean)) {
37
+ let names;
38
+ try { names = readdirSync(d); } catch { continue; }
39
+ for (const name of names) {
40
+ if (skip.has(name)) continue;
41
+ const link = join(dir, name);
42
+ if (existsSync(link)) continue;
43
+ try { symlinkSync(resolve(d, name), link); } catch { /* dup / race — ignore */ }
44
+ }
45
+ }
46
+ return dir;
47
+ };
48
+
49
+ const makeSandbox = () => {
50
+ const root = mkdtempSync(join(tmpdir(), 'codex-honesty-'));
51
+ const bin = join(root, 'bin');
52
+ mkdirSync(bin, { recursive: true });
53
+ writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
54
+ chmodSync(join(bin, 'codex'), 0o755);
55
+ const repo = join(root, 'repo');
56
+ mkdirSync(repo);
57
+ const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
58
+ g('init', '-q');
59
+ g('config', 'user.email', 'probe@example.com');
60
+ g('config', 'user.name', 'probe');
61
+ writeFileSync(join(repo, 'AGENTS.md'), '# Hard Constraints\n');
62
+ g('add', '-A');
63
+ g('commit', '-qm', 'base');
64
+ writeFileSync(join(repo, 'pending.txt'), 'PENDING\n');
65
+ return { root, bin, repo };
66
+ };
67
+
68
+ const run = (sb, { args = ['code'], env = {} } = {}) => {
69
+ const sentinel = join(sb.root, 'cap-sentinel');
70
+ const r = spawnSync('bash', [WRAPPER, ...args], {
71
+ cwd: sb.repo,
72
+ encoding: 'utf8',
73
+ timeout: 30000,
74
+ env: {
75
+ HOME: sb.repo,
76
+ PATH: `${sb.bin}:${makePathWithout(sb.root, ['codex'])}`,
77
+ TMPDIR: process.env.TMPDIR ?? '/tmp',
78
+ CODEX_HOME: join(sb.root, 'codex-home'),
79
+ CODEX_FAKE_SENTINEL: sentinel,
80
+ ...env,
81
+ },
82
+ });
83
+ return { ...r, invoked: existsSync(sentinel) };
84
+ };
85
+
86
+ const readReceipts = (repo) => {
87
+ const p = join(repo, '.git', 'agent-workflow-review-receipts.jsonl');
88
+ if (!existsSync(p)) return [];
89
+ return readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
90
+ };
91
+
92
+ describe('codex-review — schema-mode verdict is parsed STRUCTURALLY (M2)', () => {
93
+ it('a LEGAL multiline JSON final message (key/value split across lines) parses and attests', () => {
94
+ const sb = makeSandbox();
95
+ const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{\n "verdict":\n "ship",\n "findings": []\n}' } });
96
+ const receipts = readReceipts(sb.repo);
97
+ rmSync(sb.root, { recursive: true, force: true });
98
+ assert.equal(r.status, 0, r.stderr);
99
+ assert.equal(receipts[0].verdict, 'ship', 'a formatting choice never fails a legal schema payload');
100
+ });
101
+
102
+ it('a DECOY "verdict" inside a findings STRING never substitutes the top-level field', () => {
103
+ const sb = makeSandbox();
104
+ const decoy = '{"verdict":"revise","findings":["the text \\"verdict\\": \\"ship\\" appeared in a doc line"]}';
105
+ const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: decoy } });
106
+ const receipts = readReceipts(sb.repo);
107
+ rmSync(sb.root, { recursive: true, force: true });
108
+ assert.equal(r.status, 0, r.stderr);
109
+ assert.equal(receipts[0].verdict, 'revise', 'the TOP-LEVEL field is authoritative — a quoted decoy never wins');
110
+ });
111
+
112
+ it('an OUT-OF-ENUM top-level verdict dies on the D4 arm (no receipt)', () => {
113
+ const sb = makeSandbox();
114
+ const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{"verdict":"maybe","findings":[]}' } });
115
+ const receipts = readReceipts(sb.repo);
116
+ rmSync(sb.root, { recursive: true, force: true });
117
+ assert.notEqual(r.status, 0);
118
+ assert.equal(receipts.length, 0);
119
+ });
120
+
121
+ it('MALFORMED JSON in schema mode dies on the D4 arm (no receipt)', () => {
122
+ const sb = makeSandbox();
123
+ const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{not json' } });
124
+ const receipts = readReceipts(sb.repo);
125
+ rmSync(sb.root, { recursive: true, force: true });
126
+ assert.notEqual(r.status, 0);
127
+ assert.equal(receipts.length, 0);
128
+ });
129
+ });
130
+
131
+ describe('codex-review — the RAW tier is control-byte-screened BEFORE validation (M4)', () => {
132
+ it('a CODEX_SERVICE_TIER carrying a control byte refuses pre-spend — codex never runs, no warning echo', () => {
133
+ const sb = makeSandbox();
134
+ const r = run(sb, { env: { CODEX_SERVICE_TIER: `priority${String.fromCharCode(1)}` } });
135
+ const receipts = readReceipts(sb.repo);
136
+ rmSync(sb.root, { recursive: true, force: true });
137
+ assert.notEqual(r.status, 0);
138
+ assert.equal(r.invoked, false, 'refused BEFORE any spend');
139
+ assert.equal(receipts.length, 0);
140
+ assert.match(r.stderr, /control/i, 'named as the control-byte class');
141
+ assert.doesNotMatch(r.stderr, /not a supported service tier/, 'the validity warning never echoes a hostile value');
142
+ });
143
+ });
@@ -64,8 +64,14 @@ Receipt:
64
64
  verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge,
65
65
  factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on
66
66
  EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a
67
- probe review never attests) and equally rejects an unmarked one (silence is not a declaration); a
68
- write failure warns, never fails the review
67
+ probe review never attests) and equally rejects an unmarked one (silence is not a declaration);
68
+ posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written
69
+ on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5
70
+ wrapper minted it; re-run the review), one stderr banner line states the same posture, and a
71
+ posture value carrying control bytes refuses pre-spend in every mode; a run whose final message
72
+ carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing output included —
73
+ exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write
74
+ failure warns, never fails the review
69
75
 
70
76
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
71
77
  ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
@@ -73,6 +79,13 @@ Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins ov
73
79
  CODEX_HARD_TIMEOUT — hard wall-clock cap, integer seconds 1..86400 (built-in default 1800)
74
80
  CODEX_REVIEW_MAX_TOTAL_BYTES — inline-payload cap, integer bytes 1..100000000 (default 1500000); above it the diff rides via a git-dir temp file
75
81
 
82
+ Honesty + posture (D4/D5):
83
+ a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or
84
+ missing output included — exits 4 with NO receipt: a FAILED review to RE-RUN, never a fatal
85
+ session error. One stderr banner line states the ACTUAL run posture (review posture: model=…
86
+ effort=… tier=…) and the receipt records the same posture {model, effort, tier} (tier null on
87
+ the standard tier). A posture value carrying control bytes refuses pre-spend in every mode.
88
+
76
89
  Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only), AW_REVIEW_RECEIPTS (receipt file override).
77
90
  Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
78
91
  HELP
@@ -193,7 +206,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
193
206
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
194
207
  # version (drift-guarded by codex-review.test.mjs against capability.json).
195
208
  AW_RECEIPT_BACKEND="codex"
196
- AW_BRIDGE_VERSION="2.8.0"
209
+ AW_BRIDGE_VERSION="3.0.0"
197
210
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
198
211
  CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
199
212
  # Generous hard cap for a slow xhigh review (subscription latency varies).
@@ -209,6 +222,15 @@ CODEX_REVIEW_MAX_TOTAL_BYTES="${CODEX_REVIEW_MAX_TOTAL_BYTES:-1500000}"
209
222
  # the wrapper validates the effective value: an unsupported one warns and runs on the standard
210
223
  # tier — a typo can never silently masquerade as Fast.
211
224
  CODEX_SERVICE_TIER="${CODEX_SERVICE_TIER:-}"
225
+ # D5 pre-spend control-byte screen — BEFORE tier validation (a malformed value is not a policy
226
+ # question; validating first would echo the hostile value into a multiline warning and then run
227
+ # on the standard tier, defeating the stated refusal). Screens the RAW model/effort/tier.
228
+ for _posture_pair in "CODEX_MODEL=$CODEX_MODEL" "CODEX_EFFORT=$CODEX_EFFORT" "CODEX_SERVICE_TIER=$CODEX_SERVICE_TIER"; do
229
+ if [[ "${_posture_pair#*=}" == *[$'\x01'-$'\x1f']* ]]; then
230
+ echo "error: ${_posture_pair%%=*} contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
231
+ exit 2
232
+ fi
233
+ done
212
234
  if [[ -n "$CODEX_SERVICE_TIER" ]] && ! aw_settings_valid CODEX_SERVICE_TIER "$CODEX_SERVICE_TIER"; then
213
235
  echo "warning: CODEX_SERVICE_TIER='$CODEX_SERVICE_TIER' is not a supported service tier ('priority') — running on the standard tier." >&2
214
236
  CODEX_SERVICE_TIER=""
@@ -219,6 +241,11 @@ if [[ -n "$CODEX_SERVICE_TIER" ]]; then
219
241
  fi
220
242
  CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
221
243
 
244
+ # --- D5 banner (one line, the ACTUAL run posture) -------------------------------------------
245
+ # The control-byte screen already ran above (BEFORE tier validation); the banner states the
246
+ # EFFECTIVE posture (post tier validation) and the receipt records the same fields.
247
+ echo "review posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard}" >&2
248
+
222
249
  # --- Quality-first guard: refuse a silent model/effort downgrade ---------------
223
250
  # A relaxed run is RECORDED, not just warned about: the receipt carries probe:true so the kit's
224
251
  # review-state gate rejects it — "do NOT use this as a real review" becomes a mechanism, not a plea.
@@ -421,6 +448,24 @@ receipt_json_scalar() {
421
448
  if [[ -z "${1:-}" ]]; then printf 'null'; else printf '"%s"' "$1"; fi
422
449
  }
423
450
 
451
+ # STRICT JSON string encoding for the free-form posture fields (model/effort ride env/settings):
452
+ # backslash then double-quote escaped. Control bytes never reach here — the D5 pre-spend gate
453
+ # refuses them, so these two escapes make the encoding total.
454
+ json_string_escape() {
455
+ local s="${1//\\/\\\\}"
456
+ s="${s//\"/\\\"}"
457
+ printf '%s' "$s"
458
+ }
459
+
460
+ # The D5 posture object this wrapper writes into EVERY receipt: the ACTUAL {model, effort, tier}
461
+ # of the run (tier null on the standard tier — the validated CODEX_SERVICE_TIER is the one source).
462
+ posture_json() {
463
+ local tier_json='null'
464
+ if [[ -n "${CODEX_SERVICE_TIER:-}" ]]; then tier_json="\"$(json_string_escape "$CODEX_SERVICE_TIER")\""; fi
465
+ printf '{"model":"%s","effort":"%s","tier":%s}' \
466
+ "$(json_string_escape "$CODEX_MODEL")" "$(json_string_escape "$CODEX_EFFORT")" "$tier_json"
467
+ }
468
+
424
469
  # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false]
425
470
  # Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
426
471
  # to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
@@ -444,10 +489,10 @@ write_review_receipt() {
444
489
  fi
445
490
  local line probe_field=',"probe":false'
446
491
  if [[ "$probe" == "true" ]]; then probe_field=',"probe":true'; fi
447
- line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"%s}' \
492
+ line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"%s,"posture":%s}' \
448
493
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
449
494
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
450
- "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field")"
495
+ "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)")"
451
496
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
452
497
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
453
498
  echo " the review-state gate will read the current tree as un-receipted." >&2
@@ -648,7 +693,11 @@ codex_cmd=("${codex_flags[@]}" -)
648
693
  invoke_codex
649
694
 
650
695
  # Raw-text fallback: if the structured-findings run failed (not a timeout), retry
651
- # once WITHOUT the schema rather than lose the review — loud, never silent.
696
+ # once WITHOUT the schema rather than lose the review — loud, never silent. The verdict parse
697
+ # tracks the run that actually SUCCEEDED: a fallback run emits the TEXT verdict line, so parsing
698
+ # it in schema mode would read every fallback as verdict-less (a guaranteed D4 failure).
699
+ verdict_parse_mode="text"
700
+ if [[ -n "$schema_file" ]]; then verdict_parse_mode="schema"; fi
652
701
  if [[ -n "$schema_file" && $rc -ne 0 && $rc -ne 124 && $rc -ne 137 ]]; then
653
702
  echo "warning: the --output-schema run failed (exit $rc) — retrying once without the schema constraint." >&2
654
703
  codex_cmd=()
@@ -657,6 +706,7 @@ if [[ -n "$schema_file" && $rc -ne 0 && $rc -ne 124 && $rc -ne 137 ]]; then
657
706
  codex_cmd+=("$_f")
658
707
  done
659
708
  codex_cmd+=(-)
709
+ verdict_parse_mode="text"
660
710
  invoke_codex
661
711
  fi
662
712
 
@@ -688,16 +738,27 @@ fi
688
738
 
689
739
  # --- Review receipt (AD-038): parse the verdict, append one receipt line ------
690
740
  # Text mode parses the mandated literal 'Verdict: <ship|revise|rethink>' line; CODEX_REVIEW_SCHEMA=1
691
- # reads the schema's "verdict" field instead; absent either way "unknown" (recorded, never guessed).
741
+ # reads the schema's "verdict" field instead. D4 (wrapper honesty): NO recognized verdict
742
+ # empty/missing final message included — is a FAILED review: non-zero exit, NO receipt; a failed
743
+ # review to RE-RUN, never a fatal session error (documented in --help).
692
744
  verdict=""
693
745
  if [[ -f "$out" && -s "$out" ]]; then
694
- if [[ "${CODEX_REVIEW_SCHEMA:-}" == "1" ]]; then
695
- verdict="$(sed -nE 's/.*"verdict"[[:space:]]*:[[:space:]]*"(ship|revise|rethink)".*/\1/p' "$out" | tail -n1)"
746
+ if [[ "$verdict_parse_mode" == "schema" ]]; then
747
+ # STRUCTURAL parse (round-1 fold): the TOP-LEVEL `verdict` field of the schema payload,
748
+ # accepted only inside the closed enum — a legal multiline layout parses, a decoy "verdict"
749
+ # inside a findings string never substitutes, malformed/out-of-enum stays empty (the D4
750
+ # failed-run arm). Node ≥22 is a family floor, so the one-liner adds no dependency.
751
+ verdict="$(node -e 'try{const v=JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8")).verdict;if(["ship","revise","rethink"].includes(v))process.stdout.write(v);}catch{}' "$out" 2>/dev/null || true)"
696
752
  else
697
753
  verdict="$(sed -nE 's/^Verdict: (ship|revise|rethink)[[:space:]]*$/\1/p' "$out" | tail -n1)"
698
754
  fi
699
755
  fi
700
- [[ -z "$verdict" ]] && verdict="unknown"
756
+ if [[ -z "$verdict" ]]; then
757
+ echo "error: the review produced no recognized 'Verdict: <ship|revise|rethink>' line (empty or" >&2
758
+ echo " verdict-less final message) — a FAILED review; NO receipt was written. Re-run the" >&2
759
+ echo " review; if it recurs, inspect the captured output for what the model produced." >&2
760
+ exit 4
761
+ fi
701
762
  # codex is grounded by construction (AGENTS.md auto-merge + the precomputed change set): grounded
702
763
  # true, factsHash null (native grounding — no separate facts payload exists). Every codex run is a
703
764
  # full fresh run (one-shot, no resume) → fresh:true. $REVIEW_PROBE marks a guards-relaxed run.
@@ -37,7 +37,9 @@ const FAKE_CODEX = [
37
37
  ' if [[ "$prev" == "-o" || "$prev" == "--output-last-message" ]]; then out="$a"; fi',
38
38
  ' prev="$a"',
39
39
  'done',
40
- 'if [[ -n "$out" && "${CODEX_FAKE_NO_OUT:-}" != "1" ]]; then echo "${CODEX_FAKE_FINAL:-FAKE_FINAL_MESSAGE}" >"$out"; fi',
40
+ // Unset CODEX_FAKE_FINAL a verdict-carrying default (D4: a verdict-less run is a FAILURE, so
41
+ // the success-path tests need one); an EXPLICIT empty value exercises the empty-output failure.
42
+ 'if [[ -n "$out" && "${CODEX_FAKE_NO_OUT:-}" != "1" ]]; then if [[ -z "${CODEX_FAKE_FINAL+x}" ]]; then printf "FAKE_FINAL_MESSAGE\\nVerdict: ship\\n" >"$out"; else printf "%s\\n" "$CODEX_FAKE_FINAL" >"$out"; fi; fi',
41
43
  'if [[ "${CODEX_FAKE_NO_THREAD:-}" != "1" ]]; then',
42
44
  'cat <<EOF',
43
45
  '{"type":"thread.started","thread_id":"${CODEX_FAKE_THREAD_ID:-fake-thread-123}"}',
@@ -366,7 +368,8 @@ describe('codex-review.sh — precomputed diff for code mode (2.1)', () => {
366
368
  describe('codex-review.sh — optional structured findings (2.2)', () => {
367
369
  it('CODEX_REVIEW_SCHEMA=1 passes --output-schema to codex', () => {
368
370
  const sb = makeSandbox();
369
- const r = run(sb, { args: ['code'], env: { CODEX_REVIEW_SCHEMA: '1' } });
371
+ // Schema mode parses the schema's verdict FIELD (D4) the fixture output must carry it.
372
+ const r = run(sb, { args: ['code'], env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{"verdict":"ship","findings":[]}' } });
370
373
  rmSync(sb.root, { recursive: true, force: true });
371
374
  assert.equal(r.status, 0, r.stderr);
372
375
  assert.match(r.argv, /(^|\n)--output-schema(\n|$)/);
@@ -379,8 +382,11 @@ describe('codex-review.sh — optional structured findings (2.2)', () => {
379
382
  assert.doesNotMatch(r.argv, /--output-schema/);
380
383
  });
381
384
 
382
- it('falls back to a raw-text run when the schema run fails (loud; exit 0)', () => {
385
+ it('falls back to a raw-text run when the schema run fails (loud; exit 0) — and parses the TEXT verdict', () => {
383
386
  const sb = makeSandbox();
387
+ // No CODEX_FAKE_FINAL: the fallback run emits the TEXT default (FAKE_FINAL_MESSAGE +
388
+ // Verdict: ship) — the wrapper must parse the mode of the run that actually SUCCEEDED,
389
+ // or every fallback would read verdict-less and die on the D4 arm.
384
390
  const r = run(sb, { args: ['code'], env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FAIL_ON_SCHEMA: '1' } });
385
391
  rmSync(sb.root, { recursive: true, force: true });
386
392
  assert.equal(r.status, 0, r.stderr);
@@ -527,13 +533,13 @@ describe('codex-review.sh — assemble & output edge cases', () => {
527
533
  assert.match(r.capStdin, /Extra focus: watch the parser/);
528
534
  });
529
535
 
530
- it('warns and prints the trace tail when codex writes no final-message file', () => {
536
+ it('warns and prints the trace tail when codex writes no final-message file (then fails the D4 arm)', () => {
531
537
  const sb = makeSandbox();
532
538
  const r = run(sb, { args: ['code'], env: { CODEX_FAKE_NO_OUT: '1' } });
533
539
  rmSync(sb.root, { recursive: true, force: true });
534
- assert.equal(r.status, 0, r.stderr);
540
+ assert.notEqual(r.status, 0, 'no final message ⇒ no verdict ⇒ a FAILED review (D4)');
535
541
  assert.match(r.stderr, /no final-message file/);
536
- assert.match(r.stdout, /turn\.completed/, 'the trace tail carries the event stream');
542
+ assert.match(r.stdout, /turn\.completed/, 'the trace tail still carries the event stream');
537
543
  });
538
544
 
539
545
  it('prints no session line when codex emits no thread id', () => {
@@ -812,7 +818,7 @@ describe('codex-review.sh — mode catalog ⟷ wrapper reality (manifest-pinned)
812
818
  // The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (field VALUES with
813
819
  // dynamic content are asserted by shape):
814
820
  const RECEIPT_FIXTURE = JSON.parse(
815
- '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false}',
821
+ '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false,"posture":{"model":"<m>","effort":"<e>","tier":null}}',
816
822
  );
817
823
  const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
818
824
  const readReceipts = (repo) => {
@@ -905,13 +911,13 @@ describe('codex-review.sh — review receipts (AD-038)', () => {
905
911
  assert.equal(receipts[0].verdict, 'ship');
906
912
  });
907
913
 
908
- it('no parseable verdict recorded as "unknown", never guessed', () => {
914
+ it('no parseable verdict is a FAILED run — never recorded as "unknown" (D4 owns the arm)', () => {
909
915
  const sb = makeSandbox();
910
916
  const r = run(sb, { env: { CODEX_FAKE_FINAL: 'looks fine to me overall' } });
911
917
  const receipts = readReceipts(sb.repo);
912
918
  rmSync(sb.root, { recursive: true, force: true });
913
- assert.equal(r.status, 0, r.stderr);
914
- assert.equal(receipts[0].verdict, 'unknown');
919
+ assert.notEqual(r.status, 0);
920
+ assert.equal(receipts.length, 0, 'an unknown verdict never reaches the receipt store');
915
921
  });
916
922
 
917
923
  it('plan mode: artifact "plan", fingerprint = the artifact-file sha256', () => {
@@ -1195,3 +1201,80 @@ describe('codex-review.sh — settings surface ⟷ manifest (D6, manifest-pinned
1195
1201
  }
1196
1202
  });
1197
1203
  });
1204
+
1205
+ // ── strip-the-kit Phase 4: wrapper honesty (D4) + dispatch-posture labeling (D5) ────────────────
1206
+ describe('codex-review.sh — wrapper honesty: a verdict-less run is a FAILED review (D4)', () => {
1207
+ it('a VERDICT-LESS final message: non-zero exit, NO receipt, the stated re-run recovery', () => {
1208
+ const sb = makeSandbox();
1209
+ const r = run(sb, { env: { CODEX_FAKE_FINAL: 'prose without the mandated verdict line' } });
1210
+ const receipts = readReceipts(sb.repo);
1211
+ rmSync(sb.root, { recursive: true, force: true });
1212
+ assert.notEqual(r.status, 0, 'a verdict-less review never exits 0');
1213
+ assert.equal(receipts.length, 0, 'NO receipt is minted for a failed review');
1214
+ assert.match(r.stderr, /Verdict/, 'the missing line is named');
1215
+ assert.match(r.stderr, /re-run/i, 'documented as a failed review — re-run, never fatal');
1216
+ });
1217
+
1218
+ it('an EMPTY final message is the same failed run (non-zero, no receipt)', () => {
1219
+ const sb = makeSandbox();
1220
+ const r = run(sb, { env: { CODEX_FAKE_FINAL: '' } });
1221
+ const receipts = readReceipts(sb.repo);
1222
+ rmSync(sb.root, { recursive: true, force: true });
1223
+ assert.notEqual(r.status, 0);
1224
+ assert.equal(receipts.length, 0);
1225
+ });
1226
+
1227
+ it('a MISSING final-message file is the same failed run', () => {
1228
+ const sb = makeSandbox();
1229
+ const r = run(sb, { env: { CODEX_FAKE_NO_OUT: '1' } });
1230
+ const receipts = readReceipts(sb.repo);
1231
+ rmSync(sb.root, { recursive: true, force: true });
1232
+ assert.notEqual(r.status, 0);
1233
+ assert.equal(receipts.length, 0);
1234
+ });
1235
+ });
1236
+
1237
+ describe('codex-review.sh — dispatch-posture labeling (D5)', () => {
1238
+ it('ONE banner line carries the ACTUAL {model, effort, tier} and the receipt carries the SAME posture', () => {
1239
+ const sb = makeSandbox();
1240
+ const r = run(sb, {});
1241
+ const receipts = readReceipts(sb.repo);
1242
+ rmSync(sb.root, { recursive: true, force: true });
1243
+ assert.equal(r.status, 0, r.stderr);
1244
+ assert.match(r.stderr, /review posture: model=gpt-5\.6-sol effort=xhigh tier=standard/, 'the banner states the actual run posture');
1245
+ assert.deepEqual(receipts[0].posture, { model: 'gpt-5.6-sol', effort: 'xhigh', tier: null }, 'banner ↔ receipt parity (standard tier = null)');
1246
+ assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
1247
+ });
1248
+
1249
+ it('an ARMED Fast tier rides both surfaces (banner tier=priority; receipt tier "priority")', () => {
1250
+ const sb = makeSandbox();
1251
+ const r = run(sb, { env: { CODEX_SERVICE_TIER: 'priority' } });
1252
+ const receipts = readReceipts(sb.repo);
1253
+ rmSync(sb.root, { recursive: true, force: true });
1254
+ assert.equal(r.status, 0, r.stderr);
1255
+ assert.match(r.stderr, /review posture: model=gpt-5\.6-sol effort=xhigh tier=priority/);
1256
+ assert.equal(receipts[0].posture.tier, 'priority');
1257
+ });
1258
+
1259
+ it('a HOSTILE model string (quotes + backslash) rides the receipt strictly JSON-encoded (probe lane)', () => {
1260
+ const hostile = 'we"ird \\ mo"del';
1261
+ const sb = makeSandbox();
1262
+ const r = run(sb, { env: { CODEX_PROBE: '1', CODEX_MODEL: hostile } });
1263
+ const receipts = readReceipts(sb.repo); // JSON.parse throwing here IS the encoding failure
1264
+ rmSync(sb.root, { recursive: true, force: true });
1265
+ assert.equal(r.status, 0, r.stderr);
1266
+ assert.equal(receipts[0].posture.model, hostile, 'the exact bytes round-trip through strict encoding');
1267
+ assert.equal(receipts[0].probe, true, 'an off-pinned model runs only on the probe lane');
1268
+ });
1269
+
1270
+ it('a posture value carrying CONTROL BYTES refuses pre-spend, BEFORE the frontier guard', () => {
1271
+ const sb = makeSandbox();
1272
+ const r = run(sb, { env: { CODEX_MODEL: `gpt-5.6-sol${String.fromCharCode(1)}` } });
1273
+ const receipts = readReceipts(sb.repo);
1274
+ rmSync(sb.root, { recursive: true, force: true });
1275
+ assert.notEqual(r.status, 0);
1276
+ assert.equal(r.capStdin, '', 'codex is never invoked');
1277
+ assert.equal(receipts.length, 0);
1278
+ assert.match(r.stderr, /control/i, 'named as the control-byte class, not a policy refusal');
1279
+ });
1280
+ });
@@ -3,7 +3,8 @@
3
3
  "schema": 1,
4
4
  "name": "codex-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.8.0",
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
  },
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "2.0.0",
6
+ "version": "3.0.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
@@ -0,0 +1,44 @@
1
+ # Migration 3.0.0-hardened-core-loop
2
+
3
+ **From:** versions < 3.0.0 **To:** 3.0.0
4
+
5
+ ## Why
6
+
7
+ 3.0.0 replaces the recorded review-loop machinery (review-ledger / fold-completeness /
8
+ verification-profile) with a COMPUTED core: red-proof custody + changed-line coverage inside
9
+ `run-gates --final`, ship-class review receipts with self-declared probe AND posture markers,
10
+ and an optional pre-commit guard that binds a commit to the latest green final-run receipt.
11
+ The retired tools no longer exist; a declaration or receipt that references them stops working
12
+ loudly, never silently.
13
+
14
+ ## Steps
15
+
16
+ 1. **Node floor:** ensure Node **>= 22** (the built-in lcov reporter the loop consumes).
17
+ 2. **Gates declaration:** preview the consented migration —
18
+ `node <installed-kit>/references/scripts/migrate-gates.mjs --kit-tools <installed-kit>/tools --cwd <project>`
19
+ — then re-run with `--apply` on an explicit yes. It removes retired ledger/fold gates,
20
+ wires the lcov reporters into the canonical `unit-tests` cmd, appends the canonical
21
+ `coverage-check` gate LAST, and cleans retired git-dir stores. CUSTOMIZED entries are
22
+ reported with a paste-ready recovery — resolve them by hand.
23
+ 3. **Review receipts:** receipts minted by pre-3.0.0/4.0.0 bridge wrappers carry no `posture`
24
+ field and STOP satisfying the review-state gate (fail-closed, the probe-marker precedent).
25
+ Refresh the placed bridges (init/upgrade does this), then re-run the required reviews.
26
+ 4. **Commit guard (optional, consented):** a 2.x deployment's project-local
27
+ `scripts/install-git-hooks.mjs` PREDATES the arm and the upgrade preserves existing scripts —
28
+ refresh it FIRST, classified deterministically:
29
+ `cmp -s scripts/install-git-hooks.mjs <installed-kit>/references/scripts/install-git-hooks.mjs`
30
+ — identical ⇒ already current; DIFFERING ⇒ the copy is UNKNOWN (older deploy or customized —
31
+ a consumer cannot enumerate shipped priors) ⇒ STOP and merge/replace by hand, never
32
+ overwrite unasked (an explicit "my copy is an unmodified older deploy" answer is the one
33
+ auto-replace lane). Then, once the declaration is final-run-capable, arm it:
34
+ `node scripts/install-git-hooks.mjs --commit-guard "<installed-kit>/tools/commit-guard.mjs"`
35
+ (the Recommendations advisor surfaces the exact one-liner) — and VERIFY the arm took:
36
+ `grep -Fx 'node "<installed-kit>/tools/commit-guard.mjs" --check' "$(git rev-parse --git-path hooks)/pre-commit"`
37
+ (the FIXED-STRING resolved line, exactly as the installer writes it) — absent ⇒ the arm did
38
+ not take; re-run the installer refresh above before re-arming.
39
+ 5. **Re-stamp:** init/upgrade re-stamps `docs/ai/.workflow-version` to 3.0.0.
40
+
41
+ ## Verification
42
+
43
+ `node <installed-kit>/tools/run-gates.mjs --final` exits 0 and records a green receipt;
44
+ `node <installed-kit>/tools/review-state.mjs --check` names any remaining obligation.
@@ -10,7 +10,7 @@ releases add files/templates, which `upgrade` reconciles without a migration.
10
10
  1. Read the project's stamped version from `docs/ai/.workflow-version`.
11
11
  2. Select every migration whose `<version>` is **strictly newer** than the stamp.
12
12
  3. Apply them in **ascending semver order**.
13
- 4. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`2.0.0` today — the
13
+ 4. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0` today — the
14
14
  shared lineage, **not** this skill's npm package version). A stamp greater than the head → STOP.
15
15
 
16
16
  ## Authoring rules