@bookedsolid/rea 0.10.3 → 0.12.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.
Files changed (77) hide show
  1. package/.husky/pre-push +48 -162
  2. package/README.md +834 -552
  3. package/agents/codex-adversarial.md +5 -3
  4. package/commands/codex-review.md +3 -5
  5. package/dist/audit/append.d.ts +7 -32
  6. package/dist/audit/append.js +7 -35
  7. package/dist/cli/audit.d.ts +0 -31
  8. package/dist/cli/audit.js +5 -74
  9. package/dist/cli/doctor.d.ts +12 -0
  10. package/dist/cli/doctor.js +96 -17
  11. package/dist/cli/hook.d.ts +55 -0
  12. package/dist/cli/hook.js +138 -0
  13. package/dist/cli/index.js +5 -80
  14. package/dist/cli/init.js +1 -1
  15. package/dist/cli/install/gitignore.d.ts +2 -2
  16. package/dist/cli/install/gitignore.js +3 -3
  17. package/dist/cli/install/pre-push.d.ts +158 -272
  18. package/dist/cli/install/pre-push.js +491 -2633
  19. package/dist/cli/install/settings-merge.d.ts +17 -0
  20. package/dist/cli/install/settings-merge.js +48 -1
  21. package/dist/cli/upgrade.js +131 -3
  22. package/dist/config/tier-map.js +18 -25
  23. package/dist/hooks/push-gate/base.d.ts +104 -0
  24. package/dist/hooks/push-gate/base.js +198 -0
  25. package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
  26. package/dist/hooks/push-gate/codex-runner.js +223 -0
  27. package/dist/hooks/push-gate/findings.d.ts +68 -0
  28. package/dist/hooks/push-gate/findings.js +142 -0
  29. package/dist/hooks/push-gate/halt.d.ts +28 -0
  30. package/dist/hooks/push-gate/halt.js +49 -0
  31. package/dist/hooks/push-gate/index.d.ts +98 -0
  32. package/dist/hooks/push-gate/index.js +416 -0
  33. package/dist/hooks/push-gate/policy.d.ts +55 -0
  34. package/dist/hooks/push-gate/policy.js +64 -0
  35. package/dist/hooks/push-gate/report.d.ts +89 -0
  36. package/dist/hooks/push-gate/report.js +140 -0
  37. package/dist/policy/loader.d.ts +15 -10
  38. package/dist/policy/loader.js +8 -6
  39. package/dist/policy/types.d.ts +73 -22
  40. package/package.json +1 -1
  41. package/scripts/tarball-smoke.sh +7 -2
  42. package/dist/cache/review-cache.d.ts +0 -115
  43. package/dist/cache/review-cache.js +0 -200
  44. package/dist/cli/cache.d.ts +0 -84
  45. package/dist/cli/cache.js +0 -150
  46. package/dist/hooks/review-gate/args.d.ts +0 -126
  47. package/dist/hooks/review-gate/args.js +0 -315
  48. package/dist/hooks/review-gate/audit.d.ts +0 -131
  49. package/dist/hooks/review-gate/audit.js +0 -181
  50. package/dist/hooks/review-gate/banner.d.ts +0 -97
  51. package/dist/hooks/review-gate/banner.js +0 -172
  52. package/dist/hooks/review-gate/base-resolve.d.ts +0 -155
  53. package/dist/hooks/review-gate/base-resolve.js +0 -247
  54. package/dist/hooks/review-gate/cache-key.d.ts +0 -55
  55. package/dist/hooks/review-gate/cache-key.js +0 -41
  56. package/dist/hooks/review-gate/cache.d.ts +0 -108
  57. package/dist/hooks/review-gate/cache.js +0 -120
  58. package/dist/hooks/review-gate/constants.d.ts +0 -26
  59. package/dist/hooks/review-gate/constants.js +0 -34
  60. package/dist/hooks/review-gate/diff.d.ts +0 -181
  61. package/dist/hooks/review-gate/diff.js +0 -232
  62. package/dist/hooks/review-gate/errors.d.ts +0 -72
  63. package/dist/hooks/review-gate/errors.js +0 -100
  64. package/dist/hooks/review-gate/hash.d.ts +0 -43
  65. package/dist/hooks/review-gate/hash.js +0 -46
  66. package/dist/hooks/review-gate/index.d.ts +0 -31
  67. package/dist/hooks/review-gate/index.js +0 -35
  68. package/dist/hooks/review-gate/metadata.d.ts +0 -98
  69. package/dist/hooks/review-gate/metadata.js +0 -158
  70. package/dist/hooks/review-gate/policy.d.ts +0 -55
  71. package/dist/hooks/review-gate/policy.js +0 -71
  72. package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
  73. package/dist/hooks/review-gate/protected-paths.js +0 -76
  74. package/hooks/_lib/push-review-core.sh +0 -1250
  75. package/hooks/commit-review-gate.sh +0 -330
  76. package/hooks/push-review-gate-git.sh +0 -94
  77. package/hooks/push-review-gate.sh +0 -92
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.
27
92
 
