@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
@@ -1,330 +0,0 @@
1
- #!/bin/bash
2
- # PreToolUse hook: commit-review-gate.sh
3
- # Fires BEFORE every Bash tool call that matches "git commit".
4
- # Implements a triage-based review gate:
5
- # - trivial (<20 changed lines, non-sensitive paths) → pass immediately
6
- # - standard (20-200 lines) → check review cache, pass if cached
7
- # - significant (>200 lines or sensitive paths) → block, request agent review
8
- #
9
- # Exit codes:
10
- # 0 = allow (trivial change, or cached review found)
11
- # 2 = block (needs review — returns additionalContext for agent)
12
-
13
- set -uo pipefail
14
-
15
- # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
16
- INPUT=$(cat)
17
-
18
- # ── 1a. Cross-repo guard (must come FIRST — before any rea-scoped check) ──────
19
- # BUG-012 (0.6.2) — mirror of push-review-gate.sh §1a. Script-location
20
- # anchor (not CLAUDE_PROJECT_DIR) owns the trust decision. See the
21
- # push-gate comment and THREAT_MODEL.md § CLAUDE_PROJECT_DIR for the full
22
- # rationale. In short: CLAUDE_PROJECT_DIR is caller-controlled, cannot be
23
- # trusted for authorization, and the hook's own filesystem location is the
24
- # only forge-resistant anchor available to a bash script.
25
- SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd -P 2>/dev/null)"
26
- # Walk up from SCRIPT_DIR looking for `.rea/policy.yaml`. Matches every
27
- # reasonable install topology (see push-review-gate.sh §1a for the full
28
- # rationale). A hard-coded `../..` breaks the source-path invocation
29
- # (`bash hooks/commit-review-gate.sh`) and silently reads .rea state from
30
- # the WRONG directory.
31
- REA_ROOT=""
32
- _anchor_candidate="$SCRIPT_DIR"
33
- for _ in 1 2 3 4; do
34
- _anchor_candidate="$(cd -- "$_anchor_candidate/.." && pwd -P 2>/dev/null || true)"
35
- if [[ -n "$_anchor_candidate" && -f "$_anchor_candidate/.rea/policy.yaml" ]]; then
36
- REA_ROOT="$_anchor_candidate"
37
- break
38
- fi
39
- done
40
- if [[ -z "$REA_ROOT" ]]; then
41
- printf 'rea-hook: no .rea/policy.yaml found within 4 parents of %s\n' \
42
- "$SCRIPT_DIR" >&2
43
- printf 'rea-hook: is this an installed rea hook, or is `.rea/policy.yaml`\n' >&2
44
- printf 'rea-hook: nested more than 4 directories above the hook script?\n' >&2
45
- exit 2
46
- fi
47
- unset _anchor_candidate
48
-
49
- if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
50
- CPD_REAL=$(cd -- "${CLAUDE_PROJECT_DIR}" 2>/dev/null && pwd -P 2>/dev/null || true)
51
- if [[ -n "$CPD_REAL" && "$CPD_REAL" != "$REA_ROOT" ]]; then
52
- printf 'rea-hook: ignoring CLAUDE_PROJECT_DIR=%s — anchoring to script location %s\n' \
53
- "$CLAUDE_PROJECT_DIR" "$REA_ROOT" >&2
54
- fi
55
- fi
56
-
57
- CWD_REAL=$(pwd -P 2>/dev/null || pwd)
58
- CWD_COMMON=$(git -C "$CWD_REAL" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)
59
- REA_COMMON=$(git -C "$REA_ROOT" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)
60
- if [[ -n "$CWD_COMMON" && -n "$REA_COMMON" ]]; then
61
- CWD_COMMON_REAL=$(cd "$CWD_COMMON" 2>/dev/null && pwd -P 2>/dev/null || echo "$CWD_COMMON")
62
- REA_COMMON_REAL=$(cd "$REA_COMMON" 2>/dev/null && pwd -P 2>/dev/null || echo "$REA_COMMON")
63
- if [[ "$CWD_COMMON_REAL" != "$REA_COMMON_REAL" ]]; then
64
- exit 0
65
- fi
66
- elif [[ -z "$CWD_COMMON" && -z "$REA_COMMON" ]]; then
67
- case "$CWD_REAL/" in
68
- "$REA_ROOT"/*|"$REA_ROOT"/) : ;; # inside rea — run the gate
69
- *) exit 0 ;; # outside rea — not our gate
70
- esac
71
- fi
72
- # Mixed state or probe error → fail CLOSED: run the gate.
73
-
74
- # ── 2. Dependency check ──────────────────────────────────────────────────────
75
- if ! command -v jq >/dev/null 2>&1; then
76
- printf 'REA ERROR: jq is required but not installed.\n' >&2
77
- printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
78
- exit 2
79
- fi
80
-
81
- # ── 3. HALT check ────────────────────────────────────────────────────────────
82
- HALT_FILE="${REA_ROOT}/.rea/HALT"
83
- if [ -f "$HALT_FILE" ]; then
84
- printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
85
- "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
86
- exit 2
87
- fi
88
-
89
- # ── 4. Parse command ──────────────────────────────────────────────────────────
90
- CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
91
-
92
- if [[ -z "$CMD" ]]; then
93
- exit 0
94
- fi
95
-
96
- # Only trigger on git commit commands
97
- if ! printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit'; then
98
- exit 0
99
- fi
100
-
101
- # Skip --amend (reviewing amendments is a future feature)
102
- if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--amend'; then
103
- exit 0
104
- fi
105
-
106
- # ── 5. Compute diff stats ────────────────────────────────────────────────────
107
- # Get staged diff (what would be committed)
108
- DIFF_OUTPUT=$(cd "$REA_ROOT" && git diff --cached --stat 2>/dev/null || echo "")
109
- DIFF_FULL=$(cd "$REA_ROOT" && git diff --cached 2>/dev/null || echo "")
110
-
111
- if [[ -z "$DIFF_OUTPUT" ]]; then
112
- # No staged changes — let git commit handle the error
113
- exit 0
114
- fi
115
-
116
- # Count changed lines (additions + deletions)
117
- # Defect K (rea#62) sibling: `|| echo "0"` captures "0\n0" into LINE_COUNT
118
- # when grep exits non-zero on a no-match — grep still prints its own `0` and
119
- # `echo "0"` appends another. At this site the concatenated `"0\n0"` is then
120
- # evaluated as arithmetic (`-gt $SIGNIFICANT_THRESHOLD`, `-ge $TRIVIAL_THRESHOLD`
121
- # below) and bash emits a "syntax error in expression" at runtime on any
122
- # rename-only / mode-only / empty-file-add diff. `|| true` + bash-default
123
- # expansion fixes both the banner cosmetic and the arithmetic-unsafe control
124
- # flow in one shot.
125
- LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || true)
126
- LINE_COUNT="${LINE_COUNT:-0}"
127
-
128
- # Check for sensitive paths
129
- SENSITIVE=0
130
- SENSITIVE_FILES=""
131
- if printf '%s' "$DIFF_FULL" | grep -qE '^\+\+\+ .*(\.rea/|\.claude/|\.env|auth|security|\.github/workflows)'; then
132
- SENSITIVE=1
133
- SENSITIVE_FILES=$(printf '%s' "$DIFF_FULL" | grep -oE '^\+\+\+ .*(\.rea/|\.claude/|\.env|auth|security|\.github/workflows)[^ ]*' | sed 's/^\+\+\+ [ab]\// /' | head -5)
134
- fi
135
-
136
- # ── 7. Triage scoring ────────────────────────────────────────────────────────
137
- TRIVIAL_THRESHOLD=20
138
- SIGNIFICANT_THRESHOLD=200
139
-
140
- if [[ $SENSITIVE -eq 1 ]] || [[ $LINE_COUNT -gt $SIGNIFICANT_THRESHOLD ]]; then
141
- SCORE="significant"
142
- elif [[ $LINE_COUNT -ge $TRIVIAL_THRESHOLD ]]; then
143
- SCORE="standard"
144
- else
145
- SCORE="trivial"
146
- fi
147
-
148
- # ── 8. Trivial → pass immediately ─────────────────────────────────────────────
149
- if [[ "$SCORE" == "trivial" ]]; then
150
- exit 0
151
- fi
152
-
153
- # ── 9. Resolve rea CLI ────────────────────────────────────────────────────
154
- # Try local installs first, then dist build, then global PATH install.
155
- #
156
- # node_modules/.bin/rea is a launcher (pnpm writes a POSIX shell shim, npm
157
- # writes a symlink to dist/cli/index.js with its own `#!/usr/bin/env node`
158
- # shebang). Either way it is NOT a plain JS file, so running `node` on it
159
- # would parse shell syntax as JavaScript and SyntaxError. Execute the shim
160
- # directly — it handles `exec node` itself — and only prepend `node` on the
161
- # dist fallback, which is a real JS module. The `-x` guard picks up both
162
- # pnpm shims (executable regular file) and npm symlinks (executable target).
163
- REA_CLI_ARGS=()
164
- if [[ -x "${REA_ROOT}/node_modules/.bin/rea" ]]; then
165
- REA_CLI_ARGS=("${REA_ROOT}/node_modules/.bin/rea")
166
- elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
167
- REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
168
- elif command -v rea >/dev/null 2>&1; then
169
- REA_CLI_ARGS=(rea)
170
- fi
171
-
172
- # ── 10. Check review cache for all non-trivial commits ────────────────────────
173
- # Compute SHA and branch here so both standard and significant tiers share them.
174
- #
175
- # Defect L (rea#63) sibling: `shasum` is not installed on Alpine, distroless,
176
- # or most minimal Linux CI images — only `sha256sum` is. The prior chain
177
- # silently produced an empty STAGED_SHA, which the cache block then skipped
178
- # AND the banner at §11 rendered as `rea cache set pass` — a dead-end the
179
- # agent cannot execute. Portable chain mirrors push-review-core.sh §8:
180
- # sha256sum → shasum → openssl. The openssl branch uses `awk '{print $NF}'`
181
- # WITHOUT `-r` to stay compatible with OpenSSL 1.1.x (Debian 11, Ubuntu
182
- # 20.04, RHEL 8, Amazon Linux 2, Alpine 3.13–3.14).
183
- STAGED_SHA=""
184
- if command -v sha256sum >/dev/null 2>&1; then
185
- STAGED_SHA=$(printf '%s' "$DIFF_FULL" | sha256sum 2>/dev/null | awk '{print $1}')
186
- elif command -v shasum >/dev/null 2>&1; then
187
- STAGED_SHA=$(printf '%s' "$DIFF_FULL" | shasum -a 256 2>/dev/null | awk '{print $1}')
188
- elif command -v openssl >/dev/null 2>&1; then
189
- STAGED_SHA=$(printf '%s' "$DIFF_FULL" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}')
190
- else
191
- printf 'rea commit-review: WARN no sha256 hasher found (sha256sum/shasum/openssl); cache disabled\n' >&2
192
- fi
193
- if [[ -n "$STAGED_SHA" && ! "$STAGED_SHA" =~ ^[0-9a-f]{64}$ ]]; then
194
- printf 'rea commit-review: WARN hasher returned invalid output; cache disabled\n' >&2
195
- STAGED_SHA=""
196
- fi
197
- BRANCH=$(cd "$REA_ROOT" && git branch --show-current 2>/dev/null || echo "")
198
- CACHE_FILE="${REA_ROOT}/.rea/review-cache.json"
199
-
200
- # Codex pass-3 finding #1: `rea cache check` and `rea cache set` both declare
201
- # `--base` as a `requiredOption` in src/cli/index.ts. Prior versions of this
202
- # gate omitted `--base`, so (a) the CLI path exited non-zero and the
203
- # `|| echo '{"hit":false}'` fallback quietly masked the contract error, and
204
- # (b) the section-11 banner instructed the agent to run `rea cache set <sha>
205
- # pass` — also missing `--base`, rejected by the CLI on every retry. A
206
- # successful cache flow was unreachable.
207
- #
208
- # Resolve BASE_BRANCH by the same preference order the push-gate uses in
209
- # push-review-core.sh §7 (lines 778-794): origin/HEAD → origin/main →
210
- # origin/master → empty. If nothing resolves, disable the cache (the
211
- # alternative is emitting a cache command the CLI rejects on every call).
212
- BASE_BRANCH=""
213
- _origin_head=$(cd "$REA_ROOT" && git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || true)
214
- if [[ -n "$_origin_head" ]]; then
215
- BASE_BRANCH="${_origin_head#refs/remotes/origin/}"
216
- fi
217
- if [[ -z "$BASE_BRANCH" ]]; then
218
- # Use `git -C` so the current-shell cwd is never mutated — matches the
219
- # cross-repo guard at §1a and keeps the file's dominant idiom. Raw
220
- # `cd "$REA_ROOT" && git …` would leave the hook process sitting in
221
- # $REA_ROOT, which is safe today but breaks silently if a future edit
222
- # adds a relative-path command downstream.
223
- if git -C "$REA_ROOT" rev-parse --verify --quiet refs/remotes/origin/main >/dev/null 2>&1; then
224
- BASE_BRANCH="main"
225
- elif git -C "$REA_ROOT" rev-parse --verify --quiet refs/remotes/origin/master >/dev/null 2>&1; then
226
- BASE_BRANCH="master"
227
- fi
228
- fi
229
- if [[ -z "$BASE_BRANCH" && -n "$STAGED_SHA" ]]; then
230
- printf 'rea commit-review: WARN could not resolve base branch (no origin/HEAD, no origin/main, no origin/master); cache disabled\n' >&2
231
- STAGED_SHA=""
232
- fi
233
- unset _origin_head
234
-
235
- if [[ -n "$STAGED_SHA" ]]; then
236
- CACHE_HIT=false
237
-
238
- # Primary: use CLI when available — handles TTL, expiry, and branch-scoped keys.
239
- # Cache predicate must require BOTH `.hit == true` AND `.result == "pass"` —
240
- # a cached `fail` verdict would otherwise satisfy `.hit == true` and let the
241
- # commit proceed despite a recorded negative review. Mirrors the push-gate
242
- # predicate at push-review-core.sh §8; the §218-226 direct-cache fallback
243
- # already enforces `result == "pass"`, so the two paths must agree.
244
- if [[ ${#REA_CLI_ARGS[@]} -gt 0 ]]; then
245
- # Defect F (rea#75): surface cache-query errors instead of treating them as
246
- # legitimate misses. See hooks/_lib/push-review-core.sh for the rationale.
247
- # SECURITY (Codex LOW 4): require mktemp. Predictable /tmp paths are a
248
- # TOCTOU surface on shared hosts; fall-loud instead of fall-back.
249
- if ! CACHE_STDERR_FILE=$(mktemp -t rea-commit-cache-err.XXXXXX 2>/dev/null); then
250
- printf 'rea commit-review: mktemp unavailable; cannot capture cache-check stderr. Aborting.\n' >&2
251
- exit 2
252
- fi
253
- CACHE_EXIT=0
254
- CACHE_STDOUT=$("${REA_CLI_ARGS[@]}" cache check "$STAGED_SHA" --branch "$BRANCH" --base "$BASE_BRANCH" 2>"$CACHE_STDERR_FILE") || CACHE_EXIT=$?
255
- CACHE_STDERR=$(cat "$CACHE_STDERR_FILE" 2>/dev/null || true)
256
- rm -f "$CACHE_STDERR_FILE"
257
- if [[ "$CACHE_EXIT" -ne 0 ]]; then
258
- # SECURITY (Codex LOW 5): strip C0/C1 control chars before echoing CLI
259
- # stderr. Includes 0x80-0x9F because some terminals interpret bare C1
260
- # bytes (CSI 0x9B, OSC 0x9D) as escape introducers.
261
- CACHE_STDERR_SAFE=$(printf '%s' "$CACHE_STDERR" | LC_ALL=C tr -d '\000-\037\177\200-\237')
262
- printf 'rea commit-review: CACHE CHECK FAILED (exit=%d): %s\n' "$CACHE_EXIT" "$CACHE_STDERR_SAFE" >&2
263
- printf 'rea commit-review: treating as miss; file bookedsolidtech/rea issue if unexpected.\n' >&2
264
- CACHE_RESULT='{"hit":false,"reason":"query_error"}'
265
- elif [[ -z "$CACHE_STDOUT" ]]; then
266
- CACHE_RESULT='{"hit":false,"reason":"cold"}'
267
- else
268
- CACHE_RESULT="$CACHE_STDOUT"
269
- fi
270
- if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true and .result == "pass"' >/dev/null 2>&1; then
271
- CACHE_HIT=true
272
- fi
273
- fi
274
-
275
- # Fallback: read cache JSON directly — works when rea is not on PATH.
276
- # Checks branch-scoped key ("branch:sha") first, then bare SHA (empty-branch case).
277
- if [[ "$CACHE_HIT" == "false" ]] && [[ -f "$CACHE_FILE" ]]; then
278
- CACHE_KEY="${BRANCH}:${STAGED_SHA}"
279
- DIRECT_HIT=$(jq -r --arg k1 "$CACHE_KEY" --arg k2 "$STAGED_SHA" \
280
- '(.entries[$k1] // .entries[$k2]) | if . == null then "miss" elif .result == "pass" then "hit" else "miss" end' \
281
- "$CACHE_FILE" 2>/dev/null || echo "miss")
282
- if [[ "$DIRECT_HIT" == "hit" ]]; then
283
- CACHE_HIT=true
284
- fi
285
- fi
286
-
287
- if [[ "$CACHE_HIT" == "true" ]]; then
288
- exit 0
289
- fi
290
- fi
291
-
292
- # ── 11. Block and request review ──────────────────────────────────────────────
293
- {
294
- printf 'COMMIT REVIEW GATE: Review required before committing\n'
295
- printf '\n'
296
- printf ' Score: %s (%s changed lines)\n' "$SCORE" "$LINE_COUNT"
297
- if [[ $SENSITIVE -eq 1 ]]; then
298
- printf ' Sensitive paths detected:\n'
299
- printf '%s\n' "$SENSITIVE_FILES"
300
- fi
301
- printf '\n'
302
- printf ' YOU (the agent) are the reviewer. Do not ask the user to commit manually.\n'
303
- printf ' Review the staged diff, make a pass/fail decision, then proceed:\n'
304
- printf '\n'
305
- printf ' 1. Inspect: git diff --cached\n'
306
- printf ' 2. Decide: Is this safe to commit? (initial commits, refactors, and\n'
307
- printf ' feature work are normal — use judgement, not ceremony)\n'
308
- # Defect L follow-up: when no sha256 hasher is available STAGED_SHA is empty
309
- # and `rea cache set pass` is a dead-end the CLI rejects. Branch the banner
310
- # to surface an actionable path instead. Unlike push-review-core.sh there is
311
- # no `REA_SKIP_COMMIT_REVIEW` env escape hatch (the commit gate only fires
312
- # under Claude Code's Bash `PreToolUse` matcher, so a human direct-shell
313
- # commit bypasses it entirely). The only remediation is to install a sha256
314
- # hasher or ask the user to commit directly.
315
- if [[ -n "$STAGED_SHA" ]]; then
316
- printf ' 3. Approve: rea cache set %s pass --branch %s --base %s\n' \
317
- "$STAGED_SHA" "$BRANCH" "$BASE_BRANCH"
318
- printf ' 4. Retry the git commit command\n'
319
- else
320
- printf ' 3. Cache is DISABLED on this host (no sha256 hasher or no base\n'
321
- printf ' branch resolvable). Install one of: sha256sum (Linux coreutils),\n'
322
- printf ' shasum (perl-core), or openssl; or ensure origin/HEAD is set so\n'
323
- printf ' the gate can identify the merge target. Without these the cache\n'
324
- printf ' path cannot complete — escalate to the user if neither can be\n'
325
- printf ' provided.\n'
326
- fi
327
- printf '\n'
328
- printf ' Only escalate to the user if you find a genuine problem in the diff.\n'
329
- } >&2
330
- exit 2
@@ -1,94 +0,0 @@
1
- #!/bin/bash
2
- # Native git `.husky/pre-push` adapter for the REA push-review gate.
3
- # Fires BEFORE `git push` via husky. Runs a full diff analysis against the
4
- # target branch and requests security + code review before allowing the push.
5
- #
6
- # Exit codes:
7
- # 0 = allow (no meaningful diff, cached review pass, or escape hatch
8
- # invoked with successful audit-append)
9
- # 2 = block (review required — protected-path gate OR general push-review
10
- # gate — or escape hatch invoked but audit-append failed)
11
- #
12
- # ── Install ───────────────────────────────────────────────────────────────────
13
- # This adapter is the recommended entry point for husky-driven pushes. Point
14
- # `.husky/pre-push` at this file:
15
- #
16
- # #!/bin/sh
17
- # REA_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
18
- # exec "$REA_ROOT/.claude/hooks/push-review-gate-git.sh" "$@"
19
- #
20
- # `REA_ROOT` is resolved inside `.husky/pre-push` itself because neither git
21
- # nor husky provides that env var — a bare `"$REA_ROOT/..."` would expand to
22
- # `/.claude/...` and exit 126. See rea's own `.husky/pre-push` for the
23
- # reference implementation.
24
- #
25
- # Git's native pre-push contract is:
26
- # - stdin: one line per ref being pushed, `<local_ref> <local_sha> <remote_ref> <remote_sha>`
27
- # - argv: `<remote_name> <remote_url>`
28
- #
29
- # ── Architecture ──────────────────────────────────────────────────────────────
30
- # This file is a thin ADAPTER. All logic lives in
31
- # `hooks/_lib/push-review-core.sh` (see `pr_core_run`). The core ships a
32
- # `pr_parse_prepush_stdin` helper that recognises git's native refspec stdin
33
- # and synthesises an equivalent `git push <remote>` CMD for the downstream
34
- # protected-path detection.
35
- #
36
- # Two adapters share the core:
37
- # - push-review-gate.sh ← Claude Code PreToolUse stdin (JSON `.tool_input.command`)
38
- # - push-review-gate-git.sh ← this file, native `.husky/pre-push` stdin
39
- #
40
- # The core's BUG-008 stdin sniff makes either shape work from either adapter,
41
- # so a consumer CAN wire `push-review-gate.sh` into `.husky/pre-push` and it
42
- # just works. The git-native adapter exists so `.husky/pre-push` expresses
43
- # its install intent clearly and so future git-only behaviour (e.g. remote-
44
- # URL-scoped policy overrides) has a natural home that does not bloat the
45
- # generic Claude Code adapter.
46
- #
47
- # ── Escape hatches ────────────────────────────────────────────────────────────
48
- # REA_SKIP_CODEX_REVIEW=<reason> — Codex-only waiver. Since 0.8.0 (#85)
49
- # this ONLY satisfies the protected-path
50
- # Codex-audit requirement. HALT, cross-
51
- # repo guard, ref-resolution, and the
52
- # push-review cache still run. See the
53
- # authoritative docstring in
54
- # `push-review-gate.sh` for the full
55
- # scope description. Audit record
56
- # `tool_name: "codex.review.skipped"`.
57
- # REA_SKIP_PUSH_REVIEW=<reason> — bypass the WHOLE gate for this push.
58
- # Audit record
59
- # `tool_name: "push.review.skipped"`.
60
- #
61
- # Both hatches are value-carrying: the env value IS the reason recorded in
62
- # the audit receipt. An empty value (`REA_SKIP_...=`) is treated as unset.
63
- # The hatches sit behind `.rea/HALT` — HALT always wins.
64
- #
65
- # Fail-closed contract:
66
- # - `dist/audit/append.js` missing → exit 2 (build rea first)
67
- # - Node invocation failure → exit 2
68
- # - Unable to resolve actor from git config → exit 2
69
-
70
- set -uo pipefail
71
-
72
- # Read ALL stdin immediately. For husky-driven pushes this is git's refspec
73
- # list; for any other caller it is whatever they hand us. The core's sniff
74
- # decides.
75
- INPUT=$(cat)
76
-
77
- # Resolve the core library from this adapter's own on-disk location. Using
78
- # BASH_SOURCE (not argv $0) so invocations from `.husky/pre-push`, from a
79
- # consumer's `.claude/hooks/`, or from a direct `bash hooks/push-review-gate-git.sh`
80
- # all find `_lib/` next to the adapter. Consistent with the BUG-012
81
- # script-anchor rationale in core.
82
- _adapter_script="${BASH_SOURCE[0]:-$0}"
83
- _adapter_dir="$(cd -- "$(dirname -- "$_adapter_script")" && pwd -P 2>/dev/null)"
84
- _core_lib="${_adapter_dir}/_lib/push-review-core.sh"
85
- if [[ ! -f "$_core_lib" ]]; then
86
- printf 'rea-hook: push-review-core.sh not found next to %s\n' \
87
- "$_adapter_script" >&2
88
- printf 'rea-hook: expected at %s\n' "$_core_lib" >&2
89
- exit 2
90
- fi
91
- # shellcheck source=_lib/push-review-core.sh
92
- source "$_core_lib"
93
-
94
- pr_core_run "$_adapter_script" "$INPUT" "$@"
@@ -1,92 +0,0 @@
1
- #!/bin/bash
2
- # PreToolUse hook: push-review-gate.sh
3
- # Fires BEFORE every Bash tool call that matches "git push".
4
- # Runs a full diff analysis against the target branch and requests
5
- # security + code review before allowing the push.
6
- #
7
- # Exit codes:
8
- # 0 = allow (no meaningful diff, or review cached, or escape hatch invoked)
9
- # 2 = block (needs review, or escape hatch invoked but audit-append failed)
10
- #
11
- # ── Architecture (0.7.0 BUG-008 cleanup) ─────────────────────────────────────
12
- # This file is now a thin ADAPTER. All logic lives in
13
- # `hooks/_lib/push-review-core.sh` (see `pr_core_run`). The adapter's only
14
- # job is to (a) capture stdin, and (b) hand its own script path + stdin +
15
- # argv to the core so the cross-repo anchor walks up from the RIGHT script
16
- # location.
17
- #
18
- # Two adapters share the core:
19
- # - push-review-gate.sh ← this file, Claude Code PreToolUse stdin (JSON)
20
- # - push-review-gate-git.sh ← native `.husky/pre-push` stdin (git refspec)
21
- # The core's BUG-008 sniff makes either stdin shape work from either adapter,
22
- # so in practice a consumer can wire THIS file into `.husky/pre-push` and it
23
- # just works. The `-git` adapter exists for clarity of install intent.
24
- #
25
- # ── Codex-only waiver: REA_SKIP_CODEX_REVIEW ─────────────────────────────────
26
- # Env var `REA_SKIP_CODEX_REVIEW=<reason>` waives the Codex adversarial-
27
- # review requirement (section 7 protected-path check). Set to any non-empty
28
- # value; the value IS the reason recorded in the audit record (no default
29
- # reason is supplied — if the operator sets `REA_SKIP_CODEX_REVIEW=1` the
30
- # reason is literally "1").
31
- #
32
- # SCOPE (0.8.0, #85): Codex-only. The waiver only satisfies the
33
- # protected-path Codex-audit requirement. Every other gate this hook
34
- # runs still runs:
35
- # • HALT (.rea/HALT) — still blocks.
36
- # • Cross-repo guard — still blocks.
37
- # • Ref-resolution failures — still block.
38
- # • Push-review cache — a miss still falls through to section 9's general
39
- # review-required block.
40
- # (Blocked-paths enforcement is a separate hook on Edit/Write tiers, not
41
- # this push hook — it was never gated by REA_SKIP_CODEX_REVIEW.)
42
- #
43
- # For a full-gate bypass, use `REA_SKIP_PUSH_REVIEW=<reason>` (section 5a).
44
- # The 0.7.0 semantic (whole-gate bypass via the Codex hatch) was misleading
45
- # — operators reached for REA_SKIP_CODEX_REVIEW to silence a transient
46
- # Codex unavailability and accidentally bypassed every other check too.
47
- # 0.8.0 narrows it to what the name implies.
48
- #
49
- # ORDERING: the waiver fires AFTER the HALT check but BEFORE ref-resolution.
50
- # Prior to 0.7.0 the check ran inside the protected-path branch and only
51
- # fired when the diff touched a protected path — which meant an operator
52
- # who wanted to skip Codex review got blocked by a transient ref-resolution
53
- # failure (missing remote object, unresolvable source ref, etc.) before the
54
- # skip ever fired. The current ordering preserves the skip audit record
55
- # even when downstream gates (ref-resolution, cache) block: the operator's
56
- # commitment to waive is durable, even if the push itself is blocked on
57
- # another gate.
58
- #
59
- # Every invocation appends a `tool_name: "codex.review.skipped"` record to
60
- # `.rea/audit.jsonl` via the public audit helper. This record is intentionally
61
- # NOT named `codex.review` so the existing jq predicate on `.tool_name ==
62
- # "codex.review" and .metadata.verdict in {pass, concerns}` will never match
63
- # a skip — a skipped review is not a review.
64
- #
65
- # Fail-closed contract:
66
- # - `dist/audit/append.js` missing → exit 2 (build rea first)
67
- # - Node invocation failure → exit 2
68
- # - Unable to resolve actor from git config → exit 2
69
-
70
- set -uo pipefail
71
-
72
- # Read ALL stdin immediately. The core's BUG-008 sniff decides whether this
73
- # is Claude Code JSON or git's native pre-push refspec list.
74
- INPUT=$(cat)
75
-
76
- # Resolve the core library from this adapter's own on-disk location. Using
77
- # BASH_SOURCE (not argv $0) so `bash hooks/push-review-gate.sh` and
78
- # `.../.claude/hooks/push-review-gate.sh` both find `_lib/` next to the
79
- # adapter. Consistent with the BUG-012 script-anchor rationale in core.
80
- _adapter_script="${BASH_SOURCE[0]:-$0}"
81
- _adapter_dir="$(cd -- "$(dirname -- "$_adapter_script")" && pwd -P 2>/dev/null)"
82
- _core_lib="${_adapter_dir}/_lib/push-review-core.sh"
83
- if [[ ! -f "$_core_lib" ]]; then
84
- printf 'rea-hook: push-review-core.sh not found next to %s\n' \
85
- "$_adapter_script" >&2
86
- printf 'rea-hook: expected at %s\n' "$_core_lib" >&2
87
- exit 2
88
- fi
89
- # shellcheck source=_lib/push-review-core.sh
90
- source "$_core_lib"
91
-
92
- pr_core_run "$_adapter_script" "$INPUT" "$@"