@bookedsolid/rea 0.31.0 → 0.33.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 (43) hide show
  1. package/.husky/prepare-commit-msg +80 -6
  2. package/MIGRATING.md +24 -15
  3. package/dist/cli/hook.js +60 -22
  4. package/dist/hooks/_lib/halt-check.d.ts +78 -0
  5. package/dist/hooks/_lib/halt-check.js +106 -0
  6. package/dist/hooks/_lib/payload.d.ts +124 -0
  7. package/dist/hooks/_lib/payload.js +245 -0
  8. package/dist/hooks/_lib/segments.d.ts +125 -0
  9. package/dist/hooks/_lib/segments.js +766 -0
  10. package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
  11. package/dist/hooks/architecture-review-gate/index.js +250 -0
  12. package/dist/hooks/attribution-advisory/index.d.ts +72 -0
  13. package/dist/hooks/attribution-advisory/index.js +233 -0
  14. package/dist/hooks/bash-scanner/protected-scan.js +14 -2
  15. package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
  16. package/dist/hooks/changeset-security-gate/index.js +330 -0
  17. package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
  18. package/dist/hooks/dependency-audit-gate/index.js +294 -0
  19. package/dist/hooks/env-file-protection/index.d.ts +55 -0
  20. package/dist/hooks/env-file-protection/index.js +159 -0
  21. package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
  22. package/dist/hooks/pr-issue-link-gate/index.js +127 -0
  23. package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
  24. package/dist/hooks/security-disclosure-gate/index.js +502 -0
  25. package/hooks/_lib/protected-paths.sh +10 -3
  26. package/hooks/architecture-review-gate.sh +92 -77
  27. package/hooks/attribution-advisory.sh +139 -131
  28. package/hooks/changeset-security-gate.sh +114 -149
  29. package/hooks/dependency-audit-gate.sh +115 -156
  30. package/hooks/env-file-protection.sh +130 -97
  31. package/hooks/pr-issue-link-gate.sh +114 -45
  32. package/hooks/security-disclosure-gate.sh +148 -316
  33. package/hooks/settings-protection.sh +13 -9
  34. package/package.json +1 -1
  35. package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
  36. package/templates/attribution-advisory.dogfood-staged.sh +170 -0
  37. package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
  38. package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
  39. package/templates/env-file-protection.dogfood-staged.sh +157 -0
  40. package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
  41. package/templates/prepare-commit-msg.husky.sh +80 -6
  42. package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
  43. package/templates/settings-protection.dogfood.patch +58 -0
@@ -1,101 +1,116 @@
1
1
  #!/bin/bash
2
2
  # PostToolUse hook: architecture-review-gate.sh
3
- # Fires AFTER every Write or Edit tool call.
4
- # Lightweight advisory: flags when writing to architecture-sensitive paths.
5
- # Does NOT block — only returns advisory context.
3
+ # 0.33.0+ Node-binary shim for `rea hook architecture-review-gate`.
6
4
  #
7
- # Exit codes:
8
- # 0 = always (advisory only, never blocks)
5
+ # Pre-0.33.0 the gate's full body lived here as bash (101 LOC, policy-
6
+ # driven prefix-match against `architecture_review.patterns`). The
7
+ # migration moves all of that into `src/hooks/architecture-review-gate/
8
+ # index.ts`.
9
+ #
10
+ # Behavioral contract is preserved byte-for-byte: ALWAYS exit 0
11
+ # (advisory-only) except under HALT (exit 2). The hook fires for ALL
12
+ # Write/Edit PostToolUse events, but the Node body short-circuits to
13
+ # exit 0 when patterns are unset/empty — so the cost of running the
14
+ # CLI on every write is bounded.
15
+ #
16
+ # # CLI-resolution trust boundary
17
+ #
18
+ # Realpath sandbox check + version probe. Same shape as the 0.32.0
19
+ # pilots.
20
+ #
21
+ # # Fail-OPEN posture
22
+ #
23
+ # architecture-review-gate is ADVISORY-only — the pre-0.33.0 bash body
24
+ # never refused (exit 0 only). The early-exit branches (CLI missing,
25
+ # node missing, sandbox failed, version skew) all exit 0 silently
26
+ # because there is nothing to "preserve protection" for. The HALT
27
+ # check is the only path to exit 2.
9
28
 
