@bookedsolid/rea 0.1.0 → 0.2.1

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 (90) hide show
  1. package/.husky/commit-msg +130 -0
  2. package/.husky/pre-push +128 -0
  3. package/README.md +5 -5
  4. package/agents/codex-adversarial.md +23 -8
  5. package/commands/codex-review.md +2 -2
  6. package/dist/audit/append.d.ts +62 -0
  7. package/dist/audit/append.js +189 -0
  8. package/dist/audit/codex-event.d.ts +28 -0
  9. package/dist/audit/codex-event.js +15 -0
  10. package/dist/cli/doctor.d.ts +60 -1
  11. package/dist/cli/doctor.js +459 -20
  12. package/dist/cli/index.js +35 -5
  13. package/dist/cli/init.d.ts +13 -0
  14. package/dist/cli/init.js +278 -67
  15. package/dist/cli/install/canonical.d.ts +43 -0
  16. package/dist/cli/install/canonical.js +101 -0
  17. package/dist/cli/install/claude-md.d.ts +48 -0
  18. package/dist/cli/install/claude-md.js +93 -0
  19. package/dist/cli/install/commit-msg.d.ts +30 -0
  20. package/dist/cli/install/commit-msg.js +102 -0
  21. package/dist/cli/install/copy.d.ts +169 -0
  22. package/dist/cli/install/copy.js +455 -0
  23. package/dist/cli/install/fs-safe.d.ts +91 -0
  24. package/dist/cli/install/fs-safe.js +347 -0
  25. package/dist/cli/install/manifest-io.d.ts +12 -0
  26. package/dist/cli/install/manifest-io.js +44 -0
  27. package/dist/cli/install/manifest-schema.d.ts +83 -0
  28. package/dist/cli/install/manifest-schema.js +80 -0
  29. package/dist/cli/install/reagent.d.ts +59 -0
  30. package/dist/cli/install/reagent.js +160 -0
  31. package/dist/cli/install/settings-merge.d.ts +91 -0
  32. package/dist/cli/install/settings-merge.js +239 -0
  33. package/dist/cli/install/sha.d.ts +9 -0
  34. package/dist/cli/install/sha.js +21 -0
  35. package/dist/cli/serve.d.ts +11 -0
  36. package/dist/cli/serve.js +72 -6
  37. package/dist/cli/upgrade.d.ts +67 -0
  38. package/dist/cli/upgrade.js +509 -0
  39. package/dist/gateway/downstream-pool.d.ts +39 -0
  40. package/dist/gateway/downstream-pool.js +93 -0
  41. package/dist/gateway/downstream.d.ts +80 -0
  42. package/dist/gateway/downstream.js +196 -0
  43. package/dist/gateway/middleware/audit-types.d.ts +10 -0
  44. package/dist/gateway/middleware/audit.js +14 -0
  45. package/dist/gateway/middleware/injection.d.ts +59 -2
  46. package/dist/gateway/middleware/injection.js +91 -14
  47. package/dist/gateway/middleware/kill-switch.d.ts +20 -5
  48. package/dist/gateway/middleware/kill-switch.js +57 -35
  49. package/dist/gateway/middleware/redact.d.ts +83 -6
  50. package/dist/gateway/middleware/redact.js +133 -46
  51. package/dist/gateway/observability/codex-probe.d.ts +110 -0
  52. package/dist/gateway/observability/codex-probe.js +234 -0
  53. package/dist/gateway/observability/codex-telemetry.d.ts +93 -0
  54. package/dist/gateway/observability/codex-telemetry.js +221 -0
  55. package/dist/gateway/redact-safe/match-timeout.d.ts +83 -0
  56. package/dist/gateway/redact-safe/match-timeout.js +179 -0
  57. package/dist/gateway/reviewers/claude-self.d.ts +99 -0
  58. package/dist/gateway/reviewers/claude-self.js +316 -0
  59. package/dist/gateway/reviewers/codex.d.ts +64 -0
  60. package/dist/gateway/reviewers/codex.js +80 -0
  61. package/dist/gateway/reviewers/select.d.ts +64 -0
  62. package/dist/gateway/reviewers/select.js +102 -0
  63. package/dist/gateway/reviewers/types.d.ts +85 -0
  64. package/dist/gateway/reviewers/types.js +14 -0
  65. package/dist/gateway/server.d.ts +51 -0
  66. package/dist/gateway/server.js +258 -0
  67. package/dist/gateway/session.d.ts +9 -0
  68. package/dist/gateway/session.js +17 -0
  69. package/dist/policy/loader.d.ts +59 -0
  70. package/dist/policy/loader.js +65 -0
  71. package/dist/policy/profiles.d.ts +80 -0
  72. package/dist/policy/profiles.js +94 -0
  73. package/dist/policy/types.d.ts +38 -0
  74. package/dist/registry/loader.d.ts +98 -0
  75. package/dist/registry/loader.js +153 -0
  76. package/dist/registry/types.d.ts +44 -0
  77. package/dist/registry/types.js +6 -0
  78. package/dist/scripts/read-policy-field.d.ts +36 -0
  79. package/dist/scripts/read-policy-field.js +96 -0
  80. package/hooks/push-review-gate.sh +627 -17
  81. package/package.json +13 -2
  82. package/profiles/bst-internal-no-codex.yaml +40 -0
  83. package/profiles/bst-internal.yaml +23 -0
  84. package/profiles/client-engagement.yaml +23 -0
  85. package/profiles/lit-wc.yaml +17 -0
  86. package/profiles/minimal.yaml +11 -0
  87. package/profiles/open-source-no-codex.yaml +33 -0
  88. package/profiles/open-source.yaml +18 -0
  89. package/scripts/lint-safe-regex.mjs +78 -0
  90. package/scripts/postinstall.mjs +131 -0