28
- Node 22+ and pnpm 9+ required.
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
+ ```
106
+
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,258 @@ 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"
144
- ```
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`.
145
331
 
146
- Both calls produce audit entries. The middleware never clears HALT on
147
- its own.
332
+ ### Environment variables
148
333
 
149
- ### 3. Verify the install
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. |
150
339
 
151
- ```bash
152
- rea doctor
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.
153
351
  ```
154
352
 
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`.
353
+ | Key | Type | Default | Purpose |
354
+ | --- | --- | --- | --- |
355
+ | `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`. |
356
+ | `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. |
357
+ | `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. |
358
+ | `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). |
165
359
 
166
- ### 4. Watch the running gateway
360
+ ### Codex CLI dependency
167
361
 
168
- ```bash
169
- rea status # human-readable summary
170
- rea status --json # JSON pipe to jq
362
+ The gate shells out to `codex`. **`codex` is a hard prerequisite** when
363
+ `policy.review.codex_required: true` (the default for every profile other
364
+ than the `-no-codex` variants). As of 0.12.0 `rea doctor` runs a
365
+ `codex CLI on PATH` check that **fails** when codex is required by policy
366
+ but the binary is not on `PATH` — surfacing the prereq during install
367
+ rather than at first push:
368
+
369
+ ```
370
+ [fail] codex CLI on PATH
371
+ codex not found on PATH. policy.review.codex_required: true requires
372
+ the codex binary. Install: https://github.com/openai/codex
373
+ (e.g. `npm i -g @openai/codex`). To disable the push-gate instead,
374
+ set policy.review.codex_required: false in .rea/policy.yaml.
171
375
  ```
172
376
 
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:
377
+ If a push is attempted without codex on PATH, the gate also returns exit 2
378
+ with the same install hint:
205
379
 
206
- ```bash
207
- REA_METRICS_PORT=9464 rea serve
208
- # in another shell
209
- curl http://127.0.0.1:9464/metrics
380
+ ```
381
+ codex CLI not found on PATH. Install with `npm i -g @openai/codex`,
382
+ or set `review.codex_required: false` in .rea/policy.yaml to disable
383
+ the push-gate.
210
384
  ```
211
385
 
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.
386
+ Operators who do not have Codex available can either:
219
387
 
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.
388
+ - Run `rea init --profile bst-internal-no-codex` (or `open-source-no-codex`),
389
+ which sets `review.codex_required: false` on install.
390
+ - Flip `review.codex_required: false` in an existing policy file.
223
391
 
224
- ### 6. Ask the gateway how it's doing — `__rea__health`
392
+ ### Standalone usage
225
393
 
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.
394
+ `rea hook push-gate` is invoked by `.husky/pre-push`, but it is also a
395
+ first-class CLI. Run it manually to test a review without pushing:
233
396
 
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.
397
+ ```bash
398
+ # Review the working tree against the resolved base (@{upstream}, else
399
+ # origin/HEAD, else main/master, else empty-tree).
400
+ rea hook push-gate
239
401
 
240
- Operators who genuinely need error strings on the MCP wire can opt in:
402
+ # Review against an explicit base.
403
+ rea hook push-gate --base origin/main
404
+ rea hook push-gate --base refs/remotes/upstream/main
241
405
 
242
- ```yaml
243
- # .rea/policy.yaml
244
- gateway:
245
- health:
246
- expose_diagnostics: true
406
+ # Narrow the review to the last N commits (diff vs HEAD~N). Loses to
407
+ # --base when both are set; mirrors policy.review.last_n_commits.
408
+ rea hook push-gate --last-n-commits 10
247
409
  ```
248
410
 
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.
411
+ Exit codes match the pre-push contract. The JSON payload is written to
412
+ `.rea/last-review.json` regardless of invocation context.
256
413
 
257
- ## Architecture
414
+ ### What happens to a protected ref?
415
+
416
+ The gate has no concept of protected vs. unprotected branches; it reviews
417
+ whatever diff git is about to push. Protect-main is enforced by GitHub
418
+ branch protection (required status checks, required reviews, no direct
419
+ pushes to `main`), not by the gate. The gate's job is to surface blocking
420
+ issues before the push reaches the remote.
421
+
422
+ ---
423
+
424
+ ## MCP gateway
425
+
426
+ `rea serve` is an MCP stdio server. Claude Code starts it via `.mcp.json`
427
+ at the start of a session; it runs for the life of that session. The
428
+ server proxies downstream MCP servers declared in `.rea/registry.yaml`
429
+ through a fixed middleware chain.
258
430
 
259
431
  ### Middleware chain
260
432
 
