@bookedsolid/rea 0.10.2 → 0.11.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 (66) hide show
  1. package/.husky/pre-push +22 -167
  2. package/agents/codex-adversarial.md +5 -3
  3. package/commands/codex-review.md +3 -5
  4. package/dist/audit/append.d.ts +7 -32
  5. package/dist/audit/append.js +7 -35
  6. package/dist/cli/audit.d.ts +0 -31
  7. package/dist/cli/audit.js +5 -74
  8. package/dist/cli/doctor.js +6 -16
  9. package/dist/cli/hook.d.ts +48 -0
  10. package/dist/cli/hook.js +127 -0
  11. package/dist/cli/index.js +5 -80
  12. package/dist/cli/init.js +1 -1
  13. package/dist/cli/install/gitignore.d.ts +2 -2
  14. package/dist/cli/install/gitignore.js +3 -3
  15. package/dist/cli/install/pre-push.d.ts +146 -271
  16. package/dist/cli/install/pre-push.js +471 -2633
  17. package/dist/cli/install/settings-merge.d.ts +17 -0
  18. package/dist/cli/install/settings-merge.js +48 -1
  19. package/dist/cli/upgrade.js +131 -3
  20. package/dist/config/tier-map.js +18 -25
  21. package/dist/hooks/push-gate/base.d.ts +57 -0
  22. package/dist/hooks/push-gate/base.js +77 -0
  23. package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
  24. package/dist/hooks/push-gate/codex-runner.js +223 -0
  25. package/dist/hooks/push-gate/findings.d.ts +68 -0
  26. package/dist/hooks/push-gate/findings.js +142 -0
  27. package/dist/hooks/push-gate/halt.d.ts +28 -0
  28. package/dist/hooks/push-gate/halt.js +49 -0
  29. package/dist/hooks/push-gate/index.d.ts +90 -0
  30. package/dist/hooks/push-gate/index.js +351 -0
  31. package/dist/hooks/push-gate/policy.d.ts +41 -0
  32. package/dist/hooks/push-gate/policy.js +55 -0
  33. package/dist/hooks/push-gate/report.d.ts +89 -0
  34. package/dist/hooks/push-gate/report.js +140 -0
  35. package/dist/policy/loader.d.ts +10 -10
  36. package/dist/policy/loader.js +7 -6
  37. package/dist/policy/types.d.ts +31 -22
  38. package/package.json +1 -1
  39. package/dist/cache/review-cache.d.ts +0 -115
  40. package/dist/cache/review-cache.js +0 -200
  41. package/dist/cli/cache.d.ts +0 -84
  42. package/dist/cli/cache.js +0 -150
  43. package/dist/hooks/review-gate/args.d.ts +0 -126
  44. package/dist/hooks/review-gate/args.js +0 -315
  45. package/dist/hooks/review-gate/banner.d.ts +0 -97
  46. package/dist/hooks/review-gate/banner.js +0 -172
  47. package/dist/hooks/review-gate/cache-key.d.ts +0 -55
  48. package/dist/hooks/review-gate/cache-key.js +0 -41
  49. package/dist/hooks/review-gate/constants.d.ts +0 -26
  50. package/dist/hooks/review-gate/constants.js +0 -34
  51. package/dist/hooks/review-gate/errors.d.ts +0 -72
  52. package/dist/hooks/review-gate/errors.js +0 -100
  53. package/dist/hooks/review-gate/hash.d.ts +0 -43
  54. package/dist/hooks/review-gate/hash.js +0 -46
  55. package/dist/hooks/review-gate/index.d.ts +0 -21
  56. package/dist/hooks/review-gate/index.js +0 -21
  57. package/dist/hooks/review-gate/metadata.d.ts +0 -98
  58. package/dist/hooks/review-gate/metadata.js +0 -158
  59. package/dist/hooks/review-gate/policy.d.ts +0 -55
  60. package/dist/hooks/review-gate/policy.js +0 -71
  61. package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
  62. package/dist/hooks/review-gate/protected-paths.js +0 -76
  63. package/hooks/_lib/push-review-core.sh +0 -1250
  64. package/hooks/commit-review-gate.sh +0 -330
  65. package/hooks/push-review-gate-git.sh +0 -94
  66. package/hooks/push-review-gate.sh +0 -92
package/.husky/pre-push CHANGED
@@ -1,180 +1,35 @@
1
1
  #!/bin/sh
2
- # rea:husky-pre-push-gate v1
3
- # rea:gate-body-v1
4
- # .husky/pre-push — rea governance gate for terminal-initiated pushes.
2
+ # rea:husky-pre-push-gate v2
3
+ # rea:gate-body-v2
5
4
  #
6
- # Mirrors the logic of `.claude/hooks/push-review-gate.sh` but consumes the
7
- # git pre-push stdin contract directly (one line per refspec:
8
- # <local_ref> <local_sha> <remote_ref> <remote_sha>).
5
+ # Husky pre-push hook installed by `rea init` / `rea upgrade`. Do NOT
6
+ # edit by hand the file is refreshed on every rea upgrade.
9
7
  #
