@bookedsolid/rea 0.39.0 → 0.41.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.
package/README.md CHANGED
@@ -9,8 +9,13 @@
9
9
  [![DCO](https://img.shields.io/badge/DCO-required-green)](https://developercertificate.org/)
10
10
  [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)
11
11
 
12
- > Status: `0.11.0` — published to npm with SLSA v1 provenance. See
13
- > [CHANGELOG.md](./CHANGELOG.md) for the per-release history.
12
+ > Status: `0.41.0` — published to npm with SLSA v1 provenance. See
13
+ > [CHANGELOG.md](./CHANGELOG.md) for the per-release history. The
14
+ > hook-port marathon (0.32→0.35) replaced every shell hook body with
15
+ > a Node-binary shim; the bash files in `.claude/hooks/` are now
16
+ > ~30-line stubs that fork `rea hook scan-bash` / `scan-write` for
17
+ > the real work. See [Architecture](#architecture) for the runtime
18
+ > picture.
14
19
 
15
20
  REA is a single npm package that gates and audits agentic tool calls made by
16
21
  Claude Code — shell commands, filesystem writes, and MCP tool invocations —
@@ -38,6 +43,7 @@ the [migration section](#migration-from-010x) below.
38
43
  - [Quickstart](#quickstart)
39
44
  - [What REA is](#what-rea-is)
40
45
  - [What REA is NOT](#what-rea-is-not)
46
+ - [Architecture](#architecture)
41
47
  - [The pre-push Codex gate](#the-pre-push-codex-gate)
42
48
  - [MCP gateway](#mcp-gateway)
43
49
  - [Policy file](#policy-file)
@@ -141,31 +147,39 @@ does not prevent the kill-switch from firing.
141
147
 
142
148
  ### 3. A hook layer
143
149
 
144
- Eleven shell scripts ship in `hooks/` and are copied into `.claude/hooks/`
145
- by `rea init`. All eleven are wired into the default `.claude/settings.json`
146
- and fire on Claude Code's `PreToolUse` / `PostToolUse` events (secret
147
- scanning, dangerous-command interception, blocked-path enforcement,
148
- settings protection, attribution rejection, env-file protection,
149
- disclosure-policy routing, dependency audit, changeset security,
150
- PR-issue-link advisory, architecture advisory). Each hook uses
151
- `set -euo pipefail` (or `set -uo pipefail` for stdin-JSON consumers) and
152
- runs a HALT check near the top. See [Hooks shipped](#hooks-shipped) for
153
- the full inventory.
154
-
155
- **Bash-tier scanner (parser-backed since 0.23.0).** Two hooks
156
- `protected-paths-bash-gate.sh` and `blocked-paths-bash-gate.sh` are
157
- shims that forward stdin to `rea hook scan-bash`, a CLI subcommand
158
- that parses the Bash command via `mvdan-sh@0.10.1`, walks the AST,
159
- and emits a verdict JSON. Pre-0.23.0 these were 500-line bash regex
160
- pipelines; the rewrite closes 24 known-bypass classes
161
- (helix-021..023 + discord-ops Round 13 + codex round 1) by replacing
162
- re-tokenization heuristics with structural matches against the parsed
163
- argv tree. The other nine hooks remain regex-based bash. The shim
164
- re-verifies the verdict JSON shape on return so a tampered
165
- `REA_NODE_CLI` env var cannot bypass. See
150
+ Fourteen hook scripts ship in `hooks/` and are copied into
151
+ `.claude/hooks/` by `rea init`. All fourteen are wired into the default
152
+ `.claude/settings.json` and fire on Claude Code's `PreToolUse` /
153
+ `PostToolUse` events (secret scanning, dangerous-command interception,
154
+ blocked-path enforcement, settings protection, attribution rejection,
155
+ env-file protection, disclosure-policy routing, dependency audit,
156
+ changeset security, PR-issue-link advisory, architecture advisory,
157
+ local-review enforcement, protected-paths + blocked-paths bash-tier
158
+ parity). Each hook performs a HALT check near the top. See
159
+ [Hooks shipped](#hooks-shipped) for the full inventory.
160
+
161
+ **Node-binary scanners (since 0.32.0).** The hook-port marathon
162
+ (0.32.0 0.35.0) replaced every shell hook body with a Node-binary
163
+ shim. The bash files in `.claude/hooks/` are now ~30-line stubs that
164
+ fork `rea hook scan-bash` / `rea hook scan-write` (the AST-walker and
165
+ write-tier scanner respectively) and re-verify the verdict JSON shape
166
+ on return a tampered `REA_NODE_CLI` env var cannot bypass. The
167
+ parser-tier walker (`mvdan-sh@0.10.1`) handles Bash AST grammar
168
+ exhaustively; the write-tier scanner handles `Write`/`Edit`/
169
+ `MultiEdit`/`NotebookEdit` payloads against the same policy. See
166
170
  [`docs/architecture/bash-scanner.md`](docs/architecture/bash-scanner.md)
167
- for the AST-walker design and [`docs/migration/0.23.0.md`](docs/migration/0.23.0.md)
168
- for consumer migration notes.
171
+ and [`docs/migration/0.23.0.md`](docs/migration/0.23.0.md) for the
172
+ walker design and consumer migration notes.
173
+
174
+ **Four-tier policy reader.** The shim infrastructure honors a
175
+ four-tier ladder when loading policy (`hooks/_lib/policy-reader.sh`):
176
+ Tier 1 is `rea hook policy-get` (the dist CLI), Tier 2 is
177
+ `python3 + PyYAML`, Tier 3 is `awk` (block-form only), and an
178
+ optional `jq` accelerator for JSON walks. `rea doctor` probes every
179
+ tier and surfaces which one(s) are reachable in the operator's
180
+ environment — pre-0.39.0 a stale dist + missing PyYAML would
181
+ silently no-op flow-form policy when `awk` was the only working
182
+ tier. See `Self-validation` below.
169
183
 
170
184
  The hook layer runs independently of the MCP gateway — bypassing one does
171
185
  not disable the other. That redundancy is intentional.
@@ -220,6 +234,71 @@ The non-goals are the product.
220
234
 
221
235
  ---
222
236
 
237
+ ## Architecture
238
+
239
+ REA ships as a single npm package that delivers five runtime surfaces:
240
+
241
+ 1. **Node-binary CLI** (`dist/cli/index.js`) — the `rea` command, with
242
+ subcommands for install (`init`/`upgrade`), runtime
243
+ (`serve`/`check`/`status`/`doctor`), kill-switch (`freeze`/
244
+ `unfreeze`), audit (`rotate`/`verify`/`summary`/`specialists`),
245
+ review (`review`/`preflight`/`hook push-gate`), and hook
246
+ evaluation (`hook scan-bash`/`scan-write`/`policy-get`).
247
+ 2. **Shell hook shims** (`hooks/*.sh` → `.claude/hooks/*.sh`) — ~30
248
+ lines apiece. Each shim performs a HALT check, then forks the
249
+ corresponding `rea hook scan-*` Node CLI for the real verdict.
250
+ Pre-0.32.0 these were 500+ line bash regex pipelines; the rewrite
251
+ closed 24 known bypass classes and removed the bash hot-path
252
+ entirely. The shims still ship as bash so Claude Code's hook
253
+ matcher (which spawns sh) works without a Node prerequisite at
254
+ hook-fire time — the Node CLI is invoked from inside.
255
+ 3. **Husky hooks** (`.husky/commit-msg`, `.husky/pre-push`,
256
+ `.husky/prepare-commit-msg`) — written by `rea init`. Pre-push
257
+ runs `rea hook push-gate` (stateless Codex review). Commit-msg
258
+ blocks AI attribution and DCO violations. Prepare-commit-msg
259
+ optionally appends a `Co-Authored-By:` trailer when configured.
260
+ Extension surfaces in `.husky/{commit-msg,pre-push}.d/*` are
261
+ sourced after rea's body for layering commitlint, lint-staged,
262
+ etc.
263
+ 4. **MCP gateway** (`rea serve`) — a stdio MCP server started by
264
+ Claude Code via `.mcp.json`. Proxies downstream MCPs declared in
265
+ `.rea/registry.yaml` through a fixed middleware chain (audit,
266
+ kill-switch, tier, policy, blocked-paths, rate-limit, breaker,
267
+ injection, redact, size-cap). See [MCP gateway](#mcp-gateway).
268
+ 5. **Four-tier policy reader** (`hooks/_lib/policy-reader.sh`) — the
269
+ shared helper that bash shims use to read `.rea/policy.yaml`.
270
+ Tier 1 calls `rea hook policy-get` (full YAML semantics). Tier 2
271
+ falls back to `python3 + PyYAML` when the CLI is unreachable.
272
+ Tier 3 falls back to `awk` for the block-form subset. `jq` is an
273
+ optional JSON accelerator. Each tier downgrades silently to the
274
+ next; `rea doctor` probes the ladder explicitly so operators see
275
+ exactly which tier(s) work in their environment.
276
+
277
+ ### Self-validation
278
+
279
+ `rea doctor` validates every install surface in one shot:
280
+
281
+ - `.rea/policy.yaml` parses against the strict zod schema
282
+ - `.rea/` directory layout (HALT, registry, fingerprints, audit log)
283
+ - `.claude/settings.json` schema + every shipped hook registered
284
+ - `.husky/commit-msg`, `.husky/pre-push`, `.husky/prepare-commit-msg`
285
+ exist + have the expected marker, with husky-9 stub indirection
286
+ followed transparently
287
+ - `codex` binary on `PATH` when `policy.review.codex_required: true`
288
+ - Per-tier policy reader probe (`rea hook policy-get` → `python3 +
289
+ PyYAML` → `awk` → optional `jq`) so silent flow-form no-ops are
290
+ caught at install time, not at first hook fire
291
+ - Optional `--smoke` drives the real delegation-capture hook
292
+ end-to-end (writes a probe audit record + verifies chain integrity)
293
+ - Optional `--drift` reports per-file SHA drift vs. the install
294
+ manifest without mutating
295
+ - `--strict` promotes settings-schema warnings to hard fail (for CI)
296
+
297
+ This repo dogfoods every check — see `.rea/` and `.claude/` in the
298
+ checkout for the canonical `bst-internal` profile layout.
299
+
300
+ ---
301
+
223
302
  ## The pre-push Codex gate
224
303
 
225
304
  The 0.11.0 gate is stateless. Every `git push` runs Codex on the diff, and
@@ -864,12 +943,27 @@ Sync `.claude/`, `.husky/`, and managed fragments with this rea version.
864
943
  Prompts on drift; silently refreshes unmodified files.
865
944
 
866
945
  ```bash
867
- rea upgrade --dry-run # show what would change; write nothing
868
- rea upgrade # interactive
869
- rea upgrade -y # non-interactive, keep drifted files
870
- rea upgrade --force # non-interactive, overwrite drift
946
+ rea upgrade --dry-run # rehearse the interactive flow; write nothing
947
+ rea upgrade --check # structured preview + unified diffs (0.41.0)
948
+ rea upgrade --check --json # machine-readable preview document
949
+ rea upgrade --check --no-diff # paths + counts only (large repos)
950
+ rea upgrade # interactive
951
+ rea upgrade -y # non-interactive, keep drifted files
952
+ rea upgrade --force # non-interactive, overwrite drift
871
953
  ```
872
954
 
955
+ `--check` and `--dry-run` are distinct:
956
+
957
+ - `--dry-run` rehearses the full interactive flow with writes
958
+ suppressed (prompts still fire, output streams in classification
959
+ order). Useful locally to walk through the same prompts you'd see
960
+ during a real upgrade.
961
+ - `--check` is the structured, non-interactive preview: emits a
962
+ summary table + unified diffs per modified file, exits 0
963
+ regardless of what would change. The shape mirrors
964
+ `terraform plan` / `npm install --dry-run` — wire it into CI to
965
+ surface the changes an upgrade PR would produce.
966
+
873
967
  ### `rea serve`
874
968
 
875
969
  Start the MCP gateway. Invoked by Claude Code via `.mcp.json`; not a
@@ -908,27 +1002,50 @@ rea status --json # pipe to jq
908
1002
 
909
1003
  ### `rea doctor`
910
1004
 
911
- Validate the install — policy parses, `.rea/` layout, hooks, Codex plugin
912
- presence, TOFU fingerprint store.
1005
+ Validate the install — policy parses, `.rea/` layout, hooks, Codex
1006
+ plugin presence, TOFU fingerprint store, husky stub indirection,
1007
+ and the four-tier policy reader ladder (`rea hook policy-get` →
1008
+ `python3 + PyYAML` → `awk` → optional `jq`).
913
1009
 
914
1010
  ```bash
915
1011
  rea doctor
916
1012
  rea doctor --metrics # also print 7-day Codex telemetry summary
917
1013
  rea doctor --drift # report drift vs. install manifest (read-only)
1014
+ rea doctor --smoke # exercise delegation-capture hook end-to-end
1015
+ rea doctor --strict # 0.30.0 — promote settings-schema warnings to fail
918
1016
  ```
919
1017
 
920
- In non-git directories the commit-msg and pre-push checks are skipped
921
- cleanly. Audit hash-chain integrity is verified by `rea audit verify`,
922
- not by `rea doctor`.
1018
+ In non-git directories the commit-msg and pre-push checks are
1019
+ skipped cleanly. Audit hash-chain integrity is verified by `rea
1020
+ audit verify`, not by `rea doctor`. Each policy-reader tier is
1021
+ probed independently so silent flow-form no-ops (e.g. stale dist +
1022
+ missing PyYAML, with awk handling block-form only) surface at
1023
+ install time instead of at first hook fire.
923
1024
 
924
- ### `rea audit rotate` / `rea audit verify`
1025
+ ### `rea audit rotate` / `rea audit verify` / `rea audit summary` / `rea audit specialists`
925
1026
 
926
1027
  ```bash
927
1028
  rea audit rotate # force rotation now
928
1029
  rea audit verify # re-hash the chain; exit 1 on first tamper
929
1030
  rea audit verify --since <file> # walk forward from a rotated file
1031
+
1032
+ rea audit summary # 0.41.0 — counts by tool/tier/session/status
1033
+ rea audit summary --since 24h # filter to last 24h (units: s/m/h/d/w)
1034
+ rea audit summary --since 7d --json # machine-readable rollup for jq
1035
+
1036
+ rea audit specialists # delegation-telemetry roll-up
1037
+ rea audit specialists --session all # show every session (default: $CLAUDE_SESSION_ID)
1038
+ rea audit specialists --since <file> # extend the walk through rotated files
930
1039
  ```
931
1040
 
1041
+ `rea audit summary` is the high-level overview reader: total events,
1042
+ counts grouped by `tool_name` / tier / status / session, the time
1043
+ window covered, and a sample-verified chain-integrity check. Note
1044
+ that `--since` for `summary` is a duration (`24h`, `7d`) — distinct
1045
+ from `--since <rotated-file>` on `verify` / `specialists` which
1046
+ anchors on a rotated-audit basename. Use `rea audit verify` for the
1047
+ rigorous per-record re-hash; `summary` only samples.
1048
+
932
1049
  ### `rea tofu list` / `rea tofu accept`
933
1050
 
934
1051
  ```bash
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `rea audit summary` — high-level audit-log overview (0.41.0).
3
+ *
4
+ * The audit log is rich and `rea audit specialists` already exists for
5
+ * one narrow event-class. `rea audit summary` complements it with a
6
+ * broad rollup: total events, counts by `tool_name`, by tier, by
7
+ * session, by status, the time window covered, and a sample-verified
8
+ * chain-integrity check.
9
+ *
10
+ * # Filtering
11
+ *
12
+ * `--since <duration>` accepts a compact duration string
13
+ * (`s`/`m`/`h`/`d`/`w`) and filters records by `timestamp >= now -
14
+ * duration`. Examples: `24h`, `7d`, `90m`, `2w`. This is DIFFERENT
15
+ * from `rea audit verify --since <file>` / `rea audit specialists
16
+ * --since <file>` which take a rotated-file ANCHOR, not a duration —
17
+ * the two `--since` semantics serve different needs (anchor for chain
18
+ * walks, duration for summarization) and we accept the surface area
19
+ * cost. The duration form is what consumers reach for when asking
20
+ * "what happened in the last day?".
21
+ *
22
+ * # Chain integrity
23
+ *
24
+ * `rea audit verify` does the rigorous per-record re-hash. `summary`
25
+ * samples up to `CHAIN_SAMPLE_SIZE` records, evenly spaced through
26
+ * the filtered window, and reports `ok` / `tampered` / `unsampled`
27
+ * (window empty). Operators who suspect tampering should still run
28
+ * `rea audit verify` for an authoritative answer.
29
+ *
30
+ * # JSON output
31
+ *
32
+ * {
33
+ * "schema_version": 1,
34
+ * "window_seconds": 86400,
35
+ * "window_start": "2026-05-15T13:42:00Z",
36
+ * "window_end": "2026-05-16T13:42:00Z",
37
+ * "files_scanned": ["/abs/path/.rea/audit.jsonl"],
38
+ * "total_events": 1247,
39
+ * "by_tool_name": { "Bash": 612, "Edit": 289, … },
40
+ * "by_tier": { "read": 683, "write": 416, "destructive": 148 },
41
+ * "by_status": { "allowed": 1242, "denied": 5, "error": 0 },
42
+ * "by_session": { "session-abc…": 312, "session-def…": 935 },
43
+ * "session_count": 8,
44
+ * "earliest_timestamp": "2026-05-15T13:43:01.103Z",
45
+ * "latest_timestamp": "2026-05-16T13:41:57.842Z",
46
+ * "chain_integrity": "ok",
47
+ * "chain_samples_verified": 12
48
+ * }
49
+ *
50
+ * # Walk scope
51
+ *
52
+ * v1 walks the current `.rea/audit.jsonl` plus EVERY rotated file
53
+ * whose latest record falls within the window. Older rotated files
54
+ * are skipped — they cannot contain in-window records. When `--since`
55
+ * is omitted, no time filter is applied and the walk covers the
56
+ * current `audit.jsonl` only (operators wanting historical depth
57
+ * should pass `--since <DUR>`).
58
+ */
59
+ import type { Command } from 'commander';
60
+ export declare const AUDIT_SUMMARY_SCHEMA_VERSION = 1;
61
+ /** Hard cap on chain-integrity samples. Keeps `rea audit summary`
62
+ * fast even on large logs while still surfacing obvious tampering. */
63
+ export declare const CHAIN_SAMPLE_SIZE = 12;
64
+ /**
65
+ * Thrown by `computeAuditSummary` when `--since` cannot be parsed.
66
+ * The commander wrapper exits 1.
67
+ */
68
+ export declare class AuditSummarySinceError extends Error {
69
+ constructor(message: string);
70
+ }
71
+ /** Three-state classification for chain integrity. */
72
+ export type ChainIntegrity = 'ok' | 'tampered' | 'unsampled';
73
+ export interface AuditSummaryResult {
74
+ schema_version: typeof AUDIT_SUMMARY_SCHEMA_VERSION;
75
+ /** Window length in seconds. `null` when no `--since` filter was set. */
76
+ window_seconds: number | null;
77
+ /** Inclusive window start. `null` when no filter; otherwise the
78
+ * computed cutoff (now - duration). */
79
+ window_start: string | null;
80
+ /** Window end — always `now` when filter is set; `null` otherwise. */
81
+ window_end: string | null;
82
+ /** Absolute paths of audit files walked. */
83
+ files_scanned: string[];
84
+ total_events: number;
85
+ by_tool_name: Record<string, number>;
86
+ by_tier: Record<string, number>;
87
+ by_status: Record<string, number>;
88
+ by_session: Record<string, number>;
89
+ session_count: number;
90
+ /** Earliest in-window `timestamp` seen. `null` when no records. */
91
+ earliest_timestamp: string | null;
92
+ /** Latest in-window `timestamp` seen. `null` when no records. */
93
+ latest_timestamp: string | null;
94
+ chain_integrity: ChainIntegrity;
95
+ /** Number of samples actually verified. Always `<= CHAIN_SAMPLE_SIZE`. */
96
+ chain_samples_verified: number;
97
+ }
98
+ export interface ComputeAuditSummaryOptions {
99
+ /** Override CWD. Tests set this; production uses `process.cwd()`. */
100
+ baseDir?: string;
101
+ /** Raw `--since` value (e.g. `24h`, `7d`). Parsed via parseDuration. */
102
+ since?: string;
103
+ /** Test seam — pin "now" for deterministic window calculations. */
104
+ now?: Date;
105
+ }
106
+ /**
107
+ * Parse a compact duration string into seconds. Accepts:
108
+ *
109
+ * - `<N>s` — seconds
110
+ * - `<N>m` — minutes
111
+ * - `<N>h` — hours
112
+ * - `<N>d` — days
113
+ * - `<N>w` — weeks (7 days)
114
+ *
115
+ * `N` must be a positive integer with no whitespace. Returns the
116
+ * number of seconds; throws `AuditSummarySinceError` on parse failure.
117
+ *
118
+ * We deliberately do not accept bare numbers (would be ambiguous) or
119
+ * fractional units (no real use case; complicates rendering).
120
+ */
121
+ export declare function parseDurationSeconds(raw: string): number;
122
+ /**
123
+ * Compute the summary. Pure (read-only). Throws
124
+ * `AuditSummarySinceError` on bad `--since`; everything else is
125
+ * surfaced via the result.
126
+ */
127
+ export declare function computeAuditSummary(options?: ComputeAuditSummaryOptions): Promise<AuditSummaryResult>;
128
+ /**
129
+ * Render the result as a human-readable terminal block. Designed for
130
+ * the default `rea audit summary` invocation; `--json` callers bypass
131
+ * this entirely.
132
+ */
133
+ export declare function renderAuditSummary(result: AuditSummaryResult): string;
134
+ export interface RunAuditSummaryOptions {
135
+ since?: string;
136
+ json?: boolean;
137
+ /** Test seam — pin "now". */
138
+ now?: Date;
139
+ }
140
+ /** Commander entrypoint. */
141
+ export declare function runAuditSummary(options: RunAuditSummaryOptions): Promise<void>;
142
+ /**
143
+ * Register `rea audit summary` under the `audit` command group.
144
+ */
145
+ export declare function registerAuditSummaryCommand(auditCommand: Command): void;