261
- Every native MCP tool call AND every proxied downstream call flows through
433
+ Every native rea tool call AND every proxied downstream call flows through
262
434
  one chain. The order matters — each layer fails closed.
263
435
 
264
436
  ```
@@ -273,13 +445,14 @@ tool call
273
445
  │ blocked-paths — .rea/ + operator paths │
274
446
  │ rate-limit — token bucket per server │
275
447
  │ circuit-breaker — trip on downstream failure │
448
+ │ injection (args) — prompt-injection in args │
276
449
  │ redact (args) — secrets in arguments │
277
450
  │ │
278
451
  │ ==== EXECUTE ==== │
279
452
  │ │
280
453
  │ result-size-cap — bounded response │
281
454
  │ redact (result) — secrets in result │
282
- │ injection — prompt-injection in result │
455
+ │ injection (result) — prompt-injection in result │
283
456
  │ audit.exit — hash-chained record close │
284
457
  └───────────────────────────────────────────────────┘
285
458
 
@@ -287,415 +460,524 @@ tool call
287
460
  result
288
461
  ```
289
462
 
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.
297
-
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.
308
-
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.
314
-
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.
322
-
323
- ### Live state
324
-
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,
327
- 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.
463
+ `.rea/` is hardcoded as an always-blocked path. It cannot be unblocked from
464
+ policy. Policy is re-read on every invocation — any edit to `policy.yaml`
465
+ takes effect on the next tool call.
466
+
467
+ ### `__rea__health` meta-tool
468
+
469
+ The gateway advertises a single built-in tool, `__rea__health`, in every
470
+ `listTools` response. Calling it returns gateway version, uptime, HALT
471
+ state, policy summary, and per-downstream health. The handler
472
+ **short-circuits the middleware chain** — it is callable under HALT and at
473
+ any autonomy level because it is the tool an operator reaches for when
474
+ everything else is frozen. Every invocation still writes an audit record.
475
+
476
+ The wire response is sanitized by default: `halt_reason` and
477
+ `downstreams[].last_error` surface as `null`. Full detail lives in the
478
+ audit record. Operators who genuinely need error strings on the MCP wire
479
+ can opt in via `policy.gateway.health.expose_diagnostics: true`; the
480
+ short-circuit still runs the full redact + injection-classify sanitizer
481
+ pass before emitting.
482
+
483
+ ### Audit log
484
+
485
+ `.rea/audit.jsonl` is a hash-chained, append-only JSONL file. Each record
486
+ is a single line with:
487
+
488
+ - `seq` monotonic integer
489
+ - `ts` ISO-8601 UTC
490
+ - `session_id` generated at `rea serve` boot
491
+ - `server_name`, `tool_name`, `tier` (`read` | `write` | `destructive`)
492
+ - `status` (`allowed` | `denied` | `error`)
493
+ - `metadata` — tool-specific structured fields (argument digest, target,
494
+ deny reason, verdict)
495
+ - `prev_hash` — SHA-256 of the previous record; tampering is detectable by
496
+ `rea audit verify`
497
+
498
+ Records are redacted on write (secrets swapped for `[REDACTED:*]`,
499
+ injection payloads swapped for `INJECTION_REDACTED_PLACEHOLDER`), never
500
+ LLM-reachable. Rotation is policy-driven (`policy.audit.rotation.max_bytes`
501
+ and/or `max_age_days`); rotation preserves the hash chain by seeding the
502
+ new file with a rotation marker record.
503
+
504
+ ### TOFU drift detection
505
+
506
+ Downstream MCP servers are fingerprinted on first sight and stored in
507
+ `.rea/fingerprints.json`. On every `rea serve` boot, each server's canonical
508
+ shape (command path, env-key set, vault list) is hashed and compared
509
+ against the stored fingerprint:
510
+
511
+ - `first-seen` — new server; fingerprint recorded.
512
+ - `unchanged` — match; proceed.
513
+ - `drifted` — mismatch; fail-close. The operator uses `rea tofu list` to
514
+ inspect and `rea tofu accept <name> --reason "..."` to rebase.
515
+
516
+ The drift-accept operation appends a `tofu.drift_accepted_by_cli` audit
517
+ record with the reason. The next boot classifies the server as
518
+ `unchanged`.
333
519
 
334
520
  ### Downstream environment safety
335
521
 
336
- `rea serve` does **not** forward `process.env` wholesale to downstream
337
- children. Each child gets:
522
+ `rea serve` does **not** forward `process.env` wholesale. Each downstream
523
+ child gets:
338
524
 
