@bookedsolid/rea 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-push +22 -167
- package/agents/codex-adversarial.md +5 -3
- package/commands/codex-review.md +3 -5
- package/dist/audit/append.d.ts +7 -32
- package/dist/audit/append.js +7 -35
- package/dist/cli/audit.d.ts +0 -31
- package/dist/cli/audit.js +5 -74
- package/dist/cli/doctor.js +6 -16
- package/dist/cli/hook.d.ts +48 -0
- package/dist/cli/hook.js +127 -0
- package/dist/cli/index.js +5 -80
- package/dist/cli/init.js +1 -1
- package/dist/cli/install/gitignore.d.ts +2 -2
- package/dist/cli/install/gitignore.js +3 -3
- package/dist/cli/install/pre-push.d.ts +146 -271
- package/dist/cli/install/pre-push.js +471 -2633
- package/dist/cli/install/settings-merge.d.ts +17 -0
- package/dist/cli/install/settings-merge.js +48 -1
- package/dist/cli/upgrade.js +131 -3
- package/dist/config/tier-map.js +18 -25
- package/dist/hooks/push-gate/base.d.ts +57 -0
- package/dist/hooks/push-gate/base.js +77 -0
- package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
- package/dist/hooks/push-gate/codex-runner.js +223 -0
- package/dist/hooks/push-gate/findings.d.ts +68 -0
- package/dist/hooks/push-gate/findings.js +142 -0
- package/dist/hooks/push-gate/halt.d.ts +28 -0
- package/dist/hooks/push-gate/halt.js +49 -0
- package/dist/hooks/push-gate/index.d.ts +90 -0
- package/dist/hooks/push-gate/index.js +351 -0
- package/dist/hooks/push-gate/policy.d.ts +41 -0
- package/dist/hooks/push-gate/policy.js +55 -0
- package/dist/hooks/push-gate/report.d.ts +89 -0
- package/dist/hooks/push-gate/report.js +140 -0
- package/dist/policy/loader.d.ts +10 -10
- package/dist/policy/loader.js +7 -6
- package/dist/policy/types.d.ts +31 -22
- package/package.json +1 -1
- package/dist/cache/review-cache.d.ts +0 -115
- package/dist/cache/review-cache.js +0 -200
- package/dist/cli/cache.d.ts +0 -84
- package/dist/cli/cache.js +0 -150
- package/dist/hooks/review-gate/args.d.ts +0 -126
- package/dist/hooks/review-gate/args.js +0 -315
- package/dist/hooks/review-gate/banner.d.ts +0 -97
- package/dist/hooks/review-gate/banner.js +0 -172
- package/dist/hooks/review-gate/cache-key.d.ts +0 -55
- package/dist/hooks/review-gate/cache-key.js +0 -41
- package/dist/hooks/review-gate/constants.d.ts +0 -26
- package/dist/hooks/review-gate/constants.js +0 -34
- package/dist/hooks/review-gate/errors.d.ts +0 -72
- package/dist/hooks/review-gate/errors.js +0 -100
- package/dist/hooks/review-gate/hash.d.ts +0 -43
- package/dist/hooks/review-gate/hash.js +0 -46
- package/dist/hooks/review-gate/index.d.ts +0 -21
- package/dist/hooks/review-gate/index.js +0 -21
- package/dist/hooks/review-gate/metadata.d.ts +0 -98
- package/dist/hooks/review-gate/metadata.js +0 -158
- package/dist/hooks/review-gate/policy.d.ts +0 -55
- package/dist/hooks/review-gate/policy.js +0 -71
- package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
- package/dist/hooks/review-gate/protected-paths.js +0 -76
- package/hooks/_lib/push-review-core.sh +0 -1250
- package/hooks/commit-review-gate.sh +0 -330
- package/hooks/push-review-gate-git.sh +0 -94
- package/hooks/push-review-gate.sh +0 -92
package/dist/cli/hook.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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 { parsePrePushStdin, runPushGate } from '../hooks/push-gate/index.js';
|
|
32
|
+
import { err } from './utils.js';
|
|
33
|
+
/**
|
|
34
|
+
* Public runner, exposed so integration tests and the commander binding can
|
|
35
|
+
* share the same entry. Throws via `process.exit` rather than returning a
|
|
36
|
+
* code — the commander handler is async but the convention across `src/cli/`
|
|
37
|
+
* is to exit from the leaf (see `audit.ts`, `freeze.ts`). Keeping the
|
|
38
|
+
* behavior consistent prevents commander from inferring its own default.
|
|
39
|
+
*/
|
|
40
|
+
export async function runHookPushGate(options) {
|
|
41
|
+
const baseDir = process.cwd();
|
|
42
|
+
const stderr = (line) => {
|
|
43
|
+
process.stderr.write(line);
|
|
44
|
+
};
|
|
45
|
+
// Git's pre-push contract sends one refspec per line on stdin. Read it
|
|
46
|
+
// all upfront with a timeout guard so a misconfigured invocation
|
|
47
|
+
// (stdin pipe never closed) doesn't hang the gate indefinitely. TTY
|
|
48
|
+
// stdin short-circuits to empty — `rea hook push-gate` invoked from
|
|
49
|
+
// a terminal has no refspec data.
|
|
50
|
+
const refspecs = process.stdin.isTTY ? [] : parsePrePushStdin(await readStdinWithTimeout(5_000));
|
|
51
|
+
try {
|
|
52
|
+
const result = await runPushGate({
|
|
53
|
+
baseDir,
|
|
54
|
+
env: process.env,
|
|
55
|
+
stderr,
|
|
56
|
+
refspecs,
|
|
57
|
+
...(options.base !== undefined && options.base.length > 0 ? { explicitBase: options.base } : {}),
|
|
58
|
+
});
|
|
59
|
+
process.exit(result.exitCode);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
// runPushGate() is written to catch and classify every expected error.
|
|
63
|
+
// Reaching this handler means an unclassified throw — we fail closed
|
|
64
|
+
// with exit 2 so a genuine bug never masquerades as a passing review.
|
|
65
|
+
err(`push-gate internal error: ${e instanceof Error ? e.message : String(e)}`);
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read stdin to end with a timeout. Returns '' on timeout — the caller
|
|
71
|
+
* then falls through to the upstream-resolver path instead of blocking
|
|
72
|
+
* the gate on a pipe that may never close.
|
|
73
|
+
*
|
|
74
|
+
* Git ALWAYS closes stdin after sending refspecs, so the timeout is a
|
|
75
|
+
* safety net for weird invocations (running the CLI from a script that
|
|
76
|
+
* piped in nothing, a test that forgot to close the write end, etc.).
|
|
77
|
+
*/
|
|
78
|
+
async function readStdinWithTimeout(timeoutMs) {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const chunks = [];
|
|
81
|
+
let resolved = false;
|
|
82
|
+
const finish = () => {
|
|
83
|
+
if (resolved)
|
|
84
|
+
return;
|
|
85
|
+
resolved = true;
|
|
86
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
87
|
+
};
|
|
88
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
89
|
+
timer.unref?.();
|
|
90
|
+
process.stdin.on('data', (chunk) => {
|
|
91
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk);
|
|
92
|
+
});
|
|
93
|
+
process.stdin.on('end', () => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
finish();
|
|
96
|
+
});
|
|
97
|
+
process.stdin.on('error', () => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
finish();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Attach the `rea hook` subcommand tree to a commander Program. Single
|
|
105
|
+
* subcommand today (`push-gate`); new hooks should land here rather than as
|
|
106
|
+
* top-level commands so the CLI surface stays navigable.
|
|
107
|
+
*/
|
|
108
|
+
export function registerHookCommand(program) {
|
|
109
|
+
const hook = program
|
|
110
|
+
.command('hook')
|
|
111
|
+
.description('Pre-hook entry points for git (pre-push) and Claude Code. Called by `.husky/pre-push` and the optional `.git/hooks/pre-push` fallback.');
|
|
112
|
+
hook
|
|
113
|
+
.command('push-gate')
|
|
114
|
+
// Accept (and silently ignore) positional args. Git passes the
|
|
115
|
+
// pre-push hook `<remote-name> <remote-url>` as $@; the husky stub
|
|
116
|
+
// forwards them with `"$@"`. Those values aren't used by the gate
|
|
117
|
+
// directly (base ref + refspecs come from stdin + git tree probes),
|
|
118
|
+
// but commander without this option would reject the invocation.
|
|
119
|
+
// Declared as a variadic positional so an arbitrary number of
|
|
120
|
+
// trailing tokens are accepted.
|
|
121
|
+
.argument('[gitArgs...]', 'positional args forwarded by git (remote name, URL); ignored')
|
|
122
|
+
.description('Run `codex exec review` against the current diff and block on blocking findings. Exits 0/1/2: pass/HALT/blocked. No cache — every push runs Codex afresh.')
|
|
123
|
+
.option('--base <ref>', 'explicit base ref to diff against (e.g. origin/main). Defaults to @{upstream} → origin/HEAD → main/master → empty-tree.')
|
|
124
|
+
.action(async (_gitArgs, opts) => {
|
|
125
|
+
await runHookPushGate({ ...(opts.base !== undefined ? { base: opts.base } : {}) });
|
|
126
|
+
});
|
|
127
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import {
|
|
4
|
-
import { parseCacheResult, runCacheCheck, runCacheClear, runCacheList, runCacheSet, } from './cache.js';
|
|
3
|
+
import { runAuditRotate, runAuditVerify } from './audit.js';
|
|
5
4
|
import { runCheck } from './check.js';
|
|
5
|
+
import { registerHookCommand } from './hook.js';
|
|
6
6
|
import { runDoctor } from './doctor.js';
|
|
7
7
|
import { runFreeze, runUnfreeze } from './freeze.js';
|
|
8
8
|
import { runInit } from './init.js';
|
|
@@ -103,84 +103,9 @@ async function main() {
|
|
|
103
103
|
.action(async (opts) => {
|
|
104
104
|
await runAuditVerify({ ...(opts.since !== undefined ? { since: opts.since } : {}) });
|
|
105
105
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
auditRecord
|
|
110
|
-
.command('codex-review')
|
|
111
|
-
.description('Append a codex.review audit entry the push-review cache gate recognizes. With --also-set-cache, writes the review cache in the same invocation (two sequential appends in one process — not a two-phase commit).')
|
|
112
|
-
.requiredOption('--head-sha <sha>', 'git HEAD SHA the review covers')
|
|
113
|
-
.requiredOption('--branch <branch>', 'feature branch under review')
|
|
114
|
-
.requiredOption('--target <target>', 'base ref or SHA diffed against (e.g. main)')
|
|
115
|
-
.requiredOption('--verdict <verdict>', 'one of: pass | concerns | blocking | error')
|
|
116
|
-
.requiredOption('--finding-count <N>', 'non-negative integer finding count', (raw) => {
|
|
117
|
-
const n = Number.parseInt(raw, 10);
|
|
118
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
119
|
-
throw new Error(`--finding-count must be a non-negative integer; got ${JSON.stringify(raw)}`);
|
|
120
|
-
}
|
|
121
|
-
return n;
|
|
122
|
-
})
|
|
123
|
-
.option('--summary <text>', 'one-sentence review summary (optional)')
|
|
124
|
-
.option('--session-id <id>', 'session id to attribute (defaults to "external")')
|
|
125
|
-
.option('--also-set-cache', 'also update .rea/review-cache.jsonl to reflect this verdict, in the same invocation (recommended for post-review push flow)')
|
|
126
|
-
.action(async (opts) => {
|
|
127
|
-
if (opts.verdict !== 'pass' &&
|
|
128
|
-
opts.verdict !== 'concerns' &&
|
|
129
|
-
opts.verdict !== 'blocking' &&
|
|
130
|
-
opts.verdict !== 'error') {
|
|
131
|
-
throw new Error(`--verdict must be one of pass|concerns|blocking|error; got ${JSON.stringify(opts.verdict)}`);
|
|
132
|
-
}
|
|
133
|
-
await runAuditRecordCodexReview({
|
|
134
|
-
headSha: opts.headSha,
|
|
135
|
-
branch: opts.branch,
|
|
136
|
-
target: opts.target,
|
|
137
|
-
verdict: opts.verdict,
|
|
138
|
-
findingCount: opts.findingCount,
|
|
139
|
-
...(opts.summary !== undefined ? { summary: opts.summary } : {}),
|
|
140
|
-
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
141
|
-
...(opts.alsoSetCache === true ? { alsoSetCache: true } : {}),
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
const cache = program
|
|
145
|
-
.command('cache')
|
|
146
|
-
.description('Review-cache operations — check/set/clear/list .rea/review-cache.jsonl (BUG-009). Used by hooks/push-review-gate.sh to skip re-review on a previously-approved diff.');
|
|
147
|
-
cache
|
|
148
|
-
.command('check <sha>')
|
|
149
|
-
.description('Look up a cache entry. Emits JSON to stdout ONLY — hook contract. On hit: {hit,true,result,branch,base,recorded_at[,reason]}. On miss: {hit:false}. Never exits non-zero for normal miss.')
|
|
150
|
-
.requiredOption('--branch <branch>', 'feature branch being pushed')
|
|
151
|
-
.requiredOption('--base <base>', 'base branch the feature targets')
|
|
152
|
-
.action(async (sha, opts) => {
|
|
153
|
-
await runCacheCheck({ sha, branch: opts.branch, base: opts.base });
|
|
154
|
-
});
|
|
155
|
-
cache
|
|
156
|
-
.command('set <sha> <result>')
|
|
157
|
-
.description('Record a review outcome. <result> accepts pass|fail (historical) or pass|concerns|blocking|error (Codex verdicts). concerns→pass, blocking|error→fail. Idempotent line-per-invocation; last write wins on (sha, branch, base).')
|
|
158
|
-
.requiredOption('--branch <branch>', 'feature branch being pushed')
|
|
159
|
-
.requiredOption('--base <base>', 'base branch the feature targets')
|
|
160
|
-
.option('--reason <text>', 'free-text context for this entry (recommended on fail)')
|
|
161
|
-
.action(async (sha, rawResult, opts) => {
|
|
162
|
-
const result = parseCacheResult(rawResult);
|
|
163
|
-
await runCacheSet({
|
|
164
|
-
sha,
|
|
165
|
-
result,
|
|
166
|
-
branch: opts.branch,
|
|
167
|
-
base: opts.base,
|
|
168
|
-
...(opts.reason !== undefined ? { reason: opts.reason } : {}),
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
cache
|
|
172
|
-
.command('clear <sha>')
|
|
173
|
-
.description('Remove every cache entry matching <sha>. Dev convenience — prints the removed count.')
|
|
174
|
-
.action(async (sha) => {
|
|
175
|
-
await runCacheClear({ sha });
|
|
176
|
-
});
|
|
177
|
-
cache
|
|
178
|
-
.command('list')
|
|
179
|
-
.description('Print cache entries in file order. Filter with --branch.')
|
|
180
|
-
.option('--branch <branch>', 'only list entries for this branch')
|
|
181
|
-
.action(async (opts) => {
|
|
182
|
-
await runCacheList({ ...(opts.branch !== undefined ? { branch: opts.branch } : {}) });
|
|
183
|
-
});
|
|
106
|
+
// Register `rea hook push-gate` — the stateless pre-push Codex gate
|
|
107
|
+
// called by `.husky/pre-push` and `.git/hooks/pre-push`.
|
|
108
|
+
registerHookCommand(program);
|
|
184
109
|
const tofu = program
|
|
185
110
|
.command('tofu')
|
|
186
111
|
.description('TOFU fingerprint operations (G7) — inspect and rebase `.rea/fingerprints.json` when a legitimate registry edit has triggered drift fail-close. Emits audit records.');
|
package/dist/cli/init.js
CHANGED
|
@@ -455,7 +455,7 @@ export async function runInit(options) {
|
|
|
455
455
|
const mergeResult = mergeSettings(settings, desired);
|
|
456
456
|
await writeSettingsAtomic(settingsPath, mergeResult.merged);
|
|
457
457
|
const commitMsgResult = await installCommitMsgHook(targetDir);
|
|
458
|
-
const prePushResult = await installPrePushFallback(targetDir);
|
|
458
|
+
const prePushResult = await installPrePushFallback({ targetDir });
|
|
459
459
|
const fragmentInput = {
|
|
460
460
|
policyPath: `.${path.sep}rea${path.sep}policy.yaml`.replace(/\\/g, '/'),
|
|
461
461
|
profile: config.profile,
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* - `.rea/serve.pid` — G5 `rea serve` pidfile
|
|
19
19
|
* - `.rea/serve.state.json` — G5 `rea serve` state snapshot
|
|
20
20
|
* - `.rea/fingerprints.json` — G7 downstream catalog fingerprints (BUG-010)
|
|
21
|
-
* - `.rea/review
|
|
21
|
+
* - `.rea/last-review.json` — 0.11.0 push-gate last-review dump
|
|
22
22
|
* - `.rea/*.tmp` — serve temp-file-then-rename pattern
|
|
23
|
-
* - `.rea/*.tmp.*` —
|
|
23
|
+
* - `.rea/*.tmp.*` — push-gate pid-salted temp pattern
|
|
24
24
|
* - `.rea/install-manifest.json.bak` / `.tmp` — fs-safe atomic-replace sidecars
|
|
25
25
|
* - `.gitignore.rea-tmp-*` — this module's own temp files on crash
|
|
26
26
|
* (root-level — writeAtomic stages next
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* - `.rea/serve.pid` — G5 `rea serve` pidfile
|
|
19
19
|
* - `.rea/serve.state.json` — G5 `rea serve` state snapshot
|
|
20
20
|
* - `.rea/fingerprints.json` — G7 downstream catalog fingerprints (BUG-010)
|
|
21
|
-
* - `.rea/review
|
|
21
|
+
* - `.rea/last-review.json` — 0.11.0 push-gate last-review dump
|
|
22
22
|
* - `.rea/*.tmp` — serve temp-file-then-rename pattern
|
|
23
|
-
* - `.rea/*.tmp.*` —
|
|
23
|
+
* - `.rea/*.tmp.*` — push-gate pid-salted temp pattern
|
|
24
24
|
* - `.rea/install-manifest.json.bak` / `.tmp` — fs-safe atomic-replace sidecars
|
|
25
25
|
* - `.gitignore.rea-tmp-*` — this module's own temp files on crash
|
|
26
26
|
* (root-level — writeAtomic stages next
|
|
@@ -104,7 +104,7 @@ export const REA_GITIGNORE_ENTRIES = [
|
|
|
104
104
|
'.rea/serve.pid',
|
|
105
105
|
'.rea/serve.state.json',
|
|
106
106
|
'.rea/fingerprints.json',
|
|
107
|
-
'.rea/review
|
|
107
|
+
'.rea/last-review.json',
|
|
108
108
|
'.rea/*.tmp',
|
|
109
109
|
'.rea/*.tmp.*',
|
|
110
110
|
'.rea/install-manifest.json.bak',
|