@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.
Files changed (66) hide show
  1. package/.husky/pre-push +22 -167
  2. package/agents/codex-adversarial.md +5 -3
  3. package/commands/codex-review.md +3 -5
  4. package/dist/audit/append.d.ts +7 -32
  5. package/dist/audit/append.js +7 -35
  6. package/dist/cli/audit.d.ts +0 -31
  7. package/dist/cli/audit.js +5 -74
  8. package/dist/cli/doctor.js +6 -16
  9. package/dist/cli/hook.d.ts +48 -0
  10. package/dist/cli/hook.js +127 -0
  11. package/dist/cli/index.js +5 -80
  12. package/dist/cli/init.js +1 -1
  13. package/dist/cli/install/gitignore.d.ts +2 -2
  14. package/dist/cli/install/gitignore.js +3 -3
  15. package/dist/cli/install/pre-push.d.ts +146 -271
  16. package/dist/cli/install/pre-push.js +471 -2633
  17. package/dist/cli/install/settings-merge.d.ts +17 -0
  18. package/dist/cli/install/settings-merge.js +48 -1
  19. package/dist/cli/upgrade.js +131 -3
  20. package/dist/config/tier-map.js +18 -25
  21. package/dist/hooks/push-gate/base.d.ts +57 -0
  22. package/dist/hooks/push-gate/base.js +77 -0
  23. package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
  24. package/dist/hooks/push-gate/codex-runner.js +223 -0
  25. package/dist/hooks/push-gate/findings.d.ts +68 -0
  26. package/dist/hooks/push-gate/findings.js +142 -0
  27. package/dist/hooks/push-gate/halt.d.ts +28 -0
  28. package/dist/hooks/push-gate/halt.js +49 -0
  29. package/dist/hooks/push-gate/index.d.ts +90 -0
  30. package/dist/hooks/push-gate/index.js +351 -0
  31. package/dist/hooks/push-gate/policy.d.ts +41 -0
  32. package/dist/hooks/push-gate/policy.js +55 -0
  33. package/dist/hooks/push-gate/report.d.ts +89 -0
  34. package/dist/hooks/push-gate/report.js +140 -0
  35. package/dist/policy/loader.d.ts +10 -10
  36. package/dist/policy/loader.js +7 -6
  37. package/dist/policy/types.d.ts +31 -22
  38. package/package.json +1 -1
  39. package/dist/cache/review-cache.d.ts +0 -115
  40. package/dist/cache/review-cache.js +0 -200
  41. package/dist/cli/cache.d.ts +0 -84
  42. package/dist/cli/cache.js +0 -150
  43. package/dist/hooks/review-gate/args.d.ts +0 -126
  44. package/dist/hooks/review-gate/args.js +0 -315
  45. package/dist/hooks/review-gate/banner.d.ts +0 -97
  46. package/dist/hooks/review-gate/banner.js +0 -172
  47. package/dist/hooks/review-gate/cache-key.d.ts +0 -55
  48. package/dist/hooks/review-gate/cache-key.js +0 -41
  49. package/dist/hooks/review-gate/constants.d.ts +0 -26
  50. package/dist/hooks/review-gate/constants.js +0 -34
  51. package/dist/hooks/review-gate/errors.d.ts +0 -72
  52. package/dist/hooks/review-gate/errors.js +0 -100
  53. package/dist/hooks/review-gate/hash.d.ts +0 -43
  54. package/dist/hooks/review-gate/hash.js +0 -46
  55. package/dist/hooks/review-gate/index.d.ts +0 -21
  56. package/dist/hooks/review-gate/index.js +0 -21
  57. package/dist/hooks/review-gate/metadata.d.ts +0 -98
  58. package/dist/hooks/review-gate/metadata.js +0 -158
  59. package/dist/hooks/review-gate/policy.d.ts +0 -55
  60. package/dist/hooks/review-gate/policy.js +0 -71
  61. package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
  62. package/dist/hooks/review-gate/protected-paths.js +0 -76
  63. package/hooks/_lib/push-review-core.sh +0 -1250
  64. package/hooks/commit-review-gate.sh +0 -330
  65. package/hooks/push-review-gate-git.sh +0 -94
  66. package/hooks/push-review-gate.sh +0 -92
@@ -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 { runAuditRecordCodexReview, runAuditRotate, runAuditVerify } from './audit.js';
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
- const auditRecord = audit
107
- .command('record')
108
- .description('Emit a structured audit record (D).');
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-cache.jsonl` BUG-009 review cache (rea cache set/check)
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.*` — review-cache pid-salted temp pattern
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-cache.jsonl` BUG-009 review cache (rea cache set/check)
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.*` — review-cache pid-salted temp pattern
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-cache.jsonl',
107
+ '.rea/last-review.json',
108
108
  '.rea/*.tmp',
109
109
  '.rea/*.tmp.*',
110
110
  '.rea/install-manifest.json.bak',