339
525
  1. A fixed allowlist of neutral OS vars (`PATH`, `HOME`, `TZ`,
340
526
  `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
527
+ 2. Names opted into via `registry.yaml#servers[].env_passthrough` — the
528
+ schema refuses secret-looking names (`*_TOKEN`, `*_KEY`, `*_SECRET`, …),
529
+ so secrets must be named explicitly.
530
+ 3. Values from the registry's `env:` mapping, which may contain `${VAR}`
531
+ placeholders resolved against the host environment. A `${VAR}` whose
532
+ host variable is unset is treated as fatal — the downstream is marked
533
+ unhealthy rather than handed an unresolved placeholder.
491
534
 
492
- ```bash
493
- # From an interactive Claude Code session:
494
- /codex-review
495
- ```
535
+ ### Live state
496
536
 
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}`.
537
+ `.rea/serve.state.json` is the on-disk live snapshot. It is written once
538
+ at boot and again on every circuit transition or supervisor event,
539
+ debounced through a 250 ms trailing timer and flushed atomically via
540
+ temp-file + rename. `rea status` reads this file to render a
541
+ per-downstream table; a new `rea serve` whose predecessor crashed can
542
+ detect the abandoned file and take over ownership rather than stalling.
501
543
 
502
- ### 2. Record-and-cache in one CLI call
544
+ ### Optional Prometheus metrics
503
545
 
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:
546
+ `rea serve` can expose a loopback-only Prometheus endpoint when
547
+ `REA_METRICS_PORT` is set:
507
548
 
508
549
  ```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
550
+ REA_METRICS_PORT=9464 rea serve
551
+ curl http://127.0.0.1:9464/metrics
517
552
  ```
518
553
 
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.
554
+ Metrics: per-downstream call and error counters, in-flight gauge,
555
+ audit-lines-appended counter, circuit-breaker state gauge, and a
556
+ seconds-since-last-HALT-check gauge. The listener binds to `127.0.0.1`
557
+ only, serves only `GET /metrics`, and never binds by default. No TLS;
558
+ scrape through SSH or a reverse proxy for cross-host access.
559
+
560
+ ---
561
+
562
+ ## Policy file
528
563
 
529
- Verdict mapping for the cache leg:
564
+ `.rea/policy.yaml` fields. The schema is zod-strict — unknown fields are
565
+ rejected at parse time, not ignored.
530
566
 
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` |
567
+ ### Required fields
537
568
 
538
- ### 3. Push
569
+ | Field | Type | Purpose |
570
+ | --- | --- | --- |
571
+ | `version` | `"1"` | Schema version; only `"1"` accepted in the current major |
572
+ | `profile` | string | Profile name (see below) |
573
+ | `installed_by` | string | Stamped by `rea init` — identifies the installing version |
574
+ | `installed_at` | ISO-8601 | Stamped by `rea init` — install timestamp |
575
+ | `autonomy_level` | `L0` \| `L1` \| `L2` \| `L3` | Current autonomy. `L0` = read-only; `L3` = full tool access |
576
+ | `max_autonomy_level` | `L0` \| `L1` \| `L2` \| `L3` | Hard ceiling. `autonomy_level` cannot exceed this |
577
+ | `promotion_requires_human_approval` | boolean | Require operator confirmation to raise autonomy |
578
+ | `block_ai_attribution` | boolean | Enforce no-AI-attribution in commits and PR bodies |
579
+ | `blocked_paths` | string[] | Glob patterns. `.rea/` is always blocked regardless |
580
+ | `notification_channel` | string | Optional Discord webhook URL; empty string = disabled |
539
581
 
540
- ```bash
541
- git push
542
- ```
582
+ ### Optional blocks
543
583
 
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.
547
-
548
- ### SDK alternative
549
-
550
- When embedding the flow in a TypeScript tool instead of shelling out,
551
- import the public audit helper:
552
-
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';
561
-
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
- });
584
+ ```yaml
585
+ injection_detection: block # "block" | "warn" legacy 0.2.x knob
586
+ injection:
587
+ suspicious_blocks_writes: true # suspicious verdict on write/destructive tier denies
588
+ context_protection:
589
+ delegate_to_subagent:
590
+ - pnpm run build
591
+ - pnpm run test
592
+ max_bash_output_lines: 100
593
+ review:
594
+ codex_required: true
595
+ concerns_blocks: true
596
+ timeout_ms: 1800000
597
+ # last_n_commits: 10 # optional — narrow review to HEAD~N
598
+ redact:
599
+ match_timeout_ms: 100
600
+ patterns:
601
+ - name: custom-api-key
602
+ regex: 'acme_[A-Za-z0-9]{32}'
603
+ flags: 'g'
604
+ audit:
605
+ rotation:
606
+ max_bytes: 52428800 # 50 MiB
607
+ max_age_days: 30
608
+ gateway:
609
+ health:
610
+ expose_diagnostics: false
574
611
  ```
575
612
 
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.
613
+ User-supplied `redact.patterns[]` are validated via `safe-regex` at load
614
+ any pattern flagged unsafe fails the load with a specific error naming
615
+ the offender.
578
616
 