10
29
  set -uo pipefail
11
30
 
12
- # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
13
- INPUT=$(cat)
14
-
15
- # ── 2. Dependency check ──────────────────────────────────────────────────────
16
- if ! command -v jq >/dev/null 2>&1; then
17
- exit 0
18
- fi
19
-
20
- # ── 3. HALT check ────────────────────────────────────────────────────────────
21
- # 0.16.0: HALT check sourced from shared _lib/halt-check.sh.
31
+ # 1. HALT check.
22
32
  # shellcheck source=_lib/halt-check.sh
23
33
  source "$(dirname "$0")/_lib/halt-check.sh"
24
34
  check_halt
25
35
  REA_ROOT=$(rea_root)
26
36
 
27
- # ── 4. Check if enabled ──────────────────────────────────────────────────────
28
- POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
29
- if [[ -f "$POLICY_FILE" ]]; then
30
- if grep -qE 'architecture_advisory:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
31
- exit 0
32
- fi
33
- fi
37
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
34
38
 
35
- # ── 5. Extract file path ─────────────────────────────────────────────────────
36
- FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
39
+ # 2. No relevance pre-gate — architecture-review-gate fires on every
40
+ # Write/Edit, and the cost of the Node body's early-out (load
41
+ # policy, check patterns array, prefix-match) is well under the
42
+ # cost of a sandbox/probe pair. Capture stdin once.
43
+ INPUT=$(cat)
37
44
 
38
- if [[ -z "$FILE_PATH" ]]; then
39
- exit 0
45
+ # 3. Resolve the rea CLI. Advisory-tier: exit 0 silently on missing
46
+ # CLI — nothing to enforce.
47
+ REA_ARGV=()
48
+ RESOLVED_CLI_PATH=""
49
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
50
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
51
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
52
+ elif [ -f "$proj/dist/cli/index.js" ]; then
53
+ REA_ARGV=(node "$proj/dist/cli/index.js")
54
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
40
55
  fi
41
56
 
42
- # 0.16.0 fix D.1: normalize via shared `_lib/path-normalize.sh` so
43
- # Windows / Git Bash backslash paths and URL-encoded forms are handled
44
- # uniformly with the rest of the hook layer. Pre-fix, this hook only
45
- # stripped $REA_ROOT prefix — `src\gateway\foo.ts` (Windows) or
46
- # `src%2Fgateway%2Ffoo.ts` (URL-encoded) silently bypassed the
47
- # architectural review.
48
- # shellcheck source=_lib/path-normalize.sh
49
- source "$(dirname "$0")/_lib/path-normalize.sh"
50
- FILE_PATH=$(normalize_path "$FILE_PATH")
51
-
52
- # ── 6. Check architecture-sensitive paths ─────────────────────────────────────
53
- # 0.20.1 helix-round-N P2: read patterns from policy. Pre-fix the
54
- # rea-internal source-tree patterns (`src/gateway/`, `hooks/_lib/`,
55
- # `profiles/`, etc.) shipped as hardcoded defaults — irrelevant noise
56
- # in consumer projects whose architecture-sensitive paths are
57
- # different. Consumers with their own architecture surfaces declare
58
- # them in `.rea/policy.yaml::architecture_review.patterns`. The
59
- # bst-internal profile pins the rea-source patterns so the dogfood
60
- # install behaves the same as before; consumers without a pattern
61
- # set get a silent no-op.
62
- # shellcheck source=_lib/policy-read.sh
63
- source "$(dirname "$0")/_lib/policy-read.sh"
64
-
65
- ARCH_PATTERNS=()
66
- while IFS= read -r entry; do
67
- [[ -z "$entry" ]] && continue
68
- ARCH_PATTERNS+=("$entry")
69
- done < <(policy_list "architecture_review.patterns" 2>/dev/null || true)
57
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
58
+ exit 0
59
+ fi
70
60
 