10
- # Minimum viable check NOT a full replacement for the Claude Code gate:
11
- # 1. If `.rea/HALT` exists, block.
12
- # 2. If the push touches a protected path AND policy.review.codex_required
13
- # is not explicitly false, require a `codex.review` audit entry for the
14
- # HEAD SHA (or REA_SKIP_CODEX_REVIEW env var for a one-off bypass).
15
- #
16
- # Escape hatch: REA_SKIP_CODEX_REVIEW=<reason> bypasses the protected-path
17
- # check. The skip record is appended by `push-review-gate.sh` in the Claude
18
- # Code path; for terminal pushes, export the variable AND append a skip
19
- # record manually if you want it in the audit trail.
20
- #
21
- # Subshell-safety note: earlier versions piped `echo "$INPUT" | while read`,
22
- # which ran the loop in a subshell — `exit 1` inside the loop aborted the
23
- # subshell only, and the script then ran `exit 0` and allowed the push. We
24
- # now feed the loop with a here-doc so it runs in the main shell, and we
25
- # abort immediately (`exit 1`) on the first blocking refspec. The accumulator
26
- # pattern (`block_push=1; continue`) was dropped so the text-level detector
27
- # in `src/cli/install/pre-push.ts` can verify the miss-path is truly blocking
28
- # without modeling loop-carried flags and post-loop exit blocks.
8
+ # Governance contract: HALT kill-switch check, then delegate to
9
+ # `rea hook push-gate`. See src/hooks/push-gate/index.ts.
29
10
 
30
11
  set -eu
31
12
 
32
- # git passes the remote name as $1 to pre-push. Fall back to `origin` for
33
- # direct invocation (tests, manual runs). The shared core uses the same
34
- # argv_remote convention — parity required so a push to `upstream` probes
35
- # `upstream/main` rather than stale `origin/main`.
36
- REMOTE="${1:-origin}"
37
-
38
13
  REA_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
39
-
40
- # Well-known empty-tree SHA: `git hash-object -t tree /dev/null`. Every git
41
- # installation carries this object implicitly — using it as a merge-base
42
- # baseline for initial pushes lets `git diff $EMPTY_TREE $local_sha` emit
43
- # the complete change set against a truly-empty tree. The protected-path
44
- # check then sees every file in the initial push, so a first push of
45
- # protected-path changes to a fresh remote is still gated.
46
- EMPTY_TREE='4b825dc642cb6eb9a060e54bf8d69288fbee4904'
47
-
48
14
  if [ -f "${REA_ROOT}/.rea/HALT" ]; then
49
- # POSIX `head` does not specify `-c`; use awk for the first line. HALT is
50
- # a short reason string, so the first line is enough for display.
51
- reason=$(awk 'NR==1 { print; exit }' "${REA_ROOT}/.rea/HALT" 2>/dev/null || printf 'unknown')
52
- [ -z "${reason:-}" ] && reason='unknown'
53
- printf 'REA HALT: %s\nAll push operations suspended. Run: rea unfreeze\n' "$reason" >&2
15
+ reason=$(awk "NR==1 { print; exit }" "${REA_ROOT}/.rea/HALT" 2>/dev/null || printf "unknown")
16
+ [ -z "${reason:-}" ] && reason="unknown"
17
+ printf "REA HALT: %s\nAll push operations suspended. Run: rea unfreeze\n" "$reason" >&2
54
18
  exit 1
55
19
  fi
56
20
 
57
- # Read refspec lines from stdin. Each line: <local_ref> <local_sha> <remote_ref> <remote_sha>
58
- INPUT=$(cat)
59
- [ -z "$INPUT" ] && exit 0
60
-
61
- # Anchor every alternative so a legitimate file like `docs/hooks-guide.md` or
62
- # `src/thirdparty/src/policy/loader.c` is not mistaken for a protected path.
63
- # `^\.claude/hooks/` is included so someone editing the consumer install
64
- # (which ships alongside rea itself) cannot sneak past the gate.
65
- PROTECTED_RE='^src/gateway/middleware/|^hooks/|^\.claude/hooks/|^src/policy/|^\.github/workflows/'
66
- AUDIT_LOG="${REA_ROOT}/.rea/audit.jsonl"
67
-
68
- # G11.4 — honor review.codex_required. When explicitly false, skip the
69
- # protected-path Codex audit requirement entirely (first-class no-Codex
70
- # mode). Mirrors the logic in `.claude/hooks/push-review-gate.sh`.
71
- #
72
- # Fail-closed: if the helper is missing or errors, treat as true. A missing
73
- # helper means rea is unbuilt — the operator can run `pnpm build` or set
74
- # REA_SKIP_CODEX_REVIEW for a one-off bypass.
75
- CODEX_REQUIRED=true
76
- READ_FIELD_JS="${REA_ROOT}/dist/scripts/read-policy-field.js"
77
- if [ -f "$READ_FIELD_JS" ]; then
78
- field_value=$(REA_ROOT="$REA_ROOT" node "$READ_FIELD_JS" review.codex_required 2>/dev/null || printf '')
79
- if [ "$field_value" = "false" ]; then
80
- CODEX_REQUIRED=false
81
- fi
21
+ REA_BIN=""
22
+ if [ -x "${REA_ROOT}/node_modules/.bin/rea" ]; then
23
+ REA_BIN="${REA_ROOT}/node_modules/.bin/rea"
24
+ elif [ -f "${REA_ROOT}/dist/cli/index.js" ]; then
25
+ REA_BIN="node ${REA_ROOT}/dist/cli/index.js"
26
+ elif command -v rea >/dev/null 2>&1; then
27
+ REA_BIN="rea"
28
+ elif command -v npx >/dev/null 2>&1; then
29
+ REA_BIN="npx --no-install @bookedsolid/rea"
30
+ else
31
+ printf "rea: cannot locate the rea CLI.\n" >&2
32
+ exit 2
82
33
  fi