@@ -0,0 +1,130 @@
1
+ #!/bin/sh
2
+ # .husky/commit-msg — optionally BLOCKS commits that contain AI attribution
3
+ #
4
+ # OPT-IN: Only enforces when .rea/policy.yaml contains:
5
+ # block_ai_attribution: true
6
+ #
7
+ # When disabled (default), this hook does nothing — commits work normally.
8
+ # When enabled, rejects (exit 1) commit messages with structural AI attribution
9
+ # markers. This teaches agents to stop including attribution by giving clear
10
+ # feedback on what went wrong.
11
+ #
12
+ # IMPORTANT: This does NOT block casual mentions of AI tools.
13
+ # "Fix Claude API integration" or "Update OpenAI SDK" are fine.
14
+ # What gets blocked are STRUCTURAL ATTRIBUTION MARKERS:
15
+ #
16
+ # Co-Authored-By with noreply@ emails (dead giveaway)
17
+ # Co-Authored-By with known AI names (Claude, Copilot, GPT, Gemini, etc.)
18
+ # "Generated with/by [Tool]" footer lines
19
+ # Markdown-linked tool names: [Claude Code](...)
20
+ # Emoji-prefixed attribution: 🤖 Generated...
21
+ #
22
+ # SAFETY: set -e ensures any unexpected error BLOCKS the commit rather than
23
+ # silently passing a message with attribution intact.
24
+
25
+ set -e
26
+
27
+ COMMIT_MSG_FILE="$1"
28
+
29
+ # Validate input
30
+ if [ -z "$COMMIT_MSG_FILE" ]; then
31
+ echo "ERROR: commit-msg hook received no file path" >&2
32
+ exit 1
33
+ fi
34
+ if [ ! -f "$COMMIT_MSG_FILE" ]; then
35
+ echo "ERROR: commit message file not found: $COMMIT_MSG_FILE" >&2
36
+ exit 1
37
+ fi
38
+
39
+ # ── Check if attribution blocking is enabled ───────────────────────────────────
40
+ # Look for block_ai_attribution: true in .rea/policy.yaml
41
+ # If not found or not true, exit 0 (normal commit behavior)
42
+
43
+ POLICY_FILE=".rea/policy.yaml"
44
+ if [ ! -f "$POLICY_FILE" ]; then
45
+ exit 0
46
+ fi
47
+
48
+ # Simple grep — no YAML parser dependency needed for a boolean flag
49
+ if ! grep -qE '^block_ai_attribution:[[:space:]]*true' "$POLICY_FILE" 2>/dev/null; then
50
+ exit 0
51
+ fi
52
+
53
+ # ── Attribution blocking is enabled — check patterns ───────────────────────────
54
+
55
+ BLOCKED=0
56
+ MATCHES=""
57
+
58
+ # Pattern 1: Co-Authored-By with noreply@ email
59
+ if grep -qiE 'Co-Authored-By:.*noreply@' "$COMMIT_MSG_FILE" 2>/dev/null; then
60
+ BLOCKED=1
61
+ MATCHES="${MATCHES}$(grep -niE 'Co-Authored-By:.*noreply@' "$COMMIT_MSG_FILE" 2>/dev/null)
62
+ "
63
+ fi
64
+
65
+ # Pattern 2: Co-Authored-By with known AI assistant names
66
+ if grep -qiE 'Co-Authored-By:.*\b(Claude|Sonnet|Opus|Haiku|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|Amazon Q|CodeWhisperer|Devin|Windsurf|Cline|Aider|Anthropic|OpenAI|GitHub Copilot)\b' "$COMMIT_MSG_FILE" 2>/dev/null; then
67
+ BLOCKED=1
68
+ MATCHES="${MATCHES}$(grep -niE 'Co-Authored-By:.*\b(Claude|Sonnet|Opus|Haiku|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|CodeWhisperer|Devin|Windsurf|Cline|Aider|Anthropic|OpenAI|GitHub Copilot)\b' "$COMMIT_MSG_FILE" 2>/dev/null)
69
+ "
70
+ fi
71
+
72
+ # Pattern 3: "Generated/Built/Powered with/by [AI Tool]" footer lines
73
+ if grep -qiE '^\s*(Generated|Created|Built|Powered|Authored|Written|Produced)\s+(with|by)\s+(Claude|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|CodeWhisperer|Devin|Windsurf|Cline|Aider|AI|an? AI)\b' "$COMMIT_MSG_FILE" 2>/dev/null; then
74
+ BLOCKED=1
75
+ MATCHES="${MATCHES}$(grep -niE '^\s*(Generated|Created|Built|Powered|Authored|Written|Produced)\s+(with|by)\s+(Claude|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|CodeWhisperer|Devin|Windsurf|Cline|Aider|AI|an? AI)\b' "$COMMIT_MSG_FILE" 2>/dev/null)
76
+ "
77
+ fi
78
+
79
+ # Pattern 4: Markdown-linked attribution
80
+ if grep -qiE '\[Claude Code\]|\[GitHub Copilot\]|\[ChatGPT\]|\[Gemini\]|\[Cursor\]' "$COMMIT_MSG_FILE" 2>/dev/null; then
81
+ BLOCKED=1
82
+ MATCHES="${MATCHES}$(grep -niE '\[Claude Code\]|\[GitHub Copilot\]|\[ChatGPT\]|\[Gemini\]|\[Cursor\]' "$COMMIT_MSG_FILE" 2>/dev/null)
83
+ "
84
+ fi
85
+
86
+ # Pattern 5: Emoji-prefixed "Generated" lines
87
+ if grep -qE '🤖.*[Gg]enerated' "$COMMIT_MSG_FILE" 2>/dev/null; then
88
+ BLOCKED=1
89
+ MATCHES="${MATCHES}$(grep -nE '🤖.*[Gg]enerated' "$COMMIT_MSG_FILE" 2>/dev/null)
90
+ "
91
+ fi
92
+
93
+ # ── Block or allow ─────────────────────────────────────────────────────────────
94
+
95
+ if [ "$BLOCKED" -eq 1 ]; then
96
+ {
97
+ printf '\n'
98
+ printf '═══════════════════════════════════════════════════════════════════\n'
99
+ printf ' COMMIT BLOCKED: AI attribution detected in commit message\n'
100
+ printf '═══════════════════════════════════════════════════════════════════\n'
101
+ printf '\n'
102
+ printf ' Your commit message contains structural AI attribution markers\n'
103
+ printf ' that must be removed before committing.\n'
104
+ printf '\n'
105
+ printf ' Matched line(s):\n'
106
+ printf '%s' "$MATCHES" | grep -v '^$' | sed 's/^/ /'
107
+ printf '\n'
108
+ printf ' What gets BLOCKED (structural attribution):\n'
109
+ printf ' - Co-Authored-By with AI names or noreply@ emails\n'
110
+ printf ' - "Generated with/by [AI Tool]" footer lines\n'
111
+ printf ' - Markdown-linked tool names: [Claude Code](...)\n'
112
+ printf ' - Emoji attribution: 🤖 Generated...\n'
113
+ printf '\n'
114
+ printf ' What is ALLOWED (legitimate references):\n'
115
+ printf ' - "Fix Claude API integration"\n'
116
+ printf ' - "Update OpenAI SDK version"\n'
117
+ printf ' - "Add Copilot config"\n'
118
+ printf '\n'
119
+ printf ' Remove the attribution markers and retry your commit.\n'
120
+ printf ' To disable: set block_ai_attribution: false in .rea/policy.yaml\n'
121
+ printf '═══════════════════════════════════════════════════════════════════\n'
122
+ printf '\n'
123
+ } >&2
124
+ exit 1
125
+ fi
126
+
127
+ # Normalize trailing newlines (cosmetic, non-fatal)
128
+ perl -i -0777 -pe 's/\n+$/\n/' "$COMMIT_MSG_FILE" 2>/dev/null || true
129
+
130
+ exit 0
@@ -0,0 +1,128 @@
1
+ #!/bin/sh
2
+ # .husky/pre-push — rea governance gate for terminal-initiated pushes.
3
+ #
4
+ # Mirrors the logic of `.claude/hooks/push-review-gate.sh` but consumes the
5
+ # git pre-push stdin contract directly (one line per refspec:
6
+ # <local_ref> <local_sha> <remote_ref> <remote_sha>).
7
+ #
8
+ # Minimum viable check — NOT a full replacement for the Claude Code gate:
9
+ # 1. If `.rea/HALT` exists, block.
10
+ # 2. If the push touches a protected path AND policy.review.codex_required
11
+ # is not explicitly false, require a `codex.review` audit entry for the
12
+ # HEAD SHA (or REA_SKIP_CODEX_REVIEW env var for a one-off bypass).
13
+ #
14
+ # Escape hatch: REA_SKIP_CODEX_REVIEW=<reason> bypasses the protected-path
15
+ # check. The skip record is appended by `push-review-gate.sh` in the Claude
16
+ # Code path; for terminal pushes, export the variable AND append a skip
17
+ # record manually if you want it in the audit trail.
18
+ #
19
+ # Subshell-safety note: earlier versions piped `echo "$INPUT" | while read`,
20
+ # which ran the loop in a subshell — `exit 1` inside the loop aborted the
21
+ # subshell only, and the script then ran `exit 0` and allowed the push. We
22
+ # now feed the loop with a here-doc so it runs in the main shell, and we
23
+ # track `block_push` in the enclosing scope. Final `exit 1` is reached only
24
+ # if no refspec is blocked; a single blocking refspec propagates correctly.
25
+
26
+ set -eu
27
+
28
+ REA_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
29
+
30
+ if [ -f "${REA_ROOT}/.rea/HALT" ]; then
31
+ # POSIX `head` does not specify `-c`; use awk for the first line. HALT is
32
+ # a short reason string, so the first line is enough for display.
33
+ reason=$(awk 'NR==1 { print; exit }' "${REA_ROOT}/.rea/HALT" 2>/dev/null || printf 'unknown')
34
+ [ -z "${reason:-}" ] && reason='unknown'
35
+ printf 'REA HALT: %s\nAll push operations suspended. Run: rea unfreeze\n' "$reason" >&2
36
+ exit 1
37
+ fi
38
+
39
+ # Read refspec lines from stdin. Each line: <local_ref> <local_sha> <remote_ref> <remote_sha>
40
+ INPUT=$(cat)
41
+ [ -z "$INPUT" ] && exit 0
42
+
43
+ # Anchor every alternative so a legitimate file like `docs/hooks-guide.md` or
44
+ # `src/thirdparty/src/policy/loader.c` is not mistaken for a protected path.
45
+ # `^\.claude/hooks/` is included so someone editing the consumer install
46
+ # (which ships alongside rea itself) cannot sneak past the gate.
47
+ PROTECTED_RE='^src/gateway/middleware/|^hooks/|^\.claude/hooks/|^src/policy/|^\.github/workflows/'
48
+ AUDIT_LOG="${REA_ROOT}/.rea/audit.jsonl"
49
+
50
+ # G11.4 — honor review.codex_required. When explicitly false, skip the
51
+ # protected-path Codex audit requirement entirely (first-class no-Codex
52
+ # mode). Mirrors the logic in `.claude/hooks/push-review-gate.sh`.
53
+ #
54
+ # Fail-closed: if the helper is missing or errors, treat as true. A missing
55
+ # helper means rea is unbuilt — the operator can run `pnpm build` or set
56
+ # REA_SKIP_CODEX_REVIEW for a one-off bypass.
57
+ CODEX_REQUIRED=true
58
+ READ_FIELD_JS="${REA_ROOT}/dist/scripts/read-policy-field.js"
59
+ if [ -f "$READ_FIELD_JS" ]; then
60
+ field_value=$(REA_ROOT="$REA_ROOT" node "$READ_FIELD_JS" review.codex_required 2>/dev/null || printf '')
61
+ if [ "$field_value" = "false" ]; then
62
+ CODEX_REQUIRED=false
63
+ fi
64
+ fi
65
+
66
+ block_push=0
67
+
68
+ # Here-doc feeds the loop without creating a subshell, so `block_push=1`
69
+ # assignments below persist in the enclosing scope and the final `exit`
70
+ # reflects them. A pipeline would run the loop in a subshell and `exit 1`
71
+ # inside it would only abort that subshell — NOT the push — which was a
72
+ # real governance defect in the pre-review version of this file.
73
+ while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
74
+ [ -z "${local_sha:-}" ] && continue
75
+ # Branch deletion: local_sha is 40 zeros. Skip protected-path check.
76
+ case "$local_sha" in
77
+ 0000000000000000000000000000000000000000) continue ;;
78
+ esac
79
+
80
+ # Determine merge base. If remote is new (remote_sha is zeros), diff against
81
+ # the default branch; else against remote_sha.
82
+ if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
83
+ default_branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
84
+ [ -z "${default_branch:-}" ] && default_branch="main"
85
+ base=$(git merge-base "$default_branch" "$local_sha" 2>/dev/null || printf '')
86
+ else
87
+ base=$(git merge-base "$remote_sha" "$local_sha" 2>/dev/null || printf '')
88
+ fi
89
+ [ -z "${base:-}" ] && continue
90
+
91
+ # Check if the diff touches protected paths.
92
+ if git diff --name-only "$base" "$local_sha" 2>/dev/null | grep -qE "$PROTECTED_RE"; then
93
+ if [ "$CODEX_REQUIRED" = "false" ]; then
94
+ # Policy opts out of the Codex gate. The downstream `.claude/hooks/`
95
+ # path already records telemetry; terminal pushes skip silently.
96
+ continue
97
+ fi
98
+ if [ -n "${REA_SKIP_CODEX_REVIEW:-}" ]; then
99
+ printf 'rea: REA_SKIP_CODEX_REVIEW set (%s) — skipping Codex review requirement for %s\n' \
100
+ "$REA_SKIP_CODEX_REVIEW" "$local_sha" >&2
101
+ continue
102
+ fi
103
+ if [ ! -f "$AUDIT_LOG" ]; then
104
+ printf 'PUSH BLOCKED: protected paths changed but no audit log found at %s\n' "$AUDIT_LOG" >&2
105
+ printf ' Run /codex-review on HEAD %s before pushing.\n' "$local_sha" >&2
106
+ block_push=1
107
+ continue
108
+ fi
109
+ # Require both (a) a `codex.review` tool_name and (b) the exact head_sha
110
+ # on the same JSONL line. The `codex.review` pattern ends with a closing
111
+ # quote, so `codex.review.skipped` never satisfies the gate.
112
+ if ! grep -E '"tool_name":"codex\.review"' "$AUDIT_LOG" 2>/dev/null | \
113
+ grep -qF "\"head_sha\":\"$local_sha\""; then
114
+ printf 'PUSH BLOCKED: protected paths changed — /codex-review required for HEAD %s\n' "$local_sha" >&2
115
+ printf ' Run /codex-review, or set REA_SKIP_CODEX_REVIEW=<reason> to bypass.\n' >&2
116
+ block_push=1
117
+ continue
118
+ fi
119
+ fi
120
+ done <<HOOK_INPUT_EOF
121
+ $INPUT
122
+ HOOK_INPUT_EOF
123
+
124
+ if [ "$block_push" -ne 0 ]; then
125
+ exit 1
126
+ fi
127
+
128
+ exit 0
package/README.md CHANGED
@@ -206,15 +206,15 @@ REA ships it out of the box.
206
206
  | Phase | Primary model | Codex role | Governance |