71
- if [[ ${#ARCH_PATTERNS[@]} -eq 0 ]]; then
72
- # Empty/unset policy silent no-op. Consumers who haven't declared
73
- # architecture-sensitive paths see zero advisory output.
61
+ # 4. Realpath sandbox check. Advisory-tier: exit 0 silently on
62
+ # sandbox failure (with a single-line breadcrumb to stderr).
63
+ if ! command -v node >/dev/null 2>&1; then
74
64
  exit 0
75
65
  fi
76
66
 
77
- MATCHED=""
78
- for pattern in "${ARCH_PATTERNS[@]}"; do
79
- if [[ "$FILE_PATH" == "$pattern"* ]]; then
80
- MATCHED="$pattern"
81
- break
82
- fi
83
- done
67
+ sandbox_check=$(node -e '
68
+ const fs = require("fs");
69
+ const path = require("path");
70
+ const cli = process.argv[1];
71
+ const projDir = process.argv[2];
72
+ let real, realProj;
73
+ try { real = fs.realpathSync(cli); } catch (e) {
74
+ process.stdout.write("bad:realpath"); process.exit(1);
75
+ }
76
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
77
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
78
+ }
79
+ const sep = path.sep;
80
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
81
+ if (!(real === realProj || real.startsWith(projWithSep))) {
82
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
83
+ }
84
+ let cur = path.dirname(path.dirname(path.dirname(real)));
85
+ let found = false;
86
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
87
+ const pj = path.join(cur, "package.json");
88
+ if (fs.existsSync(pj)) {
89
+ try {
90
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
91
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
92
+ } catch (e) { /* keep walking */ }
93
+ }
94
+ cur = path.dirname(cur);
95
+ }
96
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
97
+ process.stdout.write("ok");
98
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
84
99
 
85
- if [[ -z "$MATCHED" ]]; then
100
+ if [ "$sandbox_check" != "ok" ]; then
101
+ printf 'rea: architecture-review-gate skipped (sandbox check: %s)\n' "$sandbox_check" >&2
86
102
  exit 0
87
103
  fi
88
104
 
89
- # ── 7. Advisory output ───────────────────────────────────────────────────────
90
- {
91
- printf 'ARCHITECTURE ADVISORY: Sensitive path modified\n'
92
- printf '\n'
93
- printf ' File: %s\n' "$FILE_PATH"
94
- printf ' Category: %s\n' "$MATCHED"
95
- printf '\n'
96
- printf ' This file is in an architecture-sensitive directory.\n'
97
- printf ' Consider: Does this change maintain backward compatibility?\n'
98
- printf ' Consider: Should this be reviewed by the principal-engineer agent?\n'
99
- } >&2
105
+ # 5. Version-probe. Advisory-tier: exit 0 on probe failure.
106
+ probe_out=$("${REA_ARGV[@]}" hook architecture-review-gate --help 2>&1)
107
+ probe_status=$?
108
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'architecture-review-gate'; then
109
+ printf 'rea: this shim requires the `rea hook architecture-review-gate` subcommand (introduced in 0.33.0).\n' >&2
110
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; falling through silently.\n' >&2
111
+ exit 0
112
+ fi
100
113
 
101
- exit 0
114
+ # 6. Forward stdin.
115
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook architecture-review-gate
116
+ exit $?
@@ -1,162 +1,170 @@
1
1
  #!/bin/bash
2
2
  # PreToolUse hook: attribution-advisory.sh
3
- # Fires BEFORE every Bash tool call.
3
+ # 0.32.0+ Node-binary shim for `rea hook attribution-advisory`.
4
4
  #
5
- # OPT-IN: Only enforces when .rea/policy.yaml contains:
6
- # block_ai_attribution: true
5
+ # Pre-0.32.0 the gate's full body lived here as bash (162 LOC,
6
+ # including the AI-attribution pattern catalog and segment-relevance
7
+ # gating). The migration to the parser-backed Node binary moves all
8
+ # of that into `src/hooks/attribution-advisory/index.ts`. This shim
9
+ # is the Claude Code dispatcher's view of the hook — it forwards
10
+ # stdin to the CLI and exits with whatever the CLI returns.
7
11
  #
8
- # When disabled (default), this hook does nothing.
9
- # When enabled, BLOCKS (exit 2) gh pr create/edit and git commit commands
10
- # that contain structural AI attribution markers.
12
+ # Behavioral contract is preserved byte-for-byte: exit 0 on
13
+ # disabled-policy / non-relevant / clean-command, exit 2 on HALT /
14
+ # attribution detected / malformed payload (fail-closed).
11
15
  #
12
- # Exit codes:
13
- # 0 = allow (disabled, no attribution found, or not a relevant command)
14
- # 2 = block (attribution detected, or HALT is active)
16
+ # # CLI-resolution trust boundary
17
+ #
18
+ # Codex round 1 P1 (2026-05-15): realpath sandbox check + version
19
+ # probe. Mirrors delegation-advisory.sh §3. Defends against
20
+ # symlink-out + tarball-replacement attacks on the resolved CLI AND
21
+ # stale-node_modules version skew that would otherwise turn every
22
+ # Bash dispatch into a hard failure.
15
23
 
16
24
  set -uo pipefail
17
25
 
18
- # ── 1. Read ALL stdin immediately before doing anything else ──────────────────
19
- INPUT=$(cat)
20
-
21
- # ── 2. Dependency check ───────────────────────────────────────────────────────
22
- if ! command -v jq >/dev/null 2>&1; then
23
- printf 'REA ERROR: jq is required but not installed.\n' >&2
24
- printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
25
- exit 2
26
- fi
27
-
28
- # ── 3. HALT check ─────────────────────────────────────────────────────────────
29
- # 0.16.0: HALT check sourced from shared _lib/halt-check.sh.
26
+ # 1. HALT check.
30
27
  # shellcheck source=_lib/halt-check.sh
31
28
  source "$(dirname "$0")/_lib/halt-check.sh"
32
29
  check_halt
33
30
  REA_ROOT=$(rea_root)
34
31
 
35
- # ── 4. Check if attribution blocking is enabled ──────────────────────────────
36
- POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
37
- if [ ! -f "$POLICY_FILE" ]; then
38
- exit 0
39
- fi
40
- if ! grep -qE '^block_ai_attribution:[[:space:]]*true' "$POLICY_FILE" 2>/dev/null; then
41
- exit 0
42
- fi
43
-
44
- # ── 5. Parse tool_input.command from the hook payload ─────────────────────────
45
- CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
46
-
47
- if [[ -z "$CMD" ]]; then
48
- exit 0
49
- fi
32
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
50
33
 
51
- # 0.15.0: source the shared shell-segment splitter. Pre-fix, the
52
- # attribution patterns greped the FULL command `git commit -m "Note:
53
- # Co-Authored-By with AI was removed in 0.14"` matched and the commit
54
- # was blocked even though the message was COMMENTING on attribution
55
- # rather than including it. Per-segment anchoring scopes detection to
56
- # segments whose first token is `git commit` / `gh pr create|edit`.
57
- # shellcheck source=_lib/cmd-segments.sh
58
- source "$(dirname "$0")/_lib/cmd-segments.sh"
59
-
60
- # ── 6. Check if this is a relevant command ────────────────────────────────────
61
- # 0.18.0 helix-020 / discord-ops Round 10 #2 fix (G4.A): use
62
- # `any_segment_starts_with`, not `any_segment_matches`. The pre-fix
63
- # matcher used the unanchored form, so a segment like
64
- # gh pr edit --body "tracked: gh pr create earlier in the run"
65
- # triggered IS_RELEVANT=1 because the substring `gh pr create` was
66
- # anywhere in the segment. The downstream attribution check then
67
- # scanned the body for the markdown-link / Co-Authored-By patterns,
68
- # and ANY mention of those terms in the body's prose got blocked
69
- # even though the actual command was a `gh pr edit` whose intent had
70
- # nothing to do with structural attribution. The same anchoring fix
71
- # `dangerous-bash-interceptor.sh` got in 0.16.3 F5 finally lands here.
72
- IS_RELEVANT=0
73
-
74
- if any_segment_starts_with "$CMD" 'gh[[:space:]]+pr[[:space:]]+(create|edit)'; then
75
- IS_RELEVANT=1
76
- fi
77
-
78
- if any_segment_starts_with "$CMD" 'git[[:space:]]+commit'; then
79
- IS_RELEVANT=1
34
+ # 2. Relevance pre-gate (0.32.0 round-5 P1, round-6 fix). PreToolUse
35
+ # Bash matchers fire on EVERY shell command, but this hook only
36
+ # enforces against `git commit` / `gh pr create|edit`. Capture
37
+ # stdin + check relevance FIRST so unrelated commands (ls,
38
+ # pnpm test, …) exit 0 even when the CLI is missing/stale/
39
+ # sandboxed-out.
40
+ #
41
+ # Match the pattern ANYWHERE in the command string (after the
42
+ # opening quote, then `[^"]*` for any leading shell prefix —
43
+ # `sudo`, `time`, env assignments like `FOO=x git commit …`).
44
+ # Round-6 P1: prior round-5 pattern anchored at the start of the
45
+ # JSON value and missed all prefixed forms.
46
+ INPUT=$(cat)
47
+ # Substring scan (NOT JSON-aware). Round-7 P2: any JSON-aware regex
48
+ # anchored on `"command":"...` gets tripped by escaped quotes in
49
+ # quoted env prefixes (`FOO="two words" git commit …` → the payload
50
+ # carries `\"two words\"` and `[^"]*` stops at the escaped quote).
51
+ # Plain substring match has no such edge: it over-triggers only on
52
+ # the rare case where the pattern appears inside a quoted argument
53
+ # (`echo "gh pr create"`), and the Node body handles that correctly.
54
+ # This hook only fires on `tool_name=Bash`, so we don't risk matching
55
+ # unrelated payload shapes.
56
+ RELEVANT=0
57
+ if printf '%s' "$INPUT" | grep -qE '(git[[:space:]]+commit|gh[[:space:]]+pr[[:space:]]+(create|edit))'; then
58
+ RELEVANT=1
80
59
  fi
81
-
82
- if [[ $IS_RELEVANT -eq 0 ]]; then
60
+ if [ "$RELEVANT" -eq 0 ]; then
61
+ # Irrelevant Bash call — nothing the pre-0.32.0 body would have
62
+ # processed. Always exit 0 regardless of CLI state.
83
63
  exit 0
84
64
  fi
85
65
 
86
- # ── 7. Check for structural AI attribution markers ───────────────────────────
87
-
88
- FOUND=0
89
-
90
- # Co-Authored-By with noreply@ email
91
- # 0.18.0 helix-020 / discord-ops Round 10 #3 fix (G4.B): exclude
92
- # GitHub's legitimate `<user>@users.noreply.github.com` collaborator
93
- # footers from the noreply match. Pre-fix the regex `Co-Authored-By:.*noreply@`
94
- # matched both AI-tool noreply addresses (anthropic.com, openai.com,
95
- # github-copilot, etc.) AND GitHub's per-user noreply form, blocking
96
- # legitimate human collaborator credits. The new regex requires
97
- # `noreply@` to be followed by something that ISN'T `users.noreply.github.com`
98
- # — covered via a negative-lookahead simulation: match `noreply@` then
99
- # either end-of-line, whitespace, `>`, or a domain that does NOT begin
100
- # with `users.noreply.github.com`. Posix ERE has no lookarounds, so we
101
- # enumerate the allowed-prefix shapes explicitly. The "AI names" branch
102
- # below catches Co-Authored-By with named tools regardless of the email
103
- # domain, so dropping `users.noreply.github.com` from the noreply
104
- # pattern only relaxes the check for human collaborators — never for AI.
105
- if any_segment_matches "$CMD" 'Co-Authored-By:.*noreply@(anthropic\.com|openai\.com|github-copilot|github\.com|claude\.ai|chatgpt\.com|googlemail\.com|google\.com|cursor\.com|codeium\.com|tabnine\.com|amazon\.com|amazonaws\.com|amazon-q\.amazonaws\.com|cody\.dev|sourcegraph\.com|mistral\.ai|xai-org|x\.ai|inflection\.ai|perplexity\.ai|replit\.com|jetbrains\.com|bito\.ai|pieces\.app|phind\.com|you\.com)'; then
106
- FOUND=1
66
+ # 2b. Policy short-circuit (round-6 P2). The pre-0.32.0 bash body
67
+ # no-op'd when `block_ai_attribution` was absent or false. Without
68
+ # this check, an unbuilt/stale install would refuse `git commit`
69
+ # even on repos that DELIBERATELY disable the attribution gate.
70
+ # Read the policy via a simple grep — the canonical loader
71
+ # handles inline forms but we only need block form here, and a
72
+ # conservative "true-and-only-true counts" rule matches the
73
+ # intent (false / absent / inline-only all no enforcement).
74
+ POLICY_FILE="$REA_ROOT/.rea/policy.yaml"
75
+ if [ ! -f "$POLICY_FILE" ] || ! grep -qE '^block_ai_attribution:[[:space:]]*true([[:space:]]|$)' "$POLICY_FILE"; then
76
+ # Attribution blocking disabled — pre-0.32.0 bash body would have
77
+ # exited 0 here. Don't refuse on stale-install grounds.
78
+ exit 0
107
79
  fi
108
80
 
109
- # Co-Authored-By with known AI names
110
- if any_segment_matches "$CMD" 'Co-Authored-By:.*\b(Claude|Sonnet|Opus|Haiku|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|Amazon Q|CodeWhisperer|Devin|Windsurf|Cline|Aider|Anthropic|OpenAI|GitHub Copilot)\b'; then
111
- FOUND=1
81
+ # 3. Resolve the rea CLI.
82
+ REA_ARGV=()
83
+ RESOLVED_CLI_PATH=""
84
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
85
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
86
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
87
+ elif [ -f "$proj/dist/cli/index.js" ]; then
88
+ REA_ARGV=(node "$proj/dist/cli/index.js")
89
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
112
90
  fi
113
91
 
114
- # "Generated/Built/Powered with/by [AI Tool]" lines
115
- if any_segment_matches "$CMD" '(Generated|Created|Built|Powered|Authored|Written|Produced)[[:space:]]+(with|by)[[:space:]]+(Claude|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|CodeWhisperer|Devin|Windsurf|Cline|Aider|AI|an? AI)\b'; then
116
- FOUND=1
92
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
93
+ # 0.32.0 round-4 P2: when `block_ai_attribution: true`, this hook is
94
+ # blocking-tier — the pre-0.32.0 bash body enforced the policy
95
+ # without a compiled CLI. Falling through to exit 0 would silently
96
+ # let AI-attribution patterns through every git commit / gh pr
97
+ # create-or-edit until the operator rebuilds. Fail closed and tell
98
+ # the operator how to restore protection.
99
+ printf 'rea: attribution-advisory cannot run — the rea CLI is not built.\n' >&2
100
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
101
+ printf 'This shim fails closed because the pre-0.32.0 bash body enforced attribution policy without a CLI.\n' >&2
102
+ exit 2
117
103
  fi
118
104
 
119
- # Markdown-linked attribution
120
- # 0.16.2 helix-017 P3 #4: anchor on `[Text](` (markdown link shape) so
121
- # legitimate bracketed mentions like `gh pr edit --body "support [Claude
122
- # Code] hook output"` don't false-positive. The actual attribution we
123
- # care about is structural — `Generated with [Claude Code](https://...)`.
124
- if any_segment_matches "$CMD" '\[Claude Code\]\(|\[GitHub Copilot\]\(|\[ChatGPT\]\(|\[Gemini\]\(|\[Cursor\]\('; then
125
- FOUND=1
105
+ # 3. Realpath sandbox check.
106
+ if ! command -v node >/dev/null 2>&1; then
107
+ printf 'rea: attribution-advisory cannot run `node` is not on PATH.\n' >&2
108
+ printf 'Install Node 22+ (engines.node) to restore enforcement.\n' >&2
109
+ exit 2
126
110
  fi
127
111
 
128
- # Emoji attribution
129
- if any_segment_matches "$CMD" '🤖.*[Gg]enerated'; then
130
- FOUND=1
112
+ sandbox_check=$(node -e '
113
+ const fs = require("fs");
114
+ const path = require("path");
115
+ const cli = process.argv[1];
116
+ const projDir = process.argv[2];
117
+ let real, realProj;
118
+ try { real = fs.realpathSync(cli); } catch (e) {
119
+ process.stdout.write("bad:realpath"); process.exit(1);
120
+ }
121
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
122
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
123
+ }
124
+ const sep = path.sep;
125
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
126
+ if (!(real === realProj || real.startsWith(projWithSep))) {
127
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
128
+ }
129
+ let cur = path.dirname(path.dirname(path.dirname(real)));
130
+ let found = false;
131
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
132
+ const pj = path.join(cur, "package.json");
133
+ if (fs.existsSync(pj)) {
134
+ try {
135
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
136
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
137
+ } catch (e) { /* keep walking */ }
138
+ }
139
+ cur = path.dirname(cur);
140
+ }
141
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
142
+ process.stdout.write("ok");
143
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
144
+
145
+ if [ "$sandbox_check" != "ok" ]; then
146
+ # 0.32.0 round-4 P2: fail closed (blocking-tier when policy enables —
147
+ # see top-of-file rationale). Sandbox failure means the CLI cannot
148
+ # be authenticated; refuse rather than silently bypass.
149
+ printf 'rea: attribution-advisory FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
150
+ exit 2
131
151
  fi
132
152
 
133
- if [[ $FOUND -eq 1 ]]; then
134
- {
135
- printf '\n'
136
- printf '═══════════════════════════════════════════════════════════════════\n'
137
- printf ' BLOCKED: AI attribution detected in command\n'
138
- printf '═══════════════════════════════════════════════════════════════════\n'
139
- printf '\n'
140
- printf ' Your command contains structural AI attribution markers.\n'
141
- printf '\n'
142
- printf ' What gets BLOCKED (structural attribution):\n'
143
- printf ' - Co-Authored-By with AI names or noreply@ emails\n'
144
- printf ' - "Generated with/by [AI Tool]" footer lines\n'
145
- printf ' - Markdown-linked tool names: [Claude Code](...)\n'
146
- printf ' - Emoji attribution: 🤖 Generated...\n'
147
- printf '\n'
148
- printf ' What is ALLOWED (legitimate references):\n'
149
- printf ' - "Fix Claude API integration"\n'
150
- printf ' - "Update OpenAI SDK version"\n'
151
- printf ' - "Add Copilot config"\n'
152
- printf '\n'
153
- printf ' Remove the attribution markers and rewrite the command.\n'
154
- printf ' To disable: set block_ai_attribution: false in .rea/policy.yaml\n'
155
- printf '═══════════════════════════════════════════════════════════════════\n'
156
- printf '\n'
157
- } >&2
153
+ # 4. Version-probe: confirm the resolved CLI implements
154
+ # `hook attribution-advisory`. Codex round 1 P1.
155
+ probe_out=$("${REA_ARGV[@]}" hook attribution-advisory --help 2>&1)
156
+ probe_status=$?
157
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'attribution-advisory'; then
158
+ # 0.32.0 round-4 P2: stale/older CLI without the new subcommand is
159
+ # NOT advisory-tier fall-through — the bash body it replaces
160
+ # enforced when policy enabled. Fail closed and tell the operator
161
+ # exactly how to fix.
162
+ printf 'rea: this shim requires the `rea hook attribution-advisory` subcommand (introduced in 0.32.0).\n' >&2
163
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
164
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
158
165
  exit 2
159
166
  fi
160
167
 
161
- # No attribution found allow
162
- exit 0
168
+ # 5. Forward stdin (already captured up-front for the relevance gate).
169
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook attribution-advisory
170
+ exit $?