@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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report output for the push-gate.
|
|
3
|
+
*
|
|
4
|
+
* Two channels:
|
|
5
|
+
*
|
|
6
|
+
* 1. `.rea/last-review.json` — machine-readable structured dump. Atomic
|
|
7
|
+
* write (write-to-tmp + rename), gitignored, overwritten every push.
|
|
8
|
+
* Claude reads this as the source of truth for file/line/body during
|
|
9
|
+
* the auto-fix loop.
|
|
10
|
+
*
|
|
11
|
+
* 2. stderr banner — human-legible severity-sorted summary capped to 20
|
|
12
|
+
* findings. The pre-push hook's stderr reaches Claude as the tool
|
|
13
|
+
* output of `Bash(git push)`, so this is the primary fast-path to
|
|
14
|
+
* surface verdict + first blocking finding.
|
|
15
|
+
*
|
|
16
|
+
* Redaction: before serializing anything to disk or stderr we run the
|
|
17
|
+
* shared `SECRET_PATTERNS` list over `title`, `body`, and `reviewText`. If
|
|
18
|
+
* Codex accidentally quoted a secret from the diff (common in password-
|
|
19
|
+
* reset flows, API-key migration PRs, env-file edits) it never hits disk
|
|
20
|
+
* in cleartext.
|
|
21
|
+
*/
|
|
22
|
+
import type { Finding, ReviewSummary, Verdict } from './findings.js';
|
|
23
|
+
export interface LastReviewPayload {
|
|
24
|
+
schema_version: 1;
|
|
25
|
+
/** ISO-8601 UTC timestamp of the review run (wall clock). */
|
|
26
|
+
generated_at: string;
|
|
27
|
+
verdict: Verdict;
|
|
28
|
+
base_ref: string;
|
|
29
|
+
head_sha: string;
|
|
30
|
+
finding_count: number;
|
|
31
|
+
findings: Finding[];
|
|
32
|
+
/** Full agent text (post-redact). Useful for debugging parser misses. */
|
|
33
|
+
review_text: string;
|
|
34
|
+
/** Number of raw JSONL events Codex emitted. */
|
|
35
|
+
event_count: number;
|
|
36
|
+
/** Wall clock seconds in the Codex subprocess. */
|
|
37
|
+
duration_seconds: number;
|
|
38
|
+
}
|
|
39
|
+
export interface WriteLastReviewInput {
|
|
40
|
+
baseDir: string;
|
|
41
|
+
summary: ReviewSummary;
|
|
42
|
+
baseRef: string;
|
|
43
|
+
headSha: string;
|
|
44
|
+
eventCount: number;
|
|
45
|
+
durationSeconds: number;
|
|
46
|
+
/** Test seam — defaults to `new Date()`. */
|
|
47
|
+
now?: Date;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Atomic write of `.rea/last-review.json`. Returns the redacted payload
|
|
51
|
+
* actually written so the caller can reuse it for stderr rendering
|
|
52
|
+
* without re-redacting.
|
|
53
|
+
*
|
|
54
|
+
* We write to `last-review.json.tmp.<pid>-<rand>` first, fsync the file
|
|
55
|
+
* descriptor, then rename. rename(2) is atomic within the same
|
|
56
|
+
* filesystem, so partial writes never surface to readers.
|
|
57
|
+
*/
|
|
58
|
+
export declare function writeLastReview(input: WriteLastReviewInput): LastReviewPayload;
|
|
59
|
+
export interface RenderBannerInput {
|
|
60
|
+
payload: LastReviewPayload;
|
|
61
|
+
/** Where was the base ref sourced from (audit / debugging). */
|
|
62
|
+
baseSource: string;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the verdict-level action is BLOCKED or SOFT. Surfaced in the
|
|
65
|
+
* banner first line. Callers infer this from verdict + concerns_blocks.
|
|
66
|
+
*/
|
|
67
|
+
blocked: boolean;
|
|
68
|
+
/** Last-review.json on-disk path — shown as a pointer. */
|
|
69
|
+
lastReviewPath: string;
|
|
70
|
+
/** Max findings to enumerate in the banner. Default 20. */
|
|
71
|
+
maxFindings?: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build the stderr banner as a single multi-line string. Ends with `\n`.
|
|
75
|
+
*
|
|
76
|
+
* Layout:
|
|
77
|
+
*
|
|
78
|
+
* ┌────────────────────────────────────────────┐
|
|
79
|
+
* │ push-gate VERDICT — BLOCKED / PROCEEDING │
|
|
80
|
+
* │ base: <ref> (<source>) │
|
|
81
|
+
* │ head: <sha> │
|
|
82
|
+
* │ findings: <count> │
|
|
83
|
+
* └────────────────────────────────────────────┘
|
|
84
|
+
* - [P1] Title — file:42
|
|
85
|
+
* body-excerpt
|
|
86
|
+
* ...
|
|
87
|
+
* see .rea/last-review.json for full details
|
|
88
|
+
*/
|
|
89
|
+
export declare function renderBanner(input: RenderBannerInput): string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report output for the push-gate.
|
|
3
|
+
*
|
|
4
|
+
* Two channels:
|
|
5
|
+
*
|
|
6
|
+
* 1. `.rea/last-review.json` — machine-readable structured dump. Atomic
|
|
7
|
+
* write (write-to-tmp + rename), gitignored, overwritten every push.
|
|
8
|
+
* Claude reads this as the source of truth for file/line/body during
|
|
9
|
+
* the auto-fix loop.
|
|
10
|
+
*
|
|
11
|
+
* 2. stderr banner — human-legible severity-sorted summary capped to 20
|
|
12
|
+
* findings. The pre-push hook's stderr reaches Claude as the tool
|
|
13
|
+
* output of `Bash(git push)`, so this is the primary fast-path to
|
|
14
|
+
* surface verdict + first blocking finding.
|
|
15
|
+
*
|
|
16
|
+
* Redaction: before serializing anything to disk or stderr we run the
|
|
17
|
+
* shared `SECRET_PATTERNS` list over `title`, `body`, and `reviewText`. If
|
|
18
|
+
* Codex accidentally quoted a secret from the diff (common in password-
|
|
19
|
+
* reset flows, API-key migration PRs, env-file edits) it never hits disk
|
|
20
|
+
* in cleartext.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { randomBytes } from 'node:crypto';
|
|
25
|
+
import { compileDefaultSecretPatterns, redactSecrets, } from '../../gateway/middleware/redact.js';
|
|
26
|
+
const LAST_REVIEW_FILENAME = 'last-review.json';
|
|
27
|
+
/**
|
|
28
|
+
* Atomic write of `.rea/last-review.json`. Returns the redacted payload
|
|
29
|
+
* actually written so the caller can reuse it for stderr rendering
|
|
30
|
+
* without re-redacting.
|
|
31
|
+
*
|
|
32
|
+
* We write to `last-review.json.tmp.<pid>-<rand>` first, fsync the file
|
|
33
|
+
* descriptor, then rename. rename(2) is atomic within the same
|
|
34
|
+
* filesystem, so partial writes never surface to readers.
|
|
35
|
+
*/
|
|
36
|
+
export function writeLastReview(input) {
|
|
37
|
+
const { baseDir, summary, baseRef, headSha, eventCount, durationSeconds } = input;
|
|
38
|
+
const now = input.now ?? new Date();
|
|
39
|
+
const patterns = compileDefaultSecretPatterns({ source: 'default' });
|
|
40
|
+
const payload = {
|
|
41
|
+
schema_version: 1,
|
|
42
|
+
generated_at: now.toISOString(),
|
|
43
|
+
verdict: summary.verdict,
|
|
44
|
+
base_ref: baseRef,
|
|
45
|
+
head_sha: headSha,
|
|
46
|
+
finding_count: summary.findings.length,
|
|
47
|
+
findings: summary.findings.map((f) => redactFinding(f, patterns)),
|
|
48
|
+
review_text: redactString(summary.reviewText, patterns),
|
|
49
|
+
event_count: eventCount,
|
|
50
|
+
duration_seconds: Number.isFinite(durationSeconds) ? durationSeconds : 0,
|
|
51
|
+
};
|
|
52
|
+
const reaDir = path.join(baseDir, '.rea');
|
|
53
|
+
ensureDir(reaDir);
|
|
54
|
+
const finalPath = path.join(reaDir, LAST_REVIEW_FILENAME);
|
|
55
|
+
const tmpPath = `${finalPath}.tmp.${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
56
|
+
const fd = fs.openSync(tmpPath, 'w', 0o600);
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(fd, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8' });
|
|
59
|
+
fs.fsyncSync(fd);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
fs.closeSync(fd);
|
|
63
|
+
}
|
|
64
|
+
fs.renameSync(tmpPath, finalPath);
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
function redactFinding(f, patterns) {
|
|
68
|
+
return {
|
|
69
|
+
severity: f.severity,
|
|
70
|
+
title: redactString(f.title, patterns),
|
|
71
|
+
body: redactString(f.body, patterns),
|
|
72
|
+
...(f.file !== undefined ? { file: f.file } : {}),
|
|
73
|
+
...(f.line !== undefined ? { line: f.line } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function redactString(s, patterns) {
|
|
77
|
+
const { output } = redactSecrets(s, patterns);
|
|
78
|
+
return output;
|
|
79
|
+
}
|
|
80
|
+
function ensureDir(dir) {
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
// mkdir -p is idempotent; EEXIST is fine, anything else surfaces to
|
|
86
|
+
// the caller and becomes an exit-2 error.
|
|
87
|
+
const code = e.code;
|
|
88
|
+
if (code !== 'EEXIST')
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const SEVERITY_ORDER = { P1: 0, P2: 1, P3: 2 };
|
|
93
|
+
/**
|
|
94
|
+
* Build the stderr banner as a single multi-line string. Ends with `\n`.
|
|
95
|
+
*
|
|
96
|
+
* Layout:
|
|
97
|
+
*
|
|
98
|
+
* ┌────────────────────────────────────────────┐
|
|
99
|
+
* │ push-gate VERDICT — BLOCKED / PROCEEDING │
|
|
100
|
+
* │ base: <ref> (<source>) │
|
|
101
|
+
* │ head: <sha> │
|
|
102
|
+
* │ findings: <count> │
|
|
103
|
+
* └────────────────────────────────────────────┘
|
|
104
|
+
* - [P1] Title — file:42
|
|
105
|
+
* body-excerpt
|
|
106
|
+
* ...
|
|
107
|
+
* see .rea/last-review.json for full details
|
|
108
|
+
*/
|
|
109
|
+
export function renderBanner(input) {
|
|
110
|
+
const { payload, baseSource, blocked, lastReviewPath } = input;
|
|
111
|
+
const max = input.maxFindings ?? 20;
|
|
112
|
+
const verdictLabel = blocked ? 'BLOCKED' : 'PROCEEDING';
|
|
113
|
+
const lines = [];
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
116
|
+
lines.push(`rea push-gate: ${payload.verdict.toUpperCase()} — ${verdictLabel}`);
|
|
117
|
+
lines.push(`base: ${payload.base_ref} (${baseSource})`);
|
|
118
|
+
lines.push(`head: ${payload.head_sha}`);
|
|
119
|
+
lines.push(`findings: ${payload.finding_count}`);
|
|
120
|
+
lines.push(`elapsed: ${payload.duration_seconds.toFixed(1)}s`);
|
|
121
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
122
|
+
if (payload.findings.length === 0) {
|
|
123
|
+
lines.push('(no findings)');
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const sorted = [...payload.findings].sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
127
|
+
const shown = sorted.slice(0, max);
|
|
128
|
+
for (const f of shown) {
|
|
129
|
+
const loc = f.file !== undefined ? ` — ${f.file}${f.line !== undefined ? `:${f.line}` : ''}` : '';
|
|
130
|
+
lines.push(`- [${f.severity}] ${f.title}${loc}`);
|
|
131
|
+
}
|
|
132
|
+
if (sorted.length > shown.length) {
|
|
133
|
+
lines.push(`... ${sorted.length - shown.length} additional finding(s) suppressed (see JSON)`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(`machine-readable: ${lastReviewPath}`);
|
|
138
|
+
lines.push('');
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
package/dist/policy/loader.d.ts
CHANGED
|
@@ -32,16 +32,19 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
32
32
|
}>>;
|
|
33
33
|
review: z.ZodOptional<z.ZodObject<{
|
|
34
34
|
codex_required: z.ZodOptional<z.ZodBoolean>;
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
concerns_blocks: z.ZodOptional<z.ZodBoolean>;
|
|
36
|
+
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
last_n_commits: z.ZodOptional<z.ZodNumber>;
|
|
37
38
|
}, "strict", z.ZodTypeAny, {
|
|
38
39
|
codex_required?: boolean | undefined;
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
concerns_blocks?: boolean | undefined;
|
|
41
|
+
timeout_ms?: number | undefined;
|
|
42
|
+
last_n_commits?: number | undefined;
|
|
41
43
|
}, {
|
|
42
44
|
codex_required?: boolean | undefined;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
concerns_blocks?: boolean | undefined;
|
|
46
|
+
timeout_ms?: number | undefined;
|
|
47
|
+
last_n_commits?: number | undefined;
|
|
45
48
|
}>>;
|
|
46
49
|
redact: z.ZodOptional<z.ZodObject<{
|
|
47
50
|
match_timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
@@ -133,8 +136,9 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
133
136
|
} | undefined;
|
|
134
137
|
review?: {
|
|
135
138
|
codex_required?: boolean | undefined;
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
concerns_blocks?: boolean | undefined;
|
|
140
|
+
timeout_ms?: number | undefined;
|
|
141
|
+
last_n_commits?: number | undefined;
|
|
138
142
|
} | undefined;
|
|
139
143
|
redact?: {
|
|
140
144
|
match_timeout_ms?: number | undefined;
|
|
@@ -176,8 +180,9 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
176
180
|
} | undefined;
|
|
177
181
|
review?: {
|
|
178
182
|
codex_required?: boolean | undefined;
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
concerns_blocks?: boolean | undefined;
|
|
184
|
+
timeout_ms?: number | undefined;
|
|
185
|
+
last_n_commits?: number | undefined;
|
|
181
186
|
} | undefined;
|
|
182
187
|
redact?: {
|
|
183
188
|
match_timeout_ms?: number | undefined;
|
package/dist/policy/loader.js
CHANGED
|
@@ -16,16 +16,18 @@ const ContextProtectionSchema = z.object({
|
|
|
16
16
|
max_bash_output_lines: z.number().int().positive().optional(),
|
|
17
17
|
});
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* defaulting.
|
|
19
|
+
* 0.11.0 push-gate review policy. Three knobs only — the stateless gate does
|
|
20
|
+
* not have a cache and does not treat CI differently. Strict mode so typos
|
|
21
|
+
* (`codex_require`, `concerns_block`) fail loudly rather than silently
|
|
22
|
+
* defaulting. `rea upgrade` strips the removed 0.10.x fields
|
|
23
|
+
* (`cache_max_age_seconds`, `allow_skip_in_ci`) from consumer policy files.
|
|
23
24
|
*/
|
|
24
25
|
const ReviewPolicySchema = z
|
|
25
26
|
.object({
|
|
26
27
|
codex_required: z.boolean().optional(),
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
concerns_blocks: z.boolean().optional(),
|
|
29
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
30
|
+
last_n_commits: z.number().int().positive().optional(),
|
|
29
31
|
})
|
|
30
32
|
.strict();
|
|
31
33
|
/**
|
package/dist/policy/types.d.ts
CHANGED
|
@@ -19,38 +19,89 @@ export interface ContextProtection {
|
|
|
19
19
|
max_bash_output_lines?: number;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Review policy knobs
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Review policy knobs for the 0.11.0 stateless push-gate.
|
|
23
|
+
*
|
|
24
|
+
* The gate runs `codex exec review --json` on every push and infers a verdict
|
|
25
|
+
* from the streamed findings (see `src/hooks/push-gate/findings.ts`). No
|
|
26
|
+
* cache, no audit-receipt consultation, no SHA-keyed attestation. These
|
|
27
|
+
* knobs shape only the immediate run.
|
|
28
|
+
*
|
|
29
|
+
* The 0.10.x knobs `cache_max_age_seconds` and `allow_skip_in_ci` were
|
|
30
|
+
* removed in 0.11.0. `rea upgrade` strips them from consumer policy files.
|
|
25
31
|
*/
|
|
26
32
|
export interface ReviewPolicy {
|
|
27
33
|
/**
|
|
28
|
-
* When `
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
34
|
+
* When `true` or unset, `git push` runs `codex exec review` before the
|
|
35
|
+
* push is allowed to proceed. When `false`, the push-gate short-circuits
|
|
36
|
+
* to `disabled` (exit 0, audit event still recorded). No middle state —
|
|
37
|
+
* either we run Codex or we don't.
|
|
38
|
+
*
|
|
39
|
+
* Profile default: `true` in `bst-internal`, `client-engagement`,
|
|
40
|
+
* `lit-wc`, `open-source`. `false` in `*-no-codex` variants.
|
|
32
41
|
*/
|
|
33
42
|
codex_required?: boolean;
|
|
34
43
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
44
|
+
* Whether a `concerns` verdict blocks the push. `true` (default) means any
|
|
45
|
+
* non-trivial Codex finding halts the push; the agent must address the
|
|
46
|
+
* concerns (or re-run with `REA_ALLOW_CONCERNS=1` for a one-push override)
|
|
47
|
+
* before retrying. `false` means only `blocking` verdicts halt — concerns
|
|
48
|
+
* are logged and written to `.rea/last-review.json` but the push proceeds.
|
|
49
|
+
*
|
|
50
|
+
* Added in 0.11.0. Default when unset is `true` — safer posture for
|
|
51
|
+
* consumers who have not thought about it.
|
|
52
|
+
*/
|
|
53
|
+
concerns_blocks?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Hard cap on the `codex exec review` subprocess in milliseconds. Exceeding
|
|
56
|
+
* this kills the subprocess and the gate returns exit 2 with a timeout
|
|
57
|
+
* error (audited). Default when unset is 1_800_000 (30 minutes) as of
|
|
58
|
+
* 0.12.0 — raised from 10 minutes after the helixir migration session
|
|
59
|
+
* 2026-04-26 showed realistic feature-branch diffs routinely exceeded
|
|
60
|
+
* the previous default. Operators with explicit `timeout_ms:` in
|
|
61
|
+
* `.rea/policy.yaml` are unaffected.
|
|
62
|
+
*
|
|
63
|
+
* Positive integer only. The loader rejects zero/negative values.
|
|
40
64
|
*/
|
|
41
|
-
|
|
65
|
+
timeout_ms?: number;
|
|
42
66
|
/**
|
|
43
|
-
*
|
|
44
|
-
* the
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
67
|
+
* When set, `rea hook push-gate` resolves the diff base to `HEAD~N`
|
|
68
|
+
* instead of the upstream → origin/HEAD ladder. Useful when a feature
|
|
69
|
+
* branch accumulates many commits and the full origin/main diff
|
|
70
|
+
* overwhelms the reviewer (the helixir 2026-04-26 case: 50+ commits
|
|
71
|
+
* relative to origin/main produced non-deterministic Codex verdicts and
|
|
72
|
+
* 10-minute timeouts).
|
|
73
|
+
*
|
|
74
|
+
* Precedence: explicit `--base <ref>` flag wins; then `--last-n-commits N`
|
|
75
|
+
* flag; then this policy key; then refspec-aware base resolution; then
|
|
76
|
+
* the upstream-ladder fallback. When `--base` AND
|
|
77
|
+
* `--last-n-commits`/`policy.last_n_commits` are both set, `--base`
|
|
78
|
+
* wins and a stderr warning is emitted.
|
|
79
|
+
*
|
|
80
|
+
* Resolution: `git rev-parse HEAD~N`. When `HEAD~N` is unreachable
|
|
81
|
+
* the resolver consults `git rev-parse --is-shallow-repository` to
|
|
82
|
+
* pick the right clamp:
|
|
83
|
+
*
|
|
84
|
+
* - FULL clone, branch shorter than N: clamps to the empty-tree
|
|
85
|
+
* sentinel so the root commit's changes are included
|
|
86
|
+
* (`git diff base..HEAD` excludes `base`, so diffing against
|
|
87
|
+
* `HEAD~K` would silently drop the root commit). Reports
|
|
88
|
+
* `last_n_commits: K+1` — every commit on the branch reviewed.
|
|
89
|
+
*
|
|
90
|
+
* - SHALLOW clone: clamps to `HEAD~K` (the deepest LOCALLY
|
|
91
|
+
* resolvable ancestor) since older history exists on the remote
|
|
92
|
+
* but isn't fetched. Using empty-tree here would balloon the
|
|
93
|
+
* review to every tracked file in the checkout. Reports
|
|
94
|
+
* `last_n_commits: K`. The K-th commit's content is excluded —
|
|
95
|
+
* accepted as the cost of the shallow clone.
|
|
96
|
+
*
|
|
97
|
+
* A stderr warning surfaces the requested-vs-clamped numbers in
|
|
98
|
+
* both cases. Audit metadata records `base_source: 'last-n-commits'`,
|
|
99
|
+
* `last_n_commits: <count actually reviewed>`, and
|
|
100
|
+
* `last_n_commits_requested: N` (only present when clamped).
|
|
50
101
|
*
|
|
51
|
-
*
|
|
102
|
+
* Positive integer. The loader rejects zero/negative values.
|
|
52
103
|
*/
|
|
53
|
-
|
|
104
|
+
last_n_commits?: number;
|
|
54
105
|
}
|
|
55
106
|
/**
|
|
56
107
|
* User-supplied redaction pattern entry. Each pattern has a stable `name` used
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|
package/scripts/tarball-smoke.sh
CHANGED
|
@@ -74,8 +74,13 @@ echo "[smoke] → $VERSION_OUT"
|
|
|
74
74
|
echo "[smoke] rea --help"
|
|
75
75
|
./node_modules/.bin/rea --help >/dev/null
|
|
76
76
|
|
|
77
|
-
echo "[smoke] rea init --yes --profile open-source"
|
|
78
|
-
|
|
77
|
+
echo "[smoke] rea init --yes --profile open-source-no-codex"
|
|
78
|
+
# 0.12.0+: doctor hard-fails when policy.review.codex_required: true and codex
|
|
79
|
+
# is not on PATH (fix C of the helixir migration unblocker — see PR #85). CI
|
|
80
|
+
# does not provision the codex CLI, so the smoke uses the -no-codex profile
|
|
81
|
+
# variant which defaults codex_required: false. The new doctor probe is
|
|
82
|
+
# covered by unit tests in src/cli/doctor.test.ts.
|
|
83
|
+
./node_modules/.bin/rea init --yes --profile open-source-no-codex
|
|
79
84
|
|
|
80
85
|
# Verify the installed layout matches what init claims it wrote.
|
|
81
86
|
for expected in .rea/policy.yaml .rea/registry.yaml .claude/settings.json CLAUDE.md .rea/install-manifest.json; do
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Review cache (BUG-009). The push-review-gate hook (`hooks/push-review-gate.sh`)
|
|
3
|
-
* has shipped since 0.3.x calling `rea cache check <sha>` to skip re-review on
|
|
4
|
-
* a previously-approved diff, and `rea cache set <sha> pass ...` as the
|
|
5
|
-
* operator's advertised way to complete the gate. Neither subcommand existed
|
|
6
|
-
* in the CLI through 0.4.0. Once BUG-008's pre-push stdin adapter lands and
|
|
7
|
-
* the gate actually fires, a protected-path push has no completion path
|
|
8
|
-
* without this cache — hence "paired ship blocker."
|
|
9
|
-
*
|
|
10
|
-
* ## File layout
|
|
11
|
-
*
|
|
12
|
-
* `.rea/review-cache.jsonl` — one JSON object per line, terminated with `\n`.
|
|
13
|
-
* Each entry:
|
|
14
|
-
*
|
|
15
|
-
* {
|
|
16
|
-
* "sha": "<diff-sha256>",
|
|
17
|
-
* "branch": "<feature-branch>",
|
|
18
|
-
* "base": "<target-branch>",
|
|
19
|
-
* "result": "pass" | "fail",
|
|
20
|
-
* "recorded_at": "<ISO-8601>",
|
|
21
|
-
* "reason"?: "<free text>" // optional, populated on fail or on skip
|
|
22
|
-
* }
|
|
23
|
-
*
|
|
24
|
-
* The `sha` is whatever the caller supplies — the hook happens to use a
|
|
25
|
-
* SHA-256 of the full diff, but the cache does not interpret or validate the
|
|
26
|
-
* value. Hash-chained is intentionally NOT required: this is a keyed cache,
|
|
27
|
-
* not an append-only integrity log. The audit log at `.rea/audit.jsonl`
|
|
28
|
-
* remains the integrity story.
|
|
29
|
-
*
|
|
30
|
-
* ## Concurrency
|
|
31
|
-
*
|
|
32
|
-
* Every write takes the same `proper-lockfile` lock on the `.rea/` parent
|
|
33
|
-
* directory that the audit helpers use (`withAuditLock`). This means a
|
|
34
|
-
* concurrent audit append and cache write serialize against each other — a
|
|
35
|
-
* negligible cost given cache writes happen once per push gate completion.
|
|
36
|
-
*
|
|
37
|
-
* ## Idempotency
|
|
38
|
-
*
|
|
39
|
-
* `appendEntry` writes a new line unconditionally. `lookup` returns the most
|
|
40
|
-
* recent entry matching `(sha, branch, base)`. This "last write wins" keeps
|
|
41
|
-
* the write path O(1) and the read path O(n) over the file; n is bounded by
|
|
42
|
-
* typical review frequency (dozens per week, not millions). If a future
|
|
43
|
-
* operator needs a compact file, `rea cache clear <sha>` drops matching
|
|
44
|
-
* entries and a separate `rea cache compact` (not in 0.5.0) could rewrite.
|
|
45
|
-
*
|
|
46
|
-
* ## TTL
|
|
47
|
-
*
|
|
48
|
-
* `lookup` honors `review.cache_max_age_seconds` (default 3600). Entries
|
|
49
|
-
* older than the window are treated as a miss. Expired entries are not
|
|
50
|
-
* garbage-collected on read — `rea cache clear` or `rea cache compact`
|
|
51
|
-
* is the operator tool for shrinking.
|
|
52
|
-
*/
|
|
53
|
-
/** Default TTL when policy does not supply one. */
|
|
54
|
-
export declare const DEFAULT_CACHE_MAX_AGE_SECONDS = 3600;
|
|
55
|
-
export type CacheResult = 'pass' | 'fail';
|
|
56
|
-
export interface CacheEntry {
|
|
57
|
-
sha: string;
|
|
58
|
-
branch: string;
|
|
59
|
-
base: string;
|
|
60
|
-
result: CacheResult;
|
|
61
|
-
recorded_at: string;
|
|
62
|
-
reason?: string;
|
|
63
|
-
}
|
|
64
|
-
export interface CacheLookupInput {
|
|
65
|
-
sha: string;
|
|
66
|
-
branch: string;
|
|
67
|
-
base: string;
|
|
68
|
-
/** Epoch ms used as the "now" reference for TTL comparison. Defaults to `Date.now()`. */
|
|
69
|
-
nowMs?: number;
|
|
70
|
-
/** TTL in seconds; defaults to {@link DEFAULT_CACHE_MAX_AGE_SECONDS}. */
|
|
71
|
-
maxAgeSeconds?: number;
|
|
72
|
-
}
|
|
73
|
-
export interface CacheLookupResult {
|
|
74
|
-
hit: boolean;
|
|
75
|
-
entry?: CacheEntry;
|
|
76
|
-
/** Reason for a miss. One of `'no-entry' | 'expired' | 'empty-file'`. Always set when `hit === false`. */
|
|
77
|
-
missReason?: 'no-entry' | 'expired' | 'empty-file';
|
|
78
|
-
}
|
|
79
|
-
export interface CacheAppendInput {
|
|
80
|
-
sha: string;
|
|
81
|
-
branch: string;
|
|
82
|
-
base: string;
|
|
83
|
-
result: CacheResult;
|
|
84
|
-
reason?: string;
|
|
85
|
-
/** ISO-8601 timestamp. Defaults to `new Date().toISOString()`. */
|
|
86
|
-
timestamp?: string;
|
|
87
|
-
}
|
|
88
|
-
export declare function resolveCacheFile(baseDir: string): string;
|
|
89
|
-
/**
|
|
90
|
-
* Append an entry to the cache. Writes are serialized through the shared
|
|
91
|
-
* `.rea/` directory lock so audit writes and cache writes do not interleave.
|
|
92
|
-
*/
|
|
93
|
-
export declare function appendEntry(baseDir: string, input: CacheAppendInput): Promise<CacheEntry>;
|
|
94
|
-
/**
|
|
95
|
-
* Find the most-recent entry matching `(sha, branch, base)` within the TTL
|
|
96
|
-
* window. Idempotent and side-effect free.
|
|
97
|
-
*/
|
|
98
|
-
export declare function lookup(baseDir: string, input: CacheLookupInput): Promise<CacheLookupResult>;
|
|
99
|
-
/**
|
|
100
|
-
* Remove every entry matching `sha`. Returns the count removed. A `0` return
|
|
101
|
-
* is a valid outcome (sha not present). Writes back via the same lock as
|
|
102
|
-
* `appendEntry`, so concurrent sets do not lose entries.
|
|
103
|
-
*
|
|
104
|
-
* Writes use temp-file + `fs.rename` (atomic within a single directory on
|
|
105
|
-
* POSIX) so unlocked readers (`lookup`, `list`) can never observe a torn or
|
|
106
|
-
* empty intermediate state. Codex F4 on the 0.5.0 PR1 review.
|
|
107
|
-
*/
|
|
108
|
-
export declare function clear(baseDir: string, sha: string): Promise<number>;
|
|
109
|
-
/**
|
|
110
|
-
* Return every entry, optionally filtered by branch. Entries are returned in
|
|
111
|
-
* file order (oldest first). Callers that want "newest first" should reverse.
|
|
112
|
-
*/
|
|
113
|
-
export declare function list(baseDir: string, options?: {
|
|
114
|
-
branch?: string;
|
|
115
|
-
}): Promise<CacheEntry[]>;
|