@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.
- package/.husky/pre-push +48 -162
- package/README.md +834 -552
- 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.d.ts +12 -0
- package/dist/cli/doctor.js +96 -17
- package/dist/cli/hook.d.ts +55 -0
- package/dist/cli/hook.js +138 -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 +158 -272
- package/dist/cli/install/pre-push.js +491 -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 +104 -0
- package/dist/hooks/push-gate/base.js +198 -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 +98 -0
- package/dist/hooks/push-gate/index.js +416 -0
- package/dist/hooks/push-gate/policy.d.ts +55 -0
- package/dist/hooks/push-gate/policy.js +64 -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 +15 -10
- package/dist/policy/loader.js +8 -6
- package/dist/policy/types.d.ts +73 -22
- package/package.json +1 -1
- package/scripts/tarball-smoke.sh +7 -2
- 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/audit.d.ts +0 -131
- package/dist/hooks/review-gate/audit.js +0 -181
- package/dist/hooks/review-gate/banner.d.ts +0 -97
- package/dist/hooks/review-gate/banner.js +0 -172
- package/dist/hooks/review-gate/base-resolve.d.ts +0 -155
- package/dist/hooks/review-gate/base-resolve.js +0 -247
- 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/cache.d.ts +0 -108
- package/dist/hooks/review-gate/cache.js +0 -120
- package/dist/hooks/review-gate/constants.d.ts +0 -26
- package/dist/hooks/review-gate/constants.js +0 -34
- package/dist/hooks/review-gate/diff.d.ts +0 -181
- package/dist/hooks/review-gate/diff.js +0 -232
- 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 -31
- package/dist/hooks/review-gate/index.js +0 -35
- 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
|
@@ -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.d.ts
CHANGED
|
@@ -46,6 +46,18 @@ export declare function checkFingerprintStore(baseDir: string): Promise<CheckRes
|
|
|
46
46
|
* NOT a trust boundary. Do not key security decisions on the return value.
|
|
47
47
|
*/
|
|
48
48
|
export declare function isGitRepo(baseDir: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Hard-fail when `policy.review.codex_required: true` but the `codex`
|
|
51
|
+
* binary is not on PATH. Pre-0.12.0 this prereq surfaced only at first
|
|
52
|
+
* push, by which point the consumer had cloned, run `pnpm install`,
|
|
53
|
+
* authored a commit, and tried to push — only then to learn that they
|
|
54
|
+
* needed a separate install. Fix C of 0.12.0 surfaces it during install.
|
|
55
|
+
*
|
|
56
|
+
* Returns `pass` when codex resolves; `fail` when missing. Operators who
|
|
57
|
+
* want to disable the gate can flip `policy.review.codex_required: false`
|
|
58
|
+
* (the doctor then short-circuits past every Codex check).
|
|
59
|
+
*/
|
|
60
|
+
export declare function checkCodexBinaryOnPath(): CheckResult;
|
|
49
61
|
/**
|
|
50
62
|
* Translate a `CodexProbeState` into two doctor CheckResults: one for
|
|
51
63
|
* responsiveness (pass/warn) and one informational line about the last
|
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
|
}
|
|
@@ -433,6 +423,95 @@ function checkCodexCommand(baseDir) {
|
|
|
433
423
|
detail: `missing: ${cmdPath}`,
|
|
434
424
|
};
|
|
435
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Resolve the absolute path of `codex` on PATH (cross-platform). Returns
|
|
428
|
+
* `null` when codex is not installed. We walk `process.env.PATH`
|
|
429
|
+
* directly rather than shelling out — earlier iterations spawned
|
|
430
|
+
* `sh -c "command -v codex"` which gave false negatives in sanitized
|
|
431
|
+
* POSIX environments where `/bin` is omitted from PATH (CI runners,
|
|
432
|
+
* hardened dev shells) but the `codex` binary lives at a project-bin
|
|
433
|
+
* path that IS on PATH. Codex [P2] 2026-04-29.
|
|
434
|
+
*
|
|
435
|
+
* On Windows we iterate `PATHEXT` (default `.COM;.EXE;.BAT;.CMD`) so
|
|
436
|
+
* `codex.cmd` (the typical npm shim) is discovered. POSIX checks the
|
|
437
|
+
* bare name and accepts any file with an execute bit set.
|
|
438
|
+
*/
|
|
439
|
+
function resolveCodexBinary() {
|
|
440
|
+
const isWindows = process.platform === 'win32';
|
|
441
|
+
const pathEnv = process.env.PATH ?? process.env.Path ?? '';
|
|
442
|
+
if (pathEnv.length === 0)
|
|
443
|
+
return null;
|
|
444
|
+
const sep = isWindows ? ';' : ':';
|
|
445
|
+
const entries = pathEnv.split(sep).filter((p) => p.length > 0);
|
|
446
|
+
if (isWindows) {
|
|
447
|
+
const pathExt = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';');
|
|
448
|
+
for (const dir of entries) {
|
|
449
|
+
for (const ext of pathExt) {
|
|
450
|
+
const candidate = path.join(dir, `codex${ext}`);
|
|
451
|
+
try {
|
|
452
|
+
const st = fs.statSync(candidate);
|
|
453
|
+
if (st.isFile())
|
|
454
|
+
return candidate;
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
// not present in this PATH entry — keep walking
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// also check the bare name in case PATHEXT is unusual
|
|
461
|
+
const bare = path.join(dir, 'codex');
|
|
462
|
+
try {
|
|
463
|
+
const st = fs.statSync(bare);
|
|
464
|
+
if (st.isFile())
|
|
465
|
+
return bare;
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// not present — keep walking
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
// POSIX: check executable bit on the file mode.
|
|
474
|
+
for (const dir of entries) {
|
|
475
|
+
const candidate = path.join(dir, 'codex');
|
|
476
|
+
try {
|
|
477
|
+
const st = fs.statSync(candidate);
|
|
478
|
+
if (st.isFile() && (st.mode & 0o111) !== 0)
|
|
479
|
+
return candidate;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// not present in this PATH entry — keep walking
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Hard-fail when `policy.review.codex_required: true` but the `codex`
|
|
489
|
+
* binary is not on PATH. Pre-0.12.0 this prereq surfaced only at first
|
|
490
|
+
* push, by which point the consumer had cloned, run `pnpm install`,
|
|
491
|
+
* authored a commit, and tried to push — only then to learn that they
|
|
492
|
+
* needed a separate install. Fix C of 0.12.0 surfaces it during install.
|
|
493
|
+
*
|
|
494
|
+
* Returns `pass` when codex resolves; `fail` when missing. Operators who
|
|
495
|
+
* want to disable the gate can flip `policy.review.codex_required: false`
|
|
496
|
+
* (the doctor then short-circuits past every Codex check).
|
|
497
|
+
*/
|
|
498
|
+
export function checkCodexBinaryOnPath() {
|
|
499
|
+
const resolved = resolveCodexBinary();
|
|
500
|
+
if (resolved !== null) {
|
|
501
|
+
return {
|
|
502
|
+
label: 'codex CLI on PATH',
|
|
503
|
+
status: 'pass',
|
|
504
|
+
detail: resolved,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
label: 'codex CLI on PATH',
|
|
509
|
+
status: 'fail',
|
|
510
|
+
detail: 'codex not found on PATH. policy.review.codex_required: true requires the codex binary. ' +
|
|
511
|
+
'Install: https://github.com/openai/codex (e.g. `npm i -g @openai/codex`). ' +
|
|
512
|
+
'To disable the push-gate instead, set policy.review.codex_required: false in .rea/policy.yaml.',
|
|
513
|
+
};
|
|
514
|
+
}
|
|
436
515
|
/**
|
|
437
516
|
* Translate a `CodexProbeState` into two doctor CheckResults: one for
|
|
438
517
|
* responsiveness (pass/warn) and one informational line about the last
|
|
@@ -531,7 +610,7 @@ export function collectChecks(baseDir, codexProbeState, prePushState) {
|
|
|
531
610
|
});
|
|
532
611
|
}
|
|
533
612
|
if (codexRequiredFromPolicy(baseDir)) {
|
|
534
|
-
checks.push(checkCodexAgent(baseDir), checkCodexCommand(baseDir));
|
|
613
|
+
checks.push(checkCodexAgent(baseDir), checkCodexCommand(baseDir), checkCodexBinaryOnPath());
|
|
535
614
|
if (codexProbeState !== undefined) {
|
|
536
615
|
checks.push(...checksFromProbeState(codexProbeState));
|
|
537
616
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
* Diff against `HEAD~N` instead of running the upstream ladder. Mirrors
|
|
36
|
+
* `policy.review.last_n_commits`; the CLI flag wins when both are set.
|
|
37
|
+
* `--base` always wins over both. Validated as a positive integer; the
|
|
38
|
+
* CLI rejects non-numeric input before reaching `runPushGate`.
|
|
39
|
+
*/
|
|
40
|
+
lastNCommits?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Public runner, exposed so integration tests and the commander binding can
|
|
44
|
+
* share the same entry. Throws via `process.exit` rather than returning a
|
|
45
|
+
* code — the commander handler is async but the convention across `src/cli/`
|
|
46
|
+
* is to exit from the leaf (see `audit.ts`, `freeze.ts`). Keeping the
|
|
47
|
+
* behavior consistent prevents commander from inferring its own default.
|
|
48
|
+
*/
|
|
49
|
+
export declare function runHookPushGate(options: HookPushGateOptions): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Attach the `rea hook` subcommand tree to a commander Program. Single
|
|
52
|
+
* subcommand today (`push-gate`); new hooks should land here rather than as
|
|
53
|
+
* top-level commands so the CLI surface stays navigable.
|
|
54
|
+
*/
|
|
55
|
+
export declare function registerHookCommand(program: Command): void;
|