207
207
  | --- | --- | --- | --- |
208
208
  | Plan | Claude Opus | — | Full middleware chain |
209
- | Pre-implementation review | — | `/codex review` — review the PLAN before code | Audited |
209
+ | Pre-implementation review | — | `/codex:review` — review the PLAN before code | Audited |
210
210
  | Build | Claude Opus | — | Full middleware chain |
211
- | Adversarial review | — | `/codex adversarial-review` on the diff (independent perspective) | Audited, redacted, kill-switched |
212
- | Pre-merge gate | — | `/codex adversarial-review` re-run; recorded in audit.jsonl | Required status check (recommended) |
211
+ | Adversarial review | — | `/codex:adversarial-review` on the diff (independent perspective) | Audited, redacted, kill-switched |
212
+ | Pre-merge gate | — | `/codex:adversarial-review` re-run; recorded in audit.jsonl | Required status check (recommended) |
213
213
 
214
214
  Three things make this work:
215
215
 
216
216
  1. The **`codex-adversarial` agent** in the curated roster wraps
217
- `/codex adversarial-review`. The orchestrator delegates to it after
217
+ `/codex:adversarial-review`. The orchestrator delegates to it after
218
218
  any non-trivial change.
219
219
  2. The **`/codex-review` slash command** is one of the five shipped