83
34
 
84
- # Here-doc feeds the loop without creating a subshell, so an `exit 1`
85
- # inside the loop terminates the hook and blocks the push. A pipeline
86
- # would run the loop in a subshell and `exit 1` inside it would only
87
- # abort that subshell — NOT the push — which was a real governance
88
- # defect in the pre-review version of this file.
89
- while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
90
- [ -z "${local_sha:-}" ] && continue
91
- # Branch deletion: local_sha is 40 zeros. Skip protected-path check.
92
- case "$local_sha" in
93
- 0000000000000000000000000000000000000000) continue ;;
94
- esac
95
-
96
- # Determine merge base. If remote is new (remote_sha is zeros), diff against
97
- # the default branch; else against remote_sha.
98
- #
99
- # Anchor on a REMOTE-TRACKING ref (refs/remotes/<remote>/<name>), NOT a bare
100
- # branch name. A bare `main` resolves to refs/heads/main, which the pusher
101
- # controls locally — a local main fast-forwarded to the feature tip would
102
- # give merge-base main <local_sha> == local_sha and silently collapse the
103
- # diff to empty. Remote-tracking refs are server-authoritative from the
104
- # last fetch and cannot be tampered with locally.
105
- #
106
- # Fallback order when $REMOTE/HEAD is not set (common on shallow or mirror
107
- # clones): probe $REMOTE/main then $REMOTE/master via rev-parse. If neither
108
- # exists — initial push to a fresh remote with no tracking refs yet — use
109
- # the well-known EMPTY_TREE as the baseline so the diff covers the FULL
110
- # change set. This keeps the protected-path check honest on first push
111
- # (prior versions of this patch `continue`d here, which was a fail-open
112
- # flagged as HIGH by adversarial review).
113
- if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
114
- default_ref=$(git symbolic-ref "refs/remotes/${REMOTE}/HEAD" 2>/dev/null || printf '')
115
- if [ -z "${default_ref:-}" ]; then
116
- if git rev-parse --verify --quiet "refs/remotes/${REMOTE}/main" >/dev/null 2>&1; then
117
- default_ref="refs/remotes/${REMOTE}/main"
118
- elif git rev-parse --verify --quiet "refs/remotes/${REMOTE}/master" >/dev/null 2>&1; then
119
- default_ref="refs/remotes/${REMOTE}/master"
120
- else
121
- default_ref=""
122
- fi
123
- fi
124
- if [ -n "${default_ref:-}" ]; then
125
- base=$(git merge-base "$default_ref" "$local_sha" 2>/dev/null || printf '')
126
- else
127
- # Bootstrap: no remote-tracking ref exists at all. Use the empty-tree
128
- # baseline so the diff covers every file in the push. git diff accepts
129
- # a tree SHA as the left-hand side.
130
- base="$EMPTY_TREE"
131
- fi
132
- else
133
- base=$(git merge-base "$remote_sha" "$local_sha" 2>/dev/null || printf '')
134
- fi
135
- # Fail CLOSED on empty merge-base when a remote ref DID resolve. The
136
- # 0.4.0..0.6.2 behavior here was to `continue` — a silent bypass. A push
137
- # whose history is unrelated to origin (or any transient git failure at
138
- # merge-base resolution) would pass through without the protected-path
139
- # check ever running. Refuse instead and force the operator to resolve it.
140
- if [ -z "${base:-}" ]; then
141
- printf 'PUSH BLOCKED: could not resolve merge-base between %s and %s (local_ref=%s remote_ref=%s).\n' \
142
- "${remote_sha:-<new>}" "${local_sha:-<missing>}" "${local_ref:-<unknown>}" "${remote_ref:-<unknown>}" >&2
143
- printf ' Run `git fetch %s` and retry. If the history is genuinely unrelated\n' "$REMOTE" >&2
144
- printf ' to %s (e.g. grafted branch), resolve manually before pushing.\n' "$REMOTE" >&2
145
- exit 1
146
- fi
147
-
148
- # Check if the diff touches protected paths.
149
- if git diff --name-only "$base" "$local_sha" 2>/dev/null | grep -qE "$PROTECTED_RE"; then
150
- if [ "$CODEX_REQUIRED" = "false" ]; then
151
- # Policy opts out of the Codex gate. The downstream `.claude/hooks/`
152
- # path already records telemetry; terminal pushes skip silently.
153
- continue
154
- fi
155
- if [ -n "${REA_SKIP_CODEX_REVIEW:-}" ]; then
156
- printf 'rea: REA_SKIP_CODEX_REVIEW set (%s) — skipping Codex review requirement for %s\n' \
157
- "$REA_SKIP_CODEX_REVIEW" "$local_sha" >&2
158
- continue
159
- fi
160
- if [ ! -f "$AUDIT_LOG" ]; then
161
- printf 'PUSH BLOCKED: protected paths changed but no audit log found at %s\n' "$AUDIT_LOG" >&2
162
- printf ' Run /codex-review on HEAD %s before pushing.\n' "$local_sha" >&2
163
- exit 1
164
- fi
165
- # Require both (a) a `codex.review` tool_name and (b) the exact head_sha
166
- # on the same JSONL line. The `codex.review` pattern ends with a closing
167
- # quote, so `codex.review.skipped` never satisfies the gate. The first
168
- # refspec that fails this check aborts the hook — no accumulator needed.
169
- if ! grep -E '"tool_name":"codex\.review"' "$AUDIT_LOG" 2>/dev/null | \
170
- grep -qF "\"head_sha\":\"$local_sha\""; then
171
- printf 'PUSH BLOCKED: protected paths changed — /codex-review required for HEAD %s\n' "$local_sha" >&2
172
- printf ' Run /codex-review, or set REA_SKIP_CODEX_REVIEW=<reason> to bypass.\n' >&2
173
- exit 1
174
- fi
175
- fi
176
- done <<HOOK_INPUT_EOF
177
- $INPUT
178
- HOOK_INPUT_EOF
179
-
180
- exit 0
35
+ exec $REA_BIN hook push-gate "$@"
@@ -11,7 +11,9 @@ This is not a bolt-on. Adversarial review is a first-class, non-optional step in
11
11
 
