@bookedsolid/rea 0.9.1 → 0.9.3

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.
@@ -341,15 +341,7 @@ pr_core_run() {
341
341
  exit 0
342
342
  fi
343
343
 
344
- # ── 5. Check if quality gates are enabled ─────────────────────────────────
345
- local POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
346
- if [[ -f "$POLICY_FILE" ]]; then
347
- if grep -qE 'push_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
348
- exit 0
349
- fi
350
- fi
351
-
352
- # ── 5a. REA_SKIP_PUSH_REVIEW — whole-gate escape hatch ────────────────────
344
+ # ── 5. REA_SKIP_PUSH_REVIEW whole-gate escape hatch ─────────────────────
353
345
  # An opt-in bypass for the ENTIRE push-review gate (not just the Codex
354
346
  # branch). Exists to unblock consumers when rea itself is broken or a
355
347
  # corrupt policy/audit file would otherwise deadlock a push. Requires an
@@ -856,7 +848,7 @@ pr_core_run() {
856
848
  } >&2
857
849
  exit 2
858
850
  fi
859
- if printf '%s\n' "$_refspec_hits" | awk -v re='^(src/gateway/middleware/|hooks/|[.]claude/hooks/|src/policy/|[.]github/workflows/)' '
851
+ if printf '%s\n' "$_refspec_hits" | awk -v re='^(src/gateway/middleware/|hooks/|[.]claude/hooks/|src/policy/|[.]github/workflows/|[.]rea/|[.]husky/)' '
860
852
  {
861
853
  status = $1
862
854
  if (status !~ /^[ACDMRTU]/) next
@@ -896,6 +888,8 @@ pr_core_run() {
896
888
  printf ' - .claude/hooks/\n'
897
889
  printf ' - src/policy/\n'
898
890
  printf ' - .github/workflows/\n'
891
+ printf ' - .rea/\n'
892
+ printf ' - .husky/\n'
899
893
  printf '\n'
900
894
  printf ' Run /codex-review against %s, then retry the push.\n' "$local_sha"
901
895
  printf ' The codex-adversarial agent emits the required audit entry.\n'
@@ -992,8 +986,15 @@ pr_core_run() {
992
986
 
993
987
  local -a REA_CLI_ARGS
994
988
  REA_CLI_ARGS=()
995
- if [[ -f "${REA_ROOT}/node_modules/.bin/rea" ]]; then
996
- REA_CLI_ARGS=(node "${REA_ROOT}/node_modules/.bin/rea")
989
+ # node_modules/.bin/rea is a launcher (pnpm writes a POSIX shell shim, npm
990
+ # writes a symlink to dist/cli/index.js with its own `#!/usr/bin/env node`
991
+ # shebang). Either way it is NOT a plain JS file, so running `node` on it
992
+ # would parse shell syntax as JavaScript and SyntaxError. Execute the shim
993
+ # directly — it handles `exec node` itself — and only prepend `node` on the
994
+ # dist fallback, which is a real JS module. The `-x` guard picks up both
995
+ # pnpm shims (executable regular file) and npm symlinks (executable target).
996
+ if [[ -x "${REA_ROOT}/node_modules/.bin/rea" ]]; then
997
+ REA_CLI_ARGS=("${REA_ROOT}/node_modules/.bin/rea")
997
998
  elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
998
999
  REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
999
1000
  fi
@@ -103,18 +103,7 @@ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--amend'; then
103
103
  exit 0
104
104
  fi
105
105
 
106
- # ── 5. Check if quality gates are enabled ─────────────────────────────────────
107
- # Fail-open if policy doesn't exist or doesn't have quality_gates
108
- POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
109
- if [[ -f "$POLICY_FILE" ]]; then
110
- if grep -qE '^quality_gates:' "$POLICY_FILE" 2>/dev/null; then
111
- if grep -qE 'commit_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
112
- exit 0
113
- fi
114
- fi
115
- fi
116
-
117
- # ── 6. Compute diff stats ────────────────────────────────────────────────────
106
+ # ── 5. Compute diff stats ────────────────────────────────────────────────────
118
107
  # Get staged diff (what would be committed)
119
108
  DIFF_OUTPUT=$(cd "$REA_ROOT" && git diff --cached --stat 2>/dev/null || echo "")
120
109
  DIFF_FULL=$(cd "$REA_ROOT" && git diff --cached 2>/dev/null || echo "")
@@ -154,9 +143,17 @@ fi
154
143
 
155
144
  # ── 9. Resolve rea CLI ────────────────────────────────────────────────────
156
145
  # Try local installs first, then dist build, then global PATH install.
146
+ #
147
+ # node_modules/.bin/rea is a launcher (pnpm writes a POSIX shell shim, npm
148
+ # writes a symlink to dist/cli/index.js with its own `#!/usr/bin/env node`
149
+ # shebang). Either way it is NOT a plain JS file, so running `node` on it
150
+ # would parse shell syntax as JavaScript and SyntaxError. Execute the shim
151
+ # directly — it handles `exec node` itself — and only prepend `node` on the
152
+ # dist fallback, which is a real JS module. The `-x` guard picks up both
153
+ # pnpm shims (executable regular file) and npm symlinks (executable target).
157
154
  REA_CLI_ARGS=()
158
- if [[ -f "${REA_ROOT}/node_modules/.bin/rea" ]]; then
159
- REA_CLI_ARGS=(node "${REA_ROOT}/node_modules/.bin/rea")
155
+ if [[ -x "${REA_ROOT}/node_modules/.bin/rea" ]]; then
156
+ REA_CLI_ARGS=("${REA_ROOT}/node_modules/.bin/rea")
160
157
  elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
161
158
  REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
162
159
  elif command -v rea >/dev/null 2>&1; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
5
5
  "license": "MIT",
6
6
  "author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
@@ -222,17 +222,134 @@ if [ -n "$SEC_CHANGESETS" ]; then
222
222
  echo "[smoke] security-claim gate: $(printf '%s\n' "$SEC_CHANGESETS" | wc -l | awk '{print $1}') changeset(s) tagged [security]"
223
223
 
224
224
  SEC_SRC_TESTS="$(cd "$REPO_ROOT" && find src -type f \( -name '*sanitize*.test.ts' -o -name '*security*.test.ts' \) 2>/dev/null | sort)"
225
- if [ -z "$SEC_SRC_TESTS" ]; then
226
- echo "[smoke] FAIL [security] changeset present but no *sanitize*.test.ts or *security*.test.ts under src/" >&2
225
+ # 0.9.3 extension some security hotfixes touch ONLY shell hooks (no TS/dist
226
+ # symbols), so the compiled-symbol gate above doesn't apply. For those, the
227
+ # regression proof lives under __tests__/hooks/*{security,bypass,injection,
228
+ # sanitize}*.test.ts and the tarball must ship the hook file(s) the test
229
+ # exercises. This block runs IN ADDITION to the src/ symbol gate — either
230
+ # layer alone can satisfy a [security] changeset, but at least one MUST.
231
+ SEC_HOOK_TESTS="$(cd "$REPO_ROOT" && find __tests__/hooks -type f \( -name '*security*.test.ts' -o -name '*bypass*.test.ts' -o -name '*sanitize*.test.ts' -o -name '*injection*.test.ts' \) 2>/dev/null | sort)"
232
+
233
+ if [ -z "$SEC_SRC_TESTS" ] && [ -z "$SEC_HOOK_TESTS" ]; then
234
+ echo "[smoke] FAIL — [security] changeset present but no matching regression test found:" >&2
235
+ echo "[smoke] - src/**/(*sanitize*|*security*).test.ts — for compiled-symbol fixes" >&2
236
+ echo "[smoke] - __tests__/hooks/(*security*|*bypass*|*sanitize*|*injection*).test.ts — for hook fixes" >&2
227
237
  echo "[smoke] a security-claim changeset with no matching regression test is a trust violation" >&2
228
238
  exit 2
229
239
  fi
230
240
 
231
- # For each security test, collect the named imports pulled from relative
241
+ # Hook-level gate: for each hook-security test FILE, extract the hook
242
+ # file path(s) it installs/exercises (relative to REPO_ROOT) and assert
243
+ # the tarball ships that hook under node_modules/@bookedsolid/rea/hooks/
244
+ # AND that `rea init` fanned it out to $SMOKE_DIR/.claude/hooks/.
245
+ #
246
+ # Known narrowness (called out honestly rather than papered over):
247
+ # - Granularity is per-test-file, not per-`it()` block. A file may
248
+ # contain multiple `it(...)` cases; the extractor scans the whole
249
+ # file body. In practice a [security]-claim test file should focus
250
+ # on one defect class; mixing unrelated `it()` cases with different
251
+ # hook refs dilutes the proof. PR review is the mitigation.
252
+ # - A [security] file with zero extractable refs fails LOUDLY (see
253
+ # EMPTY_REF_TESTS below). The narrowness only applies to files that
254
+ # do extract refs but don't scope them per `it()`.
255
+ if [ -n "$SEC_HOOK_TESTS" ]; then
256
+ HOOK_MISSING=""
257
+ HOOK_COUNT=0
258
+ EMPTY_REF_TESTS=""
259
+ while IFS= read -r hook_test; do
260
+ [ -z "$hook_test" ] && continue
261
+ # Pull hook paths referenced by the test. Matches forms like:
262
+ # 'hooks', 'push-review-gate.sh'
263
+ # 'hooks', '_lib', 'push-review-core.sh'
264
+ # 'hooks', 'commit-review-gate.sh'
265
+ HOOK_REFS="$(perl -0777 -ne '
266
+ while (/path\.join\(\s*REPO_ROOT\s*,\s*[\x27"]hooks[\x27"](?:\s*,\s*[\x27"]([^\x27"]+)[\x27"])*\s*\)/sg) {
267
+ my $all = $&;
268
+ my @parts;
269
+ while ($all =~ /[\x27"]([^\x27"]+)[\x27"]/g) {
270
+ push @parts, $1 unless $1 eq "REPO_ROOT";
271
+ }
272
+ print join("/", @parts), "\n" if @parts;
273
+ }
274
+ ' "$REPO_ROOT/$hook_test" 2>/dev/null | sort -u)"
275
+
276
+ # Per-test failure: a [security] hook-test that yields zero extractable
277
+ # refs (e.g. uses template literals, dynamic concatenation, or helper
278
+ # indirection) is invisible to this gate. Fail loud so the author is
279
+ # forced to use the extractable path.join(REPO_ROOT, 'hooks', ...) shape,
280
+ # rather than having a lone extractable neighbor test silently satisfy
281
+ # the whole gate.
282
+ if [ -z "$HOOK_REFS" ]; then
283
+ EMPTY_REF_TESTS="$EMPTY_REF_TESTS
284
+ $hook_test"
285
+ continue
286
+ fi
287
+
288
+ while IFS= read -r rel; do
289
+ [ -z "$rel" ] && continue
290
+ HOOK_COUNT=$((HOOK_COUNT + 1))
291
+ # `rel` already includes the leading `hooks/` segment from perl. It
292
+ # looks like `hooks/push-review-gate.sh` or
293
+ # `hooks/_lib/push-review-core.sh`. The tarball ships hooks under
294
+ # `node_modules/@bookedsolid/rea/hooks/` and `rea init` fans them out
295
+ # to `$SMOKE_DIR/.claude/hooks/`. Assert BOTH — the tarball
296
+ # source-of-truth AND the post-init install surface.
297
+ rel_no_prefix="${rel#hooks/}"
298
+ TARBALL_HOOK="$SMOKE_DIR/node_modules/@bookedsolid/rea/hooks/$rel_no_prefix"
299
+ INSTALLED_HOOK="$SMOKE_DIR/.claude/hooks/$rel_no_prefix"
300
+ if [ ! -f "$TARBALL_HOOK" ] || [ ! -f "$INSTALLED_HOOK" ]; then
301
+ HOOK_MISSING="$HOOK_MISSING
302
+ $rel (exercised by $hook_test)"
303
+ fi
304
+ done <<< "$HOOK_REFS"
305
+ done <<< "$SEC_HOOK_TESTS"
306
+
307
+ if [ -n "$HOOK_MISSING" ]; then
308
+ echo "[smoke] FAIL — [security] hook-test gate: hook file(s) under test are MISSING from tarball:" >&2
309
+ printf '%s\n' "$HOOK_MISSING" >&2
310
+ exit 2
311
+ fi
312
+
313
+ if [ -n "$EMPTY_REF_TESTS" ]; then
314
+ echo "[smoke] FAIL — [security] hook-test gate: one or more hook-security tests yielded zero extractable hook references:" >&2
315
+ printf '%s\n' "$EMPTY_REF_TESTS" >&2
316
+ echo "[smoke] hook-security tests MUST reference hook files via the literal shape" >&2
317
+ echo "[smoke] path.join(REPO_ROOT, 'hooks', '<name>.sh') (or with a nested '_lib' arg)" >&2
318
+ echo "[smoke] Template literals, dynamic concatenation, and helper indirection are" >&2
319
+ echo "[smoke] invisible to this gate and would let hook changes ship unverified." >&2
320
+ exit 2
321
+ fi
322
+
323
+ if [ "$HOOK_COUNT" -eq 0 ]; then
324
+ echo "[smoke] FAIL — [security] hook-test gate: no checkable hook references extracted" >&2
325
+ echo "[smoke] hook-security tests must reference hook files via path.join(REPO_ROOT, 'hooks', ...)" >&2
326
+ echo "[smoke] so the gate can verify the hook ships in the tarball" >&2
327
+ exit 2
328
+ fi
329
+
330
+ echo "[smoke] → $(printf '%s\n' "$SEC_HOOK_TESTS" | wc -l | awk '{print $1}') hook-security test(s), $HOOK_COUNT hook ref(s) all present in tarball"
331
+ fi
332
+
333
+ # The compiled-symbol gate below only runs when src/ security tests exist.
334
+ # A hook-only hotfix satisfies via the block above. Flag the src/ gate to
335
+ # skip gracefully — the remaining smoke checks (export resolution, tree
336
+ # equality) still run unconditionally below.
337
+ SKIP_SRC_SYMBOL_GATE=0
338
+ if [ -z "$SEC_SRC_TESTS" ]; then
339
+ SKIP_SRC_SYMBOL_GATE=1
340
+ fi
341
+
342
+ # For each src security test, collect the named imports pulled from relative
232
343
  # paths — those are the symbols under test and must be compiled into dist/.
233
344
  # Example line we want to match:
234
345
  # import { sanitizeHealthSnapshot, INJECTION_REDACTED_PLACEHOLDER } from './health';
235
346
  # We ignore imports from bare package names ('vitest', 'node:fs', etc.).
347
+ #
348
+ # Skipped when only __tests__/hooks/ security tests exist (hook-only hotfix);
349
+ # the hook-test gate above is authoritative for that case.
350
+ if [ "$SKIP_SRC_SYMBOL_GATE" = "1" ]; then
351
+ echo "[smoke] → src/ symbol gate skipped (no src/*{security,sanitize}*.test.ts — hook-test gate is authoritative)"
352
+ else
236
353
  MISSING_SYMBOLS=""
237
354
  SYMBOL_COUNT=0
238
355
  while IFS= read -r src_test; do
@@ -294,6 +411,7 @@ if [ -n "$SEC_CHANGESETS" ]; then
294
411
  fi
295
412
 
296
413
  echo "[smoke] → $(printf '%s\n' "$SEC_SRC_TESTS" | wc -l | awk '{print $1}') security regression test(s), $SYMBOL_COUNT imported symbol(s) all present in dist/"
414
+ fi # SKIP_SRC_SYMBOL_GATE
297
415
  fi
298
416
 
299
417
  # Verify every declared public export resolves. If the exports map points at a