220
220
  commands. It produces an audit entry including the request summary,
@@ -260,7 +260,7 @@ disclosure. It is installed as part of the Bash PreToolUse set.
260
260
  | --- | --- |
261
261
  | `/rea` | Session status — autonomy level, HALT state, last audit entries, next action |
262
262
  | `/review` | Invoke the `code-reviewer` agent on current changes |
263
- | `/codex-review` | Invoke the `codex-adversarial` agent → `/codex adversarial-review` |
263
+ | `/codex-review` | Invoke the `codex-adversarial` agent → `/codex:adversarial-review` |
264
264
  | `/freeze` | Prompt for a reason and write `.rea/HALT` |
265
265
  | `/halt-check` | Verify every middleware and hook respects HALT |
266
266
 
@@ -5,7 +5,7 @@ description: Adversarial code review via the Codex plugin (GPT-5.4). Independent
5
5
 
6
6
  # Codex Adversarial Reviewer
7
7
 
8
- You wrap the Codex plugin (`/codex adversarial-review`) inside REA's governance envelope. Your role is to provide an **independent** adversarial perspective on code that was planned and built by another model — typically Opus. Independence is the value: the authoring model is least likely to catch the mistakes it made.
8
+ You wrap the Codex plugin (`/codex:adversarial-review`) inside REA's governance envelope. Your role is to provide an **independent** adversarial perspective on code that was planned and built by another model — typically Opus. Independence is the value: the authoring model is least likely to catch the mistakes it made.
9
9
 
