@bookedsolid/rea 0.6.1 → 0.7.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.
@@ -16,37 +16,60 @@ set -uo pipefail
16
16
  INPUT=$(cat)
17
17
 
18
18
  # ── 1a. Cross-repo guard (must come FIRST — before any rea-scoped check) ──────
19
- # Mirror of push-review-gate.sh. When CLAUDE_PROJECT_DIR points to rea but
20
- # the current git checkout is a DIFFERENT repository (distinct object DB),
21
- # exit 0 rea's gate does not own that commit.
22
- #
23
- # Identity via `--git-common-dir` so linked worktrees of rea
24
- # (`git worktree add`, `.claude/worktrees/*`) are correctly recognized as
25
- # the SAME repo and kept under the gate — they share object DB, refs, and
26
- # HEAD history with rea's main checkout. Path-prefix fallback fires
27
- # when either side is not a git checkout. Must run BEFORE the jq and HALT
28
- # checks: a missing-jq or HALT-frozen rea must not block commits in other
29
- # repos that merely share a Claude Code session with rea. Fixed in 0.6.1.
30
- REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
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
+
31
49
  if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
32
- CWD_REAL=$(pwd -P 2>/dev/null || pwd)
33
- if REA_REAL=$(cd "$REA_ROOT" 2>/dev/null && pwd -P 2>/dev/null); then
34
- CWD_COMMON=$(git -C "$CWD_REAL" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)
35
- REA_COMMON=$(git -C "$REA_REAL" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)
36
- if [[ -n "$CWD_COMMON" && -n "$REA_COMMON" ]]; then
37
- CWD_COMMON_REAL=$(cd "$CWD_COMMON" 2>/dev/null && pwd -P 2>/dev/null || echo "$CWD_COMMON")
38
- REA_COMMON_REAL=$(cd "$REA_COMMON" 2>/dev/null && pwd -P 2>/dev/null || echo "$REA_COMMON")
39
- if [[ "$CWD_COMMON_REAL" != "$REA_COMMON_REAL" ]]; then
40
- exit 0
41
- fi
42
- else
43
- case "$CWD_REAL/" in
44
- "$REA_REAL"/*|"$REA_REAL"/) : ;; # inside rea run the gate
45
- *) exit 0 ;; # outside rea — not our gate
46
- esac
47
- fi
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
48
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
49
71
  fi
72
+ # Mixed state or probe error → fail CLOSED: run the gate.
50
73
 
51
74
  # ── 2. Dependency check ──────────────────────────────────────────────────────
52
75
  if ! command -v jq >/dev/null 2>&1; then
@@ -0,0 +1,92 @@
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> — bypass the Codex adversarial-review
49
+ # requirement for this push. Audit record
50
+ # `tool_name: "codex.review.skipped"`.
51
+ # Currently a whole-gate bypass (see
52
+ # task #85); the distinct audit tool_name
53
+ # keeps it from satisfying the Codex-
54
+ # review jq predicate.
55
+ # REA_SKIP_PUSH_REVIEW=<reason> — bypass the WHOLE gate for this push.
56
+ # Audit record
57
+ # `tool_name: "push.review.skipped"`.
58
+ #
59
+ # Both hatches are value-carrying: the env value IS the reason recorded in
60
+ # the audit receipt. An empty value (`REA_SKIP_...=`) is treated as unset.
61
+ # The hatches sit behind `.rea/HALT` — HALT always wins.
62
+ #
63
+ # Fail-closed contract:
64
+ # - `dist/audit/append.js` missing → exit 2 (build rea first)
65
+ # - Node invocation failure → exit 2
66
+ # - Unable to resolve actor from git config → exit 2
67
+
68
+ set -uo pipefail
69
+
70
+ # Read ALL stdin immediately. For husky-driven pushes this is git's refspec
71
+ # list; for any other caller it is whatever they hand us. The core's sniff
72
+ # decides.
73
+ INPUT=$(cat)
74
+
75
+ # Resolve the core library from this adapter's own on-disk location. Using
76
+ # BASH_SOURCE (not argv $0) so invocations from `.husky/pre-push`, from a
77
+ # consumer's `.claude/hooks/`, or from a direct `bash hooks/push-review-gate-git.sh`
78
+ # all find `_lib/` next to the adapter. Consistent with the BUG-012
79
+ # 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" "$@"