@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
@@ -1,315 +0,0 @@
1
- /**
2
- * Refspec parsing. Two input shapes the gate must accept:
3
- *
4
- * 1. Git pre-push hook stdin — one line per refspec, fields:
5
- * `<local_ref> <local_sha> <remote_ref> <remote_sha>`
6
- * (https://git-scm.com/docs/githooks#_pre_push)
7
- *
8
- * 2. Claude-Code `Bash`-PreToolUse command string — parse `git push [remote]
9
- * [refspec...]` out of the command, synthesize refspec records against
10
- * the caller's HEAD/@{upstream}.
11
- *
12
- * Shape 1 is authoritative when present; shape 2 is a fallback for the
13
- * Claude-Code adapter (BUG-008 sniff). See design §3.2 for the adapter
14
- * split and §5.1 for the scenarios covered by unit tests.
15
- *
16
- * ## Defect J — mixed-push deletion guard
17
- *
18
- * A push like `git push origin safe:safe :main` contains both a push refspec
19
- * and a deletion refspec. The bash core has been burned twice by nesting the
20
- * deletion check inside the "no SOURCE_SHA resolved" fallback branch, which
21
- * lets the deletion slip through whenever a sibling refspec DID resolve.
22
- * This module exposes `hasDeletion()` as a separate predicate so the caller
23
- * can fail-closed on deletions up front, before any refspec-selection logic.
24
- */
25
- import { ZERO_SHA } from './constants.js';
26
- import { BlockedError, DeletionBlockedError, HeadRefspecBlockedError, InvalidDeleteRefspecError, } from './errors.js';
27
- const SHA_HEX_40 = /^[0-9a-f]{40}$/;
28
- /**
29
- * Parse the git pre-push stdin contract.
30
- *
31
- * Returns `{ records, matched: true }` when at least one refspec line
32
- * parsed cleanly; `{ records: [], matched: false }` otherwise.
33
- *
34
- * ## Bash-core parity (push-review-core.sh §45-69)
35
- *
36
- * The bash parser uses `read -r local_ref local_sha remote_ref remote_sha rest`
37
- * against each line, so:
38
- * - Lines with fewer than the required fields leave some vars empty and
39
- * the loop `continue`s via the `-z` check (line 54-56). Parser does NOT
40
- * abort the overall parse — subsequent lines still get a chance.
41
- * - Extra whitespace-separated fields collapse into `rest` and are
42
- * silently dropped (line 53's `rest` capture absorbs everything past
43
- * field four).
44
- * - Only a 40-hex SHA failure on either sha triggers `return 1` (line
45
- * 57-59), aborting the whole parse — the caller falls through to argv.
46
- * - If no lines accept (`accepted=0` at line 63-65), the parser also
47
- * returns 1.
48
- *
49
- * We mirror that exactly. Codex pass-1 on phase 1 flagged an earlier
50
- * too-strict version that aborted on short/long lines and would have
51
- * starved the authoritative stdin path when consumer pre-push wrappers
52
- * emit extra trailing whitespace columns (e.g. a comment or a trailing
53
- * remote-url duplicate).
54
- *
55
- * Empty / whitespace-only lines are skipped silently.
56
- *
57
- * @param raw the full stdin bytes as a string
58
- */
59
- export function parsePrepushStdin(raw) {
60
- const records = [];
61
- if (raw.length === 0)
62
- return { records, matched: false };
63
- const lines = raw.split('\n');
64
- let accepted = false;
65
- for (const line of lines) {
66
- if (line.trim().length === 0)
67
- continue;
68
- // bash `read -r a b c d rest` takes the first 4 whitespace-separated
69
- // tokens and rolls everything else into `rest` (which we ignore).
70
- const parts = line.trim().split(/\s+/);
71
- const local_ref = parts[0] ?? '';
72
- const local_sha = parts[1] ?? '';
73
- const remote_ref = parts[2] ?? '';
74
- const remote_sha = parts[3] ?? '';
75
- // Missing required fields: bash `continue`s silently. Do the same —
76
- // DO NOT abort the whole parse, so a later well-formed line can still
77
- // be accepted.
78
- if (local_ref.length === 0 ||
79
- local_sha.length === 0 ||
80
- remote_ref.length === 0 ||
81
- remote_sha.length === 0) {
82
- continue;
83
- }
84
- // Invalid SHA on a line that otherwise has all 4 fields: bash
85
- // `return 1`s the whole parse so the caller falls through to the argv
86
- // fallback. Match that — return matched:false with no records.
87
- if (!SHA_HEX_40.test(local_sha) || !SHA_HEX_40.test(remote_sha)) {
88
- return { records: [], matched: false };
89
- }
90
- records.push({
91
- local_sha,
92
- remote_sha,
93
- local_ref,
94
- remote_ref,
95
- source_is_head: false,
96
- is_deletion: local_sha === ZERO_SHA,
97
- });
98
- accepted = true;
99
- }
100
- return { records, matched: accepted };
101
- }
102
- /**
103
- * Return true iff any refspec in the list is a branch deletion (defect J).
104
- * Callers must check this before any refspec-selection pass; the bash core
105
- * pre-0.9.4 nested the check inside the "no SOURCE_SHA resolved" branch and
106
- * let mixed pushes bypass the gate.
107
- */
108
- export function hasDeletion(records) {
109
- return records.some((r) => r.is_deletion);
110
- }
111
- /**
112
- * Parse refspecs out of a `git push [remote] [refspec...]` command string.
113
- * Used only when stdin-parsing returned `matched: false` (Claude-Code
114
- * adapter path).
115
- *
116
- * Behavior mirrors the bash core's `pr_resolve_argv_refspecs` exactly:
117
- * - Bare `git push` with no explicit refspec → synthesize a single record
118
- * against `@{upstream}` (or `main` when no upstream), local_sha = HEAD.
119
- * - `git push origin foo` → source=foo, dest=foo.
120
- * - `git push origin src:dst` → source=src, dest=dst.
121
- * - `git push origin :main` → deletion record.
122
- * - `git push origin --delete main` → deletion record.
123
- * - `git push origin HEAD:main` → resolves via `resolveHead('HEAD')`;
124
- * the bash core rejects HEAD only when it lands on the DESTINATION
125
- * side of the refspec (dst == 'HEAD'), not the source side. We match.
126
- * - `git push origin HEAD` → HeadRefspecBlockedError (dst resolves to
127
- * HEAD because src==dst when no colon is present).
128
- *
129
- * Throws `BlockedError` subclasses for operator-error conditions so the
130
- * caller can translate them to exit 2 + banner identical to the bash core.
131
- */
132
- export function resolveArgvRefspecs(cmd, deps) {
133
- const segment = extractPushSegment(cmd);
134
- const tokens = tokenizePushSegment(segment);
135
- const specs = [];
136
- let seenPush = false;
137
- let remoteSeen = false;
138
- let deleteMode = false;
139
- for (const tok of tokens) {
140
- if (tok === 'git' || tok === 'push') {
141
- seenPush = true;
142
- continue;
143
- }
144
- if (tok === '--delete' || tok === '-d') {
145
- deleteMode = true;
146
- continue;
147
- }
148
- if (tok.startsWith('--delete=')) {
149
- // Bash-core parity (push-review-core.sh §108-112): `--delete=<ref>`
150
- // sets delete_mode AND inlines the ref into specs WITHOUT the
151
- // `__REA_DELETE__` sentinel. In the existing bash implementation
152
- // this produces a non-deletion refspec record — the `delete_mode`
153
- // flag only affects tokens that appear AFTER the flag, not the
154
- // inlined ref. Documented upstream as a pre-existing bash quirk;
155
- // phase 1's job is byte-for-byte bash parity, so we mirror it even
156
- // though it looks counter-intuitive. A follow-up may harden both
157
- // implementations together (design §11.1 phase 4 window).
158
- deleteMode = true;
159
- specs.push(tok.slice('--delete='.length));
160
- continue;
161
- }
162
- if (tok.startsWith('-'))
163
- continue;
164
- if (!seenPush)
165
- continue;
166
- if (!remoteSeen) {
167
- remoteSeen = true;
168
- continue;
169
- }
170
- if (deleteMode) {
171
- specs.push(`__REA_DELETE__${tok}`);
172
- }
173
- else {
174
- specs.push(tok);
175
- }
176
- }
177
- if (specs.length === 0) {
178
- // Bare `git push` — one record against @{upstream} or main.
179
- let dstRef = 'refs/heads/main';
180
- if (deps.upstream && deps.upstream.includes('/')) {
181
- const short = deps.upstream.slice(deps.upstream.indexOf('/') + 1);
182
- dstRef = `refs/heads/${short}`;
183
- }
184
- if (deps.headSha.length === 0) {
185
- throw new BlockedError('PUSH_BLOCKED_SOURCE_UNRESOLVABLE', 'could not resolve HEAD to a commit; aborting review-gate argv fallback.');
186
- }
187
- return [
188
- {
189
- local_sha: deps.headSha,
190
- remote_sha: ZERO_SHA,
191
- local_ref: 'HEAD',
192
- remote_ref: dstRef,
193
- source_is_head: true,
194
- is_deletion: false,
195
- },
196
- ];
197
- }
198
- const records = [];
199
- for (const rawSpec of specs) {
200
- let spec = rawSpec;
201
- let isDelete = false;
202
- if (spec.startsWith('__REA_DELETE__')) {
203
- isDelete = true;
204
- spec = spec.slice('__REA_DELETE__'.length);
205
- }
206
- if (spec.startsWith('+'))
207
- spec = spec.slice(1);
208
- let src;
209
- let dst;
210
- if (spec.includes(':')) {
211
- src = spec.slice(0, spec.indexOf(':'));
212
- dst = spec.slice(spec.lastIndexOf(':') + 1);
213
- }
214
- else {
215
- src = spec;
216
- dst = spec;
217
- }
218
- if (dst.length === 0) {
219
- // `src:` with empty destination — the bash core treated this as a
220
- // deletion with dst = last component of spec. For safety, reject.
221
- dst = spec.split(':').pop() ?? '';
222
- src = '';
223
- }
224
- dst = stripRefsPrefix(dst);
225
- if (isDelete) {
226
- if (dst.length === 0 || dst === 'HEAD') {
227
- // Bash-core parity (push-review-core.sh §161-168): delete-mode
228
- // HEAD/empty destination uses a distinct operator banner —
229
- // "--delete refspec resolves to HEAD or empty" — rather than the
230
- // general "refspec resolves to HEAD" message, because the
231
- // remediation is different ("name the branch you meant to
232
- // delete", not "name the destination explicitly").
233
- throw new InvalidDeleteRefspecError(rawSpec);
234
- }
235
- records.push({
236
- local_sha: ZERO_SHA,
237
- remote_sha: ZERO_SHA,
238
- local_ref: '(delete)',
239
- remote_ref: `refs/heads/${dst}`,
240
- source_is_head: false,
241
- is_deletion: true,
242
- });
243
- continue;
244
- }
245
- if (dst === 'HEAD' || dst.length === 0) {
246
- throw new HeadRefspecBlockedError(rawSpec);
247
- }
248
- if (src.length === 0) {
249
- // `:main` — deletion.
250
- records.push({
251
- local_sha: ZERO_SHA,
252
- remote_sha: ZERO_SHA,
253
- local_ref: '(delete)',
254
- remote_ref: `refs/heads/${dst}`,
255
- source_is_head: false,
256
- is_deletion: true,
257
- });
258
- continue;
259
- }
260
- const resolved = deps.resolveHead(src);
261
- if (resolved === null || !SHA_HEX_40.test(resolved)) {
262
- throw new BlockedError('PUSH_BLOCKED_SOURCE_UNRESOLVABLE', `could not resolve source ref ${JSON.stringify(src)} to a commit.`, { ref: src });
263
- }
264
- records.push({
265
- local_sha: resolved,
266
- remote_sha: ZERO_SHA,
267
- local_ref: `refs/heads/${src}`,
268
- remote_ref: `refs/heads/${dst}`,
269
- source_is_head: false,
270
- is_deletion: false,
271
- });
272
- }
273
- // Deletion-first check (defect J): if ANY deletion resolved, the caller
274
- // will re-check via hasDeletion(). We do NOT throw here because the caller
275
- // may want to include push-side records in audit metadata before blocking.
276
- void DeletionBlockedError; // pulled so tree-shaking keeps the export chain.
277
- return records;
278
- }
279
- /**
280
- * Extract the `git push ...` segment from a command string, stopping at the
281
- * first shell separator (`;`, `&&`, `||`, `|`, `&`). Returns an empty
282
- * string when no `git push` is present — the caller bails out upstream.
283
- */
284
- function extractPushSegment(cmd) {
285
- const pushMatch = cmd.match(/git\s+push(?:\s|$)/);
286
- if (!pushMatch || pushMatch.index === undefined)
287
- return '';
288
- const tail = cmd.slice(pushMatch.index);
289
- const sepMatch = tail.match(/;|\|{1,2}|&{1,2}/);
290
- if (sepMatch && sepMatch.index !== undefined) {
291
- return tail.slice(0, sepMatch.index);
292
- }
293
- return tail;
294
- }
295
- /**
296
- * Split a `git push ...` segment into whitespace-separated tokens. This is
297
- * intentionally naive (no quote handling) — the bash core does the same
298
- * via `set -- $segment`, and preserving the bug-for-bug shape means we do
299
- * not silently start accepting quoted refspecs the bash core would have
300
- * rejected.
301
- */
302
- function tokenizePushSegment(segment) {
303
- return segment.split(/\s+/).filter((t) => t.length > 0);
304
- }
305
- /**
306
- * Strip `refs/heads/` or `refs/for/` prefixes so caller-facing code sees a
307
- * bare branch name. Exported for unit tests in `args.test.ts`.
308
- */
309
- export function stripRefsPrefix(ref) {
310
- if (ref.startsWith('refs/heads/'))
311
- return ref.slice('refs/heads/'.length);
312
- if (ref.startsWith('refs/for/'))
313
- return ref.slice('refs/for/'.length);
314
- return ref;
315
- }
@@ -1,131 +0,0 @@
1
- /**
2
- * Audit-record emission + consumption for the review gate.
3
- *
4
- * ## Responsibilities
5
- *
6
- * 1. Emit `push.review.skipped` and `codex.review.skipped` records via the
7
- * existing `appendAuditRecord()` helper. These are NEVER forgeable-
8
- * verdict records (the push-review gate never consults them as Codex
9
- * receipts), so they intentionally go through the `"other"`-stamped
10
- * public path rather than the `"rea-cli"` dedicated writer.
11
- *
12
- * 2. Scan `.rea/audit.jsonl` for a qualifying `codex.review` receipt
13
- * certifying a given `head_sha`. This is the TS equivalent of the
14
- * bash core's `jq -R 'fromjson? | select(...)'` predicate
15
- * (push-review-core.sh §959-966).
16
- *
17
- * ## Defect carry-forwards
18
- *
19
- * - **Defect P** (forgery rejection). The scan filter requires
20
- * `emission_source ∈ {"rea-cli", "codex-cli"}`. The public
21
- * `appendAuditRecord()` helper stamps `"other"`; only the dedicated
22
- * `appendCodexReviewAuditRecord()` helper and the Codex CLI write
23
- * `"rea-cli"` / `"codex-cli"`. Records with `emission_source: "other"`
24
- * or missing the field entirely are rejected here.
25
- *
26
- * - **Defect U** (streaming-parse tolerance). Every line in `.rea/
27
- * audit.jsonl` is parsed independently in a try/catch. A single
28
- * corrupt line mid-file does NOT abort the scan — later lines still
29
- * get a chance. Before 0.10.2 the bash `jq -e` scan would bail on the
30
- * first unparseable line and miss every subsequent legitimate record.
31
- *
32
- * - **Verdict whitelist**. Only `verdict ∈ {"pass", "concerns"}` records
33
- * satisfy the protected-path gate. `blocking` and `error` verdicts are
34
- * receipts that a review HAPPENED but with a negative outcome, which
35
- * does NOT unblock the push. Mirrors push-review-core.sh §964.
36
- */
37
- import { type AuditRecord } from '../../audit/append.js';
38
- import { type OsIdentity } from './metadata.js';
39
- /** Tool-names the gate emits. Kept as constants so string-literal drift is caught at compile time. */
40
- export declare const PUSH_REVIEW_SKIPPED_TOOL = "push.review.skipped";
41
- export declare const CODEX_REVIEW_SKIPPED_TOOL = "codex.review.skipped";
42
- export declare const PUSH_REVIEW_CACHE_HIT_TOOL = "push.review.cache.hit";
43
- export declare const PUSH_REVIEW_CACHE_ERROR_TOOL = "push.review.cache.error";
44
- /** Server-names for the emit paths — carry forward from bash §473/§639. */
45
- export declare const ESCAPE_HATCH_SERVER = "rea.escape_hatch";
46
- export declare const PUSH_REVIEW_SERVER = "rea.push_review";
47
- /**
48
- * Input shape for the `REA_SKIP_PUSH_REVIEW` escape hatch's audit record.
49
- *
50
- * The `os_identity` field is captured inside this module (not by the
51
- * caller) so every emitter gets the same shape and failing fields degrade
52
- * to empty strings uniformly. The pid/ppid numeric-not-string invariant
53
- * (defect M) is enforced by `metadata.ts`.
54
- */
55
- export interface SkipPushReviewAuditInput {
56
- /** Repo root (the dir containing `.rea/`). */
57
- baseDir: string;
58
- /** `HEAD` SHA at the time of the skip. */
59
- head_sha: string;
60
- /** Current branch or empty string. */
61
- branch: string;
62
- /** The non-empty value of `REA_SKIP_PUSH_REVIEW` (the reason). */
63
- reason: string;
64
- /** The resolved git actor (email, then name, else empty). */
65
- actor: string;
66
- /**
67
- * OS-identity fields. Optional — when absent, `collectOsIdentity()` runs
68
- * and fills them. Tests inject a deterministic stub for snapshot stability.
69
- */
70
- os_identity?: OsIdentity;
71
- }
72
- /**
73
- * Emit the `push.review.skipped` audit record. Wraps the public
74
- * `appendAuditRecord()` helper — emission_source lands as `"other"`.
75
- *
76
- * The skipped record is intentionally NOT a `codex.review` receipt: the
77
- * push-review cache-gate scan rejects any record whose `tool_name` is not
78
- * `codex.review` AND any record whose `emission_source` is not
79
- * `rea-cli` / `codex-cli`. So this record is on the hash chain as
80
- * forensic evidence but cannot be confused with a real Codex review.
81
- */
82
- export declare function emitPushReviewSkipped(input: SkipPushReviewAuditInput): Promise<AuditRecord>;
83
- /**
84
- * Input shape for the `REA_SKIP_CODEX_REVIEW` (Codex-only) waiver.
85
- *
86
- * `metadata_source` records whether the skip metadata came from the
87
- * pre-push stdin (`"prepush-stdin"`) or from a local HEAD fallback
88
- * (`"local-fallback"`). Bash-core §594+§606.
89
- */
90
- export interface SkipCodexReviewAuditInput {
91
- baseDir: string;
92
- head_sha: string;
93
- target: string;
94
- reason: string;
95
- actor: string;
96
- metadata_source: 'prepush-stdin' | 'local-fallback';
97
- }
98
- export declare function emitCodexReviewSkipped(input: SkipCodexReviewAuditInput): Promise<AuditRecord>;
99
- /**
100
- * Predicate: does this parsed JSON object qualify as a valid
101
- * `codex.review` receipt for the given `head_sha`?
102
- *
103
- * Exported for unit tests; callers should usually use
104
- * `hasValidCodexReview()` below.
105
- */
106
- export declare function isQualifyingCodexReview(record: unknown, head_sha: string): boolean;
107
- /**
108
- * Scan `.rea/audit.jsonl` for a qualifying `codex.review` record matching
109
- * the given `head_sha`. Returns true as soon as one is found.
110
- *
111
- * ## Defect U tolerance
112
- *
113
- * Each line is parsed independently via `JSON.parse` inside try/catch. A
114
- * malformed line logs nothing and the scan continues. The bash fix in
115
- * 0.10.2 was `jq -R 'fromjson?'`; we mirror the per-line behavior in
116
- * native JS.
117
- *
118
- * ## Path safety
119
- *
120
- * The audit file is always `<baseDir>/.rea/audit.jsonl` — baseDir flows
121
- * in from the caller and is the same resolved path used everywhere else.
122
- * No caller-supplied path segments.
123
- *
124
- * ## Missing file
125
- *
126
- * ENOENT resolves to `false` (no receipt exists yet). Any other error
127
- * propagates — the caller's policy is to fail-closed, and a permission
128
- * error on the audit file is a distinct operational concern the caller
129
- * should surface rather than silently mask as "no receipt".
130
- */
131
- export declare function hasValidCodexReview(baseDir: string, head_sha: string): Promise<boolean>;
@@ -1,181 +0,0 @@
1
- /**
2
- * Audit-record emission + consumption for the review gate.
3
- *
4
- * ## Responsibilities
5
- *
6
- * 1. Emit `push.review.skipped` and `codex.review.skipped` records via the
7
- * existing `appendAuditRecord()` helper. These are NEVER forgeable-
8
- * verdict records (the push-review gate never consults them as Codex
9
- * receipts), so they intentionally go through the `"other"`-stamped
10
- * public path rather than the `"rea-cli"` dedicated writer.
11
- *
12
- * 2. Scan `.rea/audit.jsonl` for a qualifying `codex.review` receipt
13
- * certifying a given `head_sha`. This is the TS equivalent of the
14
- * bash core's `jq -R 'fromjson? | select(...)'` predicate
15
- * (push-review-core.sh §959-966).
16
- *
17
- * ## Defect carry-forwards
18
- *
19
- * - **Defect P** (forgery rejection). The scan filter requires
20
- * `emission_source ∈ {"rea-cli", "codex-cli"}`. The public
21
- * `appendAuditRecord()` helper stamps `"other"`; only the dedicated
22
- * `appendCodexReviewAuditRecord()` helper and the Codex CLI write
23
- * `"rea-cli"` / `"codex-cli"`. Records with `emission_source: "other"`
24
- * or missing the field entirely are rejected here.
25
- *
26
- * - **Defect U** (streaming-parse tolerance). Every line in `.rea/
27
- * audit.jsonl` is parsed independently in a try/catch. A single
28
- * corrupt line mid-file does NOT abort the scan — later lines still
29
- * get a chance. Before 0.10.2 the bash `jq -e` scan would bail on the
30
- * first unparseable line and miss every subsequent legitimate record.
31
- *
32
- * - **Verdict whitelist**. Only `verdict ∈ {"pass", "concerns"}` records
33
- * satisfy the protected-path gate. `blocking` and `error` verdicts are
34
- * receipts that a review HAPPENED but with a negative outcome, which
35
- * does NOT unblock the push. Mirrors push-review-core.sh §964.
36
- */
37
- import fs from 'node:fs/promises';
38
- import path from 'node:path';
39
- import { appendAuditRecord, InvocationStatus, Tier, } from '../../audit/append.js';
40
- import { collectOsIdentity } from './metadata.js';
41
- /** Tool-names the gate emits. Kept as constants so string-literal drift is caught at compile time. */
42
- export const PUSH_REVIEW_SKIPPED_TOOL = 'push.review.skipped';
43
- export const CODEX_REVIEW_SKIPPED_TOOL = 'codex.review.skipped';
44
- export const PUSH_REVIEW_CACHE_HIT_TOOL = 'push.review.cache.hit';
45
- export const PUSH_REVIEW_CACHE_ERROR_TOOL = 'push.review.cache.error';
46
- /** Server-names for the emit paths — carry forward from bash §473/§639. */
47
- export const ESCAPE_HATCH_SERVER = 'rea.escape_hatch';
48
- export const PUSH_REVIEW_SERVER = 'rea.push_review';
49
- /**
50
- * Emit the `push.review.skipped` audit record. Wraps the public
51
- * `appendAuditRecord()` helper — emission_source lands as `"other"`.
52
- *
53
- * The skipped record is intentionally NOT a `codex.review` receipt: the
54
- * push-review cache-gate scan rejects any record whose `tool_name` is not
55
- * `codex.review` AND any record whose `emission_source` is not
56
- * `rea-cli` / `codex-cli`. So this record is on the hash chain as
57
- * forensic evidence but cannot be confused with a real Codex review.
58
- */
59
- export async function emitPushReviewSkipped(input) {
60
- const osIdentity = input.os_identity ?? collectOsIdentity();
61
- const metadata = {
62
- head_sha: input.head_sha,
63
- branch: input.branch,
64
- reason: input.reason,
65
- actor: input.actor,
66
- verdict: 'skipped',
67
- os_identity: osIdentity,
68
- };
69
- const record = {
70
- tool_name: PUSH_REVIEW_SKIPPED_TOOL,
71
- server_name: ESCAPE_HATCH_SERVER,
72
- status: InvocationStatus.Allowed,
73
- tier: Tier.Read,
74
- metadata,
75
- };
76
- return appendAuditRecord(input.baseDir, record);
77
- }
78
- export async function emitCodexReviewSkipped(input) {
79
- const metadata = {
80
- head_sha: input.head_sha,
81
- target: input.target,
82
- reason: input.reason,
83
- actor: input.actor,
84
- verdict: 'skipped',
85
- files_changed: null,
86
- metadata_source: input.metadata_source,
87
- };
88
- const record = {
89
- tool_name: CODEX_REVIEW_SKIPPED_TOOL,
90
- server_name: ESCAPE_HATCH_SERVER,
91
- status: InvocationStatus.Allowed,
92
- tier: Tier.Read,
93
- metadata,
94
- };
95
- return appendAuditRecord(input.baseDir, record);
96
- }
97
- /** Verdicts that satisfy the protected-path Codex-receipt gate. */
98
- const ACCEPTABLE_VERDICTS = new Set(['pass', 'concerns']);
99
- /** Emission sources that satisfy the protected-path Codex-receipt gate. */
100
- const ACCEPTABLE_SOURCES = new Set(['rea-cli', 'codex-cli']);
101
- /**
102
- * Predicate: does this parsed JSON object qualify as a valid
103
- * `codex.review` receipt for the given `head_sha`?
104
- *
105
- * Exported for unit tests; callers should usually use
106
- * `hasValidCodexReview()` below.
107
- */
108
- export function isQualifyingCodexReview(record, head_sha) {
109
- if (record === null || typeof record !== 'object')
110
- return false;
111
- const r = record;
112
- if (r.tool_name !== 'codex.review')
113
- return false;
114
- if (typeof r.emission_source !== 'string' || !ACCEPTABLE_SOURCES.has(r.emission_source)) {
115
- return false;
116
- }
117
- const md = r.metadata;
118
- if (md === null || md === undefined || typeof md !== 'object')
119
- return false;
120
- if (md.head_sha !== head_sha)
121
- return false;
122
- if (typeof md.verdict !== 'string' || !ACCEPTABLE_VERDICTS.has(md.verdict)) {
123
- return false;
124
- }
125
- return true;
126
- }
127
- /**
128
- * Scan `.rea/audit.jsonl` for a qualifying `codex.review` record matching
129
- * the given `head_sha`. Returns true as soon as one is found.
130
- *
131
- * ## Defect U tolerance
132
- *
133
- * Each line is parsed independently via `JSON.parse` inside try/catch. A
134
- * malformed line logs nothing and the scan continues. The bash fix in
135
- * 0.10.2 was `jq -R 'fromjson?'`; we mirror the per-line behavior in
136
- * native JS.
137
- *
138
- * ## Path safety
139
- *
140
- * The audit file is always `<baseDir>/.rea/audit.jsonl` — baseDir flows
141
- * in from the caller and is the same resolved path used everywhere else.
142
- * No caller-supplied path segments.
143
- *
144
- * ## Missing file
145
- *
146
- * ENOENT resolves to `false` (no receipt exists yet). Any other error
147
- * propagates — the caller's policy is to fail-closed, and a permission
148
- * error on the audit file is a distinct operational concern the caller
149
- * should surface rather than silently mask as "no receipt".
150
- */
151
- export async function hasValidCodexReview(baseDir, head_sha) {
152
- const auditFile = path.join(baseDir, '.rea', 'audit.jsonl');
153
- let raw;
154
- try {
155
- raw = await fs.readFile(auditFile, 'utf8');
156
- }
157
- catch (err) {
158
- if (err.code === 'ENOENT')
159
- return false;
160
- throw err;
161
- }
162
- if (raw.length === 0)
163
- return false;
164
- // Walk lines. Each line is independently parsed; a corrupt line is
165
- // silently skipped. A matching record short-circuits the scan.
166
- for (const line of raw.split('\n')) {
167
- if (line.length === 0)
168
- continue;
169
- let parsed;
170
- try {
171
- parsed = JSON.parse(line);
172
- }
173
- catch {
174
- // Defect U tolerance — move on.
175
- continue;
176
- }
177
- if (isQualifyingCodexReview(parsed, head_sha))
178
- return true;
179
- }
180
- return false;
181
- }