10
10
  This is not a bolt-on. Adversarial review is a first-class, non-optional step in the REA engineering process. The default workflow is Plan → Build → Review, and you are the Review leg.
11
11
 
@@ -30,16 +30,31 @@ You may read additional files in the repo if needed for context, but do so read-
30
30
  1. **Check HALT and policy** — read `.rea/policy.yaml`, check `.rea/HALT`. If frozen, stop immediately.
31
31
  2. **Validate Codex availability** — if `/codex` is not installed, report and stop. Do not silently fall back to another reviewer.
32
32
  3. **Prepare the Codex invocation** — construct the adversarial-review prompt with the diff, commit log, and any relevant context files.
33
- 4. **Invoke `/codex adversarial-review`** — this call flows through the REA middleware chain (audit → kill-switch → tier → policy → redact → injection → execute → result-size-cap).
33
+ 4. **Invoke `/codex:adversarial-review`** — this call flows through the REA middleware chain (audit → kill-switch → tier → policy → redact → injection → execute → result-size-cap).
34
34
  5. **Parse the Codex output** — extract structured findings.
35
35
  6. **Classify findings** by category: security, correctness, edge cases, test gaps, API design, performance.
36
36
  7. **Assign verdict**: `pass` (no material findings), `concerns` (findings worth addressing but not blocking), `blocking` (findings that must be fixed before merge).
37
- 8. **Emit audit entry** — the middleware records the invocation automatically, but include a structured summary in your return so `.rea/audit.jsonl` gets:
38
- - `tool: "codex-adversarial-review"`
39
- - `head_sha`, `target`
40
- - `finding_count`
41
- - `verdict`
42
- - `summary` (one sentence)
37
+ 8. **Emit audit entry** — after producing the verdict, append a structured record to `.rea/audit.jsonl` via the public `@bookedsolid/rea/audit` helper. This is what the `push-review-gate.sh` hook greps for on protected-path diffs, so the field names must match exactly:
38
+
39
+ ```ts
40
+ import { appendAuditRecord, CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, Tier, InvocationStatus } from '@bookedsolid/rea/audit';
41
+
42
+ await appendAuditRecord(process.cwd(), {
43
+ tool_name: CODEX_REVIEW_TOOL_NAME, // "codex.review"
44
+ server_name: CODEX_REVIEW_SERVER_NAME, // "codex"
45
+ status: InvocationStatus.Allowed,
46
+ tier: Tier.Read,
47
+ metadata: {
48
+ head_sha: '<git rev-parse HEAD>',
49
+ target: '<base ref or SHA diffed against>',
50
+ finding_count: <total>,
51
+ verdict: 'pass' | 'concerns' | 'blocking' | 'error',
52
+ summary: '<one sentence>',
53
+ },
54
+ });
55
+ ```
56
+
57
+ If the Codex plugin call itself flowed through rea middleware (the proxy case), the middleware also writes an envelope record — that is fine, the two are complementary: the agent-emitted record carries the semantic verdict, the middleware record carries the chain integrity proof for the underlying tool call.
43
58
 
