@bookedsolid/rea 0.32.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.
@@ -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,172 +1,137 @@
1
- #!/usr/bin/env bash
2
- # changeset-security-gate.sh — PreToolUse: Write|Edit
1
+ #!/bin/bash
2
+ # PreToolUse hook: changeset-security-gate.sh
3
+ # 0.33.0+ — Node-binary shim for `rea hook changeset-security-gate`.
3
4
  #
4
- # Guards .changeset/*.md files against two failure modes:
5
+ # Pre-0.33.0 the gate's full body lived here as bash (172 LOC, frontmatter
6
+ # validation + GHSA/CVE scan + MultiEdit-aware tool handling). The
7
+ # migration to the parser-backed Node binary moves all of that into
8
+ # `src/hooks/changeset-security-gate/index.ts`.
5
9
  #
6
- # 1. SECURITY DISCLOSURE LEAK GHSA IDs or CVE numbers written to a changeset
7
- # file before the advisory is published. Changeset files are committed to git
8
- # and appear verbatim in CHANGELOG.md referencing a GHSA ID pre-publish
9
- # creates public pre-disclosure in git history.
10
+ # Behavioral contract is preserved byte-for-byte: exit 0 on
11
+ # pass-through / non-changeset / valid frontmatter, exit 2 on HALT /
12
+ # disclosure leak / malformed frontmatter / malformed payload.
10
13
  #
11
- # 2. MISSING OR MALFORMED FRONTMATTER — changeset files without proper frontmatter
12
- # are silently ignored by the changesets tool, wasting the release entry.
14
+ # # CLI-resolution trust boundary
13
15
  #
14
- # Triggered by: PreToolUse Write and Edit tools
15
-
16
- set -euo pipefail
16
+ # Realpath sandbox check + version probe. Same shape as the 0.32.0
17
+ # pilots.
18
+ #
19
+ # # Fail-closed posture
20
+ #
21
+ # changeset-security-gate is BLOCKING-tier — the pre-0.33.0 bash body
22
+ # refused on GHSA/CVE patterns and on malformed frontmatter. Early-exit
23
+ # branches fail closed AFTER the relevance pre-gate passes.
17
24
 
18
- # shellcheck source=_lib/common.sh
19
- source "$(dirname "$0")/_lib/common.sh"
25
+ set -uo pipefail
20
26
 
27
+ # 1. HALT check.
28
+ # shellcheck source=_lib/halt-check.sh
29
+ source "$(dirname "$0")/_lib/halt-check.sh"
21
30
  check_halt
31
+ REA_ROOT=$(rea_root)
22
32
 
23
- INPUT="$(cat)"
24
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
25
-
26
- # 0.15.0 fix: MultiEdit was not in the allowed tool_name set, so the gate
27
- # silently exited 0 on every MultiEdit call against `.changeset/*.md` —
28
- # letting GHSA / CVE pre-disclosure through and skipping frontmatter
29
- # validation. 0.16.0: NotebookEdit added too (changesets are .md files
30
- # but a malicious agent could in principle route a .md write through
31
- # NotebookEdit's new_source path; cheap to allow, free to test).
32
- if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "MultiEdit" && "$TOOL_NAME" != "NotebookEdit" ]]; then
33
- exit 0
34
- fi
35
-
36
- require_jq
37
-
38
- # 0.16.0: payload extraction migrated to `_lib/payload-read.sh`. Shared
39
- # helpers handle every write-tier tool with the same defensive
40
- # coercion. Adding the next write-tier tool is a one-line edit there.
41
- # shellcheck source=_lib/payload-read.sh
42
- source "$(dirname "$0")/_lib/payload-read.sh"
33
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
43
34
 
44
- FILE_PATH=$(extract_file_path "$INPUT")
45
-
46
- # Only care about .changeset/*.md files — exclude README.md (changeset tool metadata)
47
- if ! echo "$FILE_PATH" | grep -qE '\.changeset/[^/]+\.md$' || echo "$FILE_PATH" | grep -qE '\.changeset/README\.md$'; then
48
- exit 0
49
- fi
50
-
51
- CONTENT=$(extract_write_content "$INPUT")
52
-
53
- # ─── 1. SECURITY DISCLOSURE CHECK ───────────────────────────────────────────
35
+ # 2. Relevance pre-gate. This is a PreToolUse Write/Edit/MultiEdit/
36
+ # NotebookEdit matcher, so the payload always has a `tool_input.
37
+ # file_path` (or `notebook_path`).
54
38
  #