12
12
  ## When You Are Invoked
13
13
 
14
- The `/codex-review` slash command calls you. The `rea-orchestrator` delegates to you after any non-trivial change. You also run automatically via the `push-review-gate` hook recipe when a recent Codex audit entry is missing on the branch being pushed.
14
+ The `/codex-review` slash command calls you. The `rea-orchestrator` delegates to you after any non-trivial change.
15
+
16
+ Note (0.11.0+): you are **not** invoked by the pre-push gate. The pre-push gate (`rea hook push-gate`) shells directly to `codex exec review --json` and parses the verdict itself — no agent wrapper, no audit-receipt consultation. When that gate blocks a push, the authoring Claude session reads the stderr banner and `.rea/last-review.json`, applies fixes, and pushes again — the auto-fix loop IS the retry mechanism. The agent wrapper (you) is kept for interactive review (`/codex-review`) where human-targeted structured output matters.
15
17
 
16
18
  ## Inputs
17
19
 
@@ -34,7 +36,7 @@ You may read additional files in the repo if needed for context, but do so read-
34
36
  5. **Parse the Codex output** — extract structured findings.
35
37
  6. **Classify findings** by category: security, correctness, edge cases, test gaps, API design, performance.
36
38
  7. **Assign verdict**: `pass` (no material findings), `concerns` (findings worth addressing but not blocking), `blocking` (findings that must be fixed before merge).
37
- 8. **Emit audit entry** after producing the verdict, append a structured record to `.rea/audit.jsonl` via the public `@bookedsolid/rea/audit` helper. This is what the `push-review-gate.sh` hook greps for on protected-path diffs, so the field names must match exactly:
39
+ 8. **Emit an audit entry** (optional in 0.11.0+) the pre-push gate does not consult audit records to decide pass/fail, so you are no longer REQUIRED to emit a `codex.review` record on every interactive review. However, append one anyway via the public `@bookedsolid/rea/audit` helper when it helps forensic traceability (investigation of an intermittent verdict, review-history audit, etc.):
38
40
 
39
41
  ```ts
40
42
  import { appendAuditRecord, CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, Tier, InvocationStatus } from '@bookedsolid/rea/audit';
@@ -54,7 +56,7 @@ You may read additional files in the repo if needed for context, but do so read-
54
56
  });
55
57
  ```
56
58
 
57
- If the Codex plugin call itself flowed through rea middleware (the proxy case), the middleware also writes an envelope record — that is fine, the two are complementary: the agent-emitted record carries the semantic verdict, the middleware record carries the chain integrity proof for the underlying tool call.
59
+ If the Codex plugin call itself flowed through rea middleware (the proxy case), the middleware also writes an envelope record — that is fine, the two are complementary.
58
60
 
59
61
  ## Finding Shape
60
62
 
@@ -90,12 +90,10 @@ If the verdict is `blocking`, state plainly: "Do not merge until the blocking fi
90
90
 
91
91
  ## Pre-merge usage
92
92
 
93
- The recommended BST workflow runs `/codex-review` twice:
93
+ This command is the **interactive** Codex adversarial review. The **pre-push** gate at `rea hook push-gate` runs Codex independently on every push — you do not need to run `/codex-review` to "prime" the push-gate. The two are complementary:
94
94
 
95
- 1. After implementation, on the feature branch catches issues early
96
- 2. Immediately before merge, on the PR branch records a fresh audit entry that the `push-review-gate` hook can check for freshness
97
-
98
- Both invocations are cheap. Run both.
95
+ - `/codex-review` — rich, interactive review output in the chat. Use during implementation to catch issues early, at review checkpoints, or whenever you want Codex's read on a specific diff.
96
+ - `rea hook push-gate` (wired to `.husky/pre-push`)fresh Codex review on every push. If Codex surfaces blocking/concerns findings, the push exits 2; Claude reads `.rea/last-review.json`, fixes, and pushes again.
99
97
 