579
- ### Agent autonomy self-consistency
617
+ ### Autonomy levels
580
618
 
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`).
619
+ | Level | Effect |
620
+ | --- | --- |
621
+ | `L0` | Read-only every write/destructive tier call denies |
622
+ | `L1` | Default. Reads allowed; writes allowed; destructive tier denied |
623
+ | `L2` | Writes and destructive tier allowed |
624
+ | `L3` | No autonomy gate — only hook-layer and policy-layer checks remain |
625
+
626
+ `autonomy_level > max_autonomy_level` is rejected at parse time.
627
+ `promotion_requires_human_approval: false` requires the CLI flag
628
+ `--i-understand-the-risks` on any operation that raises autonomy.
587
629
 
588
- ## Hooks
630
+ ### Profiles
589
631
 
590
- Fourteen hooks. Each does one thing.
632
+ Seven profiles ship in `profiles/`. The profile name is recorded in
633
+ `policy.yaml#profile` and governs which agents `rea init` copies and what
634
+ defaults apply.
591
635
 
592
- | Hook | Event | One-line purpose |
636
+ | Profile | Intended use | Codex default |
593
637
  | --- | --- | --- |
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) |
638
+ | `minimal` | Smallest possible install curated 10 + opinionated minimal hooks | `true` |
639
+ | `client-engagement` | Consulting engagement where the repo is client-owned | `true` |
640
+ | `bst-internal` | Booked Solid internal projects; conservative posture | `true` |
641
+ | `bst-internal-no-codex` | Same as above; no Codex CLI available | `false` |
642
+ | `lit-wc` | Lit web-component library projects | `true` |
643
+ | `open-source` | OSS library / CLI projects; liberal but audited | `true` |
644
+ | `open-source-no-codex` | Same; no Codex CLI available | `false` |
645
+
646
+ The `-no-codex` variants default `review.codex_required: false` on
647
+ install so teams without a Codex bench get a first-class opt-out. Flip
648
+ `--codex` / `--no-codex` on the `rea init` command line to override.
649
+
650
+ ---
651
+
652
+ ## Hooks shipped
653
+
654
+ Eleven hooks ship in `hooks/` and are copied into `.claude/hooks/` by
655
+ `rea init`. All eleven are wired by default in the shipped
656
+ `.claude/settings.json`.
657
+
658
+ | Hook | Event | Purpose | Default |
659
+ | --- | --- | --- | --- |
660
+ | `dangerous-bash-interceptor.sh` | `PreToolUse: Bash` | Block categories of destructive shell commands (`rm -rf`, `git reset --hard`, `--no-verify`, …) | Registered |
661
+ | `env-file-protection.sh` | `PreToolUse: Bash` | Block reads of `.env*` files | Registered |
662
+ | `dependency-audit-gate.sh` | `PreToolUse: Bash` | Verify packages exist on the registry before install | Registered |
663
+ | `security-disclosure-gate.sh` | `PreToolUse: Bash` | Route security-keyword `gh issue create` to private disclosure | Registered |
664
+ | `pr-issue-link-gate.sh` | `PreToolUse: Bash` | Advisory warn when `gh pr create` has no linked issue | Registered |
665
+ | `attribution-advisory.sh` | `PreToolUse: Bash` | Block commits/PRs containing AI attribution markers | Registered |
666
+ | `secret-scanner.sh` | `PreToolUse: Write\|Edit` | Scan file writes for credential patterns | Registered |
667
+ | `settings-protection.sh` | `PreToolUse: Write\|Edit` | Block agent writes to `.claude/settings.json`, hook dirs, policy | Registered |
668
+ | `blocked-paths-enforcer.sh` | `PreToolUse: Write\|Edit` | Enforce `blocked_paths` from policy | Registered |
669
+ | `changeset-security-gate.sh` | `PreToolUse: Write\|Edit` | Guard changesets against GHSA leaks and malformed frontmatter | Registered |
670
+ | `architecture-review-gate.sh` | `PostToolUse: Write\|Edit` | Flag edits crossing architectural boundaries (advisory) | Registered |
671
+
672
+ The 0.10.x review-gate scripts (`push-review-gate.sh`,
673
+ `push-review-gate-git.sh`, `commit-review-gate.sh`) and the 1,250-line
674
+ shared bash core (`hooks/_lib/push-review-core.sh`) were removed in
675
+ 0.11.0. The `hooks/_lib/` directory now contains only the three shared
676
+ helpers — `common.sh`, `halt-check.sh`, `policy-read.sh` — used by the
677
+ remaining hooks.
678
+
679
+ Every hook uses `set -euo pipefail` (or `set -uo pipefail` for stdin-JSON
680
+ consumers) and performs a HALT check near the top. Both the hook layer
681
+ and the MCP gateway middleware fail closed; bypassing one does not
682
+ disable the other.
683
+
684
+ ---
608
685
 
