@bookedsolid/rea 0.26.1 → 0.27.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.
@@ -29,6 +29,7 @@
29
29
  * This matches the protective default established in 0.10.x.
30
30
  */
31
31
  import type { Command } from 'commander';
32
+ import { runCodexReview } from '../hooks/push-gate/codex-runner.js';
32
33
  export interface HookPushGateOptions {
33
34
  base?: string;
34
35
  /**
@@ -125,9 +126,82 @@ export interface HookPolicyGetOptions {
125
126
  }
126
127
  export declare function runHookPolicyGet(options: HookPolicyGetOptions): Promise<void>;
127
128
  /**
128
- * Attach the `rea hook` subcommand tree to a commander Program. Two
129
- * subcommands today: `push-gate` and `scan-bash`. New hooks should land
130
- * here rather than as top-level commands so the CLI surface stays
131
- * navigable.
129
+ * `rea hook codex-review` the canonical Bash-direct codex invocation
130
+ * for marathon-mode review cycles (0.27.0+).
131
+ *
132
+ * The user directive is "codex should be invoked this way always to
133
+ * minimize claude consumption of all the output. we just need the log
134
+ * at the end." This command wraps `codex exec review --json --ephemeral`
135
+ * with the same iron-gate model defaults the push-gate uses, tees the
136
+ * raw JSONL stream to a tempfile so the caller can read the
137
+ * un-summarized output directly, parses out the verdict + finding
138
+ * count, writes a `codex.review` audit entry, and prints a single terse
139
+ * status line to stderr. Stdout stays clean — when `--json` is set the
140
+ * canonical JSON summary lands there for jq-style chaining.
141
+ *
142
+ * Distinct from `rea review`:
143
+ * - `rea review` writes a `rea.local_review` entry the local-review
144
+ * gate consults and prints human-readable output. Treated as the
145
+ * primary CLI surface for the local-first workflow.
146
+ * - `rea hook codex-review` writes a `codex.review` entry (the legacy
147
+ * gateway shape), keeps the raw JSONL on disk, and is intentionally
148
+ * terse. Designed for thin-shim invocation from agents and slash
149
+ * commands that DON'T need a Claude-paraphrased summary — the raw
150
+ * JSON IS the review.
151
+ *
152
+ * Exit-code contract (mirrors push-gate convention):
153
+ *
154
+ * 0 — pass verdict
155
+ * 1 — concerns verdict
156
+ * 2 — blocking verdict, codex error, or HALT active
157
+ */
158
+ export interface HookCodexReviewOptions {
159
+ base?: string;
160
+ /**
161
+ * Mirror of `--last-n-commits` on push-gate. When set, diff against
162
+ * `HEAD~N` instead of running the upstream-resolution ladder. `--base`
163
+ * always wins when both are set. Validated as a positive integer at
164
+ * the commander layer.
165
+ */
166
+ lastNCommits?: number;
167
+ /**
168
+ * Emit a single JSON line on stdout instead of a stderr-only status
169
+ * line. The JSON shape carries `verdict`, `finding_count`, `head_sha`,
170
+ * `target`, `audit_hash`, `raw_path`, and `exit_code`.
171
+ */
172
+ json?: boolean;
173
+ /**
174
+ * Override REA_ROOT. Tests set this; the production caller relies on
175
+ * `process.cwd()`.
176
+ */
177
+ reaRoot?: string;
178
+ /**
179
+ * Test seam — replaces the spawn of `codex exec review`. Same
180
+ * contract as `runCodexReview`'s `spawnImpl`. When set, the codex-
181
+ * availability probe is skipped (matches `runCodexReview` behavior).
182
+ */
183
+ spawnImpl?: Parameters<typeof runCodexReview>[0]['spawnImpl'];
184
+ /**
185
+ * Test seam — override the directory raw stdout is teed into. Default
186
+ * is `os.tmpdir()`. Tests set this so they can read the file back.
187
+ */
188
+ rawStdoutDir?: string;
189
+ }
190
+ export declare function runHookCodexReview(options: HookCodexReviewOptions): Promise<void>;
191
+ /**
192
+ * Attach the `rea hook` subcommand tree to a commander Program.
193
+ *
194
+ * Subcommands:
195
+ * - `push-gate` — stateless pre-push Codex review (called by husky).
196
+ * - `scan-bash` — parser-backed bash-tier scanner (called by Claude
197
+ * Code shim hooks).
198
+ * - `policy-get` — single-source-of-truth policy reader for bash hooks.
199
+ * - `codex-review` — thin Bash-direct codex invocation (0.27.0+) for
200
+ * marathon-mode review cycles. The canonical
201
+ * invocation that all agents and slash commands
202
+ * route through.
203
+ *
204
+ * New hooks should land here rather than as top-level commands so the
205
+ * CLI surface stays navigable.
132
206
  */
133
207
  export declare function registerHookCommand(program: Command): void;
package/dist/cli/hook.js CHANGED
@@ -29,12 +29,19 @@
29
29
  * This matches the protective default established in 0.10.x.
30
30
  */
31
31
  import fs from 'node:fs';
32
+ import os from 'node:os';
32
33
  import path from 'node:path';
34
+ import crypto from 'node:crypto';
33
35
  import { parse as parseYaml } from 'yaml';
34
36
  import { parsePrePushStdin, runPushGate } from '../hooks/push-gate/index.js';
35
37
  import { runBlockedScan, runProtectedScan } from '../hooks/bash-scanner/index.js';
36
38
  import { loadPolicy } from '../policy/loader.js';
37
39
  import { appendAuditRecord, InvocationStatus, Tier } from '../audit/append.js';
40
+ import { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, } from '../audit/codex-event.js';
41
+ import { CodexNotInstalledError, CodexProtocolError, CodexSubprocessError, CodexTimeoutError, IRON_GATE_DEFAULT_MODEL, IRON_GATE_DEFAULT_REASONING, createRealGitExecutor, runCodexReview, } from '../hooks/push-gate/codex-runner.js';
42
+ import { resolveBaseRef } from '../hooks/push-gate/base.js';
43
+ import { resolvePushGatePolicy } from '../hooks/push-gate/policy.js';
44
+ import { summarizeReview } from '../hooks/push-gate/findings.js';
38
45
  import { err } from './utils.js';
39
46
  /**
40
47
  * Public runner, exposed so integration tests and the commander binding can
@@ -319,11 +326,272 @@ export async function runHookPolicyGet(options) {
319
326
  // Object/Array → no output (caller treats as unset).
320
327
  process.exit(0);
321
328
  }
329
+ export async function runHookCodexReview(options) {
330
+ const baseDir = options.reaRoot ?? process.cwd();
331
+ // HALT check — uniform with the rest of the hook tree.
332
+ const haltPath = path.join(baseDir, '.rea', 'HALT');
333
+ if (fs.existsSync(haltPath)) {
334
+ let reason = 'Reason unknown';
335
+ try {
336
+ const content = fs.readFileSync(haltPath, 'utf8');
337
+ reason = content.slice(0, 1024).trim() || reason;
338
+ }
339
+ catch {
340
+ /* leave default */
341
+ }
342
+ process.stderr.write(`REA HALT: ${reason}\nAll agent operations suspended. Run: rea unfreeze\n`);
343
+ process.exit(2);
344
+ }
345
+ // Resolve git context + base ref using the same primitives the push-
346
+ // gate uses. Missing HEAD short-circuits with an explicit error rather
347
+ // than silently coercing — `rea hook codex-review` is intended for
348
+ // explicit invocation, not for the unborn-HEAD bootstrap path that
349
+ // `rea review` handles.
350
+ const git = createRealGitExecutor(baseDir);
351
+ const headSha = git.headSha();
352
+ if (headSha.length === 0) {
353
+ process.stderr.write('rea hook codex-review: could not resolve HEAD sha — is this a valid git repo with at least one commit?\n');
354
+ process.exit(2);
355
+ }
356
+ const resolved = await resolvePushGatePolicy(baseDir);
357
+ const explicit = options.base !== undefined && options.base.length > 0 ? options.base : undefined;
358
+ const lastN = options.lastNCommits;
359
+ // Delegate base resolution to the shared resolver so shallow-clone /
360
+ // short-history clamping matches `rea hook push-gate` behavior. The
361
+ // resolver returns a fully-resolved SHA + source tag; on a branch
362
+ // shorter than `lastN`, it clamps to the deepest ancestor (or the
363
+ // empty-tree sentinel for orphan/single-commit history) instead of
364
+ // refusing the review.
365
+ const resolvedBase = resolveBaseRef(git, {
366
+ ...(explicit !== undefined ? { explicit } : {}),
367
+ ...(lastN !== undefined && lastN > 0 ? { lastNCommits: lastN } : {}),
368
+ });
369
+ const baseRef = resolvedBase.ref;
370
+ const target = resolvedBase.ref;
371
+ // Allocate the raw-stdout sink. We write to `${tmp}/rea-codex-<sha>.json`
372
+ // where <sha> is a short hex token derived from headSha + a random
373
+ // nonce so concurrent invocations on the same HEAD don't clobber each
374
+ // other (rare in practice — agents queue serially — but cheap to
375
+ // make safe).
376
+ const tmpRoot = options.rawStdoutDir ?? os.tmpdir();
377
+ const nonce = crypto.randomBytes(4).toString('hex');
378
+ const rawPath = path.join(tmpRoot, `rea-codex-${headSha.slice(0, 12)}-${nonce}.json`);
379
+ let rawStream;
380
+ try {
381
+ // mode 0o600: review JSONL contains the unfiltered codex output for
382
+ // the repo being scanned (file paths, code excerpts, finding text).
383
+ // On shared workstations / CI runners other local users could read
384
+ // a default-mode 0644 file. Owner-only is the right floor.
385
+ rawStream = fs.createWriteStream(rawPath, { flags: 'w', mode: 0o600 });
386
+ // createWriteStream() does not throw ENOENT/EACCES/ENOSPC
387
+ // synchronously — it emits an `error` event later. Without a
388
+ // listener, the unhandled stream error terminates the process. Fall
389
+ // back to "no raw tee" instead so a logging failure can never crash
390
+ // the review itself.
391
+ rawStream.once('error', (err) => {
392
+ process.stderr.write(`rea hook codex-review: raw-stdout sink at ${rawPath} failed: ${err.message}\n`);
393
+ rawStream = null;
394
+ });
395
+ }
396
+ catch (e) {
397
+ // Synchronous failures (rare — usually invalid path shape) fall
398
+ // through the same way: the audit entry still gets written, we
399
+ // just lose the raw JSON tee.
400
+ process.stderr.write(`rea hook codex-review: could not open raw-stdout sink at ${rawPath}: ${e instanceof Error ? e.message : String(e)}\n`);
401
+ rawStream = null;
402
+ }
403
+ // Run codex. The runner enforces iron-gate defaults internally —
404
+ // gpt-5.4 + high reasoning unless policy overrides — so we pass
405
+ // policy-resolved values straight through. spawnImpl is forwarded to
406
+ // the test seam.
407
+ let reviewText = '';
408
+ let durationSeconds = 0;
409
+ let codexError;
410
+ try {
411
+ const result = await runCodexReview({
412
+ baseRef,
413
+ cwd: baseDir,
414
+ timeoutMs: resolved.timeout_ms,
415
+ env: process.env,
416
+ ...(resolved.codex_model !== undefined ? { model: resolved.codex_model } : {}),
417
+ ...(resolved.codex_reasoning_effort !== undefined
418
+ ? { reasoningEffort: resolved.codex_reasoning_effort }
419
+ : {}),
420
+ ...(options.spawnImpl !== undefined ? { spawnImpl: options.spawnImpl } : {}),
421
+ ...(rawStream !== null
422
+ ? {
423
+ rawStdoutSink: (chunk) => {
424
+ // Defensive: swallow any write error (closed/destroyed
425
+ // stream, EBADF, ENOSPC). The codex-runner already
426
+ // wraps sink calls in try/catch so a sink failure must
427
+ // never change the verdict — but throwing inside the
428
+ // 'data' handler also triggers an uncaughtException via
429
+ // the readable stream. Catch it here so it stays local.
430
+ try {
431
+ if (!rawStream.writableEnded && !rawStream.destroyed) {
432
+ rawStream.write(chunk);
433
+ }
434
+ }
435
+ catch {
436
+ /* sink failure is non-fatal */
437
+ }
438
+ },
439
+ }
440
+ : {}),
441
+ });
442
+ reviewText = result.reviewText;
443
+ durationSeconds = result.durationSeconds;
444
+ }
445
+ catch (e) {
446
+ codexError = e;
447
+ }
448
+ finally {
449
+ if (rawStream !== null) {
450
+ // End the stream — best-effort. The file is on disk either way,
451
+ // and the OS flushes pending writes when the FD closes.
452
+ try {
453
+ await new Promise((resolve) => {
454
+ rawStream.end(() => resolve());
455
+ });
456
+ }
457
+ catch {
458
+ /* swallow */
459
+ }
460
+ }
461
+ }
462
+ // Translate the codex error (if any) into a verdict + audit-error
463
+ // shape. This mirrors `rea review`'s classifyCodexError + the push-
464
+ // gate's translation, but stays inline so this CLI is self-contained.
465
+ if (codexError !== undefined) {
466
+ const msg = codexError instanceof Error ? codexError.message : String(codexError);
467
+ const kind = codexError instanceof CodexNotInstalledError
468
+ ? 'not-installed'
469
+ : codexError instanceof CodexTimeoutError
470
+ ? 'timeout'
471
+ : codexError instanceof CodexProtocolError
472
+ ? 'protocol'
473
+ : codexError instanceof CodexSubprocessError
474
+ ? 'subprocess'
475
+ : 'unknown';
476
+ let auditHash = '';
477
+ try {
478
+ const record = await appendAuditRecord(baseDir, {
479
+ tool_name: CODEX_REVIEW_TOOL_NAME,
480
+ server_name: CODEX_REVIEW_SERVER_NAME,
481
+ status: InvocationStatus.Error,
482
+ tier: Tier.Read,
483
+ metadata: {
484
+ head_sha: headSha,
485
+ target,
486
+ finding_count: 0,
487
+ verdict: 'error',
488
+ summary: `codex error (${kind}): ${msg}`,
489
+ model: resolved.codex_model ?? IRON_GATE_DEFAULT_MODEL,
490
+ reasoning_effort: resolved.codex_reasoning_effort ?? IRON_GATE_DEFAULT_REASONING,
491
+ raw_path: rawPath,
492
+ duration_seconds: durationSeconds,
493
+ },
494
+ });
495
+ auditHash = record.hash;
496
+ }
497
+ catch (auditErr) {
498
+ // Audit failure must NOT change the exit code, but we surface it.
499
+ process.stderr.write(`rea hook codex-review: audit append failed: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}\n`);
500
+ }
501
+ process.stderr.write(`[codex-review] verdict=error kind=${kind} findings=0 audit=${auditHash.slice(0, 16)} raw=${rawPath}\n`);
502
+ process.stderr.write(`[codex-review] error: ${msg}\n`);
503
+ if (options.json === true) {
504
+ process.stdout.write(JSON.stringify({
505
+ verdict: 'error',
506
+ kind,
507
+ finding_count: 0,
508
+ head_sha: headSha,
509
+ target,
510
+ audit_hash: auditHash,
511
+ raw_path: rawPath,
512
+ exit_code: 2,
513
+ message: msg,
514
+ }) + '\n');
515
+ }
516
+ process.exit(2);
517
+ }
518
+ // Codex exited cleanly — parse the review prose and translate to a
519
+ // verdict + finding count.
520
+ const summary = summarizeReview(reviewText);
521
+ const verdict = summary.verdict;
522
+ const findingCount = summary.findings.length;
523
+ // First non-empty paragraph of the review text becomes the audit
524
+ // summary line. Truncated to 240 chars so the audit log doesn't blow
525
+ // up on multi-paragraph review prose.
526
+ const summaryLine = (() => {
527
+ const firstPara = reviewText
528
+ .split(/\n{2,}/)
529
+ .map((p) => p.trim())
530
+ .find((p) => p.length > 0);
531
+ if (firstPara === undefined)
532
+ return '';
533
+ const oneLine = firstPara.replace(/\s+/g, ' ');
534
+ return oneLine.length > 240 ? oneLine.slice(0, 237) + '...' : oneLine;
535
+ })();
536
+ let auditHash = '';
537
+ try {
538
+ const record = await appendAuditRecord(baseDir, {
539
+ tool_name: CODEX_REVIEW_TOOL_NAME,
540
+ server_name: CODEX_REVIEW_SERVER_NAME,
541
+ status: verdict === 'blocking' ? InvocationStatus.Denied : InvocationStatus.Allowed,
542
+ tier: Tier.Read,
543
+ metadata: {
544
+ head_sha: headSha,
545
+ target,
546
+ finding_count: findingCount,
547
+ verdict,
548
+ ...(summaryLine.length > 0 ? { summary: summaryLine } : {}),
549
+ model: resolved.codex_model ?? IRON_GATE_DEFAULT_MODEL,
550
+ reasoning_effort: resolved.codex_reasoning_effort ?? IRON_GATE_DEFAULT_REASONING,
551
+ raw_path: rawPath,
552
+ duration_seconds: durationSeconds,
553
+ },
554
+ });
555
+ auditHash = record.hash;
556
+ }
557
+ catch (auditErr) {
558
+ process.stderr.write(`rea hook codex-review: audit append failed: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}\n`);
559
+ }
560
+ // Map verdict → exit code. Same as the push-gate's contract.
561
+ const exitCode = verdict === 'blocking' ? 2 : verdict === 'concerns' ? 1 : 0;
562
+ // Terse status line on stderr. The directive is "the codex JSON IS
563
+ // the review" — agents read raw_path to act on findings, not this
564
+ // line. The line exists so a human running this from a shell sees
565
+ // the verdict at a glance.
566
+ process.stderr.write(`[codex-review] verdict=${verdict} findings=${String(findingCount)} audit=${auditHash.slice(0, 16)} raw=${rawPath}\n`);
567
+ if (options.json === true) {
568
+ process.stdout.write(JSON.stringify({
569
+ verdict,
570
+ finding_count: findingCount,
571
+ head_sha: headSha,
572
+ target,
573
+ audit_hash: auditHash,
574
+ raw_path: rawPath,
575
+ exit_code: exitCode,
576
+ }) + '\n');
577
+ }
578
+ process.exit(exitCode);
579
+ }
322
580
  /**
323
- * Attach the `rea hook` subcommand tree to a commander Program. Two
324
- * subcommands today: `push-gate` and `scan-bash`. New hooks should land
325
- * here rather than as top-level commands so the CLI surface stays
326
- * navigable.
581
+ * Attach the `rea hook` subcommand tree to a commander Program.
582
+ *
583
+ * Subcommands:
584
+ * - `push-gate` — stateless pre-push Codex review (called by husky).
585
+ * - `scan-bash` — parser-backed bash-tier scanner (called by Claude
586
+ * Code shim hooks).
587
+ * - `policy-get` — single-source-of-truth policy reader for bash hooks.
588
+ * - `codex-review` — thin Bash-direct codex invocation (0.27.0+) for
589
+ * marathon-mode review cycles. The canonical
590
+ * invocation that all agents and slash commands
591
+ * route through.
592
+ *
593
+ * New hooks should land here rather than as top-level commands so the
594
+ * CLI surface stays navigable.
327
595
  */
