@bookedsolid/rea 0.10.3 → 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 (74) 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/audit.d.ts +0 -131
  46. package/dist/hooks/review-gate/audit.js +0 -181
  47. package/dist/hooks/review-gate/banner.d.ts +0 -97
  48. package/dist/hooks/review-gate/banner.js +0 -172
  49. package/dist/hooks/review-gate/base-resolve.d.ts +0 -155
  50. package/dist/hooks/review-gate/base-resolve.js +0 -247
  51. package/dist/hooks/review-gate/cache-key.d.ts +0 -55
  52. package/dist/hooks/review-gate/cache-key.js +0 -41
  53. package/dist/hooks/review-gate/cache.d.ts +0 -108
  54. package/dist/hooks/review-gate/cache.js +0 -120
  55. package/dist/hooks/review-gate/constants.d.ts +0 -26
  56. package/dist/hooks/review-gate/constants.js +0 -34
  57. package/dist/hooks/review-gate/diff.d.ts +0 -181
  58. package/dist/hooks/review-gate/diff.js +0 -232
  59. package/dist/hooks/review-gate/errors.d.ts +0 -72
  60. package/dist/hooks/review-gate/errors.js +0 -100
  61. package/dist/hooks/review-gate/hash.d.ts +0 -43
  62. package/dist/hooks/review-gate/hash.js +0 -46
  63. package/dist/hooks/review-gate/index.d.ts +0 -31
  64. package/dist/hooks/review-gate/index.js +0 -35
  65. package/dist/hooks/review-gate/metadata.d.ts +0 -98
  66. package/dist/hooks/review-gate/metadata.js +0 -158
  67. package/dist/hooks/review-gate/policy.d.ts +0 -55
  68. package/dist/hooks/review-gate/policy.js +0 -71
  69. package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
  70. package/dist/hooks/review-gate/protected-paths.js +0 -76
  71. package/hooks/_lib/push-review-core.sh +0 -1250
  72. package/hooks/commit-review-gate.sh +0 -330
  73. package/hooks/push-review-gate-git.sh +0 -94
  74. package/hooks/push-review-gate.sh +0 -92
@@ -32,16 +32,16 @@ declare const PolicySchema: z.ZodObject<{
32
32
  }>>;
33
33
  review: z.ZodOptional<z.ZodObject<{
34
34
  codex_required: z.ZodOptional<z.ZodBoolean>;
35
- cache_max_age_seconds: z.ZodOptional<z.ZodNumber>;
36
- allow_skip_in_ci: z.ZodOptional<z.ZodBoolean>;
35
+ concerns_blocks: z.ZodOptional<z.ZodBoolean>;
36
+ timeout_ms: z.ZodOptional<z.ZodNumber>;
37
37
  }, "strict", z.ZodTypeAny, {
38
38
  codex_required?: boolean | undefined;
39
- cache_max_age_seconds?: number | undefined;
40
- allow_skip_in_ci?: boolean | undefined;
39
+ concerns_blocks?: boolean | undefined;
40
+ timeout_ms?: number | undefined;
41
41
  }, {
42
42
  codex_required?: boolean | undefined;
43
- cache_max_age_seconds?: number | undefined;
44
- allow_skip_in_ci?: boolean | undefined;
43
+ concerns_blocks?: boolean | undefined;
44
+ timeout_ms?: number | undefined;
45
45
  }>>;
