@bookedsolid/rea 0.47.0 → 0.48.1

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.
@@ -137,6 +137,15 @@
137
137
 
138
138
  set -uo pipefail
139
139
 
140
+ # Source the per-session cache helper (0.48.0). This must be sourced
141
+ # at the top of shim-runtime.sh because `shim_run` needs all of the
142
+ # `shim_cache_*` functions available. The helper itself fails safe —
143
+ # no operations fire unless `shim_run` calls them.
144
+ # shellcheck source=shim-cache.sh
145
+ _SHIM_RUNTIME_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
146
+ # shellcheck source=shim-cache.sh
147
+ . "$_SHIM_RUNTIME_DIR/shim-cache.sh"
148
+
140
149
  # -----------------------------------------------------------------------------
141
150
  # Defaults — applied by `shim_run` when the shim hasn't set them. We use
142
151
  # the `:=` operator to assign-if-unset so callers can override.
@@ -303,18 +312,23 @@ shim_run() {
303
312
  # 4. Resolve CLI.
304
313
  shim_resolve_cli
305
314
 
306
- # 5. Sandbox check (when CLI was resolved). On failure clear REA_ARGV
307
- # + stash the reason so the eventual CLI-required branch can emit
308
- # the correct banner. Running the sandbox check BEFORE the policy
309
- # short-circuit prevents an unsandboxed CLI from being invoked by
310
- # Tier-1 of the policy reader (0.37.0 codex round-2 P1: applies to
311
- # shims like attribution-advisory whose policy_short_circuit may
312
- # use `policy_reader_get`).
315
+ # 4a. Sandbox check (0.48.1 moved earlier from step 5). Runs BEFORE
316
+ # the cache-key-prep block so the dist-tree `find` walk (added in
317
+ # 0.48.0) never recurses an out-of-project symlink target. Pre-
318
+ # 0.48.1 ordering was: cache prep cache lookup → sandbox check;
319
+ # codex 0.48.0 round-10 P2 caught that a hostile workspace whose
320
+ # `node_modules/@bookedsolid/rea` (or `dist`) symlinks outside
321
+ # CLAUDE_PROJECT_DIR caused the find walk to traverse the external
322
+ # tree before step 5 refused at `bad:cli-escapes-project`. Moving
323
+ # sandbox earlier preserves the cheap-refusal posture the
324
+ # pre-0.48.0 hot path had.
313
325
  #
314
- # Advisory-tier: a sandbox failure exits 0 with the skip banner —
315
- # nothing to enforce for nudges. Blocking-tier: deferred to the
316
- # CLI-required branch below so we emit ONE banner per refusal
317
- # (instead of double-emitting sandbox + cli-missing).
326
+ # Tradeoff vs 0.48.0: a warm cache hit no longer skips sandbox
327
+ # (pre-fix the `_shim_cache_hit` guard at this site bypassed it).
328
+ # Sandbox is a single `node -e` (~30ms warm) so the lost
329
+ # optimization is acceptable; the cache's primary win is skipping
330
+ # the version probe at step 8 (which is a full CLI spawn,
331
+ # materially more expensive).
318
332
  local sandbox_result=""
319
333
  local sandbox_failed=0
320
334
  local node_missing=0
@@ -325,7 +339,7 @@ shim_run() {
325
339
  # tier shim whose policy said "disabled" still refused when node
326
340
  # was absent (which contradicts the pre-port body's no-op-on-
327
341
  # disabled posture). Clear REA_ARGV here so Tier 1 (rea CLI)
328
- # can't fire — the policy reader degrades to Tier 2 (python3) /
342
+ # cannot fire — the policy reader degrades to Tier 2 (python3) /
329
343
  # Tier 3 (awk), neither of which needs node. Track node-missing
330
344
  # separately so the CLI-required branch below can emit the right
331
345
  # banner if the policy did NOT short-circuit us out.
@@ -347,6 +361,261 @@ shim_run() {
347
361
  fi
348
362
  fi
349
363
 
364
+ # 4b. Per-session cache lookup (0.48.0). When the cache is enabled
365
+ # AND the resolved CLI matches a recent same-session entry, the
366
+ # version probe (step 8) can be skipped — that answer does not
367
+ # change for a stable CLI inside a stable session. Cache MISS /
368
+ # disabled / corrupt → fall through to the existing uncached hot
369
+ # path. NEVER fail closed on a cache error (see
370
+ # hooks/_lib/shim-cache.sh header for the security contract). The
371
+ # cache check runs AFTER `shim_is_relevant` (per design memo
372
+ # concern #3) so we never pay a stat-per-fire cost for irrelevant
373
+ # payloads. 0.48.1: also runs AFTER step 4a sandbox check so the
374
+ # dist-tree hash walk never traverses a symlinked-out CLI tree.
375
+ local _shim_cache_hit=0
376
+ local _shim_cache_key=""
377
+ local _shim_cache_cli_real=""
378
+ local _shim_cache_cli_mtime=""
379
+ local _shim_cache_cli_size=""
380
+ local _shim_cache_pkg_real=""
381
+ local _shim_cache_pkg_mtime=""
382
+ local _shim_cache_pkg_size=""
383
+ local _shim_cache_dist_mtime=""
384
+ local _shim_cache_node_real=""
385
+ local _shim_cache_node_mtime=""
386
+ # 0.48.1: gated on `sandbox_failed -eq 0` so a sandbox refusal
387
+ # short-circuits BEFORE the dist-tree hash walk runs (was running
388
+ # against a possibly-symlinked-out target pre-0.48.1).
389
+ #
390
+ # 0.48.1 round-1 P2-A: also gated on SHIM_SKIP_VERSION_PROBE -eq 0.
391
+ # Skip-probe shims (delegation-advisory, delegation-capture) cannot
392
+ # write a cache entry (the step-8b write block also gates on
393
+ # SHIM_SKIP_VERSION_PROBE -eq 0 — 0.48.1 SOUNDNESS fix), so any
394
+ # cache-key prep + lookup is pure overhead with zero possible hit.
395
+ # Pre-fix the highest-frequency hooks paid dist-tree find/stat/hash
396
+ # + several `node -e` calls on EVERY write-class fire for nothing.
397
+ if [ "${#REA_ARGV[@]}" -gt 0 ] && [ "$sandbox_failed" -eq 0 ] \
398
+ && [ "$SHIM_SKIP_VERSION_PROBE" -eq 0 ] && ! shim_cache_disabled; then
399
+ local _stat_out=""
400
+ local _proj_real=""
401
+ local _euid=""
402
+ local _session_tok=""
403
+ _stat_out=$(shim_cache_mtime_size "$RESOLVED_CLI_PATH" 2>/dev/null || true)
404
+ # 0.48.0 codex round-4 P1 + round-7 P2: capture the ACTUAL node
405
+ # interpreter realpath + mtime via `process.execPath` (node's own
406
+ # path to itself). Pre-round-7 we resolved `command -v node` via
407
+ # `fs.realpathSync` — but version managers like Volta and asdf
408
+ # use STABLE shim scripts (e.g. ~/.volta/bin/node) that resolve
409
+ # to themselves; only the spawned node's `process.execPath`
410
+ # reveals which concrete Node binary the shim ultimately
411
+ # launched (e.g. /Users/foo/.volta/tools/image/node/22.x.x/bin/
412
+ # node). Using execPath catches `volta pin`/`nvm use` interpreter
413
+ # swaps correctly. The mtime field is captured at second
414
+ # precision (consistent with the other mtime fields) — switching
415
+ # Node versions changes the realpath so the mtime alone is not
416
+ # load-bearing.
417
+ _shim_cache_node_real=$(node -e 'process.stdout.write(require("fs").realpathSync(process.execPath))' 2>/dev/null || true)
418
+ if [ -n "$_shim_cache_node_real" ]; then
419
+ local _node_stat=""
420
+ _node_stat=$(shim_cache_mtime_size "$_shim_cache_node_real" 2>/dev/null || true)
421
+ if [ -n "$_node_stat" ]; then
422
+ _shim_cache_node_mtime="${_node_stat%% *}"
423
+ fi
424
+ fi
425
+ _shim_cache_cli_real=$(node -e 'try { process.stdout.write(require("fs").realpathSync(process.argv[1])); } catch (e) { process.exit(1); }' -- "$RESOLVED_CLI_PATH" 2>/dev/null || true)
426
+ _proj_real=$(node -e 'try { process.stdout.write(require("fs").realpathSync(process.argv[1])); } catch (e) { process.exit(1); }' -- "$proj" 2>/dev/null || true)
427
+ _euid=$(id -u 2>/dev/null || true)
428
+ _session_tok=$(shim_cache_session_token 2>/dev/null || true)
429
+ # 0.48.0 codex round-3 P2: ALSO capture the ancestor package.json
430
+ # path + mtime/size. The sandbox check walks upward to find a
431
+ # package.json whose `name` is `@bookedsolid/rea`; without it in
432
+ # the key, a same-session edit to that package.json (renaming, or
433
+ # removing the `name` field) would still see warm cache hits even
434
+ # though the uncached sandbox check would reject the new state.
435
+ # Codex round-3 P1: ALSO capture the dist/cli/ DIR mtime so a
436
+ # rebuild that adds/removes files (most fresh tsc runs after a
437
+ # source-tree change) invalidates the key even if dist/cli/
438
+ # index.js content happens to round to the same ns.
439
+ _shim_cache_pkg_real=$(node -e '
440
+ try {
441
+ const fs = require("fs");
442
+ const path = require("path");
443
+ const real = fs.realpathSync(process.argv[1]);
444
+ let cur = path.dirname(path.dirname(path.dirname(real)));
445
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i++) {
446
+ const pj = path.join(cur, "package.json");
447
+ if (fs.existsSync(pj)) {
448
+ try {
449
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
450
+ if (data && data.name === "@bookedsolid/rea") {
451
+ process.stdout.write(pj);
452
+ process.exit(0);
453
+ }
454
+ } catch (e) {}
455
+ }
456
+ cur = path.dirname(cur);
457
+ }
458
+ process.exit(1);
459
+ } catch (e) { process.exit(1); }
460
+ ' -- "$RESOLVED_CLI_PATH" 2>/dev/null || true)
461
+ if [ -n "$_shim_cache_pkg_real" ]; then
462
+ local _pkg_stat=""
463
+ _pkg_stat=$(shim_cache_mtime_size "$_shim_cache_pkg_real" 2>/dev/null || true)
464
+ if [ -n "$_pkg_stat" ]; then
465
+ _shim_cache_pkg_mtime="${_pkg_stat%% *}"
466
+ _shim_cache_pkg_size="${_pkg_stat##* }"
467
+ fi
468
+ fi
469
+ # 0.48.0 codex round-5/7/9 — the cache key incorporates a hash of
470
+ # every `*.js` file's mtime across the FULL dist tree, not just
471
+ # dist/cli/. Pre-round-9 the hash covered only dist/cli/*.js, but
472
+ # `rea hook` actually executes a much larger module graph:
473
+ # dist/cli/hook.js imports ../hooks/**, ../policy/loader.js,
474
+ # ../audit/**, etc. A same-session rebuild that rewrote one of
475
+ # those imported files in place without touching a top-level
476
+ # dist/cli/*.js file would leave the hash unchanged, the warm
477
+ # cache would survive, and shim_run would skip the version probe
478
+ # against a changed CLI runtime. Hashing dist/**/*.js closes the
479
+ # gap. Cost: ~15ms on the rea dist (141 files) via `find -exec
480
+ # stat +` batched into a single subprocess call.
481
+ local _dist_root=""
482
+ # dist root is two parents above dist/cli/index.js
483
+ _dist_root=$(dirname "$(dirname "$RESOLVED_CLI_PATH")" 2>/dev/null || true)
484
+ if [ -n "$_dist_root" ] && [ -d "$_dist_root" ]; then
485
+ # 0.48.0 codex round-6 P2: pick a hasher that exists. macOS
486
+ # ships `shasum` (perl); GNU coreutils provides `sha256sum`.
487
+ local _hasher=""
488
+ if command -v shasum >/dev/null 2>&1; then
489
+ _hasher="shasum -a 256"
490
+ elif command -v sha256sum >/dev/null 2>&1; then
491
+ _hasher="sha256sum"
492
+ fi
493
+ if [ -n "$_hasher" ]; then
494
+ # 0.48.0 codex round-7 P2: ns-precision mtime so a
495
+ # same-second rewrite is caught. Try macOS `-f` form first;
496
+ # fall through to GNU `-c` on failure. `find -exec stat +`
497
+ # batches all paths into ONE stat call (~15ms total instead
498
+ # of the per-file 365ms loop).
499
+ local _stat_macos=""
500
+ local _stat_gnu=""
501
+ _stat_macos=$(find "$_dist_root" -name '*.js' -type f -exec stat -f "%Fm %z %N" {} + 2>/dev/null || true)
502
+ if [ -n "$_stat_macos" ]; then
503
+ _shim_cache_dist_mtime=$(printf '%s' "$_stat_macos" | sort | $_hasher 2>/dev/null | awk '{print $1}' | cut -c1-32)
504
+ else
505
+ _stat_gnu=$(find "$_dist_root" -name '*.js' -type f -exec stat -c "%.Y %s %n" {} + 2>/dev/null || true)
506
+ if [ -n "$_stat_gnu" ]; then
507
+ _shim_cache_dist_mtime=$(printf '%s' "$_stat_gnu" | sort | $_hasher 2>/dev/null | awk '{print $1}' | cut -c1-32)
508
+ fi
509
+ fi
510
+ fi
511
+ # Last-ditch fallback: just the dist/cli/ dir mtime (round-3
512
+ # behavior). Keeps the cache functional even when find / stat /
513
+ # shasum / sha256sum are all unavailable (truly stripped
514
+ # container) — though that's already the case where the cache
515
+ # layer should fall back to disabled via the session token.
516
+ if [ -z "$_shim_cache_dist_mtime" ]; then
517
+ local _cli_dir=""
518
+ _cli_dir=$(dirname "$RESOLVED_CLI_PATH" 2>/dev/null || true)
519
+ if [ -n "$_cli_dir" ] && [ -d "$_cli_dir" ]; then
520
+ local _dir_stat=""
521
+ _dir_stat=$(shim_cache_mtime_size "$_cli_dir" 2>/dev/null || true)
522
+ if [ -n "$_dir_stat" ]; then
523
+ _shim_cache_dist_mtime="${_dir_stat%% *}"
524
+ fi
525
+ fi
526
+ fi
527
+ fi
528
+ if [ -n "$_stat_out" ] && [ -n "$_shim_cache_cli_real" ] && [ -n "$_proj_real" ] \
529
+ && [ -n "$_euid" ] && [ -n "$_session_tok" ] \
530
+ && [ -n "$_shim_cache_pkg_real" ] && [ -n "$_shim_cache_pkg_mtime" ] \
531
+ && [ -n "$_shim_cache_dist_mtime" ] \
532
+ && [ -n "$_shim_cache_node_real" ] && [ -n "$_shim_cache_node_mtime" ]; then
533
+ _shim_cache_cli_mtime="${_stat_out%% *}"
534
+ _shim_cache_cli_size="${_stat_out##* }"
535
+ # 0.48.0 codex round-1 P1: the key MUST include SHIM_NAME because
536
+ # step 8's version probe is `rea hook $SHIM_NAME --help` — it's
537
+ # hook-specific. Without SHIM_NAME in the key, a cache-warm shim
538
+ # could let a sibling shim with the SAME (session, project, CLI,
539
+ # mtime, size, euid, shape) skip its OWN version-skew check and
540
+ # forward straight to a CLI that does not implement that hook
541
+ # (realistic on a 0.32 CLI + newer secret-scanner shim mismatch).
542
+ #
543
+ # 0.48.0 codex round-3 P1+P2: 3 new key fields cover (a) ancestor
544
+ # package.json mtime/size — invalidates if the rea package.json
545
+ # is renamed or its `name` field is edited; (b) dist/cli/ dir
546
+ # mtime — invalidates when any file in that directory is
547
+ # added/removed (most fresh `tsc` rebuilds do both); (c) the
548
+ # package.json realpath is implicitly part of the key via these
549
+ # mtime/size fields plus the project realpath above.
550
+ _shim_cache_key=$(shim_cache_key "v1" "$_session_tok" "$_proj_real" "$_shim_cache_cli_real" \
551
+ "$_shim_cache_cli_mtime" "$_shim_cache_cli_size" "$_euid" \
552
+ "$SHIM_ENFORCE_CLI_SHAPE" "$SHIM_NAME" \
553
+ "$_shim_cache_pkg_mtime" "$_shim_cache_pkg_size" \
554
+ "$_shim_cache_dist_mtime" \
555
+ "$_shim_cache_node_real" "$_shim_cache_node_mtime" \
556
+ 2>/dev/null || true)
557
+ if [ -n "$_shim_cache_key" ]; then
558
+ local _cache_json=""
559
+ _cache_json=$(shim_cache_read "$_shim_cache_key" 2>/dev/null || true)
560
+ if [ -n "$_cache_json" ]; then
561
+ # Parse + validate the entry. Failure → treat as miss.
562
+ local _cache_validate=""
563
+ _cache_validate=$(node -e '
564
+ try {
565
+ const e = JSON.parse(process.argv[1]);
566
+ const now = Math.floor(Date.now() / 1000);
567
+ const ttl = Number(e.ttl_seconds);
568
+ const cachedAt = Number(e.cached_at_unix);
569
+ const cliMtime = String(e.cli_mtime);
570
+ const cliSize = String(e.cli_size_bytes);
571
+ const cliReal = String(e.cli_realpath);
572
+ const pkgMtime = String(e.pkg_mtime);
573
+ const pkgSize = String(e.pkg_size_bytes);
574
+ const distMtime = String(e.dist_mtime);
575
+ const sandboxOk = e.sandbox_ok === true;
576
+ const shapeOk = e.shape_ok === true;
577
+ if (e.schema_version !== "v1") process.exit(1);
578
+ if (!Number.isFinite(ttl) || ttl <= 0 || ttl > 3600) process.exit(1);
579
+ if (!Number.isFinite(cachedAt)) process.exit(1);
580
+ if ((cachedAt + ttl) < now) process.exit(1);
581
+ if (cliMtime !== process.argv[2]) process.exit(1);
582
+ if (cliSize !== process.argv[3]) process.exit(1);
583
+ if (cliReal !== process.argv[4]) process.exit(1);
584
+ // 0.48.0 codex round-3 P1+P2: re-check the package.json
585
+ // mtime/size and the dist/cli/ dir mtime in addition to
586
+ // the CLI itself. Defense-in-depth against an entry
587
+ // whose key happened to collide but whose disk state
588
+ // has drifted.
589
+ if (pkgMtime !== process.argv[5]) process.exit(1);
590
+ if (pkgSize !== process.argv[6]) process.exit(1);
591
+ if (distMtime !== process.argv[7]) process.exit(1);
592
+ // 0.48.0 codex round-4 P1: re-check the resolved node
593
+ // binary realpath + mtime. A same-session interpreter
594
+ // swap (nvm use, volta pin) would otherwise let the
595
+ // warm entry silently forward through a different node.
596
+ const nodeReal = String(e.node_realpath);
597
+ const nodeMtime = String(e.node_mtime);
598
+ if (nodeReal !== process.argv[8]) process.exit(1);
599
+ if (nodeMtime !== process.argv[9]) process.exit(1);
600
+ if (!sandboxOk || !shapeOk) process.exit(1);
601
+ process.stdout.write("ok");
602
+ } catch (e) { process.exit(1); }
603
+ ' -- "$_cache_json" "$_shim_cache_cli_mtime" "$_shim_cache_cli_size" "$_shim_cache_cli_real" "$_shim_cache_pkg_mtime" "$_shim_cache_pkg_size" "$_shim_cache_dist_mtime" "$_shim_cache_node_real" "$_shim_cache_node_mtime" 2>/dev/null || true)
604
+ if [ "$_cache_validate" = "ok" ]; then
605
+ _shim_cache_hit=1
606
+ fi
607
+ fi
608
+ fi
609
+ fi
610
+ fi
611
+
612
+ # 5. (0.48.1: sandbox check moved to step 4a — before cache prep — so
613
+ # a hostile workspace cannot make the dist-tree hash walk traverse
614
+ # a symlinked-out target. The original step-5 block lived between
615
+ # cache prep and policy short-circuit; that location was sound for
616
+ # correctness but the cache code regressed the cheap-refusal
617
+ # posture against symlink workspaces. See step 4a comment.)
618
+
350
619
  # 6. Policy short-circuit. Runs BEFORE the CLI-missing / node-missing
351
620
  # banners so a shim whose policy says "disabled" exits 0 cleanly
352
621
  # even when the CLI is unbuilt OR node is absent (matches the
@@ -399,8 +668,10 @@ shim_run() {
399
668
  # 8. Version probe (skipped when SHIM_SKIP_VERSION_PROBE=1, used by
400
669
  # delegation-capture whose pre-port body had no probe — a stale
401
670
  # CLI drops the signal silently rather than spamming the operator
402
- # on every Agent/Skill dispatch).
403
- if [ "$SHIM_SKIP_VERSION_PROBE" -eq 0 ]; then
671
+ # on every Agent/Skill dispatch). Also skipped on cache hit — the
672
+ # probe answer was recorded when the entry was written and the
673
+ # cache key invalidates if mtime / size / realpath changes.
674
+ if [ "$SHIM_SKIP_VERSION_PROBE" -eq 0 ] && [ "$_shim_cache_hit" -eq 0 ]; then
404
675
  local probe_out probe_status
405
676
  probe_out=$("${REA_ARGV[@]}" hook "$SHIM_NAME" --help 2>&1)
406
677
  probe_status=$?
@@ -414,6 +685,60 @@ shim_run() {
414
685
  fi
415
686
  fi
416
687
 
688
+ # 8b. Cache write (0.48.0). At this point sandbox + probe both
689
+ # succeeded — record the answers for the next fire in this
690
+ # session. Cache write failure NEVER blocks the gate; we ignore
691
+ # the return value. Skipped on a cache hit (we just used the
692
+ # entry; rewriting it would be wasted work AND would refresh
693
+ # `cached_at_unix` past the TTL ceiling, defeating the staleness
694
+ # bound).
695
+ #
696
+ # 0.48.1 SOUNDNESS: also skipped when SHIM_SKIP_VERSION_PROBE=1
697
+ # (delegation-capture path). Pre-fix the cache write proceeded
698
+ # after a skipped probe, recording `shape_ok: true` from
699
+ # defaulted-true logic without the probe having actually run.
700
+ # On the next fire a cache hit would read that entry and trust
701
+ # a version-probe answer that was never produced. The cache MUST
702
+ # only persist real probe results; if the probe was bypassed,
703
+ # the next fire pays the cost of running it for real.
704
+ if [ "$_shim_cache_hit" -eq 0 ] && [ -n "$_shim_cache_key" ] \
705
+ && [ "$SHIM_SKIP_VERSION_PROBE" -eq 0 ]; then
706
+ local _write_payload=""
707
+ _write_payload=$(node -e '
708
+ const args = process.argv.slice(1);
709
+ const now = Math.floor(Date.now() / 1000);
710
+ const entry = {
711
+ schema_version: "v1",
712
+ cli_realpath: args[0],
713
+ cli_mtime: args[1],
714
+ cli_size_bytes: args[2],
715
+ // 0.48.0 codex round-3 P1+P2: record the ancestor package.json
716
+ // mtime/size + dist/cli/ dir mtime so the read-side validator
717
+ // can re-check them on every hit. The cache key includes
718
+ // these too, so a drifted state produces a different key —
719
+ // but persisting them in the entry lets the validator catch
720
+ // a key collision as a stale-entry miss instead of trusting
721
+ // it.
722
+ pkg_mtime: args[3],
723
+ pkg_size_bytes: args[4],
724
+ dist_mtime: args[5],
725
+ // 0.48.0 codex round-4 P1: record the resolved node binary
726
+ // realpath + mtime so the read-side validator can re-check
727
+ // them and refuse a hit when the interpreter swapped.
728
+ node_realpath: args[6],
729
+ node_mtime: args[7],
730
+ sandbox_ok: true,
731
+ shape_ok: true,
732
+ cached_at_unix: now,
733
+ ttl_seconds: 3600,
734
+ };
735
+ process.stdout.write(JSON.stringify(entry));
736
+ ' -- "$_shim_cache_cli_real" "$_shim_cache_cli_mtime" "$_shim_cache_cli_size" "$_shim_cache_pkg_mtime" "$_shim_cache_pkg_size" "$_shim_cache_dist_mtime" "$_shim_cache_node_real" "$_shim_cache_node_mtime" 2>/dev/null || true)
737
+ if [ -n "$_write_payload" ]; then
738
+ shim_cache_write "$_shim_cache_key" "$_write_payload" >/dev/null 2>&1 || true
739
+ fi
740
+ fi
741
+
417
742
  # 9. Forward stdin.
418
743
  if declare -F shim_forward >/dev/null 2>&1; then
419
744
  shim_forward
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.47.0",
3
+ "version": "0.48.1",
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)",
@@ -461,7 +461,16 @@ function runOnce(hookPath, payload) {
461
461
  input: payload,
462
462
  encoding: 'utf8',
463
463
  timeout: 30000,
464
- env: { ...process.env, CLAUDE_PROJECT_DIR: REPO_ROOT },
464
+ // 0.48.0 charter REA_SHIM_CACHE=0 forces the per-session shim
465
+ // cache OFF for every profiled invocation. Without this, the
466
+ // cache would warm on the warmup runs and steady-state numbers
467
+ // would silently improve from one profile invocation to the next
468
+ // — masking regressions in the underlying resolve / sandbox /
469
+ // probe layers (concern #6 of the design memo). The
470
+ // measurement-time baseline is the COLD path; the cache's
471
+ // benefit is reported separately in
472
+ // `docs/hook-perf-baseline.md`.
473
+ env: { ...process.env, CLAUDE_PROJECT_DIR: REPO_ROOT, REA_SHIM_CACHE: '0' },
465
474
  });
466
475
  const elapsed = performance.now() - start;
467
476
  // spawnSync returns res.status null on timeout/signal — surface