328
596
  export function registerHookCommand(program) {
329
597
  const hook = program
@@ -366,6 +634,25 @@ export function registerHookCommand(program) {
366
634
  ...(opts.lastNCommits !== undefined ? { lastNCommits: opts.lastNCommits } : {}),
367
635
  });
368
636
  });
637
+ hook
638
+ .command('codex-review')
639
+ .description('Run `codex exec review --json --ephemeral` directly against the working tree, tee raw JSONL to a tempfile, write a `codex.review` audit entry, and emit a terse status line on stderr. Exits 0/1/2: pass/concerns/blocking. The canonical Bash-direct codex invocation (0.27.0+) — minimizes Claude consumption of codex output by NOT paraphrasing findings into prose.')
640
+ .option('--base <ref>', 'explicit base ref to diff against (default: @{upstream} → origin/HEAD → main/master)')
641
+ .option('--last-n-commits <n>', 'narrow review to the last N commits (diff against HEAD~N). Loses to --base when both are set.', (raw) => {
642
+ const n = Number(raw);
643
+ if (!Number.isInteger(n) || n <= 0) {
644
+ throw new Error(`--last-n-commits must be a positive integer, got ${JSON.stringify(raw)}`);
645
+ }
646
+ return n;
647
+ })
648
+ .option('--json', 'emit a single-line JSON result on stdout (in addition to the stderr status line)')
649
+ .action(async (opts) => {
650
+ await runHookCodexReview({
651
+ ...(opts.base !== undefined ? { base: opts.base } : {}),
652
+ ...(opts.lastNCommits !== undefined ? { lastNCommits: opts.lastNCommits } : {}),
653
+ ...(opts.json === true ? { json: true } : {}),
654
+ });
655
+ });
369
656
  hook