55
- # These patterns in a changeset mean security details are about to be committed
56
- # to git history BEFORE the advisory is published — creating pre-disclosure.
57
- # GHSA IDs and CVE numbers must NEVER appear in changeset files.
58
-
59
- DISCLOSURE_PATTERNS=(
60
- 'GHSA-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}'
61
- 'CVE-[0-9]{4}-[0-9]+'
62
- )
63
-
64
- MATCHED_PATTERN=""
65
- for PATTERN in "${DISCLOSURE_PATTERNS[@]}"; do
66
- if echo "$CONTENT" | grep -qE "$PATTERN"; then
67
- MATCHED_PATTERN="$PATTERN"
68
- break
39
+ # 2026-05-15 codex round-2 P2 fix: scan `tool_input.file_path` /
40
+ # `tool_input.notebook_path` ONLY, NOT the raw JSON payload. Pre-fix
41
+ # a Write to `README.md` whose body merely mentions `.changeset/`
42
+ # (e.g. "See .changeset/example.md") tripped the fail-closed branch
43
+ # when the CLI was unbuilt — the substring lived in the
44
+ # tool_input.content blob, not in the target path. The Node body
45
+ # correctly filters by file_path; the shim's pre-gate must match
46
+ # that posture.
47
+ INPUT=$(cat)
48
+ RELEVANT=0
49
+ PROBE=""
50
+ if command -v jq >/dev/null 2>&1; then
51
+ PROBE=$(printf '%s' "$INPUT" | jq -r '(.tool_input.file_path // .tool_input.notebook_path // "")' 2>/dev/null || true)
52
+ if printf '%s' "$PROBE" | grep -qE '\.changeset/'; then
53
+ RELEVANT=1
54
+ fi
55
+ else
56
+ if printf '%s' "$INPUT" | grep -qE '\.changeset/'; then
57
+ RELEVANT=1
69
58
  fi
70
- done
71
-
72
- if [[ -n "$MATCHED_PATTERN" ]]; then
73
- json_output "block" \
74
- "CHANGESET SECURITY GATE: This changeset contains a security advisory identifier (matched: '${MATCHED_PATTERN}').
75
-
76
- Do NOT reference GHSA IDs or CVE numbers in changeset files before the advisory is published.
77
- Changeset files are committed to git — this creates pre-disclosure in public history and CHANGELOG.
78
-
79
- CORRECT approach for security fix changesets:
80
- Use vague language only — no identifiers, no vulnerability details.
81
-
82
- WRONG: 'fix(hooks): patch GHSA-3w3m-7gg4-f82g — symlink-guard now covers Edit tool'
83
- RIGHT: 'security: extend symlink protection to cover all write-capable tools'
84
-
85
- WRONG: 'security: fix CVE-2026-1234 prompt injection via tool descriptions'
86
- RIGHT: 'security: harden middleware chain against indirect instruction attacks'
87
-
88
- After the release ships:
89
- 1. Publish the GitHub Security Advisory (Security tab → Advisories → Publish)
90
- 2. The GHSA becomes the detailed public disclosure document
91
- 3. Optionally update CHANGELOG.md post-publish to add the GHSA reference"
92
59
  fi
93
-
94
- # ─── 2. FRONTMATTER VALIDATION ───────────────────────────────────────────────
95
- #
96
- # A changeset without valid frontmatter is silently ignored by the changesets
97
- # tool — the package bump and CHANGELOG entry never appear in the release.
98
- #
99
- # 0.15.0 fix: skip frontmatter validation for MultiEdit. MultiEdit's
100
- # `tool_input.edits[].new_string` payload is a list of partial string
101
- # replacements, not the full file body — running the frontmatter
102
- # validator against the concatenation of new_strings would reject every
103
- # legitimate MultiEdit on an existing changeset (none of the edit
104
- # fragments individually contains a frontmatter block, even though the
105
- # resulting file does). The disclosure scan above still runs on
106
- # MultiEdit content because GHSA/CVE patterns match per-fragment without
107
- # any structural assumption.
108
- if [[ "$TOOL_NAME" == "MultiEdit" ]]; then
60
+ if [ "$RELEVANT" -eq 0 ]; then
109
61
  exit 0
110
62
  fi
111
63
 