609
686
  ## Slash commands
610
687
 
688
+ Five commands ship in `commands/` and are copied into `.claude/commands/`
689
+ by `rea init`.
690
+
611
691
  | Command | Purpose |
612
692
  | --- | --- |
613
- | `/rea` | Session status — autonomy level, HALT state, last audit entries, next action |
693
+ | `/rea` | Session status — autonomy level, HALT state, recent audit entries, next action |
614
694
  | `/review` | Invoke the `code-reviewer` agent on current changes |
615
- | `/codex-review` | Invoke the `codex-adversarial` agent `/codex:adversarial-review` |
695
+ | `/codex-review` | Invoke the `codex-adversarial` agent via the Codex plugin |
616
696
  | `/freeze` | Prompt for a reason and write `.rea/HALT` |
617
- | `/halt-check` | Verify every middleware and hook respects HALT |
697
+ | `/halt-check` | Smoke test — verify every hook and middleware respects HALT |
618
698
 
619
- ## Policy file reference
699
+ ---
620
700
 
621
- `.rea/policy.yaml` fields. The schema is zod-strict — unknown fields are
622
- rejected, not ignored.
701
+ ## Curated agents
623
702
 
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`.
703
+ Ten specialist agents ship in `agents/` and are copied into `.claude/agents/`
704
+ by `rea init`. Profiles layer additional specialists on top for specific
705
+ project shapes.
706
+
707
+ | Agent | When to use |
708
+ | --- | --- |
709
+ | `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. |
710
+ | `code-reviewer` | Structured review of a working-tree diff; surfaces correctness, clarity, and consistency issues without adversarial framing. |
711
+ | `codex-adversarial` | Adversarial review via the Codex plugin (`/codex:adversarial-review`). Independent model perspective; produces an audit entry with verdict. |
712
+ | `security-engineer` | Security-sensitive implementation and review auth flows, secret handling, injection surfaces. |
713
+ | `accessibility-engineer` | WCAG review, ARIA semantics, keyboard navigation, screen-reader fact-checking. |
714
+ | `typescript-specialist` | Strict-mode TypeScript correctness, generics, narrowing, inference edge cases. |
715
+ | `frontend-specialist` | UI component work, framework idioms (React, Lit, Astro), CSS architecture. |
716
+ | `backend-engineer` | API design, database schema, background jobs, MCP server implementation. |
717
+ | `qa-engineer` | Test strategy, fixture design, regression reproducers, flake triage. |
718
+ | `technical-writer` | User-facing documentation, API references, migration guides, changelog narratives. |
719
+
720
+ The `rea-orchestrator` is the single entry point for non-trivial tasks.
721
+ The CLAUDE.md fragment installed by `rea init` instructs the host agent
722
+ to route there first; delegation contracts are defined in each agent's
723
+ markdown file.
724
+
725
+ ---
726
+
727
+ ## CLI reference
728
+
729
+ ```
730
+ rea <command> [options]
731
+ ```
732
+
733
+ Run `rea <command> --help` for full per-command options.
734
+
735
+ ### `rea init`
736
+
737
+ Interactive wizard — write `.rea/policy.yaml`, install `.claude/`, the
738
+ commit-msg hook, and a CLAUDE.md fragment.
739
+
740
+ ```bash
741
+ rea init
742
+ rea init -y --profile bst-internal # non-interactive
743
+ rea init --from-reagent # migrate from .reagent/
744
+ rea init --profile open-source-no-codex # disable Codex by default
745
+ rea init --force # overwrite existing artifacts
746
+ ```
747
+
748
+ ### `rea upgrade`
749
+
750
+ Sync `.claude/`, `.husky/`, and managed fragments with this rea version.
751
+ Prompts on drift; silently refreshes unmodified files.
752
+
753
+ ```bash
754
+ rea upgrade --dry-run # show what would change; write nothing
755
+ rea upgrade # interactive
756
+ rea upgrade -y # non-interactive, keep drifted files
757
+ rea upgrade --force # non-interactive, overwrite drift
758
+ ```
759
+
760
+ ### `rea serve`
761
+
762
+ Start the MCP gateway. Invoked by Claude Code via `.mcp.json`; not a
763
+ daemon. Stdio transport only.
764
+
765
+ ```bash
766
+ rea serve
767
+ REA_METRICS_PORT=9464 rea serve
768
+ REA_LOG_LEVEL=debug rea serve
769
+ ```
770
+
771
+ ### `rea freeze` / `rea unfreeze`
772
+
773
+ Write or remove `.rea/HALT`. Every call writes an audit record.
774
+
775
+ ```bash
776
+ rea freeze --reason "incident triage"
777
+ rea unfreeze
778
+ rea unfreeze -y # skip confirmation
779
+ ```
780
+
781
+ ### `rea check`
782
+
783
+ On-disk status — autonomy, HALT, profile, recent audit entries. No live
784
+ process probe.
785
+
786
+ ### `rea status`
787
+
788
+ Running-process view — reads `.rea/serve.pid` + `.rea/serve.state.json`
789
+ to render per-downstream health.
790
+
791
+ ```bash
792
+ rea status
793
+ rea status --json # pipe to jq
794
+ ```
795
+
796
+ ### `rea doctor`
797
+
798
+ Validate the install — policy parses, `.rea/` layout, hooks, Codex plugin
799
+ presence, TOFU fingerprint store.
800
+
801
+ ```bash
802
+ rea doctor
803
+ rea doctor --metrics # also print 7-day Codex telemetry summary
804
+ rea doctor --drift # report drift vs. install manifest (read-only)
805
+ ```
806
+
807
+ In non-git directories the commit-msg and pre-push checks are skipped
808
+ cleanly. Audit hash-chain integrity is verified by `rea audit verify`,
809
+ not by `rea doctor`.
647
810
 