370
657
  .command('policy-get')
371
658
  .description('Read a value from `.rea/policy.yaml` via the canonical YAML parser. Used by bash-tier hooks (`hooks/_lib/policy-read.sh::policy_nested_scalar`) so inline AND block YAML forms agree at a single source of truth. Default scalar mode: prints raw value or empty. With `--json`: emits JSON (scalar or object/array; missing path → `null`). Unparseable YAML → empty / null, exit 1.')
@@ -119,6 +119,15 @@ export interface CodexRunOptions {
119
119
  cwd: string;
120
120
  env: NodeJS.ProcessEnv;
121
121
  }) => ChildProcessWithoutNullStreams;
122
+ /**
123
+ * 0.27.0 — optional callback fired for every raw stdout chunk from
124
+ * `codex exec review`. Used by `rea hook codex-review` (the thin Bash-
125
+ * direct CLI) to tee the JSONL stream into a tempfile so the caller can
126
+ * read the un-summarized review JSON directly. Errors thrown from the
127
+ * sink are caught and ignored — sink failure must NEVER change the
128
+ * verdict. Receives chunks in arrival order.
129
+ */
130
+ rawStdoutSink?: (chunk: Buffer) => void;
122
131
  }
123
132
  export interface CodexRunResult {
124
133
  /** The concatenated text of every `item.completed` agent_message item. */
@@ -262,7 +262,20 @@ export async function runCodexReview(options) {
262
262
  reject(new CodexTimeoutError(options.timeoutMs));
263
263
  }, options.timeoutMs);