112
- # Must start with ---
113
- if ! echo "$CONTENT" | head -1 | grep -qE '^---'; then
114
- json_output "block" \
115
- "CHANGESET FORMAT GATE: Missing frontmatter block.
116
-
117
- Every changeset must start with a frontmatter block specifying which package to bump:
118
-
119
- ---
120
- '@bookedsolid/rea': patch
121
- ---
122
-
123
- Brief description of what changed and why (close #N if applicable).
124
-
125
- Bump types: patch (bug fix/security), minor (new feature), major (breaking change)"
64
+ # 3. Resolve the rea CLI.
65
+ REA_ARGV=()
66
+ RESOLVED_CLI_PATH=""
67
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
68
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
69
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
70
+ elif [ -f "$proj/dist/cli/index.js" ]; then
71
+ REA_ARGV=(node "$proj/dist/cli/index.js")
72
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
126
73
  fi
127
74
 
128
- # Must have at least one package bump entry and a closing ---.
129
- # 0.15.0 fix: accept single-quoted, double-quoted, AND unquoted package
130
- # names (all three are valid YAML for the same string). Pre-fix the
131
- # regex required single quotes, so a tool or human authoring the
132
- # changeset with `"@scope/name": patch` was rejected as malformed even
133
- # though the Changesets tool itself accepts every form.
134
- #
135
- # Codex round-1 P2-1 fix: explicit-alternation form (no backref) so
136
- # the unquoted variant matches on BSD grep too. The earlier
137
- # `^([\"']?)[^\"']+\1: ...` shape relied on backref-with-empty-capture
138
- # semantics that BSD's grep rejects when the capture group's `?` made
139
- # it absent — quoted forms matched on macOS but unquoted did not.
140
- FRONTMATTER=$(echo "$CONTENT" | awk '/^---/{count++; if(count==2){exit} next} count==1{print}')
141
- if ! echo "$FRONTMATTER" | grep -qE "^(\"[^\"]+\"|'[^']+'|[^\"'[:space:]]+): (patch|minor|major)"; then
142
- json_output "block" \
143
- "CHANGESET FORMAT GATE: Frontmatter does not contain a valid package bump entry.
144
-
145
- The frontmatter must include at least one package/bump pair:
146
-
147
- ---
148
- '@bookedsolid/rea': patch
149
- ---
150
-
151
- Valid bump types: patch | minor | major"
75
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
76
+ printf 'rea: changeset-security-gate cannot run — the rea CLI is not built.\n' >&2
77
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
78
+ exit 2
152
79
  fi
153
80
 
154
- # Must have a non-empty description after the closing ---
155
- DESCRIPTION=$(echo "$CONTENT" | awk 'BEGIN{count=0} /^---/{count++; next} count>=2{print}' | grep -v '^[[:space:]]*$' | head -1 || true)
156
- if [[ -z "$DESCRIPTION" ]]; then
157
- json_output "block" \
158
- "CHANGESET FORMAT GATE: Missing description after frontmatter.
159
-
160
- Add a meaningful description explaining what changed and why:
161
-
162
- ---
163
- '@bookedsolid/rea': patch
164
- ---
81
+ # 4. Realpath sandbox check.
82
+ if ! command -v node >/dev/null 2>&1; then
83
+ printf 'rea: changeset-security-gate cannot run — `node` is not on PATH.\n' >&2
84
+ exit 2
85
+ fi
165
86
 
166
- fix(gateway): policy-loader now uses async I/O with 500ms TTL cache
87
+ sandbox_check=$(node -e '
88
+ const fs = require("fs");
89
+ const path = require("path");
90
+ const cli = process.argv[1];
91
+ const projDir = process.argv[2];
92
+ let real, realProj;
93
+ try { real = fs.realpathSync(cli); } catch (e) {
94
+ process.stdout.write("bad:realpath"); process.exit(1);
95
+ }
96
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
97
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
98
+ }
99
+ const sep = path.sep;
100
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
101
+ if (!(real === realProj || real.startsWith(projWithSep))) {
102
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
103
+ }
104
+ let cur = path.dirname(path.dirname(path.dirname(real)));
105
+ let found = false;
106
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
107
+ const pj = path.join(cur, "package.json");
108
+ if (fs.existsSync(pj)) {
109
+ try {
110
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
111
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
112
+ } catch (e) { /* keep walking */ }
113
+ }
114
+ cur = path.dirname(cur);
115
+ }
116
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
117
+ process.stdout.write("ok");
118
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
119
+
120
+ if [ "$sandbox_check" != "ok" ]; then
121
+ printf 'rea: changeset-security-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
122
+ exit 2
123
+ fi
167
124
 
168
- Previously, loadPolicy used fs.readFileSync on every tool invocation, blocking
169
- the event loop under concurrency. Closes #34."
125
+ # 5. Version-probe.
126
+ probe_out=$("${REA_ARGV[@]}" hook changeset-security-gate --help 2>&1)
127
+ probe_status=$?
128
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'changeset-security-gate'; then
129
+ printf 'rea: this shim requires the `rea hook changeset-security-gate` subcommand (introduced in 0.33.0).\n' >&2
130
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
131
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
132
+ exit 2
170
133
  fi
171
134
 
172
- exit 0
135
+ # 6. Forward stdin.
136
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook changeset-security-gate
137
+ exit $?