46
46
  redact: z.ZodOptional<z.ZodObject<{
47
47
  match_timeout_ms: z.ZodOptional<z.ZodNumber>;
@@ -133,8 +133,8 @@ declare const PolicySchema: z.ZodObject<{
133
133
  } | undefined;
134
134
  review?: {
135
135
  codex_required?: boolean | undefined;
136
- cache_max_age_seconds?: number | undefined;
137
- allow_skip_in_ci?: boolean | undefined;
136
+ concerns_blocks?: boolean | undefined;
137
+ timeout_ms?: number | undefined;
138
138
  } | undefined;
139
139
  redact?: {
140
140
  match_timeout_ms?: number | undefined;
@@ -176,8 +176,8 @@ declare const PolicySchema: z.ZodObject<{
176
176
  } | undefined;
177
177
  review?: {
178
178
  codex_required?: boolean | undefined;
179
- cache_max_age_seconds?: number | undefined;
180
- allow_skip_in_ci?: boolean | undefined;
179
+ concerns_blocks?: boolean | undefined;
180
+ timeout_ms?: number | undefined;
181
181
  } | undefined;
182
182
  redact?: {
183
183
  match_timeout_ms?: number | undefined;
@@ -16,16 +16,17 @@ const ContextProtectionSchema = z.object({
16
16
  max_bash_output_lines: z.number().int().positive().optional(),
17
17
  });
18
18
  /**
19
- * G11.2: minimal review policy. Only `codex_required` is recognized today;
20
- * G11.4 will expand this (profile defaults, reviewer pin, token caps).
21
- * Kept strict so a typo (`codex_require`) fails loudly instead of silently
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
- cache_max_age_seconds: z.number().int().positive().optional(),
28
- allow_skip_in_ci: z.boolean().optional(),
28
+ concerns_blocks: z.boolean().optional(),
29
+ timeout_ms: z.number().int().positive().optional(),
29
30
  })
30
31
  .strict();
31
32
  /**
@@ -19,38 +19,47 @@ export interface ContextProtection {
19
19
  max_bash_output_lines?: number;
20
20
  }
21
21
  /**
22
- * Review policy knobs. G11.2 only needs `codex_required` as an optional
23
- * signal to the reviewer selector; G11.4 will flesh this out into a full
24
- * first-class no-Codex mode (profile defaults, init defaults, etc.).
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 `false`, the selector treats ClaudeSelfReviewer as the preferred
29
- * reviewer (not degraded). When `true` or unset, Codex is preferred and
30
- * a ClaudeSelfReviewer result is marked `degraded: true` in the audit
31
- * log. Default when unset is `true` (Codex required).
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
- * Review-cache TTL used by `rea cache check` (BUG-009). Entries older
36
- * than this window are treated as a miss, forcing re-review. Default
37
- * when unset is 3600 seconds (1 hour) matches the windows the
38
- * push-review-gate hook already assumes. Express in seconds, positive
39
- * integer.
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.
40
52
  */
41
- cache_max_age_seconds?: number;
53
+ concerns_blocks?: boolean;
42
54
  /**
43
- * Authorization for `REA_SKIP_PUSH_REVIEW` / `REA_SKIP_CODEX_REVIEW` when
44
- * the `CI` environment variable is set. The skip hatches are ambient and
45
- * unauthenticated a leaked env file or a malicious parent process can
46
- * bypass the gate and record a forged actor (git config is mutable repo
47
- * config). Refusing these hatches in CI contexts by default removes that
48
- * bypass surface. Set `true` ONLY on build agents where the operator has
49
- * an independent reason to trust the environment. Default `false`.
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 600_000 (10 minutes) matches
58
+ * the upper bound we observe for a 500-line diff review on a slow link.
50
59
  *
51
- * Added in 0.5.0 as Codex F2 on the PR1 adversarial review.
60
+ * Positive integer only. The loader rejects zero/negative values.
52
61
  */
53
- allow_skip_in_ci?: boolean;
62
+ timeout_ms?: number;
54
63
  }
55
64
  /**
56
65
  * 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.10.3",
3
+ "version": "0.11.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)",
@@ -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[]>;
@@ -1,200 +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
- import fs from 'node:fs/promises';
54
- import path from 'node:path';
55
- import { withAuditLock } from '../audit/fs.js';
56
- /** Default TTL when policy does not supply one. */
57
- export const DEFAULT_CACHE_MAX_AGE_SECONDS = 3600;
58
- /**
59
- * Tolerated clock skew for future-dated entries. A `recorded_at` more than
60
- * this far in the future relative to `nowMs` is treated as tampered or
61
- * severely-drifted and forces a miss (re-review). 60s covers NTP jitter on
62
- * well-synced hosts; anything beyond that is noise we do not trust.
63
- */
64
- const FUTURE_SKEW_ALLOWANCE_MS = 60_000;
65
- const CACHE_FILENAME = 'review-cache.jsonl';
66
- const REA_DIRNAME = '.rea';
67
- export function resolveCacheFile(baseDir) {
68
- return path.join(baseDir, REA_DIRNAME, CACHE_FILENAME);
69
- }
70
- /**
71
- * Load every entry from the cache file. Returns `[]` when the file does not
72
- * exist or is empty. Malformed lines are skipped — we never throw on a
73
- * corrupt line, because the cache is advisory and a bad write (e.g. a
74
- * half-written line from a crashed host) must not block a subsequent push.
75
- */
76
- async function loadEntries(cacheFile) {
77
- let raw;
78
- try {
79
- raw = await fs.readFile(cacheFile, 'utf8');
80
- }
81
- catch (err) {
82
- if (err.code === 'ENOENT')
83
- return [];
84
- throw err;
85
- }
86
- if (raw.length === 0)
87
- return [];
88
- const entries = [];
89
- for (const line of raw.split('\n')) {
90
- if (line.length === 0)
91
- continue;
92
- try {
93
- const parsed = JSON.parse(line);
94
- if (typeof parsed.sha === 'string' &&
95
- typeof parsed.branch === 'string' &&
96
- typeof parsed.base === 'string' &&
97
- (parsed.result === 'pass' || parsed.result === 'fail') &&
98
- typeof parsed.recorded_at === 'string') {
99
- entries.push(parsed);
100
- }
101
- }
102
- catch {
103
- // Skip malformed line.
104
- }
105
- }
106
- return entries;
107
- }
108
- /**
109
- * Append an entry to the cache. Writes are serialized through the shared
110
- * `.rea/` directory lock so audit writes and cache writes do not interleave.
111
- */
112
- export async function appendEntry(baseDir, input) {
113
- const cacheFile = resolveCacheFile(baseDir);
114
- await fs.mkdir(path.dirname(cacheFile), { recursive: true });
115
- const entry = {
116
- sha: input.sha,
117
- branch: input.branch,
118
- base: input.base,
119
- result: input.result,
120
- recorded_at: input.timestamp ?? new Date().toISOString(),
121
- ...(input.reason !== undefined && input.reason.length > 0
122
- ? { reason: input.reason }
123
- : {}),
124
- };
125
- await withAuditLock(cacheFile, async () => {
126
- const line = JSON.stringify(entry) + '\n';
127
- await fs.appendFile(cacheFile, line);
128
- });
129
- return entry;
130
- }
131
- /**
132
- * Find the most-recent entry matching `(sha, branch, base)` within the TTL
133
- * window. Idempotent and side-effect free.
134
- */
135
- export async function lookup(baseDir, input) {
136
- const cacheFile = resolveCacheFile(baseDir);
137
- const entries = await loadEntries(cacheFile);
138
- if (entries.length === 0)
139
- return { hit: false, missReason: 'empty-file' };
140
- // Walk from the tail so the first match is the newest.
141
- let matched;
142
- for (let i = entries.length - 1; i >= 0; i--) {
143
- const e = entries[i];
144
- if (e.sha === input.sha && e.branch === input.branch && e.base === input.base) {
145
- matched = e;
146
- break;
147
- }
148
- }
149
- if (matched === undefined)
150
- return { hit: false, missReason: 'no-entry' };
151
- const nowMs = input.nowMs ?? Date.now();
152
- const maxAgeSeconds = input.maxAgeSeconds ?? DEFAULT_CACHE_MAX_AGE_SECONDS;
153
- const recordedMs = Date.parse(matched.recorded_at);
154
- if (Number.isNaN(recordedMs)) {
155
- // Corrupt timestamp — treat as an expired miss so the caller re-reviews.
156
- return { hit: false, missReason: 'expired', entry: matched };
157
- }
158
- if (recordedMs > nowMs + FUTURE_SKEW_ALLOWANCE_MS) {
159
- return { hit: false, missReason: 'expired', entry: matched };
160
- }
161
- if ((nowMs - recordedMs) / 1000 > maxAgeSeconds) {
162
- return { hit: false, missReason: 'expired', entry: matched };
163
- }
164
- return { hit: true, entry: matched };
165
- }
166
- /**
167
- * Remove every entry matching `sha`. Returns the count removed. A `0` return
168
- * is a valid outcome (sha not present). Writes back via the same lock as
169
- * `appendEntry`, so concurrent sets do not lose entries.
170
- *
171
- * Writes use temp-file + `fs.rename` (atomic within a single directory on
172
- * POSIX) so unlocked readers (`lookup`, `list`) can never observe a torn or
173
- * empty intermediate state. Codex F4 on the 0.5.0 PR1 review.
174
- */
175
- export async function clear(baseDir, sha) {
176
- const cacheFile = resolveCacheFile(baseDir);
177
- return withAuditLock(cacheFile, async () => {
178
- const entries = await loadEntries(cacheFile);
179
- const kept = entries.filter((e) => e.sha !== sha);
180
- const removed = entries.length - kept.length;
181
- if (removed === 0)
182
- return 0;
183
- const out = kept.length === 0 ? '' : kept.map((e) => JSON.stringify(e)).join('\n') + '\n';
184
- const tmpFile = `${cacheFile}.tmp.${process.pid}.${Date.now()}`;
185
- await fs.writeFile(tmpFile, out);
186
- await fs.rename(tmpFile, cacheFile);
187
- return removed;
188
- });
189
- }
190
- /**
191
- * Return every entry, optionally filtered by branch. Entries are returned in
192
- * file order (oldest first). Callers that want "newest first" should reverse.
193
- */
194
- export async function list(baseDir, options = {}) {
195
- const cacheFile = resolveCacheFile(baseDir);
196
- const entries = await loadEntries(cacheFile);
197
- if (options.branch === undefined)
198
- return entries;
199
- return entries.filter((e) => e.branch === options.branch);
200
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * `rea cache` — push-review cache operator subcommands (BUG-009).
3
- *
4
- * Four verbs:
5
- * - `check <sha> --branch <b> --base <b>` — JSON to stdout ONLY; never
6
- * diagnostics. `hooks/push-review-gate.sh` reads this via
7
- * `printf '%s' "$CACHE_RESULT" | jq -e '.hit == true'`, so any stray
8
- * text on stdout would poison the hook's JSON parse.
9
- * - `set <sha> pass|fail --branch <b> --base <b> [--reason <s>]` — record
10
- * a review outcome.
11
- * - `clear <sha>` — drop every entry for a sha (dev convenience).
12
- * - `list [--branch <b>]` — pretty-print entries.
13
- *
14
- * The TTL used by `check` reads `review.cache_max_age_seconds` from
15
- * `.rea/policy.yaml` when present, falling back to
16
- * {@link DEFAULT_CACHE_MAX_AGE_SECONDS} (1 hour) when the policy file or
17
- * field is absent. An unreadable/malformed policy file is NOT fatal for
18
- * `check` — it degrades to the default so a broken policy never deadlocks
19
- * the push gate; other commands that don't consume the TTL ignore the policy
20
- * entirely.
21
- */
22
- import { type CacheResult } from '../cache/review-cache.js';
23
- import type { CodexVerdict } from '../audit/codex-event.js';
24
- export interface CacheCheckOptions {
25
- sha: string;
26
- branch: string;
27
- base: string;
28
- }
29
- export interface CacheSetOptions {
30
- sha: string;
31
- result: CacheResult;
32
- branch: string;
33
- base: string;
34
- reason?: string;
35
- }
36
- export interface CacheClearOptions {
37
- sha: string;
38
- }
39
- export interface CacheListOptions {
40
- branch?: string;
41
- }
42
- /**
43
- * Print the cache-check JSON to stdout. Hook contract: stdout is ONLY JSON.
44
- * On a miss we still exit 0 with `{"hit":false}` — the hook interprets
45
- * non-zero as "rea broken, force re-review" via its `|| echo '{"hit":false}'`
46
- * fallback.
47
- */
48
- export declare function runCacheCheck(options: CacheCheckOptions): Promise<void>;
49
- export declare function runCacheSet(options: CacheSetOptions): Promise<void>;
50
- export declare function runCacheClear(options: CacheClearOptions): Promise<void>;
51
- export declare function runCacheList(options: CacheListOptions): Promise<void>;
52
- /** Parse-and-validate helper for `set` — surfaces a clean error on bad input.
53
- *
54
- * Accepts the two historical cache values (`pass`, `fail`) AND the four
55
- * canonical Codex verdicts (`pass`, `concerns`, `blocking`, `error`) per
56
- * Defect D (rea#77). Codex verdicts are mapped to cache semantics at the CLI
57
- * boundary: `pass|concerns` → gate-satisfying `pass`; `blocking|error` →
58
- * gate-failing `fail`. The cache internal vocabulary stays binary
59
- * (`pass`/`fail` = "gate-satisfying?") while the CLI accepts the full Codex
60
- * vocabulary so agents can copy the `/codex-review` verdict verbatim.
61
- */
62
- export declare function parseCacheResult(raw: string): CacheResult;
63
- /** Shape returned by {@link codexVerdictToCacheResult}: the binary cache result
64
- * plus an optional machine-readable `reason` string that records the source
65
- * Codex verdict. `reason` is populated for non-`pass` verdicts so downstream
66
- * listings expose WHY a cache fail was recorded. */
67
- export interface CodexVerdictCacheEffect {
68
- result: CacheResult;
69
- reason?: string | undefined;
70
- }
71
- /** Map a Codex verdict to the binary cache result the gate compares against.
72
- *
73
- * Mapping rationale:
74
- * - `pass` → cache `pass` (clean review, gate should pass)
75
- * - `concerns` → cache `pass` (non-blocking findings, gate should pass;
76
- * reviewer captured concerns in the audit record `metadata.summary`)
77
- * - `blocking` → cache `fail` (must address findings before merge)
78
- * - `error` → cache `fail` (Codex itself errored; no clean-bill-of-health)
79
- *
80
- * Kept separate from `parseCacheResult` so callers that already have a typed
81
- * `CodexVerdict` (e.g. `rea audit record codex-review --also-set-cache`) don't
82
- * round-trip through string parsing.
83
- */
84
- export declare function codexVerdictToCacheResult(verdict: CodexVerdict): CodexVerdictCacheEffect;