@bookedsolid/rea 0.11.0 → 0.13.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
@@ -1,6 +1,6 @@
1
1
  # REA
2
2
 
3
- **Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review.**
3
+ **Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and a stateless pre-push Codex review gate.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/%40bookedsolid%2Frea?color=cb3837&label=npm)](https://www.npmjs.com/package/@bookedsolid/rea)
6
6
  [![CI](https://github.com/bookedsolidtech/rea/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bookedsolidtech/rea/actions/workflows/ci.yml)
@@ -9,68 +9,169 @@
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.9.x` — published to npm with provenance. See
12
+ > Status: `0.11.0` — published to npm with SLSA v1 provenance. See
13
13
  > [CHANGELOG.md](./CHANGELOG.md) for the per-release history.
14
14
 
15
+ REA is a single npm package that gates and audits agentic tool calls made by
16
+ Claude Code — shell commands, filesystem writes, and MCP tool invocations —
17
+ against an operator-defined policy file. It ships an MCP middleware gateway,
18
+ a set of hook scripts, a pre-push Codex review gate, a hash-chained audit
19
+ log, and a hard kill-switch. Every layer fails closed.
20
+
21
+ **What changed in 0.11.0.** Through 0.10.x the push-review gate asked "has a
22
+ qualifying Codex receipt been recorded for this HEAD SHA?" and consulted a
23
+ `.rea/review-cache.jsonl` + audit-record lookup to decide. Agents spent a
24
+ meaningful fraction of every push cycle fabricating attestations with `rea
25
+ cache set` and `rea audit record codex-review --also-set-cache`, and the
26
+ bash core around the gate grew to ~1,250 lines. 0.11.0 deletes that stack
27
+ and replaces it with a single subcommand — `rea hook push-gate` — that
28
+ **runs Codex on every push**, parses the verdict from the streamed review
29
+ output, writes the findings to `.rea/last-review.json`, and blocks exit
30
+ code `2` on `[P1]` or (by default) `[P2]` findings. Readers landing on
31
+ 0.9/0.10-era docs should treat the "record-and-cache" flow as gone — see
32
+ the [migration section](#migration-from-010x) below.
33
+
34
+ ---
35
+
36
+ ## Table of contents
37
+
38
+ - [Quickstart](#quickstart)
39
+ - [What REA is](#what-rea-is)
40
+ - [What REA is NOT](#what-rea-is-not)
41
+ - [The pre-push Codex gate](#the-pre-push-codex-gate)
42
+ - [MCP gateway](#mcp-gateway)
43
+ - [Policy file](#policy-file)
44
+ - [Hooks shipped](#hooks-shipped)
45
+ - [Slash commands](#slash-commands)
46
+ - [Curated agents](#curated-agents)
47
+ - [CLI reference](#cli-reference)
48
+ - [Migration from 0.10.x](#migration-from-010x)
49
+ - [Contributor quality gates](#contributor-quality-gates)
50
+ - [Threat model and security](#threat-model-and-security)
51
+ - [Non-goals](#non-goals)
52
+ - [License and contributing](#license-and-contributing)
53
+
15
54
  ---
16
55
 
17
- ## Installation
56
+ ## Quickstart
18
57
 
19
58
  ```bash
20
59
  npx @bookedsolid/rea init
21
60
  ```
22
61
 
23
- The `init` command is an interactive wizard. It detects your project, writes
24
- `.rea/policy.yaml`, copies hooks and slash commands into `.claude/`, wires
25
- `.mcp.json` to run `rea serve` as a governance gateway, installs a
26
- `.husky/commit-msg` hook, and appends a managed fragment to `CLAUDE.md`.
62
+ The `init` wizard detects your project, writes `.rea/policy.yaml`, copies
63
+ curated hooks and slash commands into `.claude/`, wires `.mcp.json` to run
64
+ `rea serve` as a governance gateway, installs `.husky/commit-msg` and
65
+ `.husky/pre-push` hooks, and appends a managed fragment to `CLAUDE.md`. Run
66
+ it non-interactively with `-y`:
67
+
68
+ ```bash
69
+ npx @bookedsolid/rea init -y --profile bst-internal
70
+ ```
71
+
72
+ Node 22+ and pnpm 9.12+ are required. The package is published with npm
73
+ provenance (SLSA v1) from the `main` branch via GitHub Actions OIDC.
74
+
75
+ **Your first push.** After a feature commit, `git push` will:
76
+
77
+ 1. `.husky/pre-push` checks `.rea/HALT` and delegates to `rea hook push-gate`.
78
+ 2. `rea hook push-gate` loads `.rea/policy.yaml`, resolves a base ref, and
79
+ shells out to `codex exec review --base <ref> --json --ephemeral`.
80
+ 3. Codex streams JSONL events back; the gate parses `[P1]`/`[P2]`/`[P3]`
81
+ findings out of the `agent_message` text.
82
+ 4. A severity-sorted summary is printed to stderr; full findings with file
83
+ and line detail are written atomically to `.rea/last-review.json`.
84
+ 5. An audit record (`rea.push_gate.reviewed`) is appended to
85
+ `.rea/audit.jsonl`.
86
+ 6. Exit code is `0` (pass), `1` (HALT), or `2` (blocked by verdict or
87
+ Codex error).
88
+
89
+ On a blocking verdict the push fails; the in-session Claude agent reads the
90
+ stderr summary and the `.rea/last-review.json` payload, fixes the issues,
91
+ commits, and pushes again. No cache, no receipt to fabricate.
92
+
93
+ Verify the install:
94
+
95
+ ```bash
96
+ rea doctor
97
+ ```
98
+
99
+ Freeze everything if something unexpected happens:
100
+
101
+ ```bash
102
+ rea freeze --reason "incident triage; investigate unexpected .env write"
103
+ # later
104
+ rea unfreeze
105
+ ```
27
106
 
28
- Node 22+ and pnpm 9+ required.
107
+ ---
29
108
 
30
109
  ## What REA is
31
110
 
32
- REA is a governance layer for Claude Code. It is a single npm package that
33
- ships four things:
34
-
35
- 1. A **hook layer** — 14 shell scripts total. 12 are registered in the
36
- shipped `.claude/settings.json` and fire on Claude Code's `PreToolUse`
37
- / `PostToolUse` events (secret scanning, dangerous-command
38
- interception, blocked-path protection, settings protection,
39
- attribution rejection, env-file protection, disclosure-policy
40
- routing, dependency audit, changeset security, architecture advisory,
41
- PR-issue-link advisory, and the Claude-Code push-review adapter).
42
- One more shipped hook, `commit-review-gate.sh`, is a Claude
43
- `PreToolUse: Bash` hook that matches `git commit` it is shipped
44
- ready-to-wire but intentionally NOT registered in the default
45
- `.claude/settings.json`, so operators who want commit-time review can
46
- opt in by adding a rule. The final script,
47
- `push-review-gate-git.sh`, is a thin native-git adapter that sources
48
- `hooks/_lib/push-review-core.sh` (the same shared core used by the
49
- Claude-Code push-review adapter), so a fix to the push-review logic
50
- lands in one place. It ships for consumers who manually configure
51
- a wrapper-based `.husky/pre-push` (and as scaffolding for a future
52
- installer revision). The default `rea init` installer emits a
53
- standalone inline `.husky/pre-push` body instead of wiring the
54
- adaptersee the Hooks section for details.
55
- 2. A **gateway layer** an MCP server (`rea serve`) that proxies downstream
56
- MCP servers through a middleware chain. Every tool call native or
57
- proxied is classified, policy-checked, redacted, audited, and
58
- size-capped before it executes. The gateway also supervises downstream
59
- child processes: unexpected deaths are detected eagerly, the circuit
60
- breaker never reuses a zombie client, and a `SESSION_BLOCKER` audit
61
- event fires when a downstream crosses the per-session failure threshold.
62
- 3. A **policy runtime** — `.rea/policy.yaml` with a strict zod-validated
63
- schema. Defines autonomy level, a hard ceiling (`max_autonomy_level`),
64
- blocked paths, attribution rules, context protection, redaction and
65
- injection tuning, review/cache knobs, and an optional Discord
66
- notification webhook.
67
- 4. A **kill switch** `.rea/HALT` is a single file. If it exists, every
68
- tool call is denied at the middleware and hook layers. Use
69
- `rea freeze --reason "..."` to create it and `rea unfreeze --reason "..."`
70
- to remove it.
71
-
72
- REA is one tool that does one thing: gate and audit agentic tool calls
73
- against operator-defined policy. That is the whole product.
111
+ REA is a governance layer for Claude Code. It ships four things.
112
+
113
+ ### 1. A policy runtime
114
+
115
+ `.rea/policy.yaml`, validated by a strict zod schema — unknown fields are
116
+ **rejected**, not ignored. The policy defines:
117
+
118
+ - `autonomy_level` (L0–L3) and a hard `max_autonomy_level` ceiling
119
+ - `blocked_paths` (globs; `.rea/` is always blocked regardless)
120
+ - `block_ai_attribution` enforced by the commit-msg hook
121
+ - `review.codex_required` whether the pre-push gate runs Codex at all
122
+ - `review.concerns_blocks` whether `[P2]` verdicts halt the push
123
+ - `review.timeout_ms` hard cap on the Codex subprocess
124
+ - Redaction patterns, injection tuning, audit rotation, and MCP gateway knobs
125
+
126
+ Policy is re-read on every middleware invocation and every hook run. Editing
127
+ `.rea/policy.yaml` takes effect on the next tool call no restart, no
128
+ cache invalidation.
129
+
130
+ ### 2. A kill switch
131
+
132
+ `.rea/HALT` is a single file. If it exists, every governed tool call is
133
+ deniedthe MCP gateway middleware returns an error, the bash hooks `exit
134
+ 1`, and the pre-push gate returns exit `1` with the reason printed to
135
+ stderr. Use `rea freeze --reason "..."` to create it and `rea unfreeze` to
136
+ remove it. Both operations write audit records. The middleware never
137
+ clears HALT on its own.
138
+
139
+ HALT is checked before policy in every flow. A corrupted `.rea/policy.yaml`
140
+ does not prevent the kill-switch from firing.
141
+
142
+ ### 3. A hook layer
143
+
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
+ The hook layer runs independently of the MCP gateway — bypassing one does
156
+ not disable the other. That redundancy is intentional.
157
+
158
+ ### 4. An MCP gateway
159
+
160
+ `rea serve` is an MCP stdio server that proxies downstream MCP servers
161
+ declared in `.rea/registry.yaml` through a middleware chain. Every tool
162
+ call — native rea tools or proxied downstream tools — is classified,
163
+ policy-checked, redacted, audited, and size-capped before it executes. See
164
+ [MCP gateway](#mcp-gateway) for the chain ordering and supervisor
165
+ behavior.
166
+
167
+ **Plus** a stateless pre-push Codex review gate (new in 0.11.0) that fires
168
+ via `.husky/pre-push` on every `git push` and is covered in the next
169
+ section.
170
+
171
+ REA does one thing: gate and audit agentic tool calls against
172
+ operator-defined policy. That is the whole product.
173
+
174
+ ---
74
175
 
75
176
  ## What REA is NOT
76
177
 
@@ -78,187 +179,338 @@ These are non-goals. PRs adding any of these will be closed with a pointer
78
179
  to build a separate package that composes with REA.
79
180
 
80
181
  - **Not a project manager.** No task CRUD, no GitHub issue sync, no board
81
- scaffolding. No `task_create`, `task_update`, `repo_scaffold`.
182
+ scaffolding.
82
183
  - **Not an Obsidian integration.** No vault journaling, no note creation,
83
- no precompact summaries, no pre/post-compact Obsidian hooks.
84
- - **Not an account manager.** No `rea account add/list/env/rotate/remove`.
85
- No Keychain, no OAuth, no multi-tenant token vault. Env vars only.
86
- - **Not a Discord bot.** No Discord MCP tools. A Discord webhook URL in
87
- `policy.yaml` is the maximum surface area — one outbound POST, opt-in.
184
+ no pre/post-compact hooks.
185
+ - **Not an account manager.** No `rea account` tree, no Keychain, no OAuth,
186
+ no multi-tenant token vault. Env vars only.
187
+ - **Not a Discord bot.** A Discord webhook URL in `policy.yaml` is the
188
+ entire surface area — one outbound POST, opt-in, no MCP tools.
88
189
  - **Not a daemon supervisor.** `rea serve` is started by Claude Code via
89
190
  `.mcp.json`. Claude Code owns the lifecycle. There is no `rea start`,
90
- no `rea stop`, no systemd unit. A short-lived `.rea/serve.pid`
91
- breadcrumb is written at startup so `rea status` can detect a live
92
- gateway — it is removed on graceful shutdown and never used for
93
- locking or lifecycle management. A per-session `.rea/serve.state.json`
94
- snapshot accompanies it for live per-downstream introspection.
95
- - **Not a hosted service.** There is no REA Cloud, no SaaS tier, no
96
- multi-token workstreams, no workload isolation platform.
97
- - **Not a 70-agent roster.** 10 curated agents ship in the package. Four
98
- profiles layer additional specialists on top. No kitchen sink.
191
+ no `rea stop`, no systemd unit.
192
+ - **Not a hosted service.** No REA Cloud, no SaaS tier, no multi-tenant
193
+ workload isolation.
194
+ - **Not a 70-agent roster.** Ten curated agents ship in the package.
195
+ Profiles layer additional specialists.
196
+ - **Not a full policy engine.** No OPA/Rego, no CEL, no attribute-based
197
+ access control. A YAML file with a small, fixed schema is the entire
198
+ policy language.
199
+ - **Not a CI replacement.** REA gates agent behavior at author time. CI
200
+ still runs lint, typecheck, tests, and build on every PR.
201
+ - **Not a secret manager.** REA detects secrets in writes and redacts them
202
+ in audit records; it does not store, rotate, or provision them.
203
+
204
+ The non-goals are the product.
99
205
 
100
- The non-goals are the product. Every "but what if we just added X" belongs
101
- in a separate package.
206
+ ---
102
207
 
103
- ## Quick start
208
+ ## The pre-push Codex gate
104
209
 
105
- ### 1. Write a policy
210
+ The 0.11.0 gate is stateless. Every `git push` runs Codex on the diff, and
211
+ the gate's decision is a function of the review output — not of a cached
212
+ receipt or an audit record from a prior run.
106
213
 
107
- `.rea/policy.yaml`:
214
+ ### Flow
108
215
 
109
- ```yaml
110
- version: "1"
111
- profile: "bst-internal"
112
- autonomy_level: L1
113
- max_autonomy_level: L2
114
- promotion_requires_human_approval: true
115
- blocked_paths:
116
- - ".env"
117
- - ".env.*"
118
- - "secrets/**"
119
- block_ai_attribution: true
120
- context_protection:
121
- delegate_to_subagent:
122
- - "pnpm run preflight"
123
- - "pnpm run test"
124
- - "pnpm run build"
125
- max_bash_output_lines: 100
126
- notification_channel: "" # optional Discord webhook
216
+ ```
217
+ $ git push
218
+
219
+
220
+ ┌─────────────────────────────────────────────────────────┐
221
+ .husky/pre-push │
222
+ │ 1. Check .rea/HALT — exit 1 if present │
223
+ │ 2. Locate rea binary (node_modules, dist, PATH, npx)
224
+ │ 3. exec rea hook push-gate "$@"
225
+ └─────────────────────────────────────────────────────────┘
226
+
227
+
228
+ ┌─────────────────────────────────────────────────────────┐
229
+ rea hook push-gate │
230
+ │ 1. HALT check (again, defense-in-depth) │
231
+ │ 2. Load .rea/policy.yaml (zod-validated) │
232
+ │ 3. codex_required=false → status:disabled, exit 0 │
233
+ │ 4. REA_SKIP_PUSH_GATE / REA_SKIP_CODEX_REVIEW=<reason>│
234
+ │ → skipped, exit 0 (audit records skip_var) │
235
+ │ 5. Parse pre-push stdin refspecs │
236
+ │ 6. Resolve base ref (--base → --last-n-commits N │
237
+ │ → policy.review.last_n_commits → refspec → │
238
+ │ upstream → origin/HEAD → main/master → empty-tree) │
239
+ │ 7. Empty-diff check → status:empty-diff, exit 0 │
240
+ │ 8. codex exec review --base <ref> --json --ephemeral │
241
+ │ 9. Parse [P1]/[P2]/[P3] findings │
242
+ │ 10. Infer verdict: P1 → blocking; else P2 → concerns; │
243
+ │ else pass │
244
+ │ 11. Atomic write .rea/last-review.json (redacted) │
245
+ │ 12. Render stderr banner │
246
+ │ 13. Append audit record rea.push_gate.reviewed │
247
+ │ 14. Exit 0 (pass/disabled/skipped/empty-diff) | │
248
+ │ 1 (HALT) | 2 (blocked, timeout, or error) │
249
+ └─────────────────────────────────────────────────────────┘
127
250
  ```
128
251
 
129
- `autonomy_level` can be raised up to `max_autonomy_level` and no further.
130
- The loader rejects the file at parse time if `autonomy_level` exceeds the
131
- ceiling. The ceiling is set by the human operator and never by an agent.
132
-
133
- ### 2. Freeze when you need to stop everything
252
+ ### Verdicts and exit codes
134
253
 
135
- ```bash
136
- rea freeze --reason "incident triage; investigate unexpected .env write"
254
+ | Verdict | Default behavior | Exit code | Notes |
255
+ | --- | --- | --- | --- |
256
+ | `pass` | Push proceeds | `0` | No `[P1]` or `[P2]` findings in the review |
257
+ | `concerns` | Push blocks (default) | `2` | `[P2]` findings present; override with `REA_ALLOW_CONCERNS=1` or `review.concerns_blocks: false` |
258
+ | `blocking` | Push blocks always | `2` | `[P1]` findings present; no override |
259
+ | HALT | Push blocks | `1` | `.rea/HALT` is active; run `rea unfreeze` |
260
+ | `disabled` | Push proceeds | `0` | `review.codex_required: false` in policy |
261
+ | `skipped` | Push proceeds | `0` | `REA_SKIP_PUSH_GATE=<reason>` or `REA_SKIP_CODEX_REVIEW=<reason>` set; audited |
262
+ | `empty-diff` | Push proceeds | `0` | No file changes between base and head |
263
+ | `error` | Push blocks | `2` | Codex not installed, timeout, subprocess error, malformed policy, head-sha resolution failure |
264
+
265
+ ### The auto-fix loop
266
+
267
+ Previous gate: pre-run Codex manually, record an attestation with `rea
268
+ audit record codex-review --also-set-cache`, push, and hope the
269
+ attestation's SHA matches the tip. Agents working at speed ended up
270
+ fabricating attestations or running Codex out-of-band and recording the
271
+ verdict without anyone actually reading the output. Friction was paid on
272
+ every push.
273
+
274
+ New gate: the gate **is** the review. Codex runs on the same diff the push
275
+ is about to send, so the verdict is causally tied to the push. When the
276
+ gate blocks:
277
+
278
+ 1. The stderr banner prints the verdict, base ref, head SHA, finding
279
+ count, duration, and up to 20 severity-sorted findings with file:line
280
+ pointers. Because the pre-push hook's stderr reaches Claude as the
281
+ tool output of `Bash(git push)`, the banner is the primary fast-path
282
+ signal for the in-session agent.
283
+ 2. `.rea/last-review.json` is written atomically with the full findings,
284
+ each carrying `severity`, `title`, `body`, and optional `file`/`line`.
285
+ This file is the source of truth for the auto-fix loop — the stderr
286
+ banner is capped at 20 findings; the JSON is not.
287
+ 3. Claude reads both, applies fixes, commits (with `-s` and no AI
288
+ attribution), and pushes again. The gate runs Codex fresh on the new
289
+ diff. Repeat until `pass`.
290
+
291
+ See [CHANGELOG 0.11.0](./CHANGELOG.md) for the longer rationale on why
292
+ the cache-attestation design was removed.
293
+
294
+ ### `.rea/last-review.json` schema
295
+
296
+ ```jsonc
297
+ {
298
+ "schema_version": 1,
299
+ "generated_at": "2026-04-22T18:04:01.123Z",
300
+ "verdict": "blocking", // "pass" | "concerns" | "blocking"
301
+ "base_ref": "origin/main",
302
+ "head_sha": "c5ec101…",
303
+ "finding_count": 3,
304
+ "findings": [
305
+ {
306
+ "severity": "P1", // "P1" | "P2" | "P3"
307
+ "title": "missing input validation on /auth/callback",
308
+ "file": "src/routes/auth.ts", // optional
309
+ "line": 42, // optional
310
+ "body": "The callback accepts raw state without…"
311
+ }
312
+ // …
313
+ ],
314
+ "review_text": "[P1] missing input validation…\n[P2] …",
315
+ "event_count": 37,
316
+ "duration_seconds": 12.4
317
+ }
137
318
  ```
138
319
 
139
- `rea freeze` writes `.rea/HALT`. Every subsequent tool call is denied
140
- until an operator runs:
320
+ The file is:
141
321
 
142
- ```bash
143
- rea unfreeze --reason "false alarm resolved"
322
+ - **Atomic.** Written to `.rea/last-review.json.tmp.<pid>-<rand>`, fsynced,
323
+ then `rename(2)`d. Partial writes never surface to readers.
324
+ - **Redacted.** Both `findings[].title`/`body` and `review_text` are run
325
+ through the same `SECRET_PATTERNS` list the redact middleware uses. If
326
+ Codex quotes a credential out of the diff it never hits disk in cleartext.
327
+ - **Overwritten every push.** There is no rolling history on disk; the
328
+ audit log is the rolling history.
329
+ - **Gitignored.** The default `rea init` install adds `/.rea/last-review.json`
330
+ to `.gitignore`.
331
+
332
+ ### Environment variables
333
+
334
+ | Variable | Purpose |
335
+ | --- | --- |
336
+ | `REA_SKIP_PUSH_GATE=<reason>` | Value-carrying waiver. When set to a non-empty string, the gate short-circuits to `status:skipped` (exit 0) and appends `rea.push_gate.skipped` with the reason and `skip_var: REA_SKIP_PUSH_GATE` as metadata. HALT **always** wins over this — a frozen install still blocks. Use sparingly; every use is audited. |
337
+ | `REA_SKIP_CODEX_REVIEW=<reason>` | Equivalent alias for `REA_SKIP_PUSH_GATE`, added in 0.12.0. Same exit behavior; audit metadata records `skip_var: REA_SKIP_CODEX_REVIEW` so operators can grep their audit log to see which variant agents used. When both env vars are set, `REA_SKIP_PUSH_GATE` wins. |
338
+ | `REA_ALLOW_CONCERNS=1` | One-push override for `concerns` verdict. Accepts `1`, `true`, or `yes` (case-insensitive). Does **not** override `blocking`. The audit record is stamped `concerns_override: true` so reviewers can see the override was used. |
339
+
340
+ ### Policy knobs
341
+
342
+ ```yaml
343
+ # .rea/policy.yaml
344
+ review:
345
+ codex_required: true # default true — run Codex on every push
346
+ concerns_blocks: true # default true — [P2] halts the push
347
+ timeout_ms: 1800000 # default 1800000 (30 minutes; raised from
348
+ # 600000 in 0.12.0 — see CHANGELOG)
349
+ last_n_commits: 10 # OPTIONAL — narrow review to the last N commits
350
+ # (diff vs HEAD~N). Defaults unset.
351
+ auto_narrow_threshold: 30 # OPTIONAL — auto-narrow when commit count
352
+ # behind base > N (0 disables, default 30)
144
353
  ```
145
354
 
146
- Both calls produce audit entries. The middleware never clears HALT on
147
- its own.
355
+ | Key | Type | Default | Purpose |
356
+ | --- | --- | --- | --- |
357
+ | `review.codex_required` | boolean | `true` | Master on/off. `false` short-circuits the gate to `status:disabled`, still audited. `-no-codex` profiles set this to `false`. |
358
+ | `review.concerns_blocks` | boolean | `true` | When `true`, `[P2]` verdicts return exit 2. Flip to `false` for a looser posture where only `[P1]` halts the push. |
359
+ | `review.timeout_ms` | number | `1800000` | Hard cap on the `codex exec review` subprocess in milliseconds. Exceeding it kills the subprocess and returns exit 2 with a `timeout` kind. Positive integer; zero/negative is rejected at load. **Raised from 600000 (10 min) to 1800000 (30 min) in 0.12.0** after operator data showed realistic feature-branch reviews routinely exceeded 10 minutes; pin `timeout_ms: 600000` explicitly to retain the old default. |
360
+ | `review.last_n_commits` | number | unset | When set, the gate diffs against `HEAD~N` instead of running the upstream → origin/HEAD → main/master ladder. Useful when a feature branch has accumulated many commits and the full base diff overwhelms the reviewer. Positive integer. CLI `--last-n-commits N` overrides this; `--base <ref>` overrides both. When `HEAD~N` is unreachable the resolver clamps based on whether the repo is a shallow clone: **(full clone, branch shorter than N)** clamps to the empty-tree sentinel so the root commit's changes are included (reviewing all `K+1` commits on the branch); **(shallow clone)** clamps to `HEAD~K` SHA — the deepest locally resolvable ancestor — so the review does not balloon to every tracked file (older history exists on the remote but isn't fetched). A stderr warning surfaces the requested-vs-clamped numbers in both cases. Audit metadata records `base_source: 'last-n-commits'`, `last_n_commits: <count actually reviewed>`, and `last_n_commits_requested: N` (only present when clamped). |
361
+ | `review.auto_narrow_threshold` | number | `30` | Added in **0.13.0**. When the resolved diff base is more than N commits behind HEAD AND the base was resolved from the active refspec's `remoteSha` (i.e. previously-pushed remote tip of this branch — commits already Codex-reviewed) AND no explicit narrowing was set, the gate auto-scopes to the last 10 commits and emits a stderr warning. Set to `0` to disable. **Suppressed** when any of `--base`, `--last-n-commits`, `policy.review.last_n_commits`, OR the base was resolved via the upstream / origin-head / origin-main ladder (initial push, no upstream, fallback to trunk). Auto-narrow never fires on initial pushes — earlier commits on the branch may not have been reviewed yet, and skipping past them would silently bypass the gate's coverage contract. Audit metadata records `auto_narrowed: true` and `original_commit_count: N` so operators can grep for narrowed reviews. Background: large feature branches (50+ commits relative to a previously-pushed tip) routinely produced non-deterministic Codex verdicts and 30-minute timeouts; J makes the protective default automatic without compromising first-push coverage. |
362
+
363
+ ### Auto-narrow on large divergence (0.13.0)
148
364
 
149
- ### 3. Verify the install
365
+ When pushing a long-running branch that has already been pushed before (so
366
+ the remote tip is the previously-reviewed Codex baseline), follow-up
367
+ pushes that pile up many commits since the last push can timeout the
368
+ reviewer or produce inconsistent verdicts. **Auto-narrow** detects this
369
+ case and scopes the review down to recent commits automatically:
370
+
371
+ ```
372
+ $ git push origin feature/big-thing
373
+ rea: auto-narrow — 80 commits behind <previous-remote-tip-sha> (threshold 30);
374
+ reviewing the last 10 commits instead.
375
+ Override: pass `--last-n-commits N` or `--base <ref>`, set
376
+ `review.last_n_commits` in .rea/policy.yaml, or disable with
377
+ `review.auto_narrow_threshold: 0`.
378
+ ```
379
+
380
+ The probe runs `git rev-list --count base..HEAD` after base resolution.
381
+ **Auto-narrow only fires when the base was resolved from the active
382
+ refspec's `remoteSha`** — i.e. the previously-pushed tip of this branch,
383
+ where the older commits have already been Codex-reviewed in a prior push.
384
+ Initial pushes (where the resolver falls back to `origin/main` or the
385
+ upstream ladder) are NEVER auto-narrowed: skipping past those earlier
386
+ commits would silently bypass the advertised pre-push review for any
387
+ hook/policy/security change made early in the branch.
388
+
389
+ When eligible and the count exceeds `review.auto_narrow_threshold`
390
+ (default 30) and no narrowing override is in effect, the gate re-resolves
391
+ to `HEAD~10` and proceeds with the smaller diff. Every reviewed event
392
+ includes `auto_narrowed: true` + `original_commit_count: <N>` in audit
393
+ metadata.
394
+
395
+ To opt out for one push: pass `--last-n-commits N` or `--base <ref>`. To
396
+ opt out persistently: set `review.last_n_commits` (any value), or set
397
+ `review.auto_narrow_threshold: 0`.
398
+
399
+ ### Extension-hook chaining (0.13.0)
400
+
401
+ Drop executable scripts into `.husky/commit-msg.d/` or `.husky/pre-push.d/`
402
+ and rea will run them after its own governance work, in lexical order, with
403
+ the same positional args. Useful for layering commitlint, conventional-
404
+ commits linters, branch-policy checks, or any other per-commit / per-push
405
+ work without losing rea coverage.
150
406
 
151
407
  ```bash
152
- rea doctor
408
+ mkdir -p .husky/pre-push.d
409
+ cat > .husky/pre-push.d/10-commitlint <<'EOF'
410
+ #!/bin/sh
411
+ # Verify every new commit on the pushed range has a conventional message.
412
+ exec npx --no-install commitlint --from "origin/main" --to "HEAD"
413
+ EOF
414
+ chmod +x .husky/pre-push.d/10-commitlint
153
415
  ```
154
416
 
155
- `rea doctor` checks `.rea/` directory presence, policy parse, registry
156
- parse, curated-agent presence, hook coverage, `.claude/settings.json`
157
- wiring, commit-msg / pre-push git hooks, Codex CLI + agent availability
158
- (when `codex_required: true`), and the TOFU fingerprint store. It
159
- returns a pass/fail summary with specific remediation hints. In non-git
160
- directories (knowledge repos, docs-only projects) the commit-msg and
161
- pre-push checks are skipped cleanly REA governs policy and injection
162
- detection there, not pushes. Audit hash-chain integrity is verified by
163
- a separate command — `rea check` (on-disk tail) or the full replay
164
- verifier — not by `rea doctor`.
417
+ Rules:
418
+
419
+ - **Sourced AFTER rea's body** — HALT, attribution blocking, and Codex
420
+ review run first; fragments only fire when rea succeeds. A non-zero exit
421
+ from rea short-circuits before any fragment runs.
422
+ - **Lexical order** — `10-foo` runs before `20-bar` runs before `90-baz`.
423
+ The standard convention is to prefix with a two-digit ordering number.
424
+ - **Executable bit gates** only files with `chmod +x` are run. A README
425
+ or `.disabled` file in the directory is silently skipped.
426
+ - **Non-zero exit fails the hook** the next fragment does not run, the
427
+ push / commit is blocked. This matches husky's normal hook chaining
428
+ semantics.
429
+ - **Missing directory is a no-op** — backward compatible with consumers
430
+ who never opt into fragments.
431
+ - **Fragments cannot replay pre-push stdin** — git delivers refspec data
432
+ on stdin which rea consumes during its own review. Fragments that need
433
+ refspec data should run before rea (use a custom hook in
434
+ `core.hooksPath` instead). Fragments that need ambient repo state can
435
+ call `git rev-parse` themselves.
436
+
437
+ `rea doctor` lists every fragment it sees and warns when a non-executable
438
+ file is sitting in either directory (silently skipped at hook-fire time).
439
+
440
+ ### Codex CLI dependency
441
+
442
+ The gate shells out to `codex`. **`codex` is a hard prerequisite** when
443
+ `policy.review.codex_required: true` (the default for every profile other
444
+ than the `-no-codex` variants). As of 0.12.0 `rea doctor` runs a
445
+ `codex CLI on PATH` check that **fails** when codex is required by policy
446
+ but the binary is not on `PATH` — surfacing the prereq during install
447
+ rather than at first push:
165
448
 
166
- ### 4. Watch the running gateway
449
+ ```
450
+ [fail] codex CLI on PATH
451
+ codex not found on PATH. policy.review.codex_required: true requires
452
+ the codex binary. Install: https://github.com/openai/codex
453
+ (e.g. `npm i -g @openai/codex`). To disable the push-gate instead,
454
+ set policy.review.codex_required: false in .rea/policy.yaml.
455
+ ```
167
456
 
168
- ```bash
169
- rea status # human-readable summary
170
- rea status --json # JSON — pipe to jq
171
- ```
172
-
173
- `rea status` is the live-process view. It reads the pidfile written by
174
- `rea serve`, verifies the pid is alive, and surfaces the session id,
175
- policy summary (profile, autonomy, HALT state), audit stats (lines,
176
- last timestamp, whether the tail record's hash looks well-formed), and
177
- — as of 0.9.0 — a **per-downstream live block** sourced from
178
- `.rea/serve.state.json`. Each downstream entry includes:
179
-
180
- | Field | Type | Meaning |
181
- | --------------------------- | ------------------------------------ | --------------------------------------------------------------- |
182
- | `name` | string | Registry server name |
183
- | `connected` | boolean | MCP client currently holds an open stdio transport |
184
- | `healthy` | boolean | Gateway considers the server safe to route calls to |
185
- | `circuit_state` | `closed` \| `open` \| `half-open` | Current breaker position |
186
- | `retry_at` | ISO timestamp \| `null` | Next allowed half-open probe, when `open` |
187
- | `last_error` | string \| `null` | Bounded, redacted diagnostic from the most recent failure |
188
- | `tools_count` | integer \| `null` | Tool count from the last successful `tools/list` |
189
- | `open_transitions` | integer | Cumulative circuit-open events in this session |
190
- | `session_blocker_emitted` | boolean | Whether `SESSION_BLOCKER` has fired for this server yet |
191
-
192
- `.rea/serve.state.json` is the authoritative live source — it is written
193
- atomically (temp+rename) on every circuit transition and supervisor
194
- event, debounced through a 250 ms trailing timer so a flap storm can't
195
- spam disk. State files written by a pre-0.9.0 gateway degrade gracefully:
196
- `downstreams` surfaces as `null` with a hint to upgrade.
197
-
198
- Use `rea check` when you want the pure on-disk view (policy + HALT +
199
- tail audit) without probing for a live process.
200
-
201
- ### 5. Optional Prometheus `/metrics` endpoint
202
-
203
- `rea serve` can expose a loopback-only Prometheus endpoint when the
204
- `REA_METRICS_PORT` environment variable is set:
457
+ If a push is attempted without codex on PATH, the gate also returns exit 2
458
+ with the same install hint:
205
459
 
206
- ```bash
207
- REA_METRICS_PORT=9464 rea serve
208
- # in another shell
209
- curl http://127.0.0.1:9464/metrics
460
+ ```
461
+ codex CLI not found on PATH. Install with `npm i -g @openai/codex`,
462
+ or set `review.codex_required: false` in .rea/policy.yaml to disable
463
+ the push-gate.
210
464
  ```
211
465
 
212
- Metrics exposed: per-downstream call and error counters, in-flight
213
- gauge, audit-lines-appended counter, circuit-breaker state gauge, and a
214
- seconds-since-last-HALT-check gauge. The listener binds to `127.0.0.1`
215
- only, serves only `GET /metrics` (everything else is a fixed-body 404),
216
- and never binds by default — "no silent listeners" is a design rule.
217
- There is no TLS; scrape through SSH/a reverse proxy if you need
218
- cross-host access.
466
+ Operators who do not have Codex available can either:
219
467
 
220
- Set `REA_LOG_LEVEL=debug` for verbose gateway logs; the default is
221
- `info`. Records are JSON lines on a non-TTY stderr and pretty-printed
222
- on an interactive terminal.
468
+ - Run `rea init --profile bst-internal-no-codex` (or `open-source-no-codex`),
469
+ which sets `review.codex_required: false` on install.
470
+ - Flip `review.codex_required: false` in an existing policy file.
223
471
 
224
- ### 6. Ask the gateway how it's doing — `__rea__health`
472
+ ### Standalone usage
225
473
 
226
- The gateway advertises a single built-in tool, `__rea__health`, in
227
- every `listTools` response regardless of downstream state. Calling it
228
- returns a snapshot of gateway version, uptime, HALT state, policy
229
- summary, and per-downstream health. The handler **short-circuits the
230
- middleware chain** — it is callable under HALT and at any autonomy
231
- level — because it is the tool an operator reaches for when everything
232
- else is frozen. Every invocation still writes an audit record.
474
+ `rea hook push-gate` is invoked by `.husky/pre-push`, but it is also a
475
+ first-class CLI. Run it manually to test a review without pushing:
233
476
 
234
- The wire response is **sanitized by default**: `halt_reason` and
235
- `downstreams[].last_error` surface as `null`. Full diagnostic detail
236
- lives in the audit record's metadata (`halt_reason`,
237
- `downstream_errors[]`) local disk, hash-chained, not
238
- LLM-reachable — which is the right sink for trusted-operator text.
477
+ ```bash
478
+ # Review the working tree against the resolved base (@{upstream}, else
479
+ # origin/HEAD, else main/master, else empty-tree).
480
+ rea hook push-gate
239
481
 
240
- Operators who genuinely need error strings on the MCP wire can opt in:
482
+ # Review against an explicit base.
483
+ rea hook push-gate --base origin/main
484
+ rea hook push-gate --base refs/remotes/upstream/main
241
485
 
242
- ```yaml
243
- # .rea/policy.yaml
244
- gateway:
245
- health:
246
- expose_diagnostics: true
486
+ # Narrow the review to the last N commits (diff vs HEAD~N). Loses to
487
+ # --base when both are set; mirrors policy.review.last_n_commits.
488
+ rea hook push-gate --last-n-commits 10
247
489
  ```
248
490
 
249
- Opt-in mode still runs the full sanitizer pass: `redactSecrets` replaces
250
- known secret patterns with `[REDACTED:*]`, `classifyInjection` replaces
251
- any non-`clean` diagnostic string (verdicts `suspicious` or
252
- `likely_injection`) with the exported `INJECTION_REDACTED_PLACEHOLDER`
253
- token — the literal string `<redacted: suspected injection>` — and
254
- oversize values are bounded before scanning so an adversarial downstream
255
- can't DoS the tool with a multi-megabyte error.
491
+ Exit codes match the pre-push contract. The JSON payload is written to
492
+ `.rea/last-review.json` regardless of invocation context.
493
+
494
+ ### What happens to a protected ref?
495
+
496
+ The gate has no concept of protected vs. unprotected branches; it reviews
497
+ whatever diff git is about to push. Protect-main is enforced by GitHub
498
+ branch protection (required status checks, required reviews, no direct
499
+ pushes to `main`), not by the gate. The gate's job is to surface blocking
500
+ issues before the push reaches the remote.
256
501
 
257
- ## Architecture
502
+ ---
503
+
504
+ ## MCP gateway
505
+
506
+ `rea serve` is an MCP stdio server. Claude Code starts it via `.mcp.json`
507
+ at the start of a session; it runs for the life of that session. The
508
+ server proxies downstream MCP servers declared in `.rea/registry.yaml`
509
+ through a fixed middleware chain.
258
510
 
259
511
  ### Middleware chain
260
512
 
261
- Every native MCP tool call AND every proxied downstream call flows through
513
+ Every native rea tool call AND every proxied downstream call flows through
262
514
  one chain. The order matters — each layer fails closed.
263
515
 
264
516
  ```
@@ -273,13 +525,14 @@ tool call
273
525
  │ blocked-paths — .rea/ + operator paths │
274
526
  │ rate-limit — token bucket per server │
275
527
  │ circuit-breaker — trip on downstream failure │
528
+ │ injection (args) — prompt-injection in args │
276
529
  │ redact (args) — secrets in arguments │
277
530
  │ │
278
531
  │ ==== EXECUTE ==== │
279
532
  │ │
280
533
  │ result-size-cap — bounded response │
281
534
  │ redact (result) — secrets in result │
282
- │ injection — prompt-injection in result │
535
+ │ injection (result) — prompt-injection in result │
283
536
  │ audit.exit — hash-chained record close │
284
537
  └───────────────────────────────────────────────────┘
285
538
 
@@ -287,415 +540,526 @@ tool call
287
540
  result
288
541
  ```
289
542
 
290
- `.rea/` is hardcoded as an always-blocked path. It cannot be unblocked
291
- from policy. Policy is re-read on every invocation — any edit to
292
- `policy.yaml` takes effect on the next tool call.
293
-
294
- The `__rea__health` meta-tool is the one documented exception: it
295
- short-circuits the chain (see §6 above) and writes an audit record from
296
- the short-circuit handler itself.
543
+ `.rea/` is hardcoded as an always-blocked path. It cannot be unblocked from
544
+ policy. Policy is re-read on every invocation — any edit to `policy.yaml`
545
+ takes effect on the next tool call.
546
+
547
+ ### `__rea__health` meta-tool
548
+
549
+ The gateway advertises a single built-in tool, `__rea__health`, in every
550
+ `listTools` response. Calling it returns gateway version, uptime, HALT
551
+ state, policy summary, and per-downstream health. The handler
552
+ **short-circuits the middleware chain** — it is callable under HALT and at
553
+ any autonomy level — because it is the tool an operator reaches for when
554
+ everything else is frozen. Every invocation still writes an audit record.
555
+
556
+ The wire response is sanitized by default: `halt_reason` and
557
+ `downstreams[].last_error` surface as `null`. Full detail lives in the
558
+ audit record. Operators who genuinely need error strings on the MCP wire
559
+ can opt in via `policy.gateway.health.expose_diagnostics: true`; the
560
+ short-circuit still runs the full redact + injection-classify sanitizer
561
+ pass before emitting.
562
+
563
+ ### Audit log
564
+
565
+ `.rea/audit.jsonl` is a hash-chained, append-only JSONL file. Each record
566
+ is a single line with:
567
+
568
+ - `seq` — monotonic integer
569
+ - `ts` — ISO-8601 UTC
570
+ - `session_id` — generated at `rea serve` boot
571
+ - `server_name`, `tool_name`, `tier` (`read` | `write` | `destructive`)
572
+ - `status` (`allowed` | `denied` | `error`)
573
+ - `metadata` — tool-specific structured fields (argument digest, target,
574
+ deny reason, verdict)
575
+ - `prev_hash` — SHA-256 of the previous record; tampering is detectable by
576
+ `rea audit verify`
577
+
578
+ Records are redacted on write (secrets swapped for `[REDACTED:*]`,
579
+ injection payloads swapped for `INJECTION_REDACTED_PLACEHOLDER`), never
580
+ LLM-reachable. Rotation is policy-driven (`policy.audit.rotation.max_bytes`
581
+ and/or `max_age_days`); rotation preserves the hash chain by seeding the
582
+ new file with a rotation marker record.
583
+
584
+ ### TOFU drift detection
585
+
586
+ Downstream MCP servers are fingerprinted on first sight and stored in
587
+ `.rea/fingerprints.json`. On every `rea serve` boot, each server's canonical
588
+ shape (command path, env-key set, vault list) is hashed and compared
589
+ against the stored fingerprint:
590
+
591
+ - `first-seen` — new server; fingerprint recorded.
592
+ - `unchanged` — match; proceed.
593
+ - `drifted` — mismatch; fail-close. The operator uses `rea tofu list` to
594
+ inspect and `rea tofu accept <name> --reason "..."` to rebase.
595
+
596
+ The drift-accept operation appends a `tofu.drift_accepted_by_cli` audit
597
+ record with the reason. The next boot classifies the server as
598
+ `unchanged`.
297
599
 
298
- ### Gateway supervisor
299
-
300
- Downstream MCP servers run as child processes over stdio. The
301
- `DownstreamConnection` wrapper wires the SDK `StdioClientTransport`'s
302
- `onclose` + `onerror` callbacks, so an unexpected child death — OS
303
- OOM-kill, unhandled exception in the child, stdio pipe error outside a
304
- caller-initiated close — is detected **eagerly**: the client and
305
- transport are nulled before the next `callTool` tries to use them. The
306
- following call forces a genuine reconnect rather than invoking through a
307
- stale handle.
600
+ ### Downstream environment safety
308
601
 
309
- "Not connected" errors from the SDK (the in-flight fallback) are
310
- promoted to the same respawn path with the same eager invalidation.
311
- A 30-second flapping guard refuses a second reconnect that lands too
312
- quickly after the previous one — the child is clearly unhealthy and the
313
- circuit breaker is a better place to handle it.
602
+ `rea serve` does **not** forward `process.env` wholesale. Each downstream
603
+ child gets:
314
604
 
315
- `SessionBlockerTracker` subscribes to circuit-breaker
316
- `onStateChange` events and counts circuit-open transitions per
317
- `(session_id, server_name)`. Once the threshold (default: 3) is
318
- crossed, exactly one `SESSION_BLOCKER` audit record is appended and a
319
- LOUD structured log line is emitted — subsequent opens do not re-fire
320
- until recovery (a transition to `closed`) re-arms the emit. A new
321
- session (new `rea serve` process) drops every counter and starts fresh.
605
+ 1. A fixed allowlist of neutral OS vars (`PATH`, `HOME`, `TZ`,
606
+ `NODE_OPTIONS`, …).
607
+ 2. Names opted into via `registry.yaml#servers[].env_passthrough` the
608
+ schema refuses secret-looking names (`*_TOKEN`, `*_KEY`, `*_SECRET`, …),
609
+ so secrets must be named explicitly.
610
+ 3. Values from the registry's `env:` mapping, which may contain `${VAR}`
611
+ placeholders resolved against the host environment. A `${VAR}` whose
612
+ host variable is unset is treated as fatal — the downstream is marked
613
+ unhealthy rather than handed an unresolved placeholder.
322
614
 
323
615
  ### Live state
324
616
 
325
- `.rea/serve.state.json` is the on-disk live snapshot. It is written
326
- once at boot and again on every circuit transition or supervisor event,
617
+ `.rea/serve.state.json` is the on-disk live snapshot. It is written once
618
+ at boot and again on every circuit transition or supervisor event,
327
619
  debounced through a 250 ms trailing timer and flushed atomically via
328
- temp-file + rename. The snapshot carries a `session_id` (boot-time
329
- ownership key) and `owner_pid`; a newly-started `rea serve` whose
330
- predecessor crashed without cleanup can detect the abandoned file and
331
- take over ownership rather than stalling forever. `rea status` is a
332
- read-only consumer of this file.
620
+ temp-file + rename. `rea status` reads this file to render a
621
+ per-downstream table; a new `rea serve` whose predecessor crashed can
622
+ detect the abandoned file and take over ownership rather than stalling.
333
623
 
334
- ### Downstream environment safety
624
+ ### Optional Prometheus metrics
335
625
 
336
- `rea serve` does **not** forward `process.env` wholesale to downstream
337
- children. Each child gets:
338
-
339
- 1. A fixed allowlist of neutral OS vars (`PATH`, `HOME`, `TZ`,
340
- `NODE_OPTIONS`, …).
341
- 2. Any names opted into via `registry.yaml#servers[].env_passthrough` —
342
- the schema refuses secret-looking names (`*_TOKEN`, `*_KEY`,
343
- `*_SECRET`, …), so secrets must be named explicitly.
344
- 3. Values from the registry's `env:` mapping, which may contain
345
- `${VAR}` placeholders resolved against the host environment
346
- (0.3.0). Secret-looking values are redacted in logs by default.
347
- A `${VAR}` whose host variable is unset is treated as fatal — the
348
- downstream is marked unhealthy rather than handed an unresolved
349
- placeholder.
350
-
351
- ### Hook layer
352
-
353
- Hooks are shell scripts. 14 ship in the package; 12 are wired into
354
- the default `.claude/settings.json` and run at Claude Code
355
- tool-invocation time, independently of the gateway. The remaining
356
- two (`commit-review-gate.sh` and `push-review-gate-git.sh`) ship
357
- ready-to-wire but are not registered by default — see "What REA is"
358
- above and the inventory table at the end of this section for the full
359
- picture. Both layers (hooks and the gateway middleware) fail closed.
360
- Bypassing one does not disable the other.
361
-
362
- Every hook uses `set -euo pipefail` (or `set -uo pipefail` for the
363
- ones that process stdin JSON) and performs a HALT check near the top.
364
- The review-gate hooks (`push-review-gate.sh`, `push-review-gate-git.sh`,
365
- `commit-review-gate.sh`) additionally anchor `REA_ROOT` to their own
366
- on-disk location (BUG-012 fix, 0.6.2) — for those hooks,
367
- `CLAUDE_PROJECT_DIR` is accepted only as an advisory signal because it
368
- is caller-controlled. The remaining hooks (e.g. `secret-scanner.sh`,
369
- `settings-protection.sh`, `blocked-paths-enforcer.sh`,
370
- `dangerous-bash-interceptor.sh`) still derive `REA_ROOT` from
371
- `${CLAUDE_PROJECT_DIR:-$(pwd)}`; extending the script-anchor idiom to
372
- those hooks is tracked as an open hardening item. Cross-repo
373
- invocations (running a review-gate hook from a consumer project that
374
- is not the rea install) short-circuit cleanly using
375
- `git --git-common-dir` comparison (0.6.1).
376
-
377
- The two push-review adapters that ship in `hooks/` share a single
378
- implementation core at `hooks/_lib/push-review-core.sh` (0.7.0 BUG-008
379
- cleanup) so a fix lands in one place: `push-review-gate.sh` consumes
380
- Claude-Code PreToolUse JSON and is what `rea init` copies to
381
- `.claude/hooks/`; `push-review-gate-git.sh` consumes git's native
382
- `.husky/pre-push` refspec lines and is shipped for consumers who wire
383
- a wrapper-based `.husky/pre-push` that execs it directly. The default
384
- `rea init` installer does NOT currently emit that wrapper — it writes
385
- a standalone inline gate body as `.husky/pre-push` (source of truth:
386
- `src/cli/install/pre-push.ts`). The native-git adapter and the
387
- inline installer currently implement the same protected-path logic
388
- separately; unifying the husky installer on the adapter is tracked as
389
- follow-up hardening. `commit-review-gate.sh` is a standalone Claude
390
- `PreToolUse: Bash` hook that matches `git commit`; it does not source
391
- the push-review core.
392
-
393
- ### Slash commands
394
-
395
- Five commands ship in the package and are copied into `.claude/commands/`
396
- during `rea init`.
397
-
398
- ### Agent roster
399
-
400
- Ten curated agents ship in the package: `rea-orchestrator`, `code-reviewer`,
401
- `codex-adversarial`, `security-engineer`, `accessibility-engineer`,
402
- `typescript-specialist`, `frontend-specialist`, `backend-engineer`,
403
- `qa-engineer`, `technical-writer`. Profiles
404
- (`client-engagement`, `bst-internal`, `bst-internal-no-codex`,
405
- `lit-wc`, `open-source`, `open-source-no-codex`, `minimal`) layer
406
- additional specialists on top. The `-no-codex` variants match their
407
- parents but default `review.codex_required: false` so teams without a
408
- Codex CLI on the bench get a first-class opt-out rather than relying on
409
- `REA_SKIP_CODEX_REVIEW`.
410
-
411
- The orchestrator is the single entry point for non-trivial tasks. The
412
- CLAUDE.md template installed by `rea init` instructs the host agent:
413
- _"For any non-trivial task, delegate to the `rea-orchestrator` agent
414
- FIRST."_
415
-
416
- ## The Plan / Build / Review loop
417
-
418
- Codex is a first-class part of REA. It is not a bolt-on. The BST
419
- engineering process bakes adversarial review into the default flow, and
420
- REA ships it out of the box.
421
-
422
- | Phase | Primary model | Codex role | Governance |
423
- | --- | --- | --- | --- |
424
- | Plan | Claude Opus | — | Full middleware chain |
425
- | Pre-implementation review | — | `/codex:review` — review the PLAN before code | Audited |
426
- | Build | Claude Opus | — | Full middleware chain |
427
- | Adversarial review | — | `/codex:adversarial-review` on the diff (independent perspective) | Audited, redacted, kill-switched |
428
- | Pre-merge gate | — | `/codex:adversarial-review` re-run; recorded in audit.jsonl | Required status check (recommended) |
429
-
430
- Three things make this work:
431
-
432
- 1. The **`codex-adversarial` agent** in the curated roster wraps
433
- `/codex:adversarial-review`. The orchestrator delegates to it after
434
- any non-trivial change.
435
- 2. The **`/codex-review` slash command** is one of the five shipped
436
- commands. It produces an audit entry including the request summary,
437
- response summary, and pass/fail signal.
438
- 3. The **`push-review-gate.sh` hook** blocks (exit 2) every protected-path
439
- push that does not carry a matching `codex.review` audit entry for the
440
- pushed `head_sha` with a `verdict` of `pass` or `concerns`. The only
441
- other way through the protected-path branch is an active Codex-only
442
- waiver (`REA_SKIP_CODEX_REVIEW=<reason>`, 0.8.0 narrowing). For
443
- **non-protected-path** pushes the gate runs a separate review-cache
444
- lookup — this is where the cache predicate and pushed-ref key
445
- hardening live. The cache-hit predicate requires
446
- `.hit == true and .result == "pass"` (0.8.0 hardening — a cached
447
- `fail` verdict no longer satisfies the gate), and the cache key is
448
- derived from the **pushed source ref** (from pre-push stdin) rather
449
- than the checkout branch, so `git push origin hotfix:main` from a
450
- `feature` checkout correctly looks up the `hotfix` cache entry.
451
-
452
- ### Codex-only waiver semantics (0.8.0)
453
-
454
- Through 0.7.0, `REA_SKIP_CODEX_REVIEW=<reason>` short-circuited the
455
- **entire** push-review gate — operators reached for it to silence a
456
- transient Codex outage and accidentally bypassed HALT, the cross-repo
457
- guard, and the general push-review gate. 0.8.0 narrows it to what the
458
- name implies: the waiver satisfies **only** the protected-path Codex
459
- audit requirement. HALT, cross-repo guard, ref-resolution failures, and
460
- push-review-cache misses still block. The skip audit record is still
461
- named `codex.review.skipped` and still fails the `codex.review` jq
462
- predicate — skipping a review is not a review.
463
-
464
- For the previous whole-gate bypass, use `REA_SKIP_PUSH_REVIEW=<reason>`
465
- (unchanged, 0.5.0). It writes `push.review.skipped` with an
466
- `os_identity` sub-object (uid, whoami, hostname, pid, ppid, tty, ci)
467
- so auditors can distinguish a real operator from a forged git-config
468
- actor, and refuses on CI runners unless the policy opts in via
469
- `review.allow_skip_in_ci: true`.
470
-
471
- Codex responses are treated as untrusted input. They flow through the
472
- `redact` and `injection` middleware on return — same treatment as any
473
- other downstream tool result. Codex never receives `.rea/policy.yaml`
474
- content in its prompts; Codex reviews diffs, not policy.
475
-
476
- If Codex is not installed, `rea doctor` warns with a one-line install
477
- hint. REA does not require Codex to function — the `bst-internal-no-codex`
478
- and `open-source-no-codex` profiles disable the requirement entirely,
479
- and `ClaudeSelfReviewer` is the in-process fallback (tagged
480
- `degraded: true` in the audit record so self-review is visible and
481
- countable).
482
-
483
- ## Agent push workflow — satisfying the push-review gate
484
-
485
- When `git push` is blocked by `push-review-gate.sh` the gate prints
486
- remediation steps. This section is the canonical one-command flow the
487
- steps reduce to. Agents should copy-paste this verbatim; humans should
488
- expect agents to.
489
-
490
- ### 1. Run the adversarial review
626
+ `rea serve` can expose a loopback-only Prometheus endpoint when
627
+ `REA_METRICS_PORT` is set:
491
628
 
492
629
  ```bash
493
- # From an interactive Claude Code session:
494
- /codex-review
630
+ REA_METRICS_PORT=9464 rea serve
631
+ curl http://127.0.0.1:9464/metrics
495
632
  ```
496
633
 
497
- This invokes the `codex-adversarial` agent, which records a
498
- `codex.review` audit entry with `verdict: pass | concerns | blocking |
499
- error` and a `finding_count`. The push gate looks up that entry by
500
- `head_sha + verdict {pass, concerns}`.
634
+ Metrics: per-downstream call and error counters, in-flight gauge,
635
+ audit-lines-appended counter, circuit-breaker state gauge, and a
636
+ seconds-since-last-HALT-check gauge. The listener binds to `127.0.0.1`
637
+ only, serves only `GET /metrics`, and never binds by default. No TLS;
638
+ scrape through SSH or a reverse proxy for cross-host access.
501
639
 
502
- ### 2. Record-and-cache in one CLI call
640
+ ---
503
641
 
504
- If you already have a review verdict (from `/codex-review`, or from a
505
- manual Codex run, or from an offline review) emit the audit record AND
506
- update the push-review cache with a single command:
642
+ ## Policy file
507
643
 
508
- ```bash
509
- rea audit record codex-review \
510
- --head-sha "$(git rev-parse HEAD)" \
511
- --branch "$(git rev-parse --abbrev-ref HEAD)" \
512
- --target main \
513
- --verdict pass \
514
- --finding-count 0 \
515
- --summary "no findings" \
516
- --also-set-cache
517
- ```
518
-
519
- `--also-set-cache` writes both `.rea/audit.jsonl` and
520
- `.rea/review-cache.jsonl` in the same invocation (two sequential
521
- appends, not a two-phase commit — but close enough in practice that the
522
- push-gate lookup cannot see the audit record without the cache entry
523
- unless a crash lands between them). Without it, the audit record lands
524
- but the cache stays cold — and the next `git push` pays for a re-review
525
- even though the audit trail already shows the review happened.
526
- `--also-set-cache` is what the gate's remediation text should be reduced
527
- to.
528
-
529
- Verdict mapping for the cache leg:
530
-
531
- | `--verdict` | Cache `result` | Cache `reason` |
532
- | ------------ | -------------- | -------------- |
533
- | `pass` | `pass` | — (omitted) |
534
- | `concerns` | `pass` | `codex:concerns` |
535
- | `blocking` | `fail` | `codex:blocking` |
536
- | `error` | `fail` | `codex:error` |
537
-
538
- ### 3. Push
644
+ `.rea/policy.yaml` fields. The schema is zod-strict — unknown fields are
645
+ rejected at parse time, not ignored.
539
646
 
540
- ```bash
541
- git push
647
+ ### Required fields
648
+
649
+ | Field | Type | Purpose |
650
+ | --- | --- | --- |
651
+ | `version` | `"1"` | Schema version; only `"1"` accepted in the current major |
652
+ | `profile` | string | Profile name (see below) |
653
+ | `installed_by` | string | Stamped by `rea init` — identifies the installing version |
654
+ | `installed_at` | ISO-8601 | Stamped by `rea init` — install timestamp |
655
+ | `autonomy_level` | `L0` \| `L1` \| `L2` \| `L3` | Current autonomy. `L0` = read-only; `L3` = full tool access |
656
+ | `max_autonomy_level` | `L0` \| `L1` \| `L2` \| `L3` | Hard ceiling. `autonomy_level` cannot exceed this |
657
+ | `promotion_requires_human_approval` | boolean | Require operator confirmation to raise autonomy |
658
+ | `block_ai_attribution` | boolean | Enforce no-AI-attribution in commits and PR bodies |
659
+ | `blocked_paths` | string[] | Glob patterns. `.rea/` is always blocked regardless |
660
+ | `notification_channel` | string | Optional Discord webhook URL; empty string = disabled |
661
+
662
+ ### Optional blocks
663
+
664
+ ```yaml
665
+ injection_detection: block # "block" | "warn" — legacy 0.2.x knob
666
+ injection:
667
+ suspicious_blocks_writes: true # suspicious verdict on write/destructive tier denies
668
+ context_protection:
669
+ delegate_to_subagent:
670
+ - pnpm run build
671
+ - pnpm run test
672
+ max_bash_output_lines: 100
673
+ review:
674
+ codex_required: true
675
+ concerns_blocks: true
676
+ timeout_ms: 1800000
677
+ # last_n_commits: 10 # optional — narrow review to HEAD~N
678
+ # auto_narrow_threshold: 30 # optional — auto-narrow when commits
679
+ # behind base > N (0 disables, default 30)
680
+ redact:
681
+ match_timeout_ms: 100
682
+ patterns:
683
+ - name: custom-api-key
684
+ regex: 'acme_[A-Za-z0-9]{32}'
685
+ flags: 'g'
686
+ audit:
687
+ rotation:
688
+ max_bytes: 52428800 # 50 MiB
689
+ max_age_days: 30
690
+ gateway:
691
+ health:
692
+ expose_diagnostics: false
542
693
  ```
543
694
 
544
- The gate hits the cache, sees `{"hit":true,"result":"pass"}`, and exits
545
- 0 on the first attempt. No `!`-bash escapes, no manual audit writing,
546
- no separate `rea cache set` invocation.
695
+ User-supplied `redact.patterns[]` are validated via `safe-regex` at load —
696
+ any pattern flagged unsafe fails the load with a specific error naming
697
+ the offender.
698
+
699
+ ### Autonomy levels
547
700
 
548
- ### SDK alternative
701
+ | Level | Effect |
702
+ | --- | --- |
703
+ | `L0` | Read-only — every write/destructive tier call denies |
704
+ | `L1` | Default. Reads allowed; writes allowed; destructive tier denied |
705
+ | `L2` | Writes and destructive tier allowed |
706
+ | `L3` | No autonomy gate — only hook-layer and policy-layer checks remain |
549
707
 
550
- When embedding the flow in a TypeScript tool instead of shelling out,
551
- import the public audit helper:
708
+ `autonomy_level > max_autonomy_level` is rejected at parse time.
709
+ `promotion_requires_human_approval: false` requires the CLI flag
710
+ `--i-understand-the-risks` on any operation that raises autonomy.
552
711
 
553
- ```ts
554
- import {
555
- appendAuditRecord,
556
- CODEX_REVIEW_SERVER_NAME,
557
- CODEX_REVIEW_TOOL_NAME,
558
- InvocationStatus,
559
- Tier,
560
- } from '@bookedsolid/rea/audit';
712
+ ### Profiles
561
713
 
562
- await appendAuditRecord(process.cwd(), {
563
- tool_name: CODEX_REVIEW_TOOL_NAME,
564
- server_name: CODEX_REVIEW_SERVER_NAME,
565
- tier: Tier.Read,
566
- status: InvocationStatus.Allowed,
567
- metadata: {
568
- head_sha: headSha,
569
- target: 'main',
570
- finding_count: 0,
571
- verdict: 'pass',
572
- },
573
- });
574
- ```
714
+ Seven profiles ship in `profiles/`. The profile name is recorded in
715
+ `policy.yaml#profile` and governs which agents `rea init` copies and what
716
+ defaults apply.
575
717
 
576
- The CLI wraps exactly this — use the CLI unless the host is already a
577
- TypeScript process that wants to avoid the subprocess roundtrip.
718
+ | Profile | Intended use | Codex default |
719
+ | --- | --- | --- |
720
+ | `minimal` | Smallest possible install — curated 10 + opinionated minimal hooks | `true` |
721
+ | `client-engagement` | Consulting engagement where the repo is client-owned | `true` |
722
+ | `bst-internal` | Booked Solid internal projects; conservative posture | `true` |
723
+ | `bst-internal-no-codex` | Same as above; no Codex CLI available | `false` |
724
+ | `lit-wc` | Lit web-component library projects | `true` |
725
+ | `open-source` | OSS library / CLI projects; liberal but audited | `true` |
726
+ | `open-source-no-codex` | Same; no Codex CLI available | `false` |
727
+
728
+ The `-no-codex` variants default `review.codex_required: false` on
729
+ install so teams without a Codex bench get a first-class opt-out. Flip
730
+ `--codex` / `--no-codex` on the `rea init` command line to override.
578
731
 
579
- ### Agent autonomy self-consistency
732
+ ---
580
733
 
581
- At autonomy `L1`, `rea cache check`, `rea audit record codex-review`,
582
- `rea doctor`, and `rea status` are classified **Read tier** — they
583
- cannot be denied by REA's own middleware. `rea cache set` is Write
584
- tier and is still allowed at L1. `rea freeze` is Destructive tier and
585
- is denied at L1 (deny-reason includes the subcommand, e.g.
586
- `Bash (rea freeze)`, not just `Bash`).
734
+ ## Hooks shipped
587
735
 
588
- ## Hooks
736
+ Eleven hooks ship in `hooks/` and are copied into `.claude/hooks/` by
737
+ `rea init`. All eleven are wired by default in the shipped
738
+ `.claude/settings.json`.
589
739
 
590
- Fourteen hooks. Each does one thing.
740
+ | Hook | Event | Purpose | Default |
741
+ | --- | --- | --- | --- |
742
+ | `dangerous-bash-interceptor.sh` | `PreToolUse: Bash` | Block categories of destructive shell commands (`rm -rf`, `git reset --hard`, `--no-verify`, …) | Registered |
743
+ | `env-file-protection.sh` | `PreToolUse: Bash` | Block reads of `.env*` files | Registered |
744
+ | `dependency-audit-gate.sh` | `PreToolUse: Bash` | Verify packages exist on the registry before install | Registered |
745
+ | `security-disclosure-gate.sh` | `PreToolUse: Bash` | Route security-keyword `gh issue create` to private disclosure | Registered |
746
+ | `pr-issue-link-gate.sh` | `PreToolUse: Bash` | Advisory warn when `gh pr create` has no linked issue | Registered |
747
+ | `attribution-advisory.sh` | `PreToolUse: Bash` | Block commits/PRs containing AI attribution markers | Registered |
748
+ | `secret-scanner.sh` | `PreToolUse: Write\|Edit` | Scan file writes for credential patterns | Registered |
749
+ | `settings-protection.sh` | `PreToolUse: Write\|Edit` | Block agent writes to `.claude/settings.json`, hook dirs, policy | Registered |
750
+ | `blocked-paths-enforcer.sh` | `PreToolUse: Write\|Edit` | Enforce `blocked_paths` from policy | Registered |
751
+ | `changeset-security-gate.sh` | `PreToolUse: Write\|Edit` | Guard changesets against GHSA leaks and malformed frontmatter | Registered |
752
+ | `architecture-review-gate.sh` | `PostToolUse: Write\|Edit` | Flag edits crossing architectural boundaries (advisory) | Registered |
753
+
754
+ The 0.10.x review-gate scripts (`push-review-gate.sh`,
755
+ `push-review-gate-git.sh`, `commit-review-gate.sh`) and the 1,250-line
756
+ shared bash core (`hooks/_lib/push-review-core.sh`) were removed in
757
+ 0.11.0. The `hooks/_lib/` directory now contains only the three shared
758
+ helpers — `common.sh`, `halt-check.sh`, `policy-read.sh` — used by the
759
+ remaining hooks.
760
+
761
+ Every hook uses `set -euo pipefail` (or `set -uo pipefail` for stdin-JSON
762
+ consumers) and performs a HALT check near the top. Both the hook layer
763
+ and the MCP gateway middleware fail closed; bypassing one does not
764
+ disable the other.
591
765
 
592
- | Hook | Event | One-line purpose |
593
- | --- | --- | --- |
594
- | `dangerous-bash-interceptor` | PreToolUse: Bash | Block categories of destructive shell commands |
595
- | `env-file-protection` | PreToolUse: Bash | Block reads of `.env*` files |
596
- | `dependency-audit-gate` | PreToolUse: Bash | Verify packages exist on the registry before install |
597
- | `commit-review-gate` | PreToolUse: Bash | Intercept `git commit`; require review on non-trivial diffs |
598
- | `push-review-gate` | PreToolUse: Bash | Intercept `git push` (Claude-Code-JSON adapter); protected-path + Codex audit |
599
- | `push-review-gate-git` | `.husky/pre-push` | Native git adapter around the same core |
600
- | `attribution-advisory` | PreToolUse: Bash | Block commits / PRs containing AI attribution markers |
601
- | `pr-issue-link-gate` | PreToolUse: Bash | Advisory warn when `gh pr create` has no linked issue |
602
- | `security-disclosure-gate` | PreToolUse: Bash | Route security-keyword `gh issue create` to private disclosure |
603
- | `secret-scanner` | PreToolUse: Write\|Edit | Scan file writes for credential patterns |
604
- | `settings-protection` | PreToolUse: Write\|Edit | Block agent writes to `.claude/settings.json`, hook dirs, policy |
605
- | `blocked-paths-enforcer` | PreToolUse: Write\|Edit | Enforce `blocked_paths` from policy |
606
- | `changeset-security-gate` | PreToolUse: Write\|Edit | Guard changesets against GHSA leaks and malformed frontmatter |
607
- | `architecture-review-gate` | PostToolUse: Write\|Edit | Flag edits crossing architectural boundaries (advisory) |
766
+ ---
608
767
 
609
768
  ## Slash commands
610
769
 
770
+ Five commands ship in `commands/` and are copied into `.claude/commands/`
771
+ by `rea init`.
772
+
611
773
  | Command | Purpose |
612
774
  | --- | --- |
613
- | `/rea` | Session status — autonomy level, HALT state, last audit entries, next action |
775
+ | `/rea` | Session status — autonomy level, HALT state, recent audit entries, next action |
614
776
  | `/review` | Invoke the `code-reviewer` agent on current changes |
615
- | `/codex-review` | Invoke the `codex-adversarial` agent `/codex:adversarial-review` |
777
+ | `/codex-review` | Invoke the `codex-adversarial` agent via the Codex plugin |
616
778
  | `/freeze` | Prompt for a reason and write `.rea/HALT` |
617
- | `/halt-check` | Verify every middleware and hook respects HALT |
779
+ | `/halt-check` | Smoke test — verify every hook and middleware respects HALT |
618
780
 
619
- ## Policy file reference
781
+ ---
620
782
 
621
- `.rea/policy.yaml` fields. The schema is zod-strict — unknown fields are
622
- rejected, not ignored.
783
+ ## Curated agents
623
784
 
624
- | Field | Type | Purpose |
625
- | --- | --- | --- |
626
- | `version` | string, `"1"` | Schema version; only `"1"` accepted in the current major |
627
- | `profile` | string | Profile name from `profiles/` (e.g. `bst-internal`) |
628
- | `autonomy_level` | `L0`\|`L1`\|`L2`\|`L3` | Current autonomy. `L0` = read-only; `L3` = full tool access |
629
- | `max_autonomy_level` | `L0`\|`L1`\|`L2`\|`L3` | Hard ceiling. `autonomy_level` cannot exceed this |
630
- | `promotion_requires_human_approval` | boolean | Require operator confirmation to raise autonomy. Default `true` |
631
- | `blocked_paths` | string[] | Glob patterns. `.rea/` is always blocked regardless of this list |
632
- | `block_ai_attribution` | boolean | Enforce no-AI-attribution in commits and PR bodies |
633
- | `context_protection.delegate_to_subagent` | string[] | Commands that must run in a subagent context to preserve the parent's context window |
634
- | `context_protection.max_bash_output_lines` | number | Truncate long bash output at this line count |
635
- | `notification_channel` | string | Optional Discord webhook URL. Empty string = no notifications |
636
- | `review.codex_required` | boolean | When `false`, protected-path pushes don't require a Codex audit (first-class no-Codex mode). Default `true` |
637
- | `review.cache_max_age_seconds` | number | TTL for entries in `.rea/review-cache.jsonl`. Default 3600 |
638
- | `review.allow_skip_in_ci` | boolean | When `true`, `REA_SKIP_PUSH_REVIEW` is accepted on CI runners. Default `false` |
639
- | `injection.suspicious_blocks_writes` | boolean | `bst-internal` posture — `suspicious` verdict on a write/destructive tool denies instead of warning. Default `false` |
640
- | `redact.patterns[]` | string[] | User-supplied secret patterns; vetted via `safe-regex` at load |
641
- | `redact.match_timeout_ms` | number | Per-call regex budget. Default 100 |
642
- | `gateway.health.expose_diagnostics` | boolean | When `true`, `__rea__health` emits redacted+classified diagnostic strings on the wire. Default `false` (null) |
643
-
644
- `autonomy_level > max_autonomy_level` is rejected at parse time. Setting
645
- `promotion_requires_human_approval: false` requires the CLI flag
646
- `--i-understand-the-risks`.
785
+ Ten specialist agents ship in `agents/` and are copied into `.claude/agents/`
786
+ by `rea init`. Profiles layer additional specialists on top for specific
787
+ project shapes.
647
788
 
648
- ## Migration from `@bookedsolid/reagent`
789
+ | Agent | When to use |
790
+ | --- | --- |
791
+ | `rea-orchestrator` | **First stop for any non-trivial task.** Reads policy, checks HALT, routes to the right specialist(s), coordinates multi-step work, enforces the plan/build/review loop. |
792
+ | `code-reviewer` | Structured review of a working-tree diff; surfaces correctness, clarity, and consistency issues without adversarial framing. |
793
+ | `codex-adversarial` | Adversarial review via the Codex plugin (`/codex:adversarial-review`). Independent model perspective; produces an audit entry with verdict. |
794
+ | `security-engineer` | Security-sensitive implementation and review — auth flows, secret handling, injection surfaces. |
795
+ | `accessibility-engineer` | WCAG review, ARIA semantics, keyboard navigation, screen-reader fact-checking. |
796
+ | `typescript-specialist` | Strict-mode TypeScript correctness, generics, narrowing, inference edge cases. |
797
+ | `frontend-specialist` | UI component work, framework idioms (React, Lit, Astro), CSS architecture. |
798
+ | `backend-engineer` | API design, database schema, background jobs, MCP server implementation. |
799
+ | `qa-engineer` | Test strategy, fixture design, regression reproducers, flake triage. |
800
+ | `technical-writer` | User-facing documentation, API references, migration guides, changelog narratives. |
801
+
802
+ The `rea-orchestrator` is the single entry point for non-trivial tasks.
803
+ The CLAUDE.md fragment installed by `rea init` instructs the host agent
804
+ to route there first; delegation contracts are defined in each agent's
805
+ markdown file.
806
+
807
+ ---
808
+
809
+ ## CLI reference
810
+
811
+ ```
812
+ rea <command> [options]
813
+ ```
814
+
815
+ Run `rea <command> --help` for full per-command options.
816
+
817
+ ### `rea init`
818
+
819
+ Interactive wizard — write `.rea/policy.yaml`, install `.claude/`, the
820
+ commit-msg hook, and a CLAUDE.md fragment.
821
+
822
+ ```bash
823
+ rea init
824
+ rea init -y --profile bst-internal # non-interactive
825
+ rea init --from-reagent # migrate from .reagent/
826
+ rea init --profile open-source-no-codex # disable Codex by default
827
+ rea init --force # overwrite existing artifacts
828
+ ```
829
+
830
+ ### `rea upgrade`
831
+
832
+ Sync `.claude/`, `.husky/`, and managed fragments with this rea version.
833
+ Prompts on drift; silently refreshes unmodified files.
649
834
 
650
835
  ```bash
651
- npx @bookedsolid/rea init --from-reagent
836
+ rea upgrade --dry-run # show what would change; write nothing
837
+ rea upgrade # interactive
838
+ rea upgrade -y # non-interactive, keep drifted files
839
+ rea upgrade --force # non-interactive, overwrite drift
652
840
  ```
653
841
 
654
- `--from-reagent`:
842
+ ### `rea serve`
655
843
 
656
- - Reads `.reagent/policy.yaml` and translates field-for-field into
657
- `.rea/policy.yaml`. Field names are identical, so the translation is a
658
- rename.
659
- - Moves `.reagent/audit.jsonl` to `.rea/audit.jsonl` and verifies the
660
- hash chain.
661
- - Un-wires dropped hooks (Obsidian, PM-layer, account) from
662
- `.claude/settings.json`.
663
- - Replaces `reagent` slash commands and agents with the REA equivalents.
664
- - Leaves `.reagent/` in place; you delete it manually after verifying
665
- `rea doctor` passes and a dogfood run completes.
844
+ Start the MCP gateway. Invoked by Claude Code via `.mcp.json`; not a
845
+ daemon. Stdio transport only.
666
846
 
667
- See [MIGRATION-0.5.0.md](./MIGRATION-0.5.0.md) for the BUG-008 / BUG-009
668
- / BUG-010 coordinated fix window. Between 0.5.0 and 0.9.0, the breaking
669
- semantic change worth calling out is 0.8.0's narrowing of
670
- `REA_SKIP_CODEX_REVIEW` to a Codex-only waiver — see the CHANGELOG
671
- entry for the migration steps.
847
+ ```bash
848
+ rea serve
849
+ REA_METRICS_PORT=9464 rea serve
850
+ REA_LOG_LEVEL=debug rea serve
851
+ ```
672
852
 
673
- ## Security
853
+ ### `rea freeze` / `rea unfreeze`
854
+
855
+ Write or remove `.rea/HALT`. Every call writes an audit record.
856
+
857
+ ```bash
858
+ rea freeze --reason "incident triage"
859
+ rea unfreeze
860
+ rea unfreeze -y # skip confirmation
861
+ ```
862
+
863
+ ### `rea check`
864
+
865
+ On-disk status — autonomy, HALT, profile, recent audit entries. No live
866
+ process probe.
867
+
868
+ ### `rea status`
869
+
870
+ Running-process view — reads `.rea/serve.pid` + `.rea/serve.state.json`
871
+ to render per-downstream health.
872
+
873
+ ```bash
874
+ rea status
875
+ rea status --json # pipe to jq
876
+ ```
877
+
878
+ ### `rea doctor`
879
+
880
+ Validate the install — policy parses, `.rea/` layout, hooks, Codex plugin
881
+ presence, TOFU fingerprint store.
882
+
883
+ ```bash
884
+ rea doctor
885
+ rea doctor --metrics # also print 7-day Codex telemetry summary
886
+ rea doctor --drift # report drift vs. install manifest (read-only)
887
+ ```
888
+
889
+ In non-git directories the commit-msg and pre-push checks are skipped
890
+ cleanly. Audit hash-chain integrity is verified by `rea audit verify`,
891
+ not by `rea doctor`.
892
+
893
+ ### `rea audit rotate` / `rea audit verify`
894
+
895
+ ```bash
896
+ rea audit rotate # force rotation now
897
+ rea audit verify # re-hash the chain; exit 1 on first tamper
898
+ rea audit verify --since <file> # walk forward from a rotated file
899
+ ```
900
+
901
+ ### `rea tofu list` / `rea tofu accept`
902
+
903
+ ```bash
904
+ rea tofu list
905
+ rea tofu list --json
906
+ rea tofu accept my-mcp-server --reason "added vault path /Volumes/Work"
907
+ ```
908
+
909
+ ### `rea hook push-gate`
910
+
911
+ Pre-push Codex review gate. Normally invoked by `.husky/pre-push`; run
912
+ manually to test.
913
+
914
+ ```bash
915
+ rea hook push-gate
916
+ rea hook push-gate --base origin/main
917
+ ```
918
+
919
+ ---
920
+
921
+ ## Migration from 0.10.x
922
+
923
+ `rea upgrade` handles the policy and on-disk pieces. You run it once:
924
+
925
+ ```bash
926
+ rea upgrade
927
+ ```
928
+
929
+ It performs:
930
+
931
+ 1. **Backup** — writes `.rea/policy.yaml.bak-<timestamp>` before any edit.
932
+ 2. **Strip removed fields** — removes `review.cache_max_age_seconds` and
933
+ `review.allow_skip_in_ci` from the policy file. Both were cache-gate
934
+ concepts with no meaning under the stateless gate.
935
+ 3. **Add defaults** — inserts `review.concerns_blocks: true` if absent.
936
+ 4. **Prune settings.json** — removes hook registrations for the removed
937
+ scripts (`push-review-gate`, `commit-review-gate`) from
938
+ `.claude/settings.json`, leaving the other registrations intact.
939
+ 5. **Refresh `.claude/hooks/`** — deletes the removed scripts with a
940
+ change-log entry.
941
+ 6. **Rewrite `.husky/pre-push`** iff it carries a rea marker. A
942
+ foreign `.husky/pre-push` (no marker) is left untouched and a loud
943
+ warning is printed.
944
+ 7. **Refresh `.git/hooks/pre-push`** fallback with the new 15-line stub
945
+ (when `core.hooksPath` is unset and git is using the default hooks
946
+ directory).
947
+
948
+ ### What the new `.husky/pre-push` looks like
949
+
950
+ Fifteen lines of POSIX `sh`. HALT check, locate the rea binary, `exec rea
951
+ hook push-gate "$@"`. That is the entire body — all real work lives in
952
+ the TypeScript gate composer at `src/hooks/push-gate/index.ts`.
953
+
954
+ ### Rollback
955
+
956
+ If the stateless gate's behavior is blocking a specific workflow you
957
+ cannot yet address:
958
+
959
+ ```bash
960
+ npm install -g @bookedsolid/rea@0.10.3
961
+ # restore policy from the backup rea upgrade wrote
962
+ cp .rea/policy.yaml.bak-<ts> .rea/policy.yaml
963
+ # re-run the 0.10.3 install to put the old hooks back
964
+ rea init --force
965
+ ```
966
+
967
+ The 0.10.3 cache-attestation gate remains on npm and continues to work.
968
+ Rollback is a supported path — the `rea upgrade` backup is specifically
969
+ there to make it a one-liner.
970
+
971
+ ### What you will not need to do anymore
972
+
973
+ - `rea cache check` / `rea cache set` / `rea cache list` / `rea cache clear`
974
+ — all removed. The stateless gate consults no cache.
975
+ - `rea audit record codex-review --also-set-cache` — removed. The gate
976
+ writes its own audit records from the actual Codex run.
977
+ - Setting `REA_SKIP_PUSH_REVIEW` — removed. Use either
978
+ `REA_SKIP_PUSH_GATE=<reason>` or `REA_SKIP_CODEX_REVIEW=<reason>`
979
+ (both value-carrying and always audited; identical effect, distinct
980
+ `skip_var` in audit metadata) or flip `review.codex_required: false`
981
+ in policy. `REA_SKIP_CODEX_REVIEW` was reinstated in 0.12.0 as an
982
+ audited alias for `REA_SKIP_PUSH_GATE` — it had been documented in the
983
+ gateway-tier reviewers but not in the push-gate, leaving agents
984
+ setting the documented variant blocked.
985
+
986
+ ---
987
+
988
+ ## Contributor quality gates
989
+
990
+ Before push, run the four checks locally. CI runs them as required status
991
+ checks on every PR to `main`:
992
+
993
+ ```bash
994
+ pnpm lint # ESLint 10 — zero warnings
995
+ pnpm type-check # tsc --noEmit (strict)
996
+ pnpm test # vitest run
997
+ pnpm build # tsc -p tsconfig.build.json
998
+ ```
999
+
1000
+ Additionally, every PR needs:
1001
+
1002
+ - **DCO sign-off** on all commits (`git commit -s`). The DCO bot rejects
1003
+ unsigned commits.
1004
+ - **Changeset** entry (`pnpm changeset`) unless the change is purely
1005
+ non-publishable (CI, docs, meta). CI flags missing changesets.
1006
+ - **Secret scan** clean. Gitleaks runs in CI and via the
1007
+ `secret-scanner.sh` hook.
1008
+ - **No AI attribution** anywhere — commit messages, PR bodies, code
1009
+ comments, changeset content. The commit-msg hook and
1010
+ `attribution-advisory.sh` reject structural attribution.
1011
+
1012
+ Security-sensitive paths (`src/gateway/middleware/**`, `src/policy/**`,
1013
+ `src/hooks/**`, `hooks/**`, `.github/workflows/**`) require explicit
1014
+ maintainer review and a threat-model update in the same PR.
1015
+
1016
+ Releases flow through Changesets: a "Version Packages" PR is auto-opened
1017
+ when a changeset lands on `main`. Merging it triggers `npm publish
1018
+ --provenance` via OIDC from `.github/workflows/release.yml`. Do not
1019
+ manually `npm publish`.
1020
+
1021
+ ---
1022
+
1023
+ ## Threat model and security
674
1024
 
675
1025
  - [SECURITY.md](./SECURITY.md) — disclosure policy, supported versions,
676
- and scope. Do not report vulnerabilities via public GitHub issues.
1026
+ GHSA coordination. 72-hour acknowledgment target, 90-day window. Do
1027
+ not report vulnerabilities via public GitHub issues.
677
1028
  - [THREAT_MODEL.md](./THREAT_MODEL.md) — attack surface, mitigations,
678
- residual risks. This is the contract REA holds itself to.
1029
+ residual risks. The contract rea holds itself to.
679
1030
 
680
- Short version: gateway and hook layers operate independently. Both fail
681
- closed. `.rea/` is always blocked. Audit is hash-chained. Policy is
682
- re-read on every invocation. npm publish uses OIDC provenance, not
683
- long-lived tokens.
1031
+ Short version: the MCP gateway and the hook layer run independently.
1032
+ Both fail closed. `.rea/` is always blocked. The audit log is
1033
+ hash-chained. Policy is re-read on every invocation. npm publish uses
1034
+ OIDC provenance, not long-lived tokens. The pre-push gate runs Codex
1035
+ on every push and treats Codex responses as untrusted input — findings
1036
+ flow through the same redact pattern set used by the middleware before
1037
+ anything hits disk.
684
1038
 
685
- ## Contributing
1039
+ ---
686
1040
 
687
- See [CONTRIBUTING.md](./CONTRIBUTING.md). Short version:
1041
+ ## Non-goals
688
1042
 
689
- - DCO sign-off required on every commit (`git commit -s`). CI rejects
690
- unsigned commits.
691
- - No AI attribution in commit messages, PR bodies, or code. The
692
- commit-msg hook enforces this.
693
- - Conventional commits, TypeScript strict, ESLint zero-warnings,
694
- Prettier, vitest.
695
- - Security-sensitive paths (`src/gateway/middleware/**`, `src/policy/**`,
696
- `hooks/**`, `.github/workflows/**`) require explicit maintainer
697
- review and a threat-model update in the same PR.
1043
+ See [What REA is NOT](#what-rea-is-not) above. Every "but what if we
1044
+ just added X" belongs in a separate package that composes with REA. The
1045
+ non-goals are the product.
1046
+
1047
+ ---
698
1048
 
699
- ## License
1049
+ ## License and contributing
700
1050
 
701
- [MIT](./LICENSE)
1051
+ [MIT](./LICENSE). See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full
1052
+ contributor guide and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) for
1053
+ the Contributor Covenant.
1054
+
1055
+ - DCO sign-off required (`git commit -s`) — no CLA.
1056
+ - Conventional commits, TypeScript strict, ESLint zero-warnings,
1057
+ Prettier, vitest.
1058
+ - Changeset on every publishable change; merge the auto-generated
1059
+ Version Packages PR to release.
1060
+ - Security-sensitive paths gated by CODEOWNERS; human review required.
1061
+
1062
+ This repo dogfoods itself. rea's governance layer enforces rea's own
1063
+ commit, hook, and attribution rules. The install under `.rea/`,
1064
+ `.claude/`, and `.husky/` is the reference example of the
1065
+ `bst-internal` profile.