648
- ## Migration from `@bookedsolid/reagent`
811
+ ### `rea audit rotate` / `rea audit verify`
649
812
 
650
813
  ```bash
651
- npx @bookedsolid/rea init --from-reagent
814
+ rea audit rotate # force rotation now
815
+ rea audit verify # re-hash the chain; exit 1 on first tamper
816
+ rea audit verify --since <file> # walk forward from a rotated file
652
817
  ```
653
818
 
654
- `--from-reagent`:
819
+ ### `rea tofu list` / `rea tofu accept`
820
+
821
+ ```bash
822
+ rea tofu list
823
+ rea tofu list --json
824
+ rea tofu accept my-mcp-server --reason "added vault path /Volumes/Work"
825
+ ```
826
+
827
+ ### `rea hook push-gate`
828
+
829
+ Pre-push Codex review gate. Normally invoked by `.husky/pre-push`; run
830
+ manually to test.
831
+
832
+ ```bash
833
+ rea hook push-gate
834
+ rea hook push-gate --base origin/main
835
+ ```
836
+
837
+ ---
838
+
839
+ ## Migration from 0.10.x
840
+
841
+ `rea upgrade` handles the policy and on-disk pieces. You run it once:
842
+
843
+ ```bash
844
+ rea upgrade
845
+ ```
846
+
847
+ It performs:
848
+
849
+ 1. **Backup** — writes `.rea/policy.yaml.bak-<timestamp>` before any edit.
850
+ 2. **Strip removed fields** — removes `review.cache_max_age_seconds` and
851
+ `review.allow_skip_in_ci` from the policy file. Both were cache-gate
852
+ concepts with no meaning under the stateless gate.
853
+ 3. **Add defaults** — inserts `review.concerns_blocks: true` if absent.
854
+ 4. **Prune settings.json** — removes hook registrations for the removed
855
+ scripts (`push-review-gate`, `commit-review-gate`) from
856
+ `.claude/settings.json`, leaving the other registrations intact.
857
+ 5. **Refresh `.claude/hooks/`** — deletes the removed scripts with a
858
+ change-log entry.
859
+ 6. **Rewrite `.husky/pre-push`** iff it carries a rea marker. A
860
+ foreign `.husky/pre-push` (no marker) is left untouched and a loud
861
+ warning is printed.
862
+ 7. **Refresh `.git/hooks/pre-push`** fallback with the new 15-line stub
863
+ (when `core.hooksPath` is unset and git is using the default hooks
864
+ directory).
865
+
866
+ ### What the new `.husky/pre-push` looks like
655
867
 
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.
868
+ Fifteen lines of POSIX `sh`. HALT check, locate the rea binary, `exec rea
869
+ hook push-gate "$@"`. That is the entire body all real work lives in
870
+ the TypeScript gate composer at `src/hooks/push-gate/index.ts`.
666
871
 
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.
872
+ ### Rollback
672
873
 
