@bookedsolid/rea 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-push +22 -167
- package/agents/codex-adversarial.md +5 -3
- package/commands/codex-review.md +3 -5
- package/dist/audit/append.d.ts +7 -32
- package/dist/audit/append.js +7 -35
- package/dist/cli/audit.d.ts +0 -31
- package/dist/cli/audit.js +5 -74
- package/dist/cli/doctor.js +6 -16
- package/dist/cli/hook.d.ts +48 -0
- package/dist/cli/hook.js +127 -0
- package/dist/cli/index.js +5 -80
- package/dist/cli/init.js +1 -1
- package/dist/cli/install/gitignore.d.ts +2 -2
- package/dist/cli/install/gitignore.js +3 -3
- package/dist/cli/install/pre-push.d.ts +146 -271
- package/dist/cli/install/pre-push.js +471 -2633
- package/dist/cli/install/settings-merge.d.ts +17 -0
- package/dist/cli/install/settings-merge.js +48 -1
- package/dist/cli/upgrade.js +131 -3
- package/dist/config/tier-map.js +18 -25
- package/dist/hooks/push-gate/base.d.ts +57 -0
- package/dist/hooks/push-gate/base.js +77 -0
- package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
- package/dist/hooks/push-gate/codex-runner.js +223 -0
- package/dist/hooks/push-gate/findings.d.ts +68 -0
- package/dist/hooks/push-gate/findings.js +142 -0
- package/dist/hooks/push-gate/halt.d.ts +28 -0
- package/dist/hooks/push-gate/halt.js +49 -0
- package/dist/hooks/push-gate/index.d.ts +90 -0
- package/dist/hooks/push-gate/index.js +351 -0
- package/dist/hooks/push-gate/policy.d.ts +41 -0
- package/dist/hooks/push-gate/policy.js +55 -0
- package/dist/hooks/push-gate/report.d.ts +89 -0
- package/dist/hooks/push-gate/report.js +140 -0
- package/dist/policy/loader.d.ts +10 -10
- package/dist/policy/loader.js +7 -6
- package/dist/policy/types.d.ts +31 -22
- package/package.json +1 -1
- package/dist/cache/review-cache.d.ts +0 -115
- package/dist/cache/review-cache.js +0 -200
- package/dist/cli/cache.d.ts +0 -84
- package/dist/cli/cache.js +0 -150
- package/dist/hooks/review-gate/args.d.ts +0 -126
- package/dist/hooks/review-gate/args.js +0 -315
- package/dist/hooks/review-gate/banner.d.ts +0 -97
- package/dist/hooks/review-gate/banner.js +0 -172
- package/dist/hooks/review-gate/cache-key.d.ts +0 -55
- package/dist/hooks/review-gate/cache-key.js +0 -41
- package/dist/hooks/review-gate/constants.d.ts +0 -26
- package/dist/hooks/review-gate/constants.js +0 -34
- package/dist/hooks/review-gate/errors.d.ts +0 -72
- package/dist/hooks/review-gate/errors.js +0 -100
- package/dist/hooks/review-gate/hash.d.ts +0 -43
- package/dist/hooks/review-gate/hash.js +0 -46
- package/dist/hooks/review-gate/index.d.ts +0 -21
- package/dist/hooks/review-gate/index.js +0 -21
- package/dist/hooks/review-gate/metadata.d.ts +0 -98
- package/dist/hooks/review-gate/metadata.js +0 -158
- package/dist/hooks/review-gate/policy.d.ts +0 -55
- package/dist/hooks/review-gate/policy.js +0 -71
- package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
- package/dist/hooks/review-gate/protected-paths.js +0 -76
- package/hooks/_lib/push-review-core.sh +0 -1250
- package/hooks/commit-review-gate.sh +0 -330
- package/hooks/push-review-gate-git.sh +0 -94
- package/hooks/push-review-gate.sh +0 -92
package/.husky/pre-push
CHANGED
|
@@ -1,180 +1,35 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# rea:husky-pre-push-gate
|
|
3
|
-
# rea:gate-body-
|
|
4
|
-
# .husky/pre-push — rea governance gate for terminal-initiated pushes.
|
|
2
|
+
# rea:husky-pre-push-gate v2
|
|
3
|
+
# rea:gate-body-v2
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# <local_ref> <local_sha> <remote_ref> <remote_sha>).
|
|
5
|
+
# Husky pre-push hook installed by `rea init` / `rea upgrade`. Do NOT
|
|
6
|
+
# edit by hand — the file is refreshed on every rea upgrade.
|
|
9
7
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# 2. If the push touches a protected path AND policy.review.codex_required
|
|
13
|
-
# is not explicitly false, require a `codex.review` audit entry for the
|
|
14
|
-
# HEAD SHA (or REA_SKIP_CODEX_REVIEW env var for a one-off bypass).
|
|
15
|
-
#
|
|
16
|
-
# Escape hatch: REA_SKIP_CODEX_REVIEW=<reason> bypasses the protected-path
|
|
17
|
-
# check. The skip record is appended by `push-review-gate.sh` in the Claude
|
|
18
|
-
# Code path; for terminal pushes, export the variable AND append a skip
|
|
19
|
-
# record manually if you want it in the audit trail.
|
|
20
|
-
#
|
|
21
|
-
# Subshell-safety note: earlier versions piped `echo "$INPUT" | while read`,
|
|
22
|
-
# which ran the loop in a subshell — `exit 1` inside the loop aborted the
|
|
23
|
-
# subshell only, and the script then ran `exit 0` and allowed the push. We
|
|
24
|
-
# now feed the loop with a here-doc so it runs in the main shell, and we
|
|
25
|
-
# abort immediately (`exit 1`) on the first blocking refspec. The accumulator
|
|
26
|
-
# pattern (`block_push=1; continue`) was dropped so the text-level detector
|
|
27
|
-
# in `src/cli/install/pre-push.ts` can verify the miss-path is truly blocking
|
|
28
|
-
# without modeling loop-carried flags and post-loop exit blocks.
|
|
8
|
+
# Governance contract: HALT kill-switch check, then delegate to
|
|
9
|
+
# `rea hook push-gate`. See src/hooks/push-gate/index.ts.
|
|
29
10
|
|
|
30
11
|
set -eu
|
|
31
12
|
|
|
32
|
-
# git passes the remote name as $1 to pre-push. Fall back to `origin` for
|
|
33
|
-
# direct invocation (tests, manual runs). The shared core uses the same
|
|
34
|
-
# argv_remote convention — parity required so a push to `upstream` probes
|
|
35
|
-
# `upstream/main` rather than stale `origin/main`.
|
|
36
|
-
REMOTE="${1:-origin}"
|
|
37
|
-
|
|
38
13
|
REA_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
39
|
-
|
|
40
|
-
# Well-known empty-tree SHA: `git hash-object -t tree /dev/null`. Every git
|
|
41
|
-
# installation carries this object implicitly — using it as a merge-base
|
|
42
|
-
# baseline for initial pushes lets `git diff $EMPTY_TREE $local_sha` emit
|
|
43
|
-
# the complete change set against a truly-empty tree. The protected-path
|
|
44
|
-
# check then sees every file in the initial push, so a first push of
|
|
45
|
-
# protected-path changes to a fresh remote is still gated.
|
|
46
|
-
EMPTY_TREE='4b825dc642cb6eb9a060e54bf8d69288fbee4904'
|
|
47
|
-
|
|
48
14
|
if [ -f "${REA_ROOT}/.rea/HALT" ]; then
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
[ -z "${reason:-}" ] && reason='unknown'
|
|
53
|
-
printf 'REA HALT: %s\nAll push operations suspended. Run: rea unfreeze\n' "$reason" >&2
|
|
15
|
+
reason=$(awk "NR==1 { print; exit }" "${REA_ROOT}/.rea/HALT" 2>/dev/null || printf "unknown")
|
|
16
|
+
[ -z "${reason:-}" ] && reason="unknown"
|
|
17
|
+
printf "REA HALT: %s\nAll push operations suspended. Run: rea unfreeze\n" "$reason" >&2
|
|
54
18
|
exit 1
|
|
55
19
|
fi
|
|
56
20
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# protected-path Codex audit requirement entirely (first-class no-Codex
|
|
70
|
-
# mode). Mirrors the logic in `.claude/hooks/push-review-gate.sh`.
|
|
71
|
-
#
|
|
72
|
-
# Fail-closed: if the helper is missing or errors, treat as true. A missing
|
|
73
|
-
# helper means rea is unbuilt — the operator can run `pnpm build` or set
|
|
74
|
-
# REA_SKIP_CODEX_REVIEW for a one-off bypass.
|
|
75
|
-
CODEX_REQUIRED=true
|
|
76
|
-
READ_FIELD_JS="${REA_ROOT}/dist/scripts/read-policy-field.js"
|
|
77
|
-
if [ -f "$READ_FIELD_JS" ]; then
|
|
78
|
-
field_value=$(REA_ROOT="$REA_ROOT" node "$READ_FIELD_JS" review.codex_required 2>/dev/null || printf '')
|
|
79
|
-
if [ "$field_value" = "false" ]; then
|
|
80
|
-
CODEX_REQUIRED=false
|
|
81
|
-
fi
|
|
21
|
+
REA_BIN=""
|
|
22
|
+
if [ -x "${REA_ROOT}/node_modules/.bin/rea" ]; then
|
|
23
|
+
REA_BIN="${REA_ROOT}/node_modules/.bin/rea"
|
|
24
|
+
elif [ -f "${REA_ROOT}/dist/cli/index.js" ]; then
|
|
25
|
+
REA_BIN="node ${REA_ROOT}/dist/cli/index.js"
|
|
26
|
+
elif command -v rea >/dev/null 2>&1; then
|
|
27
|
+
REA_BIN="rea"
|
|
28
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
29
|
+
REA_BIN="npx --no-install @bookedsolid/rea"
|
|
30
|
+
else
|
|
31
|
+
printf "rea: cannot locate the rea CLI.\n" >&2
|
|
32
|
+
exit 2
|
|
82
33
|
fi
|
|
83
34
|
|
|
84
|
-
|
|
85
|
-
# inside the loop terminates the hook and blocks the push. A pipeline
|
|
86
|
-
# would run the loop in a subshell and `exit 1` inside it would only
|
|
87
|
-
# abort that subshell — NOT the push — which was a real governance
|
|
88
|
-
# defect in the pre-review version of this file.
|
|
89
|
-
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
|
|
90
|
-
[ -z "${local_sha:-}" ] && continue
|
|
91
|
-
# Branch deletion: local_sha is 40 zeros. Skip protected-path check.
|
|
92
|
-
case "$local_sha" in
|
|
93
|
-
0000000000000000000000000000000000000000) continue ;;
|
|
94
|
-
esac
|
|
95
|
-
|
|
96
|
-
# Determine merge base. If remote is new (remote_sha is zeros), diff against
|
|
97
|
-
# the default branch; else against remote_sha.
|
|
98
|
-
#
|
|
99
|
-
# Anchor on a REMOTE-TRACKING ref (refs/remotes/<remote>/<name>), NOT a bare
|
|
100
|
-
# branch name. A bare `main` resolves to refs/heads/main, which the pusher
|
|
101
|
-
# controls locally — a local main fast-forwarded to the feature tip would
|
|
102
|
-
# give merge-base main <local_sha> == local_sha and silently collapse the
|
|
103
|
-
# diff to empty. Remote-tracking refs are server-authoritative from the
|
|
104
|
-
# last fetch and cannot be tampered with locally.
|
|
105
|
-
#
|
|
106
|
-
# Fallback order when $REMOTE/HEAD is not set (common on shallow or mirror
|
|
107
|
-
# clones): probe $REMOTE/main then $REMOTE/master via rev-parse. If neither
|
|
108
|
-
# exists — initial push to a fresh remote with no tracking refs yet — use
|
|
109
|
-
# the well-known EMPTY_TREE as the baseline so the diff covers the FULL
|
|
110
|
-
# change set. This keeps the protected-path check honest on first push
|
|
111
|
-
# (prior versions of this patch `continue`d here, which was a fail-open
|
|
112
|
-
# flagged as HIGH by adversarial review).
|
|
113
|
-
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
114
|
-
default_ref=$(git symbolic-ref "refs/remotes/${REMOTE}/HEAD" 2>/dev/null || printf '')
|
|
115
|
-
if [ -z "${default_ref:-}" ]; then
|
|
116
|
-
if git rev-parse --verify --quiet "refs/remotes/${REMOTE}/main" >/dev/null 2>&1; then
|
|
117
|
-
default_ref="refs/remotes/${REMOTE}/main"
|
|
118
|
-
elif git rev-parse --verify --quiet "refs/remotes/${REMOTE}/master" >/dev/null 2>&1; then
|
|
119
|
-
default_ref="refs/remotes/${REMOTE}/master"
|
|
120
|
-
else
|
|
121
|
-
default_ref=""
|
|
122
|
-
fi
|
|
123
|
-
fi
|
|
124
|
-
if [ -n "${default_ref:-}" ]; then
|
|
125
|
-
base=$(git merge-base "$default_ref" "$local_sha" 2>/dev/null || printf '')
|
|
126
|
-
else
|
|
127
|
-
# Bootstrap: no remote-tracking ref exists at all. Use the empty-tree
|
|
128
|
-
# baseline so the diff covers every file in the push. git diff accepts
|
|
129
|
-
# a tree SHA as the left-hand side.
|
|
130
|
-
base="$EMPTY_TREE"
|
|
131
|
-
fi
|
|
132
|
-
else
|
|
133
|
-
base=$(git merge-base "$remote_sha" "$local_sha" 2>/dev/null || printf '')
|
|
134
|
-
fi
|
|
135
|
-
# Fail CLOSED on empty merge-base when a remote ref DID resolve. The
|
|
136
|
-
# 0.4.0..0.6.2 behavior here was to `continue` — a silent bypass. A push
|
|
137
|
-
# whose history is unrelated to origin (or any transient git failure at
|
|
138
|
-
# merge-base resolution) would pass through without the protected-path
|
|
139
|
-
# check ever running. Refuse instead and force the operator to resolve it.
|
|
140
|
-
if [ -z "${base:-}" ]; then
|
|
141
|
-
printf 'PUSH BLOCKED: could not resolve merge-base between %s and %s (local_ref=%s remote_ref=%s).\n' \
|
|
142
|
-
"${remote_sha:-<new>}" "${local_sha:-<missing>}" "${local_ref:-<unknown>}" "${remote_ref:-<unknown>}" >&2
|
|
143
|
-
printf ' Run `git fetch %s` and retry. If the history is genuinely unrelated\n' "$REMOTE" >&2
|
|
144
|
-
printf ' to %s (e.g. grafted branch), resolve manually before pushing.\n' "$REMOTE" >&2
|
|
145
|
-
exit 1
|
|
146
|
-
fi
|
|
147
|
-
|
|
148
|
-
# Check if the diff touches protected paths.
|
|
149
|
-
if git diff --name-only "$base" "$local_sha" 2>/dev/null | grep -qE "$PROTECTED_RE"; then
|
|
150
|
-
if [ "$CODEX_REQUIRED" = "false" ]; then
|
|
151
|
-
# Policy opts out of the Codex gate. The downstream `.claude/hooks/`
|
|
152
|
-
# path already records telemetry; terminal pushes skip silently.
|
|
153
|
-
continue
|
|
154
|
-
fi
|
|
155
|
-
if [ -n "${REA_SKIP_CODEX_REVIEW:-}" ]; then
|
|
156
|
-
printf 'rea: REA_SKIP_CODEX_REVIEW set (%s) — skipping Codex review requirement for %s\n' \
|
|
157
|
-
"$REA_SKIP_CODEX_REVIEW" "$local_sha" >&2
|
|
158
|
-
continue
|
|
159
|
-
fi
|
|
160
|
-
if [ ! -f "$AUDIT_LOG" ]; then
|
|
161
|
-
printf 'PUSH BLOCKED: protected paths changed but no audit log found at %s\n' "$AUDIT_LOG" >&2
|
|
162
|
-
printf ' Run /codex-review on HEAD %s before pushing.\n' "$local_sha" >&2
|
|
163
|
-
exit 1
|
|
164
|
-
fi
|
|
165
|
-
# Require both (a) a `codex.review` tool_name and (b) the exact head_sha
|
|
166
|
-
# on the same JSONL line. The `codex.review` pattern ends with a closing
|
|
167
|
-
# quote, so `codex.review.skipped` never satisfies the gate. The first
|
|
168
|
-
# refspec that fails this check aborts the hook — no accumulator needed.
|
|
169
|
-
if ! grep -E '"tool_name":"codex\.review"' "$AUDIT_LOG" 2>/dev/null | \
|
|
170
|
-
grep -qF "\"head_sha\":\"$local_sha\""; then
|
|
171
|
-
printf 'PUSH BLOCKED: protected paths changed — /codex-review required for HEAD %s\n' "$local_sha" >&2
|
|
172
|
-
printf ' Run /codex-review, or set REA_SKIP_CODEX_REVIEW=<reason> to bypass.\n' >&2
|
|
173
|
-
exit 1
|
|
174
|
-
fi
|
|
175
|
-
fi
|
|
176
|
-
done <<HOOK_INPUT_EOF
|
|
177
|
-
$INPUT
|
|
178
|
-
HOOK_INPUT_EOF
|
|
179
|
-
|
|
180
|
-
exit 0
|
|
35
|
+
exec $REA_BIN hook push-gate "$@"
|
|
@@ -11,7 +11,9 @@ This is not a bolt-on. Adversarial review is a first-class, non-optional step in
|
|
|
11
11
|
|
|
12
12
|
## When You Are Invoked
|
|
13
13
|
|
|
14
|
-
The `/codex-review` slash command calls you. The `rea-orchestrator` delegates to you after any non-trivial change.
|
|
14
|
+
The `/codex-review` slash command calls you. The `rea-orchestrator` delegates to you after any non-trivial change.
|
|
15
|
+
|
|
16
|
+
Note (0.11.0+): you are **not** invoked by the pre-push gate. The pre-push gate (`rea hook push-gate`) shells directly to `codex exec review --json` and parses the verdict itself — no agent wrapper, no audit-receipt consultation. When that gate blocks a push, the authoring Claude session reads the stderr banner and `.rea/last-review.json`, applies fixes, and pushes again — the auto-fix loop IS the retry mechanism. The agent wrapper (you) is kept for interactive review (`/codex-review`) where human-targeted structured output matters.
|
|
15
17
|
|
|
16
18
|
## Inputs
|
|
17
19
|
|
|
@@ -34,7 +36,7 @@ You may read additional files in the repo if needed for context, but do so read-
|
|
|
34
36
|
5. **Parse the Codex output** — extract structured findings.
|
|
35
37
|
6. **Classify findings** by category: security, correctness, edge cases, test gaps, API design, performance.
|
|
36
38
|
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**
|
|
39
|
+
8. **Emit an audit entry** (optional in 0.11.0+) — the pre-push gate does not consult audit records to decide pass/fail, so you are no longer REQUIRED to emit a `codex.review` record on every interactive review. However, append one anyway via the public `@bookedsolid/rea/audit` helper when it helps forensic traceability (investigation of an intermittent verdict, review-history audit, etc.):
|
|
38
40
|
|
|
39
41
|
```ts
|
|
40
42
|
import { appendAuditRecord, CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, Tier, InvocationStatus } from '@bookedsolid/rea/audit';
|
|
@@ -54,7 +56,7 @@ You may read additional files in the repo if needed for context, but do so read-
|
|
|
54
56
|
});
|
|
55
57
|
```
|
|
56
58
|
|
|
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
|
|
59
|
+
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.
|
|
58
60
|
|
|
59
61
|
## Finding Shape
|
|
60
62
|
|
package/commands/codex-review.md
CHANGED
|
@@ -90,12 +90,10 @@ If the verdict is `blocking`, state plainly: "Do not merge until the blocking fi
|
|
|
90
90
|
|
|
91
91
|
## Pre-merge usage
|
|
92
92
|
|
|
93
|
-
The
|
|
93
|
+
This command is the **interactive** Codex adversarial review. The **pre-push** gate at `rea hook push-gate` runs Codex independently on every push — you do not need to run `/codex-review` to "prime" the push-gate. The two are complementary:
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Both invocations are cheap. Run both.
|
|
95
|
+
- `/codex-review` — rich, interactive review output in the chat. Use during implementation to catch issues early, at review checkpoints, or whenever you want Codex's read on a specific diff.
|
|
96
|
+
- `rea hook push-gate` (wired to `.husky/pre-push`) — fresh Codex review on every push. If Codex surfaces blocking/concerns findings, the push exits 2; Claude reads `.rea/last-review.json`, fixes, and pushes again.
|
|
99
97
|
|
|
100
98
|
## Constraints
|
|
101
99
|
|
package/dist/audit/append.d.ts
CHANGED
|
@@ -65,45 +65,20 @@ export interface AppendAuditInput {
|
|
|
65
65
|
* Append a structured audit record to `${baseDir}/.rea/audit.jsonl` with a
|
|
66
66
|
* hash chained against the tail of the existing log.
|
|
67
67
|
*
|
|
68
|
-
* ## emission_source
|
|
68
|
+
* ## emission_source
|
|
69
69
|
*
|
|
70
|
-
* Records written through this public helper are
|
|
71
|
-
* `emission_source: "other"`.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* ONLY path that stamps `"rea-cli"`.
|
|
77
|
-
*
|
|
78
|
-
* The push-review cache gate rejects `codex.review` records whose
|
|
79
|
-
* `emission_source` is `"other"` (or missing, for legacy records), so
|
|
80
|
-
* forging a `codex.review` record through this helper produces a line that
|
|
81
|
-
* is on the hash chain but does NOT satisfy the gate.
|
|
70
|
+
* Records written through this public helper are stamped with
|
|
71
|
+
* `emission_source: "other"`. The field is retained for forensic analysis
|
|
72
|
+
* (who wrote this line) but no gate consults it — the 0.11.0 stateless
|
|
73
|
+
* push-gate (see `src/hooks/push-gate/`) decides on Codex's live verdict,
|
|
74
|
+
* not on a receipt in the audit log. Pre-0.11.0 `emission_source`-gated
|
|
75
|
+
* predicates have been removed.
|
|
82
76
|
*
|
|
83
77
|
* @param baseDir - Repo/project root (the directory that contains `.rea/`).
|
|
84
78
|
* @param input - Event data. `tool_name` and `server_name` are required.
|
|
85
79
|
* @returns The full written record, including the computed `hash`.
|
|
86
80
|
*/
|
|
87
81
|
export declare function appendAuditRecord(baseDir: string, input: AppendAuditInput): Promise<AuditRecord>;
|
|
88
|
-
/**
|
|
89
|
-
* Append a `tool_name: "codex.review"` audit record certifying that a Codex
|
|
90
|
-
* adversarial review ran on a specific commit SHA (defect P).
|
|
91
|
-
*
|
|
92
|
-
* This is the ONLY write path in `@bookedsolid/rea` that produces
|
|
93
|
-
* `emission_source: "rea-cli"` for `codex.review` records. Consumers MUST
|
|
94
|
-
* reach this helper through the `rea audit record codex-review` CLI (which
|
|
95
|
-
* is classified as a Write-tier Bash invocation by `reaCommandTier`, defect
|
|
96
|
-
* E). Any other code path calling the generic {@link appendAuditRecord}
|
|
97
|
-
* with `tool_name: "codex.review"` lands with `emission_source: "other"`
|
|
98
|
-
* and does NOT satisfy the push-review cache gate — closing the forgery
|
|
99
|
-
* surface that `.reports/hook-patches/emit-audit-*.mjs` scripts exploited
|
|
100
|
-
* before this patch.
|
|
101
|
-
*
|
|
102
|
-
* `tool_name` and `server_name` are fixed to the canonical values
|
|
103
|
-
* (`"codex.review"` / `"codex"`) and are NOT accepted as caller inputs —
|
|
104
|
-
* the type excludes them so the contract is self-documenting.
|
|
105
|
-
*/
|
|
106
|
-
export declare function appendCodexReviewAuditRecord(baseDir: string, input: Omit<AppendAuditInput, 'tool_name' | 'server_name'>): Promise<AuditRecord>;
|
|
107
82
|
export type { AuditRecord, EmissionSource } from '../gateway/middleware/audit-types.js';
|
|
108
83
|
export { Tier, InvocationStatus } from '../policy/types.js';
|
|
109
84
|
export { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, type CodexVerdict, type CodexReviewMetadata, } from './codex-event.js';
|
package/dist/audit/append.js
CHANGED
|
@@ -37,7 +37,6 @@ import path from 'node:path';
|
|
|
37
37
|
import { Tier, InvocationStatus } from '../policy/types.js';
|
|
38
38
|
import { GENESIS_HASH, computeHash, fsyncFile, readLastRecord, withAuditLock, } from './fs.js';
|
|
39
39
|
import { maybeRotate } from '../gateway/audit/rotator.js';
|
|
40
|
-
import { CODEX_REVIEW_SERVER_NAME, CODEX_REVIEW_TOOL_NAME } from './codex-event.js';
|
|
41
40
|
const REA_DIR = '.rea';
|
|
42
41
|
const AUDIT_FILE = 'audit.jsonl';
|
|
43
42
|
/** Per-file write queue to preserve linear hash-chain order within a process. */
|
|
@@ -186,20 +185,14 @@ async function enqueueAppend(baseDir, input, emissionSource) {
|
|
|
186
185
|
* Append a structured audit record to `${baseDir}/.rea/audit.jsonl` with a
|
|
187
186
|
* hash chained against the tail of the existing log.
|
|
188
187
|
*
|
|
189
|
-
* ## emission_source
|
|
188
|
+
* ## emission_source
|
|
190
189
|
*
|
|
191
|
-
* Records written through this public helper are
|
|
192
|
-
* `emission_source: "other"`.
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* ONLY path that stamps `"rea-cli"`.
|
|
198
|
-
*
|
|
199
|
-
* The push-review cache gate rejects `codex.review` records whose
|
|
200
|
-
* `emission_source` is `"other"` (or missing, for legacy records), so
|
|
201
|
-
* forging a `codex.review` record through this helper produces a line that
|
|
202
|
-
* is on the hash chain but does NOT satisfy the gate.
|
|
190
|
+
* Records written through this public helper are stamped with
|
|
191
|
+
* `emission_source: "other"`. The field is retained for forensic analysis
|
|
192
|
+
* (who wrote this line) but no gate consults it — the 0.11.0 stateless
|
|
193
|
+
* push-gate (see `src/hooks/push-gate/`) decides on Codex's live verdict,
|
|
194
|
+
* not on a receipt in the audit log. Pre-0.11.0 `emission_source`-gated
|
|
195
|
+
* predicates have been removed.
|
|
203
196
|
*
|
|
204
197
|
* @param baseDir - Repo/project root (the directory that contains `.rea/`).
|
|
205
198
|
* @param input - Event data. `tool_name` and `server_name` are required.
|
|
@@ -208,26 +201,5 @@ async function enqueueAppend(baseDir, input, emissionSource) {
|
|
|
208
201
|
export async function appendAuditRecord(baseDir, input) {
|
|
209
202
|
return enqueueAppend(baseDir, input, 'other');
|
|
210
203
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Append a `tool_name: "codex.review"` audit record certifying that a Codex
|
|
213
|
-
* adversarial review ran on a specific commit SHA (defect P).
|
|
214
|
-
*
|
|
215
|
-
* This is the ONLY write path in `@bookedsolid/rea` that produces
|
|
216
|
-
* `emission_source: "rea-cli"` for `codex.review` records. Consumers MUST
|
|
217
|
-
* reach this helper through the `rea audit record codex-review` CLI (which
|
|
218
|
-
* is classified as a Write-tier Bash invocation by `reaCommandTier`, defect
|
|
219
|
-
* E). Any other code path calling the generic {@link appendAuditRecord}
|
|
220
|
-
* with `tool_name: "codex.review"` lands with `emission_source: "other"`
|
|
221
|
-
* and does NOT satisfy the push-review cache gate — closing the forgery
|
|
222
|
-
* surface that `.reports/hook-patches/emit-audit-*.mjs` scripts exploited
|
|
223
|
-
* before this patch.
|
|
224
|
-
*
|
|
225
|
-
* `tool_name` and `server_name` are fixed to the canonical values
|
|
226
|
-
* (`"codex.review"` / `"codex"`) and are NOT accepted as caller inputs —
|
|
227
|
-
* the type excludes them so the contract is self-documenting.
|
|
228
|
-
*/
|
|
229
|
-
export async function appendCodexReviewAuditRecord(baseDir, input) {
|
|
230
|
-
return enqueueAppend(baseDir, { ...input, tool_name: CODEX_REVIEW_TOOL_NAME, server_name: CODEX_REVIEW_SERVER_NAME }, 'rea-cli');
|
|
231
|
-
}
|
|
232
204
|
export { Tier, InvocationStatus } from '../policy/types.js';
|
|
233
205
|
export { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, } from './codex-event.js';
|
package/dist/cli/audit.d.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* explicit by definition, and verify operates on existing files regardless
|
|
11
11
|
* of policy.
|
|
12
12
|
*/
|
|
13
|
-
import { type CodexVerdict } from '../audit/append.js';
|
|
14
13
|
/**
|
|
15
14
|
* Reserved for future rotate knobs (e.g. `--retain N` to prune old rotated
|
|
16
15
|
* files). Empty today — kept as a typed record so the call site's option
|
|
@@ -39,33 +38,3 @@ export declare function runAuditRotate(_options: AuditRotateOptions): Promise<vo
|
|
|
39
38
|
* exit code is the primary signal.
|
|
40
39
|
*/
|
|
41
40
|
export declare function runAuditVerify(options: AuditVerifyOptions): Promise<void>;
|
|
42
|
-
export interface AuditRecordCodexReviewOptions {
|
|
43
|
-
headSha: string;
|
|
44
|
-
branch: string;
|
|
45
|
-
target: string;
|
|
46
|
-
verdict: CodexVerdict;
|
|
47
|
-
findingCount: number;
|
|
48
|
-
summary?: string | undefined;
|
|
49
|
-
sessionId?: string | undefined;
|
|
50
|
-
alsoSetCache?: boolean | undefined;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* `rea audit record codex-review` (Defect D / rea#77). Emits the single audit
|
|
54
|
-
* event the push-review cache gate looks up by `tool_name == "codex.review"` +
|
|
55
|
-
* `metadata.head_sha == <sha>` + `metadata.verdict in {pass, concerns}`. Prior
|
|
56
|
-
* to this command, agents had to reverse-engineer the canonical `tool_name`
|
|
57
|
-
* string, the hash-chain append path, and the `CodexReviewMetadata` shape —
|
|
58
|
-
* the most common failure mode was emitting `tool_name: "codex-adversarial-review"`
|
|
59
|
-
* (the agent's name) instead of `codex.review` (the event type), which the
|
|
60
|
-
* gate's jq predicate silently missed.
|
|
61
|
-
*
|
|
62
|
-
* `--also-set-cache` performs the audit record AND the review-cache write
|
|
63
|
-
* in one invocation — two sequential appends in a single process, not a
|
|
64
|
-
* two-phase commit. A crash between them leaves the audit entry without
|
|
65
|
-
* a cache row; the cache is recomputable from audit, the audit chain is
|
|
66
|
-
* the source of truth. What this DOES eliminate is the two-step race where
|
|
67
|
-
* `rea cache set` is denied by permission middleware (Defect E) after the
|
|
68
|
-
* audit has already been emitted, leaving the gate stuck on "audit present
|
|
69
|
-
* but cache cold" with no way forward.
|
|
70
|
-
*/
|
|
71
|
-
export declare function runAuditRecordCodexReview(options: AuditRecordCodexReviewOptions): Promise<void>;
|
package/dist/cli/audit.js
CHANGED
|
@@ -13,12 +13,8 @@
|
|
|
13
13
|
import fs from 'node:fs/promises';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { forceRotate } from '../gateway/audit/rotator.js';
|
|
16
|
-
import { appendCodexReviewAuditRecord, } from '../audit/append.js';
|
|
17
16
|
import { computeHash, GENESIS_HASH } from '../audit/fs.js';
|
|
18
|
-
import { appendEntry as appendCacheEntry } from '../cache/review-cache.js';
|
|
19
17
|
import { AUDIT_FILE, REA_DIR, err, log, reaPath } from './utils.js';
|
|
20
|
-
import { Tier, InvocationStatus } from '../policy/types.js';
|
|
21
|
-
import { codexVerdictToCacheResult } from './cache.js';
|
|
22
18
|
/**
|
|
23
19
|
* `rea audit rotate`. Forces a rotation now regardless of thresholds.
|
|
24
20
|
* Empty audit files are a no-op — rotating an empty chain would produce a
|
|
@@ -300,73 +296,8 @@ export async function runAuditVerify(options) {
|
|
|
300
296
|
}
|
|
301
297
|
log(`Audit chain verified: ${totalRecords} records across ${filesToVerify.length} file(s) — clean.`);
|
|
302
298
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
* string, the hash-chain append path, and the `CodexReviewMetadata` shape —
|
|
309
|
-
* the most common failure mode was emitting `tool_name: "codex-adversarial-review"`
|
|
310
|
-
* (the agent's name) instead of `codex.review` (the event type), which the
|
|
311
|
-
* gate's jq predicate silently missed.
|
|
312
|
-
*
|
|
313
|
-
* `--also-set-cache` performs the audit record AND the review-cache write
|
|
314
|
-
* in one invocation — two sequential appends in a single process, not a
|
|
315
|
-
* two-phase commit. A crash between them leaves the audit entry without
|
|
316
|
-
* a cache row; the cache is recomputable from audit, the audit chain is
|
|
317
|
-
* the source of truth. What this DOES eliminate is the two-step race where
|
|
318
|
-
* `rea cache set` is denied by permission middleware (Defect E) after the
|
|
319
|
-
* audit has already been emitted, leaving the gate stuck on "audit present
|
|
320
|
-
* but cache cold" with no way forward.
|
|
321
|
-
*/
|
|
322
|
-
export async function runAuditRecordCodexReview(options) {
|
|
323
|
-
if (options.headSha.length === 0) {
|
|
324
|
-
err('--head-sha must not be empty');
|
|
325
|
-
process.exit(1);
|
|
326
|
-
}
|
|
327
|
-
if (options.branch.length === 0) {
|
|
328
|
-
err('--branch must not be empty');
|
|
329
|
-
process.exit(1);
|
|
330
|
-
}
|
|
331
|
-
if (options.target.length === 0) {
|
|
332
|
-
err('--target must not be empty');
|
|
333
|
-
process.exit(1);
|
|
334
|
-
}
|
|
335
|
-
if (!Number.isFinite(options.findingCount) || options.findingCount < 0) {
|
|
336
|
-
err(`--finding-count must be a non-negative integer; got ${options.findingCount}`);
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
const baseDir = process.cwd();
|
|
340
|
-
const metadata = {
|
|
341
|
-
head_sha: options.headSha,
|
|
342
|
-
target: options.target,
|
|
343
|
-
finding_count: options.findingCount,
|
|
344
|
-
verdict: options.verdict,
|
|
345
|
-
};
|
|
346
|
-
if (options.summary !== undefined && options.summary.length > 0) {
|
|
347
|
-
metadata.summary = options.summary;
|
|
348
|
-
}
|
|
349
|
-
// Defect P: stamps emission_source: "rea-cli" so the record satisfies the
|
|
350
|
-
// push-review gate's new integrity predicate. Legacy records (without
|
|
351
|
-
// emission_source) and records written through the generic
|
|
352
|
-
// appendAuditRecord() helper (emission_source: "other") are rejected.
|
|
353
|
-
// tool_name/server_name are fixed inside the helper.
|
|
354
|
-
await appendCodexReviewAuditRecord(baseDir, {
|
|
355
|
-
tier: Tier.Read,
|
|
356
|
-
status: InvocationStatus.Allowed,
|
|
357
|
-
...(options.sessionId !== undefined ? { session_id: options.sessionId } : {}),
|
|
358
|
-
metadata,
|
|
359
|
-
});
|
|
360
|
-
log(`Recorded codex.review (${options.verdict}, ${options.findingCount} finding${options.findingCount === 1 ? '' : 's'}) for ${options.headSha.slice(0, 12)}.`);
|
|
361
|
-
if (options.alsoSetCache === true) {
|
|
362
|
-
const effect = codexVerdictToCacheResult(options.verdict);
|
|
363
|
-
const cacheEntry = await appendCacheEntry(baseDir, {
|
|
364
|
-
sha: options.headSha,
|
|
365
|
-
branch: options.branch,
|
|
366
|
-
base: options.target,
|
|
367
|
-
result: effect.result,
|
|
368
|
-
...(effect.reason !== undefined ? { reason: effect.reason } : {}),
|
|
369
|
-
});
|
|
370
|
-
log(`Cached ${cacheEntry.result} for ${cacheEntry.sha.slice(0, 12)} (${cacheEntry.branch} → ${cacheEntry.base}).`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
299
|
+
// `rea audit record codex-review` was removed in 0.11.0. The 0.11.0 push-gate
|
|
300
|
+
// is stateless — every `git push` runs `codex exec review --json` afresh,
|
|
301
|
+
// parses the verdict from the stream, and blocks or proceeds on the spot.
|
|
302
|
+
// There is no audit-receipt the gate consults, so no command to emit one.
|
|
303
|
+
// See `src/hooks/push-gate/index.ts` for the replacement gate.
|
package/dist/cli/doctor.js
CHANGED
|
@@ -147,12 +147,10 @@ const EXPECTED_HOOKS = [
|
|
|
147
147
|
'attribution-advisory.sh',
|
|
148
148
|
'blocked-paths-enforcer.sh',
|
|
149
149
|
'changeset-security-gate.sh',
|
|
150
|
-
'commit-review-gate.sh',
|
|
151
150
|
'dangerous-bash-interceptor.sh',
|
|
152
151
|
'dependency-audit-gate.sh',
|
|
153
152
|
'env-file-protection.sh',
|
|
154
153
|
'pr-issue-link-gate.sh',
|
|
155
|
-
'push-review-gate.sh',
|
|
156
154
|
'secret-scanner.sh',
|
|
157
155
|
'security-disclosure-gate.sh',
|
|
158
156
|
'settings-protection.sh',
|
|
@@ -366,7 +364,7 @@ function checkPrePushHook(state) {
|
|
|
366
364
|
const kind = active?.reaManaged === true
|
|
367
365
|
? 'rea-managed'
|
|
368
366
|
: active?.delegatesToGate === true
|
|
369
|
-
? 'external (delegates to push-
|
|
367
|
+
? 'external (delegates to `rea hook push-gate`)'
|
|
370
368
|
: 'external';
|
|
371
369
|
const detail = active !== undefined ? `${kind} at ${active.path}` : undefined;
|
|
372
370
|
return detail !== undefined
|
|
@@ -374,23 +372,15 @@ function checkPrePushHook(state) {
|
|
|
374
372
|
: { label: 'pre-push hook installed', status: 'pass' };
|
|
375
373
|
}
|
|
376
374
|
if (state.activeForeign) {
|
|
377
|
-
// Executable file exists at the active path but
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
// R13 F3: previously, a substring match of the gate path in the hook
|
|
382
|
-
// downgraded this to WARN. That was unsafe — any comment, echo, or
|
|
383
|
-
// dead string mentioning the path would mask a silent-bypass hook.
|
|
384
|
-
// The classifier now fails closed: either the structural parser
|
|
385
|
-
// (`referencesReviewGate` in `pre-push.ts`) recognizes a real
|
|
386
|
-
// invocation, or doctor reports fail.
|
|
375
|
+
// Executable file exists at the active path but neither carries a rea
|
|
376
|
+
// marker nor invokes `rea hook push-gate` — the push-gate is silently
|
|
377
|
+
// bypassed. Always a hard fail.
|
|
387
378
|
return {
|
|
388
379
|
label: 'pre-push hook installed',
|
|
389
380
|
status: 'fail',
|
|
390
381
|
detail: `active pre-push at ${state.activePath} is present and executable but does NOT ` +
|
|
391
|
-
`
|
|
392
|
-
`
|
|
393
|
-
'`exec .claude/hooks/push-review-gate.sh "$@"` to the existing hook, or ' +
|
|
382
|
+
'invoke `rea hook push-gate` — the 0.11.0 push-gate is silently bypassed. ' +
|
|
383
|
+
'Either add `exec rea hook push-gate "$@"` to the existing hook, or ' +
|
|
394
384
|
'remove it and re-run `rea init` to install the fallback.',
|
|
395
385
|
};
|
|
396
386
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rea hook push-gate` — the CLI surface the husky `.husky/pre-push` stub
|
|
3
|
+
* calls. Stateless pre-push Codex review.
|
|
4
|
+
*
|
|
5
|
+
* Exit-code contract:
|
|
6
|
+
*
|
|
7
|
+
* 0 — push proceeds (pass verdict, empty diff, disabled by policy, or
|
|
8
|
+
* REA_SKIP_PUSH_GATE waiver)
|
|
9
|
+
* 1 — HALT kill-switch active; block push
|
|
10
|
+
* 2 — blocked by verdict (blocking, or concerns when concerns_blocks=true
|
|
11
|
+
* and REA_ALLOW_CONCERNS not set), or by codex error (timeout, not
|
|
12
|
+
* installed, subprocess failure, protocol error)
|
|
13
|
+
*
|
|
14
|
+
* Invocation contract:
|
|
15
|
+
*
|
|
16
|
+
* rea hook push-gate
|
|
17
|
+
* rea hook push-gate --base origin/main
|
|
18
|
+
* rea hook push-gate --base refs/remotes/upstream/main
|
|
19
|
+
*
|
|
20
|
+
* The husky stub does NOT parse the git pre-push stdin contract itself —
|
|
21
|
+
* the 0.10.x bash gate did, to diff refspec-by-refspec; the 0.11.0 gate
|
|
22
|
+
* diffs `HEAD` against the resolved base (upstream → origin/HEAD → …).
|
|
23
|
+
* That is strictly less granular than refspec parsing, but Codex reviews
|
|
24
|
+
* the whole diff anyway and pushing multiple branches simultaneously is
|
|
25
|
+
* vanishingly rare in practice.
|
|
26
|
+
*
|
|
27
|
+
* A missing `.rea/policy.yaml` is treated as "defaults apply" —
|
|
28
|
+
* `codex_required: true`, `concerns_blocks: true`. The gate still fires.
|
|
29
|
+
* This matches the protective default established in 0.10.x.
|
|
30
|
+
*/
|
|
31
|
+
import type { Command } from 'commander';
|
|
32
|
+
export interface HookPushGateOptions {
|
|
33
|
+
base?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Public runner, exposed so integration tests and the commander binding can
|
|
37
|
+
* share the same entry. Throws via `process.exit` rather than returning a
|
|
38
|
+
* code — the commander handler is async but the convention across `src/cli/`
|
|
39
|
+
* is to exit from the leaf (see `audit.ts`, `freeze.ts`). Keeping the
|
|
40
|
+
* behavior consistent prevents commander from inferring its own default.
|
|
41
|
+
*/
|
|
42
|
+
export declare function runHookPushGate(options: HookPushGateOptions): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Attach the `rea hook` subcommand tree to a commander Program. Single
|
|
45
|
+
* subcommand today (`push-gate`); new hooks should land here rather than as
|
|
46
|
+
* top-level commands so the CLI surface stays navigable.
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerHookCommand(program: Command): void;
|