264
264
  timer.unref?.();
265
- child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
265
+ child.stdout.on('data', (chunk) => {
266
+ stdoutChunks.push(chunk);
267
+ // 0.27.0 raw-stdout tee for `rea hook codex-review`. Sink errors
268
+ // are swallowed — a bad sink must not make a passing review fail.
269
+ const sink = options.rawStdoutSink;
270
+ if (sink !== undefined) {
271
+ try {
272
+ sink(chunk);
273
+ }
274
+ catch {
275
+ /* sink failure is non-fatal */
276
+ }
277
+ }
278
+ });
266
279
  child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
267
280
  child.on('error', (e) => {
268
281
  clearTimeout(timer);
@@ -162,6 +162,11 @@ _rea_unwrap_at_depth() {
162
162
  local _unwrap_sep
163
163
  _unwrap_sep=$'\x1c\x1d'
164
164
  local masked
165
+ # shellcheck disable=SC1078
166
+ # SC1078 fires inside the awk program because shellcheck's bash parser
167
+ # cannot model awk's nested-quote semantics (`'\''` here is the
168
+ # bash-to-awk single-apostrophe escape pattern, not an unclosed shell
169
+ # string). Verified false-positive — the awk program parses cleanly.
165
170
  masked=$(printf '%s%s' "$cmd" "$_unwrap_sep" | awk '
166
171
  BEGIN { RS = "\034\035" }
167
172
  {
@@ -527,6 +532,11 @@ _rea_split_segments() {
527
532
  # records; the existing pipeline then quote-masks and splits each
528
533
  # record independently. Inner payload anchors trigger words for the
529
534
  # `any_segment_*` checks downstream.
535
+ # shellcheck disable=SC1078
536
+ # SC1078 fires inside the awk program because shellcheck's bash parser
537
+ # cannot model awk's nested-quote semantics (`'\''` here is the
538
+ # bash-to-awk single-apostrophe escape pattern, not an unclosed shell
539
+ # string). Verified false-positive — the awk program parses cleanly.
530
540
  _rea_unwrap_nested_shells "$cmd" \
531
541
  | awk '
532
542
  BEGIN {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.26.1",
3
+ "version": "0.27.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)",