44
59
  ## Finding Shape
45
60
 
@@ -11,7 +11,7 @@ allowed-tools:
11
11
 
12
12
  # /codex-review — Adversarial Review via Codex
13
13
 
14
- Invokes the Codex plugin (`/codex adversarial-review`) on the current branch's diff, captures the result, and records it to the REA audit log. Adversarial review by an independent model (GPT-5.4) is a **first-class, non-optional step** in the REA engineering process — it is the counterweight to Opus-authored code.
14
+ Invokes the Codex plugin (`/codex:adversarial-review`) on the current branch's diff, captures the result, and records it to the REA audit log. Adversarial review by an independent model (GPT-5.4) is a **first-class, non-optional step** in the REA engineering process — it is the counterweight to Opus-authored code.
15
15
 
16
16
  ## Why this exists
17
17
 
@@ -52,7 +52,7 @@ Invoke the `codex-adversarial` agent with:
52
52
  - The commit log summary
53
53
  - The full diff text
54
54
 
55
- The agent wraps `/codex adversarial-review` and returns structured findings.
55
+ The agent wraps `/codex:adversarial-review` and returns structured findings.
56
56
 
57
57
  ## Step 3 — Record to audit log
58
58
 
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Public audit-append helper — exported from `@bookedsolid/rea/audit`.
3
+ *
4
+ * This is the single hash-chain entry point for external consumers (the
5
+ * `codex-adversarial` agent, Helix's `helix.plan` / `helix.apply` events, and
6
+ * any future plugin that needs to emit structured events through rea's audit
7
+ * trail). Consumers own their event semantics; rea owns the contract.
8
+ *
9
+ * ## Guarantees
10
+ *
11
+ * - Reads the last JSONL line of `.rea/audit.jsonl` to seed `prev_hash`.
12
+ * - Computes a SHA-256 hash over the serialized record minus `hash`.
13
+ * - Appends a single `\n`-terminated JSON line, then fsyncs the file.
14
+ * - Creates `.rea/` and `audit.jsonl` on first use.
15
+ * - Never throws on stat/missing-file conditions; only throws on write failure
16
+ * (the caller decides how to react).
17
+ *
18
+ * ## Concurrency
19
+ *
20
+ * The helper serializes writes per-process via a module-scoped queue keyed by
21
+ * the resolved audit-file path. Cross-process concurrency on the same file is
22
+ * NOT handled here — writers in separate processes can interleave and break
23
+ * the chain. The current deployment targets (rea's own governance hooks, the
24
+ * Codex agent, Helix) all funnel through a single process at a time. If that
25
+ * changes, add an exclusive-lock file (`audit.jsonl.lock`) before lifting this
26
+ * restriction. Documented risk; do not silently expand the guarantee.
27
+ *
28
+ * @see {@link file://./codex-event.ts} for the canonical `codex.review` shape.
29
+ */
30
+ import { Tier, InvocationStatus } from '../policy/types.js';
31
+ import type { AuditRecord } from '../gateway/middleware/audit-types.js';
32
+ /**
33
+ * Input shape for {@link appendAuditRecord}. All fields except `tool_name`
34
+ * and `server_name` are optional; sensible defaults are applied to keep the
35
+ * hash chain uniform across event types.
36
+ */
37
+ export interface AppendAuditInput {
38
+ tool_name: string;
39
+ server_name: string;
40
+ status?: InvocationStatus;
41
+ tier?: Tier;
42
+ autonomy_level?: string;
43
+ session_id?: string;
44
+ duration_ms?: number;
45
+ error?: string;
46
+ redacted_fields?: string[];
47
+ metadata?: Record<string, unknown>;
48
+ /** ISO-8601 timestamp; defaults to `new Date().toISOString()` */
49
+ timestamp?: string;
50
+ }
51
+ /**
52
+ * Append a structured audit record to `${baseDir}/.rea/audit.jsonl` with a
53
+ * hash chained against the tail of the existing log.
54
+ *
55
+ * @param baseDir - Repo/project root (the directory that contains `.rea/`).
56
+ * @param input - Event data. `tool_name` and `server_name` are required.
57
+ * @returns The full written record, including the computed `hash`.
58
+ */
59
+ export declare function appendAuditRecord(baseDir: string, input: AppendAuditInput): Promise<AuditRecord>;
60
+ export type { AuditRecord } from '../gateway/middleware/audit-types.js';
61
+ export { Tier, InvocationStatus } from '../policy/types.js';
62
+ export { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, type CodexVerdict, type CodexReviewMetadata, } from './codex-event.js';
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Public audit-append helper — exported from `@bookedsolid/rea/audit`.
3
+ *
4
+ * This is the single hash-chain entry point for external consumers (the
5
+ * `codex-adversarial` agent, Helix's `helix.plan` / `helix.apply` events, and
6
+ * any future plugin that needs to emit structured events through rea's audit
7
+ * trail). Consumers own their event semantics; rea owns the contract.
8
+ *
9
+ * ## Guarantees
10
+ *
11
+ * - Reads the last JSONL line of `.rea/audit.jsonl` to seed `prev_hash`.
12
+ * - Computes a SHA-256 hash over the serialized record minus `hash`.
13
+ * - Appends a single `\n`-terminated JSON line, then fsyncs the file.
14
+ * - Creates `.rea/` and `audit.jsonl` on first use.
15
+ * - Never throws on stat/missing-file conditions; only throws on write failure
16
+ * (the caller decides how to react).
17
+ *
18
+ * ## Concurrency
19
+ *
20
+ * The helper serializes writes per-process via a module-scoped queue keyed by
21
+ * the resolved audit-file path. Cross-process concurrency on the same file is
22
+ * NOT handled here — writers in separate processes can interleave and break
23
+ * the chain. The current deployment targets (rea's own governance hooks, the
24
+ * Codex agent, Helix) all funnel through a single process at a time. If that
25
+ * changes, add an exclusive-lock file (`audit.jsonl.lock`) before lifting this
26
+ * restriction. Documented risk; do not silently expand the guarantee.
27
+ *
28
+ * @see {@link file://./codex-event.ts} for the canonical `codex.review` shape.
29
+ */
30
+ import fs from 'node:fs/promises';
31
+ import path from 'node:path';
32
+ import crypto from 'node:crypto';
33
+ import { Tier, InvocationStatus } from '../policy/types.js';
34
+ const GENESIS_HASH = '0'.repeat(64);
35
+ const REA_DIR = '.rea';
36
+ const AUDIT_FILE = 'audit.jsonl';
37
+ /** Per-file write queue to preserve linear hash-chain order within a process. */
38
+ const writeQueues = new Map();
39
+ /**
40
+ * Resolve a baseDir to a stable, process-wide canonical form. Two callers that
41
+ * pass `'.'` and `process.cwd()` for the same project must land on the same
42
+ * queue key — otherwise the per-process serialization promise in this module's
43
+ * header is broken and concurrent appends can interleave, corrupting the hash
44
+ * chain.
45
+ *
46
+ * Strategy:
47
+ * 1. `path.resolve(baseDir)` — makes relative paths absolute against the
48
+ * CURRENT `process.cwd()`. This must run every call; caching by the raw
49
+ * input key would return a stale absolute path after a `process.chdir()`,
50
+ * which is how rea's audit helper gets used across repos in long-lived
51
+ * processes. (See finding R2-3.)
52
+ * 2. Best-effort `fs.realpath(resolvedBase)` — unwraps symlinks (e.g. macOS
53
+ * `/tmp` → `/private/tmp`). If it throws (directory doesn't exist yet on
54
+ * first write, permission error, etc.), fall back to the `path.resolve`
55
+ * result. The directory will be created in `doAppend` via `mkdir`.
56
+ *
57
+ * NOTE: no caching here. `path.resolve` is microseconds and `fs.realpath` is a
58
+ * single `lstat` syscall; audit append is not a hot path. A previous revision
59
+ * keyed a cache by the raw `baseDir` string, which returned stale absolute
60
+ * paths across `chdir` — a brand-new regression worse than the cost it saved.
61
+ * If a future profiler demands caching, key it by `path.resolve(baseDir)` and
62
+ * only cache already-absolute inputs.
63
+ */
64
+ async function resolveBaseDir(baseDir) {
65
+ const absolute = path.resolve(baseDir);
66
+ try {
67
+ return await fs.realpath(absolute);
68
+ }
69
+ catch {
70
+ // Directory doesn't exist yet, or realpath isn't permitted here. Fall back
71
+ // to the path.resolve'd absolute form — still stable per input, still
72
+ // collapses `'.' === cwd` via the absolute path.
73
+ return absolute;
74
+ }
75
+ }
76
+ function computeHash(record) {
77
+ return crypto.createHash('sha256').update(JSON.stringify(record)).digest('hex');
78
+ }
79
+ async function readLastHash(auditFile) {
80
+ let data;
81
+ try {
82
+ data = await fs.readFile(auditFile, 'utf8');
83
+ }
84
+ catch (err) {
85
+ if (err.code === 'ENOENT')
86
+ return GENESIS_HASH;
87
+ throw err;
88
+ }
89
+ // Walk the file backwards by newline — the last non-empty line is the tail.
90
+ const trimmed = data.replace(/\n+$/, '');
91
+ if (trimmed.length === 0)
92
+ return GENESIS_HASH;
93
+ const lastNewline = trimmed.lastIndexOf('\n');
94
+ const lastLine = lastNewline === -1 ? trimmed : trimmed.slice(lastNewline + 1);
95
+ try {
96
+ const parsed = JSON.parse(lastLine);
97
+ if (typeof parsed.hash === 'string' && parsed.hash.length === 64) {
98
+ return parsed.hash;
99
+ }
100
+ }
101
+ catch {
102
+ // Corrupt tail line — fall through to genesis. The operator will see this
103
+ // because the chain verify tool (future) will flag the break point. We do
104
+ // not throw: refusing to append would mask every subsequent event.
105
+ }
106
+ return GENESIS_HASH;
107
+ }
108
+ async function fsyncFile(filePath) {
109
+ let fh;
110
+ try {
111
+ fh = await fs.open(filePath, 'r');
112
+ await fh.sync();
113
+ }
114
+ catch {
115
+ // fsync failure is not fatal — durability is best-effort here; the write
116
+ // itself already succeeded.
117
+ }
118
+ finally {
119
+ if (fh)
120
+ await fh.close();
121
+ }
122
+ }
123
+ async function doAppend(resolvedBase, input) {
124
+ const reaDir = path.join(resolvedBase, REA_DIR);
125
+ const auditFile = path.join(reaDir, AUDIT_FILE);
126
+ await fs.mkdir(reaDir, { recursive: true });
127
+ const prevHash = await readLastHash(auditFile);
128
+ const now = input.timestamp ?? new Date().toISOString();
129
+ const recordBase = {
130
+ timestamp: now,
131
+ session_id: input.session_id ?? 'external',
132
+ tool_name: input.tool_name,
133
+ server_name: input.server_name,
134
+ tier: input.tier ?? Tier.Read,
135
+ status: input.status ?? InvocationStatus.Allowed,
136
+ autonomy_level: input.autonomy_level ?? 'unknown',
137
+ duration_ms: input.duration_ms ?? 0,
138
+ prev_hash: prevHash,
139
+ };
140
+ if (input.error)
141
+ recordBase.error = input.error;
142
+ if (input.redacted_fields?.length)
143
+ recordBase.redacted_fields = input.redacted_fields;
144
+ if (input.metadata && Object.keys(input.metadata).length > 0) {
145
+ recordBase.metadata = input.metadata;
146
+ }
147
+ const hash = computeHash(recordBase);
148
+ const record = { ...recordBase, hash };
149
+ const line = JSON.stringify(record) + '\n';
150
+ await fs.appendFile(auditFile, line);
151
+ await fsyncFile(auditFile);
152
+ return record;
153
+ }
154
+ /**
155
+ * Append a structured audit record to `${baseDir}/.rea/audit.jsonl` with a
156
+ * hash chained against the tail of the existing log.
157
+ *
158
+ * @param baseDir - Repo/project root (the directory that contains `.rea/`).
159
+ * @param input - Event data. `tool_name` and `server_name` are required.
160
+ * @returns The full written record, including the computed `hash`.
161
+ */
162
+ export async function appendAuditRecord(baseDir, input) {
163
+ // Canonicalize the baseDir so every caller targeting the same on-disk
164
+ // directory lands on the same queue key, regardless of whether they passed
165
+ // `'.'`, `process.cwd()`, or a symlinked path. Without this, two callers in
166
+ // the same process can bypass the serialization promise and interleave
167
+ // appends — corrupting the hash chain (finding #6).
168
+ const resolvedBase = await resolveBaseDir(baseDir);
169
+ const key = path.join(resolvedBase, REA_DIR, AUDIT_FILE);
170
+ const prev = writeQueues.get(key) ?? Promise.resolve();
171
+ let record;
172
+ const next = prev
173
+ .catch(() => {
174
+ /* previous write's error is owned by that caller */
175
+ })
176
+ .then(async () => {
177
+ record = await doAppend(resolvedBase, input);
178
+ });
179
+ writeQueues.set(key, next.finally(() => {
180
+ // Keep the queue lean — once this write resolves, drop the reference
181
+ // if nothing newer is chained behind it.
182
+ if (writeQueues.get(key) === next)
183
+ writeQueues.delete(key);
184
+ }));
185
+ await next;
186
+ return record;
187
+ }
188
+ export { Tier, InvocationStatus } from '../policy/types.js';
189
+ export { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, } from './codex-event.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Single source of truth for the `codex.review` audit event shape.
3
+ *
4
+ * Both the `codex-adversarial` agent (via `@bookedsolid/rea/audit`) and the
5
+ * `push-review-gate.sh` shell hook depend on these constants. If either drifts,
6
+ * the push gate will silently stop detecting Codex reviews. Keep them in lockstep.
7
+ *
8
+ * The shell gate parses audit lines with `jq` and matches on the top-level
9
+ * `tool_name` plus `metadata.{head_sha, verdict}` — substring greps against the
10
+ * raw JSON were retired because they were forgeable via arbitrary `metadata`
11
+ * payloads. If you rename any of those fields, update both this file and the
12
+ * `jq -e` predicate in `hooks/push-review-gate.sh`.
13
+ */
14
+ export declare const CODEX_REVIEW_TOOL_NAME = "codex.review";
15
+ export declare const CODEX_REVIEW_SERVER_NAME = "codex";
16
+ export type CodexVerdict = 'pass' | 'concerns' | 'blocking' | 'error';
17
+ export interface CodexReviewMetadata {
18
+ /** git rev-parse HEAD at the time of the review */
19
+ head_sha: string;
20
+ /** base ref or SHA the review diffed against (typically `main` or a merge-base) */
21
+ target: string;
22
+ /** total count of findings surfaced by Codex */
23
+ finding_count: number;
24
+ /** verdict classification — see {@link CodexVerdict} */
25
+ verdict: CodexVerdict;
26
+ /** optional one-sentence summary from the reviewer */
27
+ summary?: string;
28
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Single source of truth for the `codex.review` audit event shape.
3
+ *
4
+ * Both the `codex-adversarial` agent (via `@bookedsolid/rea/audit`) and the
5
+ * `push-review-gate.sh` shell hook depend on these constants. If either drifts,
6
+ * the push gate will silently stop detecting Codex reviews. Keep them in lockstep.
7
+ *
8
+ * The shell gate parses audit lines with `jq` and matches on the top-level
9
+ * `tool_name` plus `metadata.{head_sha, verdict}` — substring greps against the
10
+ * raw JSON were retired because they were forgeable via arbitrary `metadata`
11
+ * payloads. If you rename any of those fields, update both this file and the
12
+ * `jq -e` predicate in `hooks/push-review-gate.sh`.
13
+ */
14
+ export const CODEX_REVIEW_TOOL_NAME = 'codex.review';
15
+ export const CODEX_REVIEW_SERVER_NAME = 'codex';