100
98
  ## Constraints
101
99
 
@@ -65,45 +65,20 @@ export interface AppendAuditInput {
65
65
  * Append a structured audit record to `${baseDir}/.rea/audit.jsonl` with a
66
66
  * hash chained against the tail of the existing log.
67
67
  *
68
- * ## emission_source (defect P)
68
+ * ## emission_source
69
69
  *
70
- * Records written through this public helper are ALWAYS stamped with
71
- * `emission_source: "other"`. External consumers (Helix, ad-hoc scripts,
72
- * plugins) have no way to self-assert `"rea-cli"` or `"codex-cli"` through
73
- * this entry point the parameter is not part of the public
74
- * {@link AppendAuditInput} shape. Records emitted by the rea CLI itself use
75
- * the dedicated {@link appendCodexReviewAuditRecord} helper, which is the
76
- * ONLY path that stamps `"rea-cli"`.
77
- *
78
- * The push-review cache gate rejects `codex.review` records whose
79
- * `emission_source` is `"other"` (or missing, for legacy records), so
80
- * forging a `codex.review` record through this helper produces a line that
81
- * is on the hash chain but does NOT satisfy the gate.
70
+ * Records written through this public helper are stamped with
71
+ * `emission_source: "other"`. The field is retained for forensic analysis
72
+ * (who wrote this line) but no gate consults it the 0.11.0 stateless
73
+ * push-gate (see `src/hooks/push-gate/`) decides on Codex's live verdict,
74
+ * not on a receipt in the audit log. Pre-0.11.0 `emission_source`-gated
75
+ * predicates have been removed.
82
76
  *
83
77
  * @param baseDir - Repo/project root (the directory that contains `.rea/`).
84
78
  * @param input - Event data. `tool_name` and `server_name` are required.
85
79
  * @returns The full written record, including the computed `hash`.
86
80
  */
87
81
  export declare function appendAuditRecord(baseDir: string, input: AppendAuditInput): Promise<AuditRecord>;
88
- /**
89
- * Append a `tool_name: "codex.review"` audit record certifying that a Codex
90
- * adversarial review ran on a specific commit SHA (defect P).
91
- *
92
- * This is the ONLY write path in `@bookedsolid/rea` that produces
93
- * `emission_source: "rea-cli"` for `codex.review` records. Consumers MUST
94
- * reach this helper through the `rea audit record codex-review` CLI (which
95
- * is classified as a Write-tier Bash invocation by `reaCommandTier`, defect
96
- * E). Any other code path calling the generic {@link appendAuditRecord}
97
- * with `tool_name: "codex.review"` lands with `emission_source: "other"`
98
- * and does NOT satisfy the push-review cache gate — closing the forgery
99
- * surface that `.reports/hook-patches/emit-audit-*.mjs` scripts exploited
100
- * before this patch.
101
- *
102
- * `tool_name` and `server_name` are fixed to the canonical values
103
- * (`"codex.review"` / `"codex"`) and are NOT accepted as caller inputs —
104
- * the type excludes them so the contract is self-documenting.
105
- */
106
- export declare function appendCodexReviewAuditRecord(baseDir: string, input: Omit<AppendAuditInput, 'tool_name' | 'server_name'>): Promise<AuditRecord>;
107
82
  export type { AuditRecord, EmissionSource } from '../gateway/middleware/audit-types.js';
108
83
  export { Tier, InvocationStatus } from '../policy/types.js';
109
84
  export { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, type CodexVerdict, type CodexReviewMetadata, } from './codex-event.js';
@@ -37,7 +37,6 @@ import path from 'node:path';
37
37
  import { Tier, InvocationStatus } from '../policy/types.js';
38
38
  import { GENESIS_HASH, computeHash, fsyncFile, readLastRecord, withAuditLock, } from './fs.js';
39
39
  import { maybeRotate } from '../gateway/audit/rotator.js';
40
- import { CODEX_REVIEW_SERVER_NAME, CODEX_REVIEW_TOOL_NAME } from './codex-event.js';
41
40
  const REA_DIR = '.rea';
42
41
  const AUDIT_FILE = 'audit.jsonl';
43
42
  /** Per-file write queue to preserve linear hash-chain order within a process. */
@@ -186,20 +185,14 @@ async function enqueueAppend(baseDir, input, emissionSource) {
186
185
  * Append a structured audit record to `${baseDir}/.rea/audit.jsonl` with a
187
186
  * hash chained against the tail of the existing log.
188
187
  *
189
- * ## emission_source (defect P)
188
+ * ## emission_source
190
189
  *
191
- * Records written through this public helper are ALWAYS stamped with
192
- * `emission_source: "other"`. External consumers (Helix, ad-hoc scripts,
193
- * plugins) have no way to self-assert `"rea-cli"` or `"codex-cli"` through
194
- * this entry point the parameter is not part of the public
195
- * {@link AppendAuditInput} shape. Records emitted by the rea CLI itself use
196
- * the dedicated {@link appendCodexReviewAuditRecord} helper, which is the
197
- * ONLY path that stamps `"rea-cli"`.
198
- *
199
- * The push-review cache gate rejects `codex.review` records whose
200
- * `emission_source` is `"other"` (or missing, for legacy records), so
201
- * forging a `codex.review` record through this helper produces a line that
202
- * is on the hash chain but does NOT satisfy the gate.
190
+ * Records written through this public helper are stamped with
191
+ * `emission_source: "other"`. The field is retained for forensic analysis
192
+ * (who wrote this line) but no gate consults it the 0.11.0 stateless
193
+ * push-gate (see `src/hooks/push-gate/`) decides on Codex's live verdict,
194
+ * not on a receipt in the audit log. Pre-0.11.0 `emission_source`-gated
195
+ * predicates have been removed.
203
196
  *
204
197
  * @param baseDir - Repo/project root (the directory that contains `.rea/`).
205
198
  * @param input - Event data. `tool_name` and `server_name` are required.
@@ -208,26 +201,5 @@ async function enqueueAppend(baseDir, input, emissionSource) {
208
201
  export async function appendAuditRecord(baseDir, input) {
209
202
  return enqueueAppend(baseDir, input, 'other');
210
203
  }
211
- /**
212
- * Append a `tool_name: "codex.review"` audit record certifying that a Codex
213
- * adversarial review ran on a specific commit SHA (defect P).
214
- *
215
- * This is the ONLY write path in `@bookedsolid/rea` that produces
216
- * `emission_source: "rea-cli"` for `codex.review` records. Consumers MUST
217
- * reach this helper through the `rea audit record codex-review` CLI (which
218
- * is classified as a Write-tier Bash invocation by `reaCommandTier`, defect
219
- * E). Any other code path calling the generic {@link appendAuditRecord}
220
- * with `tool_name: "codex.review"` lands with `emission_source: "other"`
221
- * and does NOT satisfy the push-review cache gate — closing the forgery
222
- * surface that `.reports/hook-patches/emit-audit-*.mjs` scripts exploited
223
- * before this patch.
224
- *
225
- * `tool_name` and `server_name` are fixed to the canonical values
226
- * (`"codex.review"` / `"codex"`) and are NOT accepted as caller inputs —
227
- * the type excludes them so the contract is self-documenting.
228
- */
229
- export async function appendCodexReviewAuditRecord(baseDir, input) {
230
- return enqueueAppend(baseDir, { ...input, tool_name: CODEX_REVIEW_TOOL_NAME, server_name: CODEX_REVIEW_SERVER_NAME }, 'rea-cli');
231
- }
232
204
  export { Tier, InvocationStatus } from '../policy/types.js';
233
205
  export { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, } from './codex-event.js';
@@ -10,7 +10,6 @@
10
10
  * explicit by definition, and verify operates on existing files regardless
11
11
  * of policy.
12
12
  */
13
- import { type CodexVerdict } from '../audit/append.js';
14
13
  /**
15
14
  * Reserved for future rotate knobs (e.g. `--retain N` to prune old rotated
16
15
  * files). Empty today — kept as a typed record so the call site's option
@@ -39,33 +38,3 @@ export declare function runAuditRotate(_options: AuditRotateOptions): Promise<vo
39
38
  * exit code is the primary signal.
40
39
  */
41
40
  export declare function runAuditVerify(options: AuditVerifyOptions): Promise<void>;
42
- export interface AuditRecordCodexReviewOptions {
43
- headSha: string;
44
- branch: string;
45
- target: string;
46
- verdict: CodexVerdict;
47
- findingCount: number;
48
- summary?: string | undefined;
49
- sessionId?: string | undefined;
50
- alsoSetCache?: boolean | undefined;
51
- }
52
- /**
53
- * `rea audit record codex-review` (Defect D / rea#77). Emits the single audit
54
- * event the push-review cache gate looks up by `tool_name == "codex.review"` +
55
- * `metadata.head_sha == <sha>` + `metadata.verdict in {pass, concerns}`. Prior
56
- * to this command, agents had to reverse-engineer the canonical `tool_name`
57
- * string, the hash-chain append path, and the `CodexReviewMetadata` shape —
58
- * the most common failure mode was emitting `tool_name: "codex-adversarial-review"`
59
- * (the agent's name) instead of `codex.review` (the event type), which the
60
- * gate's jq predicate silently missed.
61
- *
62
- * `--also-set-cache` performs the audit record AND the review-cache write
63
- * in one invocation — two sequential appends in a single process, not a
64
- * two-phase commit. A crash between them leaves the audit entry without
65
- * a cache row; the cache is recomputable from audit, the audit chain is
66
- * the source of truth. What this DOES eliminate is the two-step race where
67
- * `rea cache set` is denied by permission middleware (Defect E) after the
68
- * audit has already been emitted, leaving the gate stuck on "audit present
69
- * but cache cold" with no way forward.
70
- */
71
- export declare function runAuditRecordCodexReview(options: AuditRecordCodexReviewOptions): Promise<void>;
package/dist/cli/audit.js CHANGED
@@ -13,12 +13,8 @@
13
13
  import fs from 'node:fs/promises';
14
14
  import path from 'node:path';
15
15
  import { forceRotate } from '../gateway/audit/rotator.js';
16
- import { appendCodexReviewAuditRecord, } from '../audit/append.js';
17
16
  import { computeHash, GENESIS_HASH } from '../audit/fs.js';
18
- import { appendEntry as appendCacheEntry } from '../cache/review-cache.js';
19
17
  import { AUDIT_FILE, REA_DIR, err, log, reaPath } from './utils.js';
20
- import { Tier, InvocationStatus } from '../policy/types.js';
21
- import { codexVerdictToCacheResult } from './cache.js';
22
18
  /**
23
19
  * `rea audit rotate`. Forces a rotation now regardless of thresholds.
24
20
  * Empty audit files are a no-op — rotating an empty chain would produce a
@@ -300,73 +296,8 @@ export async function runAuditVerify(options) {
300
296
  }
301
297
  log(`Audit chain verified: ${totalRecords} records across ${filesToVerify.length} file(s) — clean.`);
302
298
  }
303
- /**
304
- * `rea audit record codex-review` (Defect D / rea#77). Emits the single audit
305
- * event the push-review cache gate looks up by `tool_name == "codex.review"` +
306
- * `metadata.head_sha == <sha>` + `metadata.verdict in {pass, concerns}`. Prior
307
- * to this command, agents had to reverse-engineer the canonical `tool_name`
308
- * string, the hash-chain append path, and the `CodexReviewMetadata` shape —
309
- * the most common failure mode was emitting `tool_name: "codex-adversarial-review"`
310
- * (the agent's name) instead of `codex.review` (the event type), which the
311
- * gate's jq predicate silently missed.
312
- *
313
- * `--also-set-cache` performs the audit record AND the review-cache write
314
- * in one invocation — two sequential appends in a single process, not a
315
- * two-phase commit. A crash between them leaves the audit entry without
316
- * a cache row; the cache is recomputable from audit, the audit chain is
317
- * the source of truth. What this DOES eliminate is the two-step race where
318
- * `rea cache set` is denied by permission middleware (Defect E) after the
319
- * audit has already been emitted, leaving the gate stuck on "audit present
320
- * but cache cold" with no way forward.
321
- */
322
- export async function runAuditRecordCodexReview(options) {
323
- if (options.headSha.length === 0) {
324
- err('--head-sha must not be empty');
325
- process.exit(1);
326
- }
327
- if (options.branch.length === 0) {
328
- err('--branch must not be empty');
329
- process.exit(1);
330
- }
331
- if (options.target.length === 0) {
332
- err('--target must not be empty');
333
- process.exit(1);
334
- }
335
- if (!Number.isFinite(options.findingCount) || options.findingCount < 0) {
336
- err(`--finding-count must be a non-negative integer; got ${options.findingCount}`);
337
- process.exit(1);
338
- }
339
- const baseDir = process.cwd();
340
- const metadata = {
341
- head_sha: options.headSha,
342
- target: options.target,
343
- finding_count: options.findingCount,
344
- verdict: options.verdict,
345
- };
346
- if (options.summary !== undefined && options.summary.length > 0) {
347
- metadata.summary = options.summary;
348
- }
349
- // Defect P: stamps emission_source: "rea-cli" so the record satisfies the
350
- // push-review gate's new integrity predicate. Legacy records (without
351
- // emission_source) and records written through the generic
352
- // appendAuditRecord() helper (emission_source: "other") are rejected.
353
- // tool_name/server_name are fixed inside the helper.
354
- await appendCodexReviewAuditRecord(baseDir, {
355
- tier: Tier.Read,
356
- status: InvocationStatus.Allowed,
357
- ...(options.sessionId !== undefined ? { session_id: options.sessionId } : {}),
358
- metadata,
359
- });
360
- log(`Recorded codex.review (${options.verdict}, ${options.findingCount} finding${options.findingCount === 1 ? '' : 's'}) for ${options.headSha.slice(0, 12)}.`);
361
- if (options.alsoSetCache === true) {
362
- const effect = codexVerdictToCacheResult(options.verdict);
363
- const cacheEntry = await appendCacheEntry(baseDir, {
364
- sha: options.headSha,
365
- branch: options.branch,
366
- base: options.target,
367
- result: effect.result,
368
- ...(effect.reason !== undefined ? { reason: effect.reason } : {}),
369
- });
370
- log(`Cached ${cacheEntry.result} for ${cacheEntry.sha.slice(0, 12)} (${cacheEntry.branch} → ${cacheEntry.base}).`);
371
- }
372
- }
299
+ // `rea audit record codex-review` was removed in 0.11.0. The 0.11.0 push-gate
300
+ // is stateless every `git push` runs `codex exec review --json` afresh,
301
+ // parses the verdict from the stream, and blocks or proceeds on the spot.
302
+ // There is no audit-receipt the gate consults, so no command to emit one.
303
+ // See `src/hooks/push-gate/index.ts` for the replacement gate.
@@ -147,12 +147,10 @@ const EXPECTED_HOOKS = [
147
147
  'attribution-advisory.sh',
148
148
  'blocked-paths-enforcer.sh',
149
149
  'changeset-security-gate.sh',
150
- 'commit-review-gate.sh',
151
150
  'dangerous-bash-interceptor.sh',
152
151
  'dependency-audit-gate.sh',
153
152
  'env-file-protection.sh',
154
153
  'pr-issue-link-gate.sh',
155
- 'push-review-gate.sh',
156
154
  'secret-scanner.sh',
157
155
  'security-disclosure-gate.sh',
158
156
  'settings-protection.sh',
@@ -366,7 +364,7 @@ function checkPrePushHook(state) {
366
364
  const kind = active?.reaManaged === true
367
365
  ? 'rea-managed'
368
366
  : active?.delegatesToGate === true
369
- ? 'external (delegates to push-review-gate.sh)'
367
+ ? 'external (delegates to `rea hook push-gate`)'
370
368
  : 'external';
371
369
  const detail = active !== undefined ? `${kind} at ${active.path}` : undefined;
372
370
  return detail !== undefined
@@ -374,23 +372,15 @@ function checkPrePushHook(state) {
374
372
  : { label: 'pre-push hook installed', status: 'pass' };
375
373
  }
376
374
  if (state.activeForeign) {
377
- // Executable file exists at the active path but does not carry
378
- // governance the parser could not confirm the review gate is
379
- // invoked unconditionally. Always a hard fail.
380
- //
381
- // R13 F3: previously, a substring match of the gate path in the hook
382
- // downgraded this to WARN. That was unsafe — any comment, echo, or
383
- // dead string mentioning the path would mask a silent-bypass hook.
384
- // The classifier now fails closed: either the structural parser
385
- // (`referencesReviewGate` in `pre-push.ts`) recognizes a real
386
- // invocation, or doctor reports fail.
375
+ // Executable file exists at the active path but neither carries a rea
376
+ // marker nor invokes `rea hook push-gate` the push-gate is silently
377
+ // bypassed. Always a hard fail.
387
378
  return {
388
379
  label: 'pre-push hook installed',
389
380
  status: 'fail',
390
381
  detail: `active pre-push at ${state.activePath} is present and executable but does NOT ` +
391
- `reference \`.claude/hooks/push-review-gate.sh\` — the protected-path ` +
392
- `Codex audit gate is silently bypassed. Either add ` +
393
- '`exec .claude/hooks/push-review-gate.sh "$@"` to the existing hook, or ' +
382
+ 'invoke `rea hook push-gate` — the 0.11.0 push-gate is silently bypassed. ' +
383
+ 'Either add `exec rea hook push-gate "$@"` to the existing hook, or ' +
394
384
  'remove it and re-run `rea init` to install the fallback.',
395
385
  };
396
386
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `rea hook push-gate` — the CLI surface the husky `.husky/pre-push` stub
3
+ * calls. Stateless pre-push Codex review.
4
+ *
5
+ * Exit-code contract:
6
+ *
7
+ * 0 — push proceeds (pass verdict, empty diff, disabled by policy, or
8
+ * REA_SKIP_PUSH_GATE waiver)
9
+ * 1 — HALT kill-switch active; block push
10
+ * 2 — blocked by verdict (blocking, or concerns when concerns_blocks=true
11
+ * and REA_ALLOW_CONCERNS not set), or by codex error (timeout, not
12
+ * installed, subprocess failure, protocol error)
13
+ *
14
+ * Invocation contract:
15
+ *
16
+ * rea hook push-gate
17
+ * rea hook push-gate --base origin/main
18
+ * rea hook push-gate --base refs/remotes/upstream/main
19
+ *
20
+ * The husky stub does NOT parse the git pre-push stdin contract itself —
21
+ * the 0.10.x bash gate did, to diff refspec-by-refspec; the 0.11.0 gate
22
+ * diffs `HEAD` against the resolved base (upstream → origin/HEAD → …).
23
+ * That is strictly less granular than refspec parsing, but Codex reviews
24
+ * the whole diff anyway and pushing multiple branches simultaneously is
25
+ * vanishingly rare in practice.
26
+ *
27
+ * A missing `.rea/policy.yaml` is treated as "defaults apply" —
28
+ * `codex_required: true`, `concerns_blocks: true`. The gate still fires.
29
+ * This matches the protective default established in 0.10.x.
30
+ */
31
+ import type { Command } from 'commander';
32
+ export interface HookPushGateOptions {
33
+ base?: string;
34
+ }
35
+ /**
36
+ * Public runner, exposed so integration tests and the commander binding can
37
+ * share the same entry. Throws via `process.exit` rather than returning a
38
+ * code — the commander handler is async but the convention across `src/cli/`
39
+ * is to exit from the leaf (see `audit.ts`, `freeze.ts`). Keeping the
40
+ * behavior consistent prevents commander from inferring its own default.
41
+ */
42
+ export declare function runHookPushGate(options: HookPushGateOptions): Promise<void>;
43
+ /**
44
+ * Attach the `rea hook` subcommand tree to a commander Program. Single
45
+ * subcommand today (`push-gate`); new hooks should land here rather than as
46
+ * top-level commands so the CLI surface stays navigable.
47
+ */
48
+ export declare function registerHookCommand(program: Command): void;