@bookedsolid/rea 0.39.0 → 0.40.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.
@@ -89,7 +89,47 @@ export interface HookDelegationAdvisoryOptions {
89
89
  * Production omits.
90
90
  */
91
91
  stdinOverride?: string;
92
+ /**
93
+ * Test seam — override the sleep used by
94
+ * `sessionHasRealDelegation`'s poll-and-backoff loop. Production
95
+ * omits and uses real `setTimeout`-backed sleeps; tests pass a
96
+ * controllable fake so the race-coverage tests don't wall-clock on
97
+ * the real 500ms budget.
98
+ *
99
+ * 0.40.0 charter item 1.
100
+ */
101
+ sleepOverride?: (ms: number) => Promise<void>;
92
102
  }
103
+ /**
104
+ * Backoff schedule (in milliseconds) for `sessionHasRealDelegation`'s
105
+ * poll-and-backoff loop.
106
+ *
107
+ * 0.40.0 charter item 1 — closes the `& disown` race between
108
+ * `delegation-capture.sh` (which fire-and-forgets `rea hook
109
+ * delegation-signal --detach &` for sub-50ms PreToolUse latency) and the
110
+ * `delegation-advisory.sh` PostToolUse path (which reads the audit log
111
+ * to decide whether the session has delegated). A `git commit` landing
112
+ * within the narrow window between an Agent dispatch and the audit
113
+ * append-on-disk would read the stale chain, see no delegation, fire
114
+ * the nudge spuriously, AND write the `.fired` sentinel — silencing
115
+ * every future advisory in the session even though delegation DID
116
+ * happen.
117
+ *
118
+ * The schedule is delays BETWEEN re-reads (NOT cumulative): 50ms,
119
+ * 150ms, 300ms. Total worst-case 500ms — acceptable hot-path budget
120
+ * for a PostToolUse hook running on `Bash|Edit|Write|MultiEdit|
121
+ * NotebookEdit`. The first read is immediate (no upfront delay), so a
122
+ * session that DID delegate before threshold-crossing pays zero extra
123
+ * latency. Only the rare "we crossed threshold while a recent
124
+ * delegation signal hasn't yet hit disk" case pays the full budget,
125
+ * and only ONCE per session (the `.fired` sentinel suppresses future
126
+ * scans).
127
+ *
128
+ * Exported for the race-coverage test in
129
+ * `delegation-advisory.test.ts` so it can assert on the schedule
130
+ * without duplicating the constant.
131
+ */
132
+ export declare const DELEGATION_POLL_BACKOFF_MS: readonly number[];
93
133
  /**
94
134
  * Derive a filesystem-safe, **collision-free** per-session state-key
95
135
  * basename from an untrusted session id.
@@ -70,6 +70,36 @@ import { loadDelegationRecords, listRotatedAuditFiles, } from './audit-specialis
70
70
  import { discoverRoster, countsAsRealDelegation, DEFAULT_EXEMPT_SUBAGENTS, } from './roster.js';
71
71
  import { REA_DIR } from './utils.js';
72
72
  const DEFAULT_THRESHOLD = 25;
73
+ /**
74
+ * Backoff schedule (in milliseconds) for `sessionHasRealDelegation`'s
75
+ * poll-and-backoff loop.
76
+ *
77
+ * 0.40.0 charter item 1 — closes the `& disown` race between
78
+ * `delegation-capture.sh` (which fire-and-forgets `rea hook
79
+ * delegation-signal --detach &` for sub-50ms PreToolUse latency) and the
80
+ * `delegation-advisory.sh` PostToolUse path (which reads the audit log
81
+ * to decide whether the session has delegated). A `git commit` landing
82
+ * within the narrow window between an Agent dispatch and the audit
83
+ * append-on-disk would read the stale chain, see no delegation, fire
84
+ * the nudge spuriously, AND write the `.fired` sentinel — silencing
85
+ * every future advisory in the session even though delegation DID
86
+ * happen.
87
+ *
88
+ * The schedule is delays BETWEEN re-reads (NOT cumulative): 50ms,
89
+ * 150ms, 300ms. Total worst-case 500ms — acceptable hot-path budget
90
+ * for a PostToolUse hook running on `Bash|Edit|Write|MultiEdit|
91
+ * NotebookEdit`. The first read is immediate (no upfront delay), so a
92
+ * session that DID delegate before threshold-crossing pays zero extra
93
+ * latency. Only the rare "we crossed threshold while a recent
94
+ * delegation signal hasn't yet hit disk" case pays the full budget,
95
+ * and only ONCE per session (the `.fired` sentinel suppresses future
96
+ * scans).
97
+ *
98
+ * Exported for the race-coverage test in
99
+ * `delegation-advisory.test.ts` so it can assert on the schedule
100
+ * without duplicating the constant.
101
+ */
102
+ export const DELEGATION_POLL_BACKOFF_MS = [50, 150, 300];
73
103
  /**
74
104
  * Maximum length of the human-readable prefix in a state key. The full
75
105
  * key is `<prefix>-<16-hex-hash>`, so a 64-char cap keeps basenames well
@@ -250,7 +280,22 @@ export function advisoryMessage(count, threshold) {
250
280
  * `since` anchor is `undefined` and behavior is the pre-0.31.0
251
281
  * single-file walk.
252
282
  */
