@bookedsolid/rea 0.19.0 → 0.20.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/commit-msg +2 -2
- package/dist/hooks/push-gate/codex-runner.d.ts +17 -0
- package/dist/hooks/push-gate/codex-runner.js +24 -2
- package/dist/hooks/push-gate/index.js +42 -18
- package/dist/hooks/push-gate/verdict-cache.d.ts +36 -8
- package/dist/hooks/push-gate/verdict-cache.js +156 -70
- package/dist/policy/loader.js +6 -1
- package/hooks/_lib/cmd-segments.sh +8 -1
- package/hooks/_lib/protected-paths.sh +5 -0
- package/hooks/attribution-advisory.sh +1 -1
- package/package.json +2 -1
- package/scripts/postinstall.mjs +7 -0
package/.husky/commit-msg
CHANGED
|
@@ -86,9 +86,9 @@ MATCHES=""
|
|
|
86
86
|
# Pattern 2 below catches Co-Authored-By with named tools regardless of
|
|
87
87
|
# email, so dropping users.noreply.github.com from this branch only
|
|
88
88
|
# relaxes the check for human collaborators — never for AI.
|
|
89
|
-
if grep -qiE 'Co-Authored-By:.*noreply@(anthropic\.com|openai\.com|github-copilot|github\.com|claude\.ai|chatgpt\.com|googlemail\.com|google\.com|cursor\.com|codeium\.com|tabnine\.com|amazon\.com|amazonaws\.com|amazon-q\.amazonaws\.com|cody\.dev|sourcegraph\.com)' "$COMMIT_MSG_FILE" 2>/dev/null; then
|
|
89
|
+
if grep -qiE 'Co-Authored-By:.*noreply@(anthropic\.com|openai\.com|github-copilot|github\.com|claude\.ai|chatgpt\.com|googlemail\.com|google\.com|cursor\.com|codeium\.com|tabnine\.com|amazon\.com|amazonaws\.com|amazon-q\.amazonaws\.com|cody\.dev|sourcegraph\.com|mistral\.ai|xai-org|x\.ai|inflection\.ai|perplexity\.ai|replit\.com|jetbrains\.com|bito\.ai|pieces\.app|phind\.com|you\.com)' "$COMMIT_MSG_FILE" 2>/dev/null; then
|
|
90
90
|
BLOCKED=1
|
|
91
|
-
MATCHES="${MATCHES}$(grep -niE 'Co-Authored-By:.*noreply@(anthropic\.com|openai\.com|github-copilot|github\.com|claude\.ai|chatgpt\.com|googlemail\.com|google\.com|cursor\.com|codeium\.com|tabnine\.com|amazon\.com|amazonaws\.com|amazon-q\.amazonaws\.com|cody\.dev|sourcegraph\.com)' "$COMMIT_MSG_FILE" 2>/dev/null)
|
|
91
|
+
MATCHES="${MATCHES}$(grep -niE 'Co-Authored-By:.*noreply@(anthropic\.com|openai\.com|github-copilot|github\.com|claude\.ai|chatgpt\.com|googlemail\.com|google\.com|cursor\.com|codeium\.com|tabnine\.com|amazon\.com|amazonaws\.com|amazon-q\.amazonaws\.com|cody\.dev|sourcegraph\.com|mistral\.ai|xai-org|x\.ai|inflection\.ai|perplexity\.ai|replit\.com|jetbrains\.com|bito\.ai|pieces\.app|phind\.com|you\.com)' "$COMMIT_MSG_FILE" 2>/dev/null)
|
|
92
92
|
"
|
|
93
93
|
fi
|
|
94
94
|
|
|
@@ -19,6 +19,23 @@
|
|
|
19
19
|
* and so the one git dependency surface is in one place.
|
|
20
20
|
*/
|
|
21
21
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
22
|
+
/**
|
|
23
|
+
* Default codex model when policy doesn't pin one. Always passed via
|
|
24
|
+
* `-c model="<name>"` so codex's own default (`codex-auto-review` at
|
|
25
|
+
* medium reasoning) is unreachable through the rea push-gate.
|
|
26
|
+
*
|
|
27
|
+
* 0.19.0 code-reviewer P3-4: exported as a single source of truth.
|
|
28
|
+
* `src/hooks/push-gate/index.ts` imports this for the verdict-cache
|
|
29
|
+
* write so the cached `model` field reflects the same constant the
|
|
30
|
+
* runner actually used. Bump here to bump everywhere.
|
|
31
|
+
*/
|
|
32
|
+
export declare const IRON_GATE_DEFAULT_MODEL = "gpt-5.4";
|
|
33
|
+
/**
|
|
34
|
+
* Default reasoning effort when policy doesn't pin one. `high` for
|
|
35
|
+
* verdict stability — the helixir 2026-04-26 thrashing came from the
|
|
36
|
+
* lower-reasoning default.
|
|
37
|
+
*/
|
|
38
|
+
export declare const IRON_GATE_DEFAULT_REASONING: 'low' | 'medium' | 'high';
|
|
22
39
|
export declare class CodexNotInstalledError extends Error {
|
|
23
40
|
readonly kind: "not-installed";
|
|
24
41
|
constructor();
|
|
@@ -20,6 +20,26 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { spawn, spawnSync } from 'node:child_process';
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
|
+
// Iron-gate runtime defaults (0.18.0+)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/**
|
|
26
|
+
* Default codex model when policy doesn't pin one. Always passed via
|
|
27
|
+
* `-c model="<name>"` so codex's own default (`codex-auto-review` at
|
|
28
|
+
* medium reasoning) is unreachable through the rea push-gate.
|
|
29
|
+
*
|
|
30
|
+
* 0.19.0 code-reviewer P3-4: exported as a single source of truth.
|
|
31
|
+
* `src/hooks/push-gate/index.ts` imports this for the verdict-cache
|
|
32
|
+
* write so the cached `model` field reflects the same constant the
|
|
33
|
+
* runner actually used. Bump here to bump everywhere.
|
|
34
|
+
*/
|
|
35
|
+
export const IRON_GATE_DEFAULT_MODEL = 'gpt-5.4';
|
|
36
|
+
/**
|
|
37
|
+
* Default reasoning effort when policy doesn't pin one. `high` for
|
|
38
|
+
* verdict stability — the helixir 2026-04-26 thrashing came from the
|
|
39
|
+
* lower-reasoning default.
|
|
40
|
+
*/
|
|
41
|
+
export const IRON_GATE_DEFAULT_REASONING = 'high';
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
23
43
|
// Errors
|
|
24
44
|
// ---------------------------------------------------------------------------
|
|
25
45
|
export class CodexNotInstalledError extends Error {
|
|
@@ -151,8 +171,10 @@ export async function runCodexReview(options) {
|
|
|
151
171
|
// Codex's TOML parser interprets the value, so we wrap strings in TOML
|
|
152
172
|
// quotes — `-c model="gpt-5.4"` not `-c model=gpt-5.4` — to ensure the
|
|
153
173
|
// value lands as a string regardless of upstream parsing changes.
|
|
154
|
-
const effectiveModel = options.model !== undefined && options.model.length > 0
|
|
155
|
-
|
|
174
|
+
const effectiveModel = options.model !== undefined && options.model.length > 0
|
|
175
|
+
? options.model
|
|
176
|
+
: IRON_GATE_DEFAULT_MODEL;
|
|
177
|
+
const effectiveReasoning = options.reasoningEffort ?? IRON_GATE_DEFAULT_REASONING;
|
|
156
178
|
const overrideArgs = [
|
|
157
179
|
'-c',
|
|
158
180
|
`model="${escapeTomlString(effectiveModel)}"`,
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
import { appendAuditRecord } from '../../audit/append.js';
|
|
26
|
+
import { loadPolicyAsync } from '../../policy/loader.js';
|
|
26
27
|
import { Tier, InvocationStatus } from '../../policy/types.js';
|
|
27
28
|
import { resolvePushGatePolicy, PUSH_GATE_DEFAULT_LAST_N_COMMITS_FALLBACK, } from './policy.js';
|
|
28
29
|
import { readHalt } from './halt.js';
|
|
29
30
|
import { resolveBaseRef } from './base.js';
|
|
30
|
-
import { createRealGitExecutor, runCodexReview, CodexNotInstalledError, CodexProtocolError, CodexSubprocessError, CodexTimeoutError, } from './codex-runner.js';
|
|
31
|
+
import { createRealGitExecutor, runCodexReview, CodexNotInstalledError, CodexProtocolError, CodexSubprocessError, CodexTimeoutError, IRON_GATE_DEFAULT_MODEL, IRON_GATE_DEFAULT_REASONING, } from './codex-runner.js';
|
|
31
32
|
import { summarizeReview } from './findings.js';
|
|
32
33
|
import { renderBanner, writeLastReview } from './report.js';
|
|
33
34
|
import { isFlip, lookupVerdict, writeVerdict, } from './verdict-cache.js';
|
|
@@ -87,13 +88,27 @@ export async function runPushGate(deps) {
|
|
|
87
88
|
const runCodexFn = deps.runCodex ?? runCodexReview;
|
|
88
89
|
const appendAuditFn = deps.appendAudit ?? appendAuditRecord;
|
|
89
90
|
const git = deps.git ?? createRealGitExecutor(deps.baseDir);
|
|
91
|
+
// 0.19.0 backend-engineer review P1-1: load the full Policy once and
|
|
92
|
+
// thread it to every safeAppend so audit rotation actually fires.
|
|
93
|
+
// Pre-fix the rotator short-circuited because policy was never passed
|
|
94
|
+
// through, silently disabling the `audit.rotation: {}` opt-in shipped
|
|
95
|
+
// in 0.18.1 for the bst-internal profile. A failure to load policy
|
|
96
|
+
// here is non-fatal — the gate continues; audit rotation just stays
|
|
97
|
+
// disabled for this run (back-compat).
|
|
98
|
+
let fullPolicy;
|
|
99
|
+
try {
|
|
100
|
+
fullPolicy = await loadPolicyAsync(deps.baseDir);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
fullPolicy = undefined;
|
|
104
|
+
}
|
|
90
105
|
// 1. HALT wins over everything, including `review.codex_required: false`.
|
|
91
106
|
// Reading it before policy also means a corrupted policy.yaml doesn't
|
|
92
107
|
// prevent the kill-switch from firing.
|
|
93
108
|
const halt = readHaltFn(deps.baseDir);
|
|
94
109
|
if (halt.halted) {
|
|
95
110
|
stderr(`REA HALT: ${halt.reason ?? 'unknown'}\nAll push operations suspended. Run: rea unfreeze\n`);
|
|
96
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_HALTED, {
|
|
111
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_HALTED, fullPolicy, {
|
|
97
112
|
reason: halt.reason ?? 'unknown',
|
|
98
113
|
});
|
|
99
114
|
return {
|
|
@@ -111,14 +126,14 @@ export async function runPushGate(deps) {
|
|
|
111
126
|
catch (e) {
|
|
112
127
|
const msg = e instanceof Error ? e.message : String(e);
|
|
113
128
|
stderr(`PUSH BLOCKED: failed to load .rea/policy.yaml — ${msg}\n`);
|
|
114
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_ERROR, {
|
|
129
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_ERROR, fullPolicy, {
|
|
115
130
|
kind: 'policy-load',
|
|
116
131
|
error: msg,
|
|
117
132
|
});
|
|
118
133
|
return { status: 'error', exitCode: 2, summary: `policy-load error: ${msg}` };
|
|
119
134
|
}
|
|
120
135
|
if (!policy.codex_required) {
|
|
121
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_DISABLED, {
|
|
136
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_DISABLED, fullPolicy, {
|
|
122
137
|
policy_missing: policy.policyMissing,
|
|
123
138
|
});
|
|
124
139
|
return {
|
|
@@ -156,7 +171,7 @@ export async function runPushGate(deps) {
|
|
|
156
171
|
const skipVar = skipPush.length > 0 ? 'REA_SKIP_PUSH_GATE' : 'REA_SKIP_CODEX_REVIEW';
|
|
157
172
|
const skipReason = skipVar === 'REA_SKIP_PUSH_GATE' ? skipPush : skipCodex;
|
|
158
173
|
stderr(`rea: ${skipVar}=${skipReason} — push-gate skipped (audited).\n`);
|
|
159
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_SKIPPED, {
|
|
174
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_SKIPPED, fullPolicy, {
|
|
160
175
|
reason: skipReason,
|
|
161
176
|
skip_var: skipVar,
|
|
162
177
|
});
|
|
@@ -251,7 +266,7 @@ export async function runPushGate(deps) {
|
|
|
251
266
|
}
|
|
252
267
|
if (headSha.length === 0) {
|
|
253
268
|
stderr('PUSH BLOCKED: could not resolve HEAD SHA. Is this a valid git repo?\n');
|
|
254
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_ERROR, { kind: 'head-sha-missing' });
|
|
269
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_ERROR, fullPolicy, { kind: 'head-sha-missing' });
|
|
255
270
|
return { status: 'error', exitCode: 2, summary: 'head-sha-missing' };
|
|
256
271
|
}
|
|
257
272
|
// 4b. Auto-narrow probe (J / 0.13.0). When the resolved base is far
|
|
@@ -321,7 +336,7 @@ export async function runPushGate(deps) {
|
|
|
321
336
|
// no-op relative to base.
|
|
322
337
|
const diff = git.diffNames(base.ref, headSha);
|
|
323
338
|
if (diff.length === 0) {
|
|
324
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_EMPTY, {
|
|
339
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_EMPTY, fullPolicy, {
|
|
325
340
|
base_ref: base.ref,
|
|
326
341
|
base_source: base.source,
|
|
327
342
|
head_sha: headSha,
|
|
@@ -348,7 +363,7 @@ export async function runPushGate(deps) {
|
|
|
348
363
|
const cached = cacheLookup.entry;
|
|
349
364
|
const cachedBlocked = cached.verdict === 'blocking'
|
|
350
365
|
|| (cached.verdict === 'concerns' && policy.concerns_blocks && !isConcernsOverrideSet(env));
|
|
351
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_CACHE_HIT, {
|
|
366
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_CACHE_HIT, fullPolicy, {
|
|
352
367
|
verdict: cached.verdict,
|
|
353
368
|
finding_count: cached.finding_count,
|
|
354
369
|
base_ref: base.ref,
|
|
@@ -423,7 +438,7 @@ export async function runPushGate(deps) {
|
|
|
423
438
|
if (policy.cache_ttl_ms > 0) {
|
|
424
439
|
const flipped = isFlip(cacheLookup.entry, summary.verdict);
|
|
425
440
|
if (flipped && cacheLookup.entry !== undefined) {
|
|
426
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_VERDICT_FLIP, {
|
|
441
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_VERDICT_FLIP, fullPolicy, {
|
|
427
442
|
head_sha: headSha,
|
|
428
443
|
prior_verdict: cacheLookup.entry.verdict,
|
|
429
444
|
fresh_verdict: summary.verdict,
|
|
@@ -435,20 +450,22 @@ export async function runPushGate(deps) {
|
|
|
435
450
|
verdict: summary.verdict,
|
|
436
451
|
finding_count: summary.findings.length,
|
|
437
452
|
reviewed_at: deps.now !== undefined ? deps.now().toISOString() : new Date().toISOString(),
|
|
438
|
-
model: policy.codex_model ??
|
|
439
|
-
reasoning_effort: policy.codex_reasoning_effort ??
|
|
453
|
+
model: policy.codex_model ?? IRON_GATE_DEFAULT_MODEL,
|
|
454
|
+
reasoning_effort: policy.codex_reasoning_effort ?? IRON_GATE_DEFAULT_REASONING,
|
|
440
455
|
ttl_ms: policy.cache_ttl_ms,
|
|
441
456
|
};
|
|
442
457
|
try {
|
|
443
|
-
writeVerdict(deps.baseDir, headSha, entry);
|
|
458
|
+
await writeVerdict(deps.baseDir, headSha, entry);
|
|
444
459
|
}
|
|
445
460
|
catch {
|
|
446
461
|
// Cache writes are best-effort. A failure here must NOT
|
|
447
462
|
// affect the verdict — log to stderr (already done by the
|
|
448
|
-
// caller via banner) and proceed.
|
|
463
|
+
// caller via banner) and proceed. Foreign-schema (v3+ cache
|
|
464
|
+
// from a future rea version) lands here and is correctly
|
|
465
|
+
// declined — overwriting would lose forward-compat data.
|
|
449
466
|
}
|
|
450
467
|
}
|
|
451
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_REVIEWED, {
|
|
468
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_REVIEWED, fullPolicy, {
|
|
452
469
|
verdict: summary.verdict,
|
|
453
470
|
finding_count: summary.findings.length,
|
|
454
471
|
base_ref: base.ref,
|
|
@@ -492,7 +509,7 @@ export async function runPushGate(deps) {
|
|
|
492
509
|
};
|
|
493
510
|
}
|
|
494
511
|
catch (e) {
|
|
495
|
-
return handleCodexError(e, deps, base, headSha, appendAuditFn);
|
|
512
|
+
return handleCodexError(e, deps, base, headSha, appendAuditFn, fullPolicy);
|
|
496
513
|
}
|
|
497
514
|
}
|
|
498
515
|
function isConcernsOverrideSet(env) {
|
|
@@ -502,7 +519,7 @@ function isConcernsOverrideSet(env) {
|
|
|
502
519
|
const normalized = raw.trim().toLowerCase();
|
|
503
520
|
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
|
504
521
|
}
|
|
505
|
-
async function handleCodexError(e, deps, base, headSha, appendAuditFn) {
|
|
522
|
+
async function handleCodexError(e, deps, base, headSha, appendAuditFn, policy) {
|
|
506
523
|
const stderr = deps.stderr;
|
|
507
524
|
const runError = classifyCodexError(e);
|
|
508
525
|
const metadata = {
|
|
@@ -514,7 +531,7 @@ async function handleCodexError(e, deps, base, headSha, appendAuditFn) {
|
|
|
514
531
|
if (runError.message.length > 0)
|
|
515
532
|
metadata.error = runError.message;
|
|
516
533
|
stderr(`PUSH BLOCKED: ${runError.message}\n`);
|
|
517
|
-
await safeAppend(appendAuditFn, deps.baseDir, EVT_ERROR, metadata);
|
|
534
|
+
await safeAppend(appendAuditFn, deps.baseDir, EVT_ERROR, policy, metadata);
|
|
518
535
|
return {
|
|
519
536
|
status: 'error',
|
|
520
537
|
exitCode: 2,
|
|
@@ -542,7 +559,7 @@ function classifyCodexError(e) {
|
|
|
542
559
|
* its primary result. The hash chain remains intact if this succeeds; on
|
|
543
560
|
* failure we've already made the gate decision based on the actual review.
|
|
544
561
|
*/
|
|
545
|
-
async function safeAppend(appendFn, baseDir, toolName, metadata) {
|
|
562
|
+
async function safeAppend(appendFn, baseDir, toolName, policy, metadata) {
|
|
546
563
|
try {
|
|
547
564
|
// Prune undefined values — the audit record schema's `metadata` is an
|
|
548
565
|
// arbitrary map, but `undefined` values cause JSON.stringify to emit
|
|
@@ -552,12 +569,19 @@ async function safeAppend(appendFn, baseDir, toolName, metadata) {
|
|
|
552
569
|
if (v !== undefined)
|
|
553
570
|
cleanMeta[k] = v;
|
|
554
571
|
}
|
|
572
|
+
// 0.19.0 P1-1 fix (backend-engineer review): pass the loaded Policy
|
|
573
|
+
// through so `appendAuditRecord` → `maybeRotate` actually fires.
|
|
574
|
+
// Pre-fix the policy was never threaded; rotation short-circuited
|
|
575
|
+
// to `{ rotated: false }` on the entire push-gate audit-emission
|
|
576
|
+
// path, silently disabling the `audit.rotation: {}` opt-in shipped
|
|
577
|
+
// in 0.18.1 for the bst-internal profile.
|
|
555
578
|
await appendFn(baseDir, {
|
|
556
579
|
tool_name: toolName,
|
|
557
580
|
server_name: AUDIT_SERVER_NAME,
|
|
558
581
|
tier: Tier.Read,
|
|
559
582
|
status: InvocationStatus.Allowed,
|
|
560
583
|
...(Object.keys(cleanMeta).length > 0 ? { metadata: cleanMeta } : {}),
|
|
584
|
+
...(policy !== undefined ? { policy } : {}),
|
|
561
585
|
});
|
|
562
586
|
}
|
|
563
587
|
catch (e) {
|
|
@@ -37,6 +37,21 @@
|
|
|
37
37
|
* audit log alone (helixir #8).
|
|
38
38
|
* - REA_SKIP_CODEX_REVIEW short-circuits BEFORE cache lookup (unchanged).
|
|
39
39
|
*
|
|
40
|
+
* 0.19.0 review fixes:
|
|
41
|
+
* - Concurrent writes are now serialized via `withAuditLock` on the
|
|
42
|
+
* `.rea/` directory (backend-engineer P1-2; security M3). Two
|
|
43
|
+
* concurrent push-gate runs no longer race read-modify-write.
|
|
44
|
+
* - Tmp filenames carry a high-entropy suffix (PID + millis + random)
|
|
45
|
+
* and are unlinked in finally so a crash mid-write doesn't leave
|
|
46
|
+
* stale state (backend-engineer P1-3; code-reviewer P2-1).
|
|
47
|
+
* - All three writers (writeVerdict, clearVerdict, pruneOlderThan,
|
|
48
|
+
* clearAll) route through one `_atomicWrite` helper — no asymmetry
|
|
49
|
+
* between paths (code-reviewer P2-2).
|
|
50
|
+
* - On unrecognized schema_version, reads return undefined AND
|
|
51
|
+
* writes refuse to overwrite — the v3 cache stays intact for a
|
|
52
|
+
* future rea version that knows how to read it (code-reviewer P3-5;
|
|
53
|
+
* backend-engineer P2-2).
|
|
54
|
+
*
|
|
40
55
|
* The cache is OPTIONAL by design: existing callers that don't pass a
|
|
41
56
|
* `cacheImpl` get the legacy stateless path. Tests inject a fake.
|
|
42
57
|
*/
|
|
@@ -66,33 +81,46 @@ export interface VerdictCacheLookupResult {
|
|
|
66
81
|
* miss with `entry: undefined` — the caller proceeds to codex.
|
|
67
82
|
*/
|
|
68
83
|
export declare function lookupVerdict(baseDir: string, headSha: string, now?: Date): VerdictCacheLookupResult;
|
|
69
|
-
/**
|
|
70
|
-
* Write a fresh verdict entry. Atomic via tmp-file + rename. Unrecognized
|
|
71
|
-
* pre-existing entries are preserved (forward-compat for v3+).
|
|
72
|
-
*/
|
|
73
|
-
export declare function writeVerdict(baseDir: string, headSha: string, entry: VerdictCacheEntry): void;
|
|
74
84
|
/**
|
|
75
85
|
* Detect whether a new verdict contradicts a previously-cached verdict
|
|
76
86
|
* on the same SHA. Used by `runPushGate` to set the flip-flag on
|
|
77
87
|
* last-review.json and emit the `verdict_flip` audit event.
|
|
78
88
|
*/
|
|
79
89
|
export declare function isFlip(prior: VerdictCacheEntry | undefined, fresh: ReviewVerdict): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Write a fresh verdict entry. Atomic via tmp-file + rename, serialized
|
|
92
|
+
* via `withAuditLock` on `.rea/`. Refuses to overwrite when the existing
|
|
93
|
+
* cache has an unrecognized schema_version (forward-compat — a v3 cache
|
|
94
|
+
* from a future rea version stays intact for that version to read).
|
|
95
|
+
*/
|
|
96
|
+
export declare function writeVerdict(baseDir: string, headSha: string, entry: VerdictCacheEntry): Promise<void>;
|
|
80
97
|
/**
|
|
81
98
|
* Remove a single SHA from the cache. Returns true if the entry existed.
|
|
82
99
|
*/
|
|
83
|
-
export declare function clearVerdict(baseDir: string, headSha: string): boolean
|
|
100
|
+
export declare function clearVerdict(baseDir: string, headSha: string): Promise<boolean>;
|
|
84
101
|
/**
|
|
85
102
|
* Remove ALL entries from the cache. Returns the count of removed entries.
|
|
86
103
|
*/
|
|
87
|
-
export declare function clearAll(baseDir: string): number
|
|
104
|
+
export declare function clearAll(baseDir: string): Promise<number>;
|
|
88
105
|
/**
|
|
89
106
|
* Remove entries whose `reviewed_at` is older than `olderThanMs` from `now`.
|
|
90
107
|
* Returns the count of removed entries.
|
|
91
108
|
*/
|
|
92
|
-
export declare function pruneOlderThan(baseDir: string, olderThanMs: number, now?: Date): number
|
|
109
|
+
export declare function pruneOlderThan(baseDir: string, olderThanMs: number, now?: Date): Promise<number>;
|
|
93
110
|
/**
|
|
94
111
|
* Read all entries (used by `rea cache stats` / `rea cache show`).
|
|
95
112
|
* Returns empty object on any read error (missing file, malformed JSON,
|
|
96
113
|
* unsupported schema_version).
|
|
97
114
|
*/
|
|
98
115
|
export declare function listEntries(baseDir: string): Record<string, VerdictCacheEntry>;
|
|
116
|
+
/**
|
|
117
|
+
* Thrown by writeVerdict when the existing cache file has an
|
|
118
|
+
* unrecognized schema_version. The caller (push-gate) catches this
|
|
119
|
+
* and treats the write as best-effort failure (log to stderr,
|
|
120
|
+
* continue) rather than overwriting forward-compat data.
|
|
121
|
+
*/
|
|
122
|
+
export declare class VerdictCacheForeignSchemaError extends Error {
|
|
123
|
+
readonly cachePath: string;
|
|
124
|
+
readonly kind: "foreign-schema";
|
|
125
|
+
constructor(cachePath: string);
|
|
126
|
+
}
|
|
@@ -37,11 +37,28 @@
|
|
|
37
37
|
* audit log alone (helixir #8).
|
|
38
38
|
* - REA_SKIP_CODEX_REVIEW short-circuits BEFORE cache lookup (unchanged).
|
|
39
39
|
*
|
|
40
|
+
* 0.19.0 review fixes:
|
|
41
|
+
* - Concurrent writes are now serialized via `withAuditLock` on the
|
|
42
|
+
* `.rea/` directory (backend-engineer P1-2; security M3). Two
|
|
43
|
+
* concurrent push-gate runs no longer race read-modify-write.
|
|
44
|
+
* - Tmp filenames carry a high-entropy suffix (PID + millis + random)
|
|
45
|
+
* and are unlinked in finally so a crash mid-write doesn't leave
|
|
46
|
+
* stale state (backend-engineer P1-3; code-reviewer P2-1).
|
|
47
|
+
* - All three writers (writeVerdict, clearVerdict, pruneOlderThan,
|
|
48
|
+
* clearAll) route through one `_atomicWrite` helper — no asymmetry
|
|
49
|
+
* between paths (code-reviewer P2-2).
|
|
50
|
+
* - On unrecognized schema_version, reads return undefined AND
|
|
51
|
+
* writes refuse to overwrite — the v3 cache stays intact for a
|
|
52
|
+
* future rea version that knows how to read it (code-reviewer P3-5;
|
|
53
|
+
* backend-engineer P2-2).
|
|
54
|
+
*
|
|
40
55
|
* The cache is OPTIONAL by design: existing callers that don't pass a
|
|
41
56
|
* `cacheImpl` get the legacy stateless path. Tests inject a fake.
|
|
42
57
|
*/
|
|
58
|
+
import crypto from 'node:crypto';
|
|
43
59
|
import fs from 'node:fs';
|
|
44
60
|
import path from 'node:path';
|
|
61
|
+
import { withAuditLock } from '../../audit/fs.js';
|
|
45
62
|
export const VERDICT_CACHE_FILE = 'last-review.cache.json';
|
|
46
63
|
export const VERDICT_CACHE_SCHEMA_VERSION = 2;
|
|
47
64
|
export const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1_000; // 24h
|
|
@@ -66,25 +83,6 @@ export function lookupVerdict(baseDir, headSha, now = new Date()) {
|
|
|
66
83
|
}
|
|
67
84
|
return { hit: true, entry };
|
|
68
85
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Write a fresh verdict entry. Atomic via tmp-file + rename. Unrecognized
|
|
71
|
-
* pre-existing entries are preserved (forward-compat for v3+).
|
|
72
|
-
*/
|
|
73
|
-
export function writeVerdict(baseDir, headSha, entry) {
|
|
74
|
-
const reaDir = path.join(baseDir, '.rea');
|
|
75
|
-
if (!fs.existsSync(reaDir)) {
|
|
76
|
-
fs.mkdirSync(reaDir, { recursive: true });
|
|
77
|
-
}
|
|
78
|
-
const cachePath = path.join(reaDir, VERDICT_CACHE_FILE);
|
|
79
|
-
const existing = readCacheFile(baseDir);
|
|
80
|
-
const next = {
|
|
81
|
-
schema_version: VERDICT_CACHE_SCHEMA_VERSION,
|
|
82
|
-
entries: { ...(existing?.entries ?? {}), [headSha]: entry },
|
|
83
|
-
};
|
|
84
|
-
const tmp = `${cachePath}.tmp.${process.pid}`;
|
|
85
|
-
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
86
|
-
fs.renameSync(tmp, cachePath);
|
|
87
|
-
}
|
|
88
86
|
/**
|
|
89
87
|
* Detect whether a new verdict contradicts a previously-cached verdict
|
|
90
88
|
* on the same SHA. Used by `runPushGate` to set the flip-flag on
|
|
@@ -95,68 +93,99 @@ export function isFlip(prior, fresh) {
|
|
|
95
93
|
return false;
|
|
96
94
|
return prior.verdict !== fresh;
|
|
97
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Write a fresh verdict entry. Atomic via tmp-file + rename, serialized
|
|
98
|
+
* via `withAuditLock` on `.rea/`. Refuses to overwrite when the existing
|
|
99
|
+
* cache has an unrecognized schema_version (forward-compat — a v3 cache
|
|
100
|
+
* from a future rea version stays intact for that version to read).
|
|
101
|
+
*/
|
|
102
|
+
export async function writeVerdict(baseDir, headSha, entry) {
|
|
103
|
+
const reaDir = path.join(baseDir, '.rea');
|
|
104
|
+
if (!fs.existsSync(reaDir)) {
|
|
105
|
+
fs.mkdirSync(reaDir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
const cachePath = path.join(reaDir, VERDICT_CACHE_FILE);
|
|
108
|
+
await withAuditLock(cachePath, async () => {
|
|
109
|
+
if (foreignSchemaPresent(baseDir)) {
|
|
110
|
+
throw new VerdictCacheForeignSchemaError(cachePath);
|
|
111
|
+
}
|
|
112
|
+
const existing = readCacheFile(baseDir);
|
|
113
|
+
const next = {
|
|
114
|
+
schema_version: VERDICT_CACHE_SCHEMA_VERSION,
|
|
115
|
+
entries: { ...(existing?.entries ?? {}), [headSha]: entry },
|
|
116
|
+
};
|
|
117
|
+
_atomicWriteJson(cachePath, next);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
98
120
|
/**
|
|
99
121
|
* Remove a single SHA from the cache. Returns true if the entry existed.
|
|
100
122
|
*/
|
|
101
|
-
export function clearVerdict(baseDir, headSha) {
|
|
102
|
-
const file = readCacheFile(baseDir);
|
|
103
|
-
if (file === undefined || file.entries[headSha] === undefined)
|
|
104
|
-
return false;
|
|
105
|
-
const next = {
|
|
106
|
-
schema_version: VERDICT_CACHE_SCHEMA_VERSION,
|
|
107
|
-
entries: { ...file.entries },
|
|
108
|
-
};
|
|
109
|
-
delete next.entries[headSha];
|
|
123
|
+
export async function clearVerdict(baseDir, headSha) {
|
|
110
124
|
const cachePath = path.join(baseDir, '.rea', VERDICT_CACHE_FILE);
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
return withAuditLock(cachePath, async () => {
|
|
126
|
+
const file = readCacheFile(baseDir);
|
|
127
|
+
if (file === undefined || file.entries[headSha] === undefined)
|
|
128
|
+
return false;
|
|
129
|
+
const next = {
|
|
130
|
+
schema_version: VERDICT_CACHE_SCHEMA_VERSION,
|
|
131
|
+
entries: { ...file.entries },
|
|
132
|
+
};
|
|
133
|
+
delete next.entries[headSha];
|
|
134
|
+
_atomicWriteJson(cachePath, next);
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
113
137
|
}
|
|
114
138
|
/**
|
|
115
139
|
* Remove ALL entries from the cache. Returns the count of removed entries.
|
|
116
140
|
*/
|
|
117
|
-
export function clearAll(baseDir) {
|
|
118
|
-
const
|
|
119
|
-
const cachePath = path.join(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
schema_version: VERDICT_CACHE_SCHEMA_VERSION,
|
|
123
|
-
entries: {},
|
|
124
|
-
};
|
|
125
|
-
if (!fs.existsSync(path.dirname(cachePath))) {
|
|
126
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
141
|
+
export async function clearAll(baseDir) {
|
|
142
|
+
const reaDir = path.join(baseDir, '.rea');
|
|
143
|
+
const cachePath = path.join(reaDir, VERDICT_CACHE_FILE);
|
|
144
|
+
if (!fs.existsSync(reaDir)) {
|
|
145
|
+
fs.mkdirSync(reaDir, { recursive: true });
|
|
127
146
|
}
|
|
128
|
-
|
|
129
|
-
|
|
147
|
+
return withAuditLock(cachePath, async () => {
|
|
148
|
+
const file = readCacheFile(baseDir);
|
|
149
|
+
const count = file === undefined ? 0 : Object.keys(file.entries).length;
|
|
150
|
+
const empty = {
|
|
151
|
+
schema_version: VERDICT_CACHE_SCHEMA_VERSION,
|
|
152
|
+
entries: {},
|
|
153
|
+
};
|
|
154
|
+
_atomicWriteJson(cachePath, empty);
|
|
155
|
+
return count;
|
|
156
|
+
});
|
|
130
157
|
}
|
|
131
158
|
/**
|
|
132
159
|
* Remove entries whose `reviewed_at` is older than `olderThanMs` from `now`.
|
|
133
160
|
* Returns the count of removed entries.
|
|
134
161
|
*/
|
|
135
|
-
export function pruneOlderThan(baseDir, olderThanMs, now = new Date()) {
|
|
136
|
-
const file = readCacheFile(baseDir);
|
|
137
|
-
if (file === undefined)
|
|
138
|
-
return 0;
|
|
139
|
-
const cutoff = now.getTime() - olderThanMs;
|
|
140
|
-
const surviving = {};
|
|
141
|
-
let removed = 0;
|
|
142
|
-
for (const [sha, entry] of Object.entries(file.entries)) {
|
|
143
|
-
const reviewedAtMs = Date.parse(entry.reviewed_at);
|
|
144
|
-
if (Number.isNaN(reviewedAtMs) || reviewedAtMs >= cutoff) {
|
|
145
|
-
surviving[sha] = entry;
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
removed += 1;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (removed === 0)
|
|
152
|
-
return 0;
|
|
153
|
-
const next = {
|
|
154
|
-
schema_version: VERDICT_CACHE_SCHEMA_VERSION,
|
|
155
|
-
entries: surviving,
|
|
156
|
-
};
|
|
162
|
+
export async function pruneOlderThan(baseDir, olderThanMs, now = new Date()) {
|
|
157
163
|
const cachePath = path.join(baseDir, '.rea', VERDICT_CACHE_FILE);
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
return withAuditLock(cachePath, async () => {
|
|
165
|
+
const file = readCacheFile(baseDir);
|
|
166
|
+
if (file === undefined)
|
|
167
|
+
return 0;
|
|
168
|
+
const cutoff = now.getTime() - olderThanMs;
|
|
169
|
+
const surviving = {};
|
|
170
|
+
let removed = 0;
|
|
171
|
+
for (const [sha, entry] of Object.entries(file.entries)) {
|
|
172
|
+
const reviewedAtMs = Date.parse(entry.reviewed_at);
|
|
173
|
+
if (Number.isNaN(reviewedAtMs) || reviewedAtMs >= cutoff) {
|
|
174
|
+
surviving[sha] = entry;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
removed += 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (removed === 0)
|
|
181
|
+
return 0;
|
|
182
|
+
const next = {
|
|
183
|
+
schema_version: VERDICT_CACHE_SCHEMA_VERSION,
|
|
184
|
+
entries: surviving,
|
|
185
|
+
};
|
|
186
|
+
_atomicWriteJson(cachePath, next);
|
|
187
|
+
return removed;
|
|
188
|
+
});
|
|
160
189
|
}
|
|
161
190
|
/**
|
|
162
191
|
* Read all entries (used by `rea cache stats` / `rea cache show`).
|
|
@@ -167,18 +196,43 @@ export function listEntries(baseDir) {
|
|
|
167
196
|
const file = readCacheFile(baseDir);
|
|
168
197
|
return file?.entries ?? {};
|
|
169
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Thrown by writeVerdict when the existing cache file has an
|
|
201
|
+
* unrecognized schema_version. The caller (push-gate) catches this
|
|
202
|
+
* and treats the write as best-effort failure (log to stderr,
|
|
203
|
+
* continue) rather than overwriting forward-compat data.
|
|
204
|
+
*/
|
|
205
|
+
export class VerdictCacheForeignSchemaError extends Error {
|
|
206
|
+
cachePath;
|
|
207
|
+
kind = 'foreign-schema';
|
|
208
|
+
constructor(cachePath) {
|
|
209
|
+
super(`Refused to overwrite ${cachePath}: existing cache has unrecognized schema_version. ` +
|
|
210
|
+
`Either delete the file or run with a newer rea that supports it.`);
|
|
211
|
+
this.cachePath = cachePath;
|
|
212
|
+
this.name = 'VerdictCacheForeignSchemaError';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
170
215
|
function readCacheFile(baseDir) {
|
|
216
|
+
const parsed = readForeignCacheFile(baseDir);
|
|
217
|
+
if (parsed === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
if (parsed.schema_version !== VERDICT_CACHE_SCHEMA_VERSION)
|
|
220
|
+
return undefined;
|
|
221
|
+
// We checked schema_version exactly; entries shape is the v2 contract.
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
function readForeignCacheFile(baseDir) {
|
|
171
225
|
const cachePath = path.join(baseDir, '.rea', VERDICT_CACHE_FILE);
|
|
172
226
|
if (!fs.existsSync(cachePath))
|
|
173
227
|
return undefined;
|
|
174
228
|
try {
|
|
175
229
|
const raw = fs.readFileSync(cachePath, 'utf8');
|
|
176
230
|
const parsed = JSON.parse(raw);
|
|
177
|
-
if (typeof parsed !== 'object' ||
|
|
178
|
-
|
|
179
|
-
|
|
231
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
232
|
+
return undefined;
|
|
233
|
+
const sv = parsed.schema_version;
|
|
234
|
+
if (typeof sv !== 'number')
|
|
180
235
|
return undefined;
|
|
181
|
-
}
|
|
182
236
|
const entries = parsed.entries;
|
|
183
237
|
if (typeof entries !== 'object' || entries === null)
|
|
184
238
|
return undefined;
|
|
@@ -188,3 +242,35 @@ function readCacheFile(baseDir) {
|
|
|
188
242
|
return undefined;
|
|
189
243
|
}
|
|
190
244
|
}
|
|
245
|
+
function foreignSchemaPresent(baseDir) {
|
|
246
|
+
const parsed = readForeignCacheFile(baseDir);
|
|
247
|
+
if (parsed === undefined)
|
|
248
|
+
return false;
|
|
249
|
+
return parsed.schema_version !== VERDICT_CACHE_SCHEMA_VERSION;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Atomic JSON write: stringify → write tmp → fsync → rename.
|
|
253
|
+
*
|
|
254
|
+
* Tmp filename: `${target}.tmp.${pid}.${ms}.${random8}` — collision-
|
|
255
|
+
* resistant under concurrent writes, PID reuse, and same-process
|
|
256
|
+
* parallel calls. On any failure, the tmp file is unlinked so a crash
|
|
257
|
+
* mid-write doesn't leave stale state.
|
|
258
|
+
*/
|
|
259
|
+
function _atomicWriteJson(targetPath, payload) {
|
|
260
|
+
const tmp = `${targetPath}.tmp.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}`;
|
|
261
|
+
try {
|
|
262
|
+
fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
263
|
+
fs.renameSync(tmp, targetPath);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
try {
|
|
267
|
+
if (fs.existsSync(tmp))
|
|
268
|
+
fs.unlinkSync(tmp);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Tmp already gone or unlink failed — caller's error is the
|
|
272
|
+
// important signal.
|
|
273
|
+
}
|
|
274
|
+
throw e;
|
|
275
|
+
}
|
|
276
|
+
}
|
package/dist/policy/loader.js
CHANGED
|
@@ -58,7 +58,12 @@ const ReviewPolicySchema = z
|
|
|
58
58
|
* NOT want to lock consumers to a hardcoded enum that drifts behind
|
|
59
59
|
* upstream. Codex itself validates the model name at exec time.
|
|
60
60
|
*/
|
|
61
|
-
|
|
61
|
+
// 0.19.0 security review M4: restrict to a safe character class so
|
|
62
|
+
// a typo or malicious value can't smuggle TOML control characters
|
|
63
|
+
// (NUL, NL, CR, escape sequences) through the `-c model="<value>"`
|
|
64
|
+
// injection point. Accepts published codex model names; rejects
|
|
65
|
+
// re-quote / TOML-escape edge cases.
|
|
66
|
+
codex_model: z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/).optional(),
|
|
62
67
|
/**
|
|
63
68
|
* Codex reasoning effort knob (0.13.4+). Pinned via
|
|
64
69
|
* `-c model_reasoning_effort="<level>"` on every invocation. Only
|
|
@@ -181,7 +181,14 @@ _rea_unwrap_nested_shells() {
|
|
|
181
181
|
# alternation `(^|[[:space:]&|;])` therefore cannot anchor on a
|
|
182
182
|
# masked separator, and the shell-name token itself can no longer
|
|
183
183
|
# appear adjacent to a masked quote-introducer.
|
|
184
|
-
|
|
184
|
+
# 0.19.0 security review M1: extend the shell-name set to cover
|
|
185
|
+
# every commonly-installed POSIX-style shell. mksh / oksh / yash /
|
|
186
|
+
# posh ship on minimal containers, csh/tcsh on legacy macOS,
|
|
187
|
+
# fish on dev workstations. Each accepts -c with a quoted body.
|
|
188
|
+
# NOTE: pwsh (PowerShell) uses -Command / -EncodedCommand and is
|
|
189
|
+
# NOT covered here. Adding pwsh requires a separate code path
|
|
190
|
+
# because EncodedCommand base64-decodes at runtime.
|
|
191
|
+
WRAP = "(^|[[:space:]&|;])(bash|sh|zsh|dash|ksh|mksh|oksh|posh|yash|csh|tcsh|fish)([[:space:]]+-[a-zA-Z]+)*[[:space:]]+-(c|lc|lic|ic|cl|cli|li|il)[[:space:]]+"
|
|
185
192
|
# Track the cursor in BOTH raw and masked. Because the mask is
|
|
186
193
|
# byte-for-byte width-preserving, the same RSTART/RLENGTH applies
|
|
187
194
|
# to both — but each iteration of the loop must SLICE both strings
|
|
@@ -45,6 +45,10 @@ REA_PROTECTED_PATTERNS_FULL=(
|
|
|
45
45
|
'.husky/'
|
|
46
46
|
'.rea/policy.yaml'
|
|
47
47
|
'.rea/HALT'
|
|
48
|
+
# 0.19.0 security review C1: the verdict cache is a security boundary
|
|
49
|
+
# since 0.18.1. A forged entry would skip codex on next push of that
|
|
50
|
+
# SHA. Protect it like the kill-switch.
|
|
51
|
+
'.rea/last-review.cache.json'
|
|
48
52
|
)
|
|
49
53
|
|
|
50
54
|
# Kill-switch invariants — never relaxable. Subset of FULL.
|
|
@@ -52,6 +56,7 @@ REA_KILL_SWITCH_INVARIANTS=(
|
|
|
52
56
|
'.claude/settings.json'
|
|
53
57
|
'.rea/policy.yaml'
|
|
54
58
|
'.rea/HALT'
|
|
59
|
+
'.rea/last-review.cache.json'
|
|
55
60
|
)
|
|
56
61
|
|
|
57
62
|
# Effective patterns after applying the relax list. Computed lazily on
|
|
@@ -102,7 +102,7 @@ FOUND=0
|
|
|
102
102
|
# below catches Co-Authored-By with named tools regardless of the email
|
|
103
103
|
# domain, so dropping `users.noreply.github.com` from the noreply
|
|
104
104
|
# pattern only relaxes the check for human collaborators — never for AI.
|
|
105
|
-
if any_segment_matches "$CMD" 'Co-Authored-By:.*noreply@(anthropic\.com|openai\.com|github-copilot|github\.com|claude\.ai|chatgpt\.com|googlemail\.com|google\.com|cursor\.com|codeium\.com|tabnine\.com|amazon\.com|amazonaws\.com|amazon-q\.amazonaws\.com|cody\.dev|sourcegraph\.com)'; then
|
|
105
|
+
if any_segment_matches "$CMD" 'Co-Authored-By:.*noreply@(anthropic\.com|openai\.com|github-copilot|github\.com|claude\.ai|chatgpt\.com|googlemail\.com|google\.com|cursor\.com|codeium\.com|tabnine\.com|amazon\.com|amazonaws\.com|amazon-q\.amazonaws\.com|cody\.dev|sourcegraph\.com|mistral\.ai|xai-org|x\.ai|inflection\.ai|perplexity\.ai|replit\.com|jetbrains\.com|bito\.ai|pieces\.app|phind\.com|you\.com)'; then
|
|
106
106
|
FOUND=1
|
|
107
107
|
fi
|
|
108
108
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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)",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
85
85
|
"@typescript-eslint/parser": "^8.0.0",
|
|
86
86
|
"@vitest/coverage-v8": "^3.2.4",
|
|
87
|
+
"ajv": "^8.17.1",
|
|
87
88
|
"eslint": "^10.2.0",
|
|
88
89
|
"prettier": "^3.8.1",
|
|
89
90
|
"typescript": "^5.8.0",
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -135,10 +135,17 @@ try {
|
|
|
135
135
|
const reaCli = path.join(consumerRoot, 'node_modules', '.bin', 'rea');
|
|
136
136
|
if (fs.existsSync(reaCli)) {
|
|
137
137
|
const { spawnSync } = await import('node:child_process');
|
|
138
|
+
// 0.19.0 backend-engineer P2-1: 5-min wall-clock cap so a hung
|
|
139
|
+
// upgrade falls through to print-only instead of hanging the
|
|
140
|
+
// consumer's `npm install`. 0.19.0 code-reviewer P3-6:
|
|
141
|
+
// Windows shim (.bin/rea.cmd) requires `shell: true` —
|
|
142
|
+
// detect via process.platform.
|
|
138
143
|
const res = spawnSync(reaCli, ['upgrade', '--yes'], {
|
|
139
144
|
cwd: consumerRoot,
|
|
140
145
|
stdio: 'inherit',
|
|
141
146
|
env: process.env,
|
|
147
|
+
timeout: 5 * 60 * 1000,
|
|
148
|
+
shell: process.platform === 'win32',
|
|
142
149
|
});
|
|
143
150
|
if (res.status === 0) {
|
|
144
151
|
NOTE([
|