673
- ## Security
874
+ If the stateless gate's behavior is blocking a specific workflow you
875
+ cannot yet address:
876
+
877
+ ```bash
878
+ npm install -g @bookedsolid/rea@0.10.3
879
+ # restore policy from the backup rea upgrade wrote
880
+ cp .rea/policy.yaml.bak-<ts> .rea/policy.yaml
881
+ # re-run the 0.10.3 install to put the old hooks back
882
+ rea init --force
883
+ ```
884
+
885
+ The 0.10.3 cache-attestation gate remains on npm and continues to work.
886
+ Rollback is a supported path — the `rea upgrade` backup is specifically
887
+ there to make it a one-liner.
888
+
889
+ ### What you will not need to do anymore
890
+
891
+ - `rea cache check` / `rea cache set` / `rea cache list` / `rea cache clear`
892
+ — all removed. The stateless gate consults no cache.
893
+ - `rea audit record codex-review --also-set-cache` — removed. The gate
894
+ writes its own audit records from the actual Codex run.
895
+ - Setting `REA_SKIP_PUSH_REVIEW` — removed. Use either
896
+ `REA_SKIP_PUSH_GATE=<reason>` or `REA_SKIP_CODEX_REVIEW=<reason>`
897
+ (both value-carrying and always audited; identical effect, distinct
898
+ `skip_var` in audit metadata) or flip `review.codex_required: false`
899
+ in policy. `REA_SKIP_CODEX_REVIEW` was reinstated in 0.12.0 as an
900
+ audited alias for `REA_SKIP_PUSH_GATE` — it had been documented in the
901
+ gateway-tier reviewers but not in the push-gate, leaving agents
902
+ setting the documented variant blocked.
903
+
904
+ ---
905
+
906
+ ## Contributor quality gates
907
+
908
+ Before push, run the four checks locally. CI runs them as required status
909
+ checks on every PR to `main`:
910
+
911
+ ```bash
912
+ pnpm lint # ESLint 10 — zero warnings
913
+ pnpm type-check # tsc --noEmit (strict)
914
+ pnpm test # vitest run
915
+ pnpm build # tsc -p tsconfig.build.json
916
+ ```
917
+
918
+ Additionally, every PR needs:
919
+
920
+ - **DCO sign-off** on all commits (`git commit -s`). The DCO bot rejects
921
+ unsigned commits.
922
+ - **Changeset** entry (`pnpm changeset`) unless the change is purely
923
+ non-publishable (CI, docs, meta). CI flags missing changesets.
924
+ - **Secret scan** clean. Gitleaks runs in CI and via the
925
+ `secret-scanner.sh` hook.
926
+ - **No AI attribution** anywhere — commit messages, PR bodies, code
927
+ comments, changeset content. The commit-msg hook and
928
+ `attribution-advisory.sh` reject structural attribution.
929
+
930
+ Security-sensitive paths (`src/gateway/middleware/**`, `src/policy/**`,
931
+ `src/hooks/**`, `hooks/**`, `.github/workflows/**`) require explicit
932
+ maintainer review and a threat-model update in the same PR.
933
+
934
+ Releases flow through Changesets: a "Version Packages" PR is auto-opened
935
+ when a changeset lands on `main`. Merging it triggers `npm publish
936
+ --provenance` via OIDC from `.github/workflows/release.yml`. Do not
937
+ manually `npm publish`.
938
+
939
+ ---
940
+
941
+ ## Threat model and security
674
942
 
675
943
  - [SECURITY.md](./SECURITY.md) — disclosure policy, supported versions,
676
- and scope. Do not report vulnerabilities via public GitHub issues.
944
+ GHSA coordination. 72-hour acknowledgment target, 90-day window. Do
945
+ not report vulnerabilities via public GitHub issues.
677
946
  - [THREAT_MODEL.md](./THREAT_MODEL.md) — attack surface, mitigations,
678
- residual risks. This is the contract REA holds itself to.
947
+ residual risks. The contract rea holds itself to.
679
948
 
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.
949
+ Short version: the MCP gateway and the hook layer run independently.
950
+ Both fail closed. `.rea/` is always blocked. The audit log is
951
+ hash-chained. Policy is re-read on every invocation. npm publish uses
952
+ OIDC provenance, not long-lived tokens. The pre-push gate runs Codex
953
+ on every push and treats Codex responses as untrusted input — findings
954
+ flow through the same redact pattern set used by the middleware before
955
+ anything hits disk.
684
956
 
685
- ## Contributing
957
+ ---
686
958
 
687
- See [CONTRIBUTING.md](./CONTRIBUTING.md). Short version:
959
+ ## Non-goals
688
960
 
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.
961
+ See [What REA is NOT](#what-rea-is-not) above. Every "but what if we
962
+ just added X" belongs in a separate package that composes with REA. The
963
+ non-goals are the product.
964
+
965
+ ---
698
966
 
699
- ## License
967
+ ## License and contributing
700
968
 
701
- [MIT](./LICENSE)
969
+ [MIT](./LICENSE). See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full
970
+ contributor guide and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) for
971
+ the Contributor Covenant.
972
+
973
+ - DCO sign-off required (`git commit -s`) — no CLA.
974
+ - Conventional commits, TypeScript strict, ESLint zero-warnings,
975
+ Prettier, vitest.
976
+ - Changeset on every publishable change; merge the auto-generated
977
+ Version Packages PR to release.
978
+ - Security-sensitive paths gated by CODEOWNERS; human review required.
979
+
980
+ This repo dogfoods itself. rea's governance layer enforces rea's own
981
+ commit, hook, and attribution rules. The install under `.rea/`,
982
+ `.claude/`, and `.husky/` is the reference example of the
983
+ `bst-internal` profile.