253
- async function sessionHasRealDelegation(reaRoot, sessionId, exemptSubagents) {
283
+ /**
284
+ * Real-clock sleep used by the production poll-and-backoff loop.
285
+ * Factored out so tests can swap it for a fake controllable scheduler
286
+ * via `HookDelegationAdvisoryOptions.sleepOverride`.
287
+ */
288
+ function realSleep(ms) {
289
+ return new Promise((resolve) => setTimeout(resolve, ms));
290
+ }
291
+ /**
292
+ * Single audit-chain scan: returns `'delegated'` when a real
293
+ * delegation signal is found, `'not-delegated'` when scanning succeeds
294
+ * but no real signal is in the chain, and `'unreadable'` when audit
295
+ * loading throws (the chain is missing / unreadable). Split out from
296
+ * the polling loop so each retry runs identical scan logic.
297
+ */
298
+ async function scanForRealDelegationOnce(reaRoot, sessionId, exemptSubagents) {
254
299
  let records;
255
300
  try {
256
301
  // Resolve the rotated-file set the same way `rea audit specialists`
@@ -263,13 +308,10 @@ async function sessionHasRealDelegation(reaRoot, sessionId, exemptSubagents) {
263
308
  records = loaded.records;
264
309
  }
265
310
  catch {
266
- // Audit log unreadable — we cannot prove the session delegated, so
267
- // we DON'T fire (fail toward silence, not toward a false-positive
268
- // nudge). Returning `true` here suppresses the advisory.
269
- return true;
311
+ return 'unreadable';
270
312
  }
271
313
  if (records.length === 0)
272
- return false;
314
+ return 'not-delegated';
273
315
  const roster = discoverRoster(reaRoot);
274
316
  for (const rec of records) {
275
317
  if (countsAsRealDelegation({
@@ -278,9 +320,45 @@ async function sessionHasRealDelegation(reaRoot, sessionId, exemptSubagents) {
278
320
  roster,
279
321
  exempt: exemptSubagents,
280
322
  })) {
281
- return true;
323
+ return 'delegated';
282
324
  }
283
325
  }
326
+ return 'not-delegated';
327
+ }
328
+ async function sessionHasRealDelegation(reaRoot, sessionId, exemptSubagents, sleep = realSleep) {
329
+ // 0.40.0 charter item 1 — poll-and-backoff before declaring
330
+ // "no delegation in this session".
331
+ //
332
+ // The producer (`delegation-capture.sh`) calls `rea hook
333
+ // delegation-signal --detach &` to fire-and-forget the audit append.
334
+ // For sub-50ms PreToolUse latency this is the right call, but it
335
+ // opens a narrow race: a write-class call (Bash/Edit/Write/…)
336
+ // landing in the same tick as an Agent/Skill dispatch can run this
337
+ // predicate BEFORE the audit append commits to disk. Pre-fix, the
338
+ // function then returned `false`, the caller fired the advisory,
339
+ // wrote the `.fired` sentinel, and silenced every future nudge in
340
+ // the session — even though delegation DID happen.
341
+ //
342
+ // Each retry runs a full audit scan. The first scan is immediate
343
+ // (no upfront delay); subsequent scans wait per
344
+ // `DELEGATION_POLL_BACKOFF_MS`. Worst-case total: 50+150+300 = 500ms
345
+ // for the four-scan path. We exit early as soon as a delegation is
346
+ // observed OR the chain becomes unreadable (preserving the pre-fix
347
+ // "audit log unreadable → suppress the advisory" posture so a
348
+ // missing chain never produces a false-positive nudge).
349
+ let outcome = await scanForRealDelegationOnce(reaRoot, sessionId, exemptSubagents);
350
+ if (outcome === 'delegated')
351
+ return true;
352
+ if (outcome === 'unreadable')
353
+ return true;
354
+ for (const waitMs of DELEGATION_POLL_BACKOFF_MS) {
355
+ await sleep(waitMs);
356
+ outcome = await scanForRealDelegationOnce(reaRoot, sessionId, exemptSubagents);
357
+ if (outcome === 'delegated')
358
+ return true;
359
+ if (outcome === 'unreadable')
360
+ return true;
361
+ }
284
362
  return false;
285
363
  }
286
364
  /**
@@ -379,7 +457,7 @@ export async function computeDelegationAdvisory(options) {
379
457
  // verbatim (or `'unknown'` for untagged sessions), so the `stateKey`
380
458
  // filesystem form would never match (see the comment at the
381
459
  // `auditSessionId` / `stateKey` split above).
382
- const delegated = await sessionHasRealDelegation(reaRoot, auditSessionId, policy.exemptSubagents);
460
+ const delegated = await sessionHasRealDelegation(reaRoot, auditSessionId, policy.exemptSubagents, options.sleepOverride);
383
461
  if (delegated) {
384
462
  // Session DID delegate to a real specialist — no nudge warranted.
385
463
  // We deliberately do NOT write the `.fired` sentinel here: if the
@@ -124,7 +124,20 @@ export interface PolicyReaderProbes {
124
124
  cliDistExists?: (baseDir: string) => boolean;
125
125
  cliInvokable?: (baseDir: string) => boolean;
126
126
  python3OnPath?: () => string | null;
127
- python3PyYamlReachable?: () => boolean;
127
+ /**
128
+ * 0.40.0 charter item 3 — accepts the consumer's `baseDir` so the
129
+ * probe can thread it as `cwd` to the spawned `python3 -c` process.
130
+ * Pre-fix, the spawn happened in doctor's own cwd, which meant the
131
+ * sys.path scrub (which removes "", ".", CWD, realpath(CWD) to
132
+ * mirror policy-reader.sh's defense against a malicious repo-local
133
+ * `./yaml.py`) operated on the wrong directory when `rea doctor`
134
+ * was invoked from outside the consumer tree (e.g. `cd /tmp && rea
135
+ * doctor --base-dir /Users/.../consumer-repo`).
136
+ *
137
+ * Probes that don't care about cwd (test stubs, fakes) can simply
138
+ * ignore the argument; the default production probe uses it.
139
+ */
140
+ python3PyYamlReachable?: (baseDir: string) => boolean;
128
141
  awkOnPath?: () => string | null;
129
142
  jqOnPath?: () => string | null;
130
143
  }
@@ -164,15 +177,41 @@ export declare function checkPolicyReaderTier1(baseDir: string, probes?: PolicyR
164
177
  * The warning highlights the silent no-op risk for flow-form lookups
165
178
  * when CLI is also unreachable.
166
179
  */
167
- export declare function checkPolicyReaderTier2(probes?: PolicyReaderProbes): CheckResult;
180
+ export declare function checkPolicyReaderTier2(baseDir: string, probes?: PolicyReaderProbes): CheckResult;
168
181
  /**
169
182
  * Tier 3 — awk block-form parser. Last-resort no-dep fallback.
170
- * Practically always present (POSIX requirement); hard-fail only when
171
- * truly absent (in which case the consumer has ZERO working fallback
172
- * tiers and any CLI-absent shim invocation will silently fail-closed
173
- * on every policy lookup).
183
+ * Practically always present (POSIX requirement).
184
+ *
185
+ * 0.40.0 charter item 2 conditional verdict, refined by codex
186
+ * round 1 P2:
187
+ * - awk present → `pass`
188
+ * - awk absent AND Tier 2 reachable → `warn`
189
+ * (Tier 2 implies python3, which is a list-walker)
190
+ * - awk absent AND Tier 1 reachable AND a list walker
191
+ * (jq OR python3) is on PATH → `warn`
192
+ * - awk absent AND Tier 1 reachable BUT no list walker → `fail`
193
+ * (codex round 1 P2 — list-valued policy reads silently
194
+ * fail-closed even though scalar reads work, so the
195
+ * downgrade-to-warn is misleading; doctor would exit 0 on a
196
+ * broken install)
197
+ * - awk absent AND no other tier reachable → `fail`
198
+ *
199
+ * Pre-fix the absent-awk branch always returned `fail` — but when
200
+ * Tier 1 (rea CLI) AND/OR Tier 2 (python3+PyYAML) are reachable AND
201
+ * the list walker exists, the operator's effective floor is fine even
202
+ * without awk; Tier 3 is the LAST fallback, not a hard requirement.
203
+ * The summary check (`checkPolicyReaderTierSummary`) already
204
+ * aggregates correctly; this per-tier verdict now reflects the same
205
+ * severity logic so an operator who reads ONLY the Tier 3 row isn't
206
+ * misled into thinking the install is broken on a perfectly-
207
+ * functional box that has python3 + jq + the rea CLI all wired but
208
+ * happens to lack awk.
209
+ *
210
+ * Takes `baseDir` so it can evaluate Tier 1's two-stage check (dist
211
+ * present + CLI invokable) and Tier 2's reachability. Probes are
212
+ * threaded through identically.
174
213
  */
175
- export declare function checkPolicyReaderTier3(probes?: PolicyReaderProbes): CheckResult;
214
+ export declare function checkPolicyReaderTier3(baseDir: string, probes?: PolicyReaderProbes): CheckResult;
176
215
  /**
177
216
  * jq — optional accelerator used by Tier 1/2's JSON subtree parsing.
178
217
  * Per the 0.37.0 round-1 P2 fix the helper falls back to a python3
@@ -1202,7 +1202,7 @@ function defaultCliInvokable(baseDir) {
1202
1202
  return false;
1203
1203
  }
1204
1204
  }
1205
- function defaultPython3PyYamlReachable() {
1205
+ function defaultPython3PyYamlReachable(baseDir) {
1206
1206
  // The Tier 2 loader runs `python3 -c "import yaml"`. We mirror that
1207
1207
  // probe verbatim so a `yaml`-installable-but-broken interpreter is
1208
1208
  // not falsely reported as "reachable". Apply the SAME env scrub
@@ -1244,7 +1244,16 @@ function defaultPython3PyYamlReachable() {
1244
1244
  'sys.path[:] = [p for p in sys.path if p not in ("", ".", _cwd, _cwd_real)]',
1245
1245
  'import yaml',
1246
1246
  ].join('\n');
1247
+ // 0.40.0 charter item 3 — thread `baseDir` as cwd so the sys.path
1248
+ // scrub above strips THIS consumer's repo root (the directory the
1249
+ // production shim chain runs from), not doctor's own cwd. Pre-fix,
1250
+ // `rea doctor --base-dir <consumer>` invoked from `/tmp/foo` would
1251
+ // scrub against `/tmp/foo`, leaving any `<consumer>/yaml.py`
1252
+ // shadowing potential undetected — exactly the multi-repo workflow
1253
+ // every other doctor probe (cliInvokable, …) already handles by
1254
+ // setting cwd to baseDir.
1247
1255
  const res = spawnSync('python3', ['-c', probeBody], {
1256
+ cwd: baseDir,
1248
1257
  env: probeEnv,
1249
1258
  timeout: 5_000,
1250
1259
  stdio: ['ignore', 'ignore', 'ignore'],
@@ -1332,7 +1341,7 @@ export function checkPolicyReaderTier1(baseDir, probes) {
1332
1341
  * The warning highlights the silent no-op risk for flow-form lookups
1333
1342
  * when CLI is also unreachable.
1334
1343
  */
1335
- export function checkPolicyReaderTier2(probes) {
1344
+ export function checkPolicyReaderTier2(baseDir, probes) {
1336
1345
  const label = 'policy-reader Tier 2 (python3 + PyYAML)';
1337
1346
  const p = resolveProbes(probes);
1338
1347
  const py = p.python3OnPath();
@@ -1345,7 +1354,7 @@ export function checkPolicyReaderTier2(probes) {
1345
1354
  'no-ops when the rea CLI is also unreachable. Install python3 to close this gap.',
1346
1355
  };
1347
1356
  }
1348
- if (!p.python3PyYamlReachable()) {
1357
+ if (!p.python3PyYamlReachable(baseDir)) {
1349
1358
  return {
1350
1359
  label,
1351
1360
  status: 'warn',
@@ -1362,12 +1371,38 @@ export function checkPolicyReaderTier2(probes) {
1362
1371
  }
1363
1372
  /**
1364
1373
  * Tier 3 — awk block-form parser. Last-resort no-dep fallback.
1365
- * Practically always present (POSIX requirement); hard-fail only when
1366
- * truly absent (in which case the consumer has ZERO working fallback
1367
- * tiers and any CLI-absent shim invocation will silently fail-closed
1368
- * on every policy lookup).
1374
+ * Practically always present (POSIX requirement).
1375
+ *
1376
+ * 0.40.0 charter item 2 conditional verdict, refined by codex
1377
+ * round 1 P2:
1378
+ * - awk present → `pass`
1379
+ * - awk absent AND Tier 2 reachable → `warn`
1380
+ * (Tier 2 implies python3, which is a list-walker)
1381
+ * - awk absent AND Tier 1 reachable AND a list walker
1382
+ * (jq OR python3) is on PATH → `warn`
1383
+ * - awk absent AND Tier 1 reachable BUT no list walker → `fail`
1384
+ * (codex round 1 P2 — list-valued policy reads silently
1385
+ * fail-closed even though scalar reads work, so the
1386
+ * downgrade-to-warn is misleading; doctor would exit 0 on a
1387
+ * broken install)
1388
+ * - awk absent AND no other tier reachable → `fail`
1389
+ *
1390
+ * Pre-fix the absent-awk branch always returned `fail` — but when
1391
+ * Tier 1 (rea CLI) AND/OR Tier 2 (python3+PyYAML) are reachable AND
1392
+ * the list walker exists, the operator's effective floor is fine even
1393
+ * without awk; Tier 3 is the LAST fallback, not a hard requirement.
1394
+ * The summary check (`checkPolicyReaderTierSummary`) already
1395
+ * aggregates correctly; this per-tier verdict now reflects the same
1396
+ * severity logic so an operator who reads ONLY the Tier 3 row isn't
1397
+ * misled into thinking the install is broken on a perfectly-
1398
+ * functional box that has python3 + jq + the rea CLI all wired but
1399
+ * happens to lack awk.
1400
+ *
1401
+ * Takes `baseDir` so it can evaluate Tier 1's two-stage check (dist
1402
+ * present + CLI invokable) and Tier 2's reachability. Probes are
1403
+ * threaded through identically.
1369
1404
  */
1370
- export function checkPolicyReaderTier3(probes) {
1405
+ export function checkPolicyReaderTier3(baseDir, probes) {
1371
1406
  const label = 'policy-reader Tier 3 (awk)';
1372
1407
  const p = resolveProbes(probes);
1373
1408
  const awk = p.awkOnPath();
@@ -1378,6 +1413,61 @@ export function checkPolicyReaderTier3(probes) {
1378
1413
  detail: `awk at ${awk} — block-form fallback available`,
1379
1414
  };
1380
1415
  }
1416
+ // 0.40.0 — awk is absent. Decide whether this is `warn` (other tiers
1417
+ // cover) or `fail` (catastrophic — no working policy lookup tier).
1418
+ // Mirror Tier 1's two-stage check (dist + invokable) and Tier 2's
1419
+ // python3 + PyYAML pair so the verdict here matches what the shim
1420
+ // ladder would actually do at runtime.
1421
+ const tier1 = p.cliDistExists(baseDir) && p.cliInvokable(baseDir);
1422
+ const tier2 = p.python3OnPath() !== null && p.python3PyYamlReachable(baseDir);
1423
+ // Codex round 1 P2 (2026-05-16): the downgrade-to-warn branch needs
1424
+ // a list walker too. `policy_reader_get_list` (the helper that reads
1425
+ // list-valued keys like `blocked_paths`) iterates the parsed JSON
1426
+ // array via jq OR python3, falling back to Tier 3 awk for inline
1427
+ // arrays. With awk gone AND no jq AND no python3, list-valued
1428
+ // policy reads silently fail-closed even when Tier 1 is reachable —
1429
+ // `blocked-paths-bash-gate.sh` etc. would see an EMPTY blocked-paths
1430
+ // set and stop enforcing entries the operator declared. Pre-fix
1431
+ // this concrete shape (cliInvokable + no python3 + no jq + no awk)
1432
+ // returned `warn` and the doctor exited 0 on a broken install.
1433
+ // Post-fix the downgrade requires a list walker; otherwise we stay
1434
+ // on `fail`. Tier 2 implies python3 on PATH (the interpreter that
1435
+ // ran PyYAML), so Tier 2 always brings list-walker support — no
1436
+ // additional check needed for the Tier-2 branch.
1437
+ const py = p.python3OnPath();
1438
+ const listWalker = p.jqOnPath() !== null || py !== null;
1439
+ if (tier2 || (tier1 && listWalker)) {
1440
+ const reachable = [];
1441
+ if (tier1)
1442
+ reachable.push('Tier 1 (rea CLI)');
1443
+ if (tier2)
1444
+ reachable.push('Tier 2 (python3+PyYAML)');
1445
+ return {
1446
+ label,
1447
+ status: 'warn',
1448
+ detail: `awk not on PATH — Tier 3 (block-form fallback) unreachable. ${reachable.join(' and ')} ` +
1449
+ 'still cover the shim ladder, so policy lookups continue to work; this is a ' +
1450
+ 'soft degradation, not a hard failure. Install awk (`mawk`, `gawk`, or `nawk`) ' +
1451
+ 'to restore the last-resort fallback.',
1452
+ };
1453
+ }
1454
+ // Codex round 1 P2: separate "no list walker" diagnosis from the
1455
+ // catastrophic "no tier at all" case. Tier 1 reachable but no jq
1456
+ // AND no python3 AND no awk means list-valued policy reads
1457
+ // fail-closed silently — distinct from the truly-empty
1458
+ // no-CLI-no-python-no-awk shape, and worth a precise remediation.
1459
+ if (tier1) {
1460
+ return {
1461
+ label,
1462
+ status: 'fail',
1463
+ detail: 'awk not on PATH AND neither jq nor python3 is on PATH — Tier 1 (rea CLI) parses ' +
1464
+ 'flow-form scalars, but `policy_reader_get_list` cannot iterate list-valued keys ' +
1465
+ '(e.g. `blocked_paths: [.env, ...]`) without jq, python3, OR awk to walk the ' +
1466
+ 'resulting JSON arrays. Affected hooks (`blocked-paths-bash-gate.sh`, ' +
1467
+ '`blocked-paths-enforcer.sh`, …) see an EMPTY list and silently stop enforcing. ' +
1468
+ 'Install awk OR jq OR python3 to restore list-iteration.',
1469
+ };
1470
+ }
1381
1471
  return {
1382
1472
  label,
1383
1473
  status: 'fail',
@@ -1449,7 +1539,7 @@ export function checkPolicyReaderTierSummary(baseDir, probes) {
1449
1539
  // ladder would actually do at runtime.
1450
1540
  const tier1 = p.cliDistExists(baseDir) && p.cliInvokable(baseDir);
1451
1541
  const py = p.python3OnPath();
1452
- const tier2 = py !== null && p.python3PyYamlReachable();
1542
+ const tier2 = py !== null && p.python3PyYamlReachable(baseDir);
1453
1543
  const tier3 = p.awkOnPath() !== null;
1454
1544
  const jq = p.jqOnPath();
1455
1545
  // List iteration after Tier 1/2 needs jq OR python3 to walk the
@@ -2059,8 +2149,8 @@ export function collectChecks(baseDir, codexProbeState, prePushState, options =
2059
2149
  ...(policyParsesResult.status === 'pass'
2060
2150
  ? [
2061
2151
  checkPolicyReaderTier1(baseDir),
2062
- checkPolicyReaderTier2(),
2063
- checkPolicyReaderTier3(),
2152
+ checkPolicyReaderTier2(baseDir),
2153
+ checkPolicyReaderTier3(baseDir),
2064
2154
  checkPolicyReaderJq(),
2065
2155
  checkPolicyReaderTierSummary(baseDir),
2066
2156
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
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)",