@bookedsolid/rea 0.27.0 → 0.28.1

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.
@@ -0,0 +1,51 @@
1
+ {
2
+ "id": "helix-022",
3
+ "title": "Bash-tier bypass classes (F1 parent-doesn't-exist symlink walk-up; F2 interpreter writes; F3 nested-shell unwrap depth; F4 cp/mv argv walker; F5 fail-closed unresolved expansion in redirect targets)",
4
+ "introduced_in": "<=0.21.0",
5
+ "closed_in": "0.22.0",
6
+ "summary": "Five adjacent Bash-tier bypass classes against 0.21.0 closed by structural fixes: nearest-existing-ancestor resolution, shared interpreter-scanner.sh, fixed-point recursion (depth bound 8) for nested shells, explicit cp/mv destination walker, and __rea_unresolved_expansion__ sentinel for $-substitution / backticks in redirect targets.",
7
+ "pocs": [
8
+ {
9
+ "id": "F1.symlink-walkup-parent-missing",
10
+ "type": "scan-bash",
11
+ "input": "ln -s ../../etc/passwd .rea/no-such-dir/x; echo y > .rea/no-such-dir/x",
12
+ "mode": "protected",
13
+ "expected_verdict": "block"
14
+ },
15
+ {
16
+ "id": "F2.interpreter-write-node",
17
+ "type": "scan-bash",
18
+ "input": "node -e 'require(\"fs\").writeFileSync(\".rea/HALT\", \"x\")'",
19
+ "mode": "protected",
20
+ "expected_verdict": "block"
21
+ },
22
+ {
23
+ "id": "F2.interpreter-write-python",
24
+ "type": "scan-bash",
25
+ "input": "python3 -c 'open(\".rea/HALT\", \"w\").write(\"x\")'",
26
+ "mode": "protected",
27
+ "expected_verdict": "block"
28
+ },
29
+ {
30
+ "id": "F3.nested-shell-depth",
31
+ "type": "scan-bash",
32
+ "input": "bash -c 'bash -c \"bash -c \\\"echo x > .rea/HALT\\\"\"'",
33
+ "mode": "protected",
34
+ "expected_verdict": "block"
35
+ },
36
+ {
37
+ "id": "F4.cp-explicit-destination",
38
+ "type": "scan-bash",
39
+ "input": "cp /tmp/source -t .rea -- HALT",
40
+ "mode": "protected",
41
+ "expected_verdict": "block"
42
+ },
43
+ {
44
+ "id": "F5.unresolved-expansion-redirect",
45
+ "type": "scan-bash",
46
+ "input": "echo x > .rea/$(printf HALT)",
47
+ "mode": "protected",
48
+ "expected_verdict": "block"
49
+ }
50
+ ]
51
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "id": "helix-023",
3
+ "title": "0.23.0 structural rewrite — bash hooks → Node-binary parser-backed scanner via mvdan-sh AST",
4
+ "introduced_in": "<=0.22.0",
5
+ "closed_in": "0.23.0",
6
+ "summary": "13 codex adversarial rounds + 12,875-fixture corpus across 23 classes A-W. Closure ladder 34 → 14 → 9 → 8 → 5 → 2 → 1 → 1 → 3 → 2 → 7 → 9 → 9 (round-13 deferred to 0.24.0 as denylist-asymptotic per codex). PoCs sample one fixture from each closure-ladder round to verify the parser-backed scanner still rejects them.",
7
+ "pocs": [
8
+ {
9
+ "id": "round1.alias-then-invoke",
10
+ "type": "scan-bash",
11
+ "input": "alias evil='echo x > .rea/HALT'; evil",
12
+ "mode": "protected",
13
+ "expected_verdict": "block"
14
+ },
15
+ {
16
+ "id": "round1.heredoc-into-shell",
17
+ "type": "scan-bash",
18
+ "input": "bash <<'EOF'\necho x > .rea/HALT\nEOF",
19
+ "mode": "protected",
20
+ "expected_verdict": "block"
21
+ },
22
+ {
23
+ "id": "round1.process-substitution",
24
+ "type": "scan-bash",
25
+ "input": "diff <(echo a) <(rm -rf .rea)",
26
+ "mode": "protected",
27
+ "expected_verdict": "block"
28
+ },
29
+ {
30
+ "id": "round2.tee-redirect",
31
+ "type": "scan-bash",
32
+ "input": "echo x | tee .rea/HALT",
33
+ "mode": "protected",
34
+ "expected_verdict": "block"
35
+ },
36
+ {
37
+ "id": "round3.pipe-into-bash",
38
+ "type": "scan-bash",
39
+ "input": "echo 'echo x > .rea/HALT' | bash",
40
+ "mode": "protected",
41
+ "expected_verdict": "block"
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "id": "helix-024",
3
+ "title": "Round-24 kill-switch bypass classes (cd-cwd, double-eval, ln-symlink) — 9 PoCs",
4
+ "introduced_in": "0.23.0",
5
+ "closed_in": "0.23.1",
6
+ "summary": "Three P1 bypass classes against 0.23.0 closed via cd-target tracking + bash-eval payload re-parse + ln-symlink target sandbox. 5 codex local pre-push rounds caught every refinement-introduced over-block. PR #131 landed green-first-try.",
7
+ "pocs": [
8
+ {
9
+ "id": "F1.1.cd-and-write-literal",
10
+ "type": "scan-bash",
11
+ "input": "cd .rea && echo x > HALT",
12
+ "mode": "protected",
13
+ "expected_verdict": "block"
14
+ },
15
+ {
16
+ "id": "F1.2.cd-with-quoted-target",
17
+ "type": "scan-bash",
18
+ "input": "cd '.rea' && echo x > HALT",
19
+ "mode": "protected",
20
+ "expected_verdict": "block"
21
+ },
22
+ {
23
+ "id": "F1.3.cd-then-write-via-redirect",
24
+ "type": "scan-bash",
25
+ "input": "cd .husky; echo x > pre-push",
26
+ "mode": "protected",
27
+ "expected_verdict": "block"
28
+ },
29
+ {
30
+ "id": "F1.4.pushd-then-write",
31
+ "type": "scan-bash",
32
+ "input": "pushd .rea && cat /tmp/payload > HALT",
33
+ "mode": "protected",
34
+ "expected_verdict": "block"
35
+ },
36
+ {
37
+ "id": "F1.5.cd-with-binary-and-write",
38
+ "type": "scan-bash",
39
+ "input": "cd .claude && echo x > settings.json",
40
+ "mode": "protected",
41
+ "expected_verdict": "block"
42
+ },
43
+ {
44
+ "id": "F2.1.double-eval-string",
45
+ "type": "scan-bash",
46
+ "input": "eval 'echo x > .rea/HALT'",
47
+ "mode": "protected",
48
+ "expected_verdict": "block"
49
+ },
50
+ {
51
+ "id": "F2.2.eval-bash-c-string",
52
+ "type": "scan-bash",
53
+ "input": "eval \"bash -c 'echo x > .rea/HALT'\"",
54
+ "mode": "protected",
55
+ "expected_verdict": "block"
56
+ },
57
+ {
58
+ "id": "F3.1.ln-symlink-into-protected",
59
+ "type": "scan-bash",
60
+ "input": "ln -s /tmp/x .rea/HALT",
61
+ "mode": "protected",
62
+ "expected_verdict": "block"
63
+ },
64
+ {
65
+ "id": "F3.2.ln-symlink-into-husky",
66
+ "type": "scan-bash",
67
+ "input": "ln -sf /tmp/payload .husky/pre-push",
68
+ "mode": "protected",
69
+ "expected_verdict": "block"
70
+ }
71
+ ]
72
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "helix-028",
3
+ "title": "0.23.0 cmd-segments multiline awk + ANSI-C bypass classes",
4
+ "introduced_in": "0.23.0",
5
+ "closed_in": "0.26.1",
6
+ "summary": "Multiline awk inside command-substitutions and ANSI-C $'...' string literals were silently dropped from segment scanning. Two P1 bypass classes closed in 0.26.1: awk multiline body now flagged when destination is protected; ANSI-C strings normalize to their decoded form before scanning.",
7
+ "pocs": [
8
+ {
9
+ "id": "multiline-awk-redirect",
10
+ "type": "scan-bash",
11
+ "input": "awk 'BEGIN { print \"x\" > \".rea/HALT\" }'",
12
+ "mode": "protected",
13
+ "expected_verdict": "block"
14
+ },
15
+ {
16
+ "id": "ansi-c-string-redirect",
17
+ "type": "scan-bash",
18
+ "input": "echo $'\\x78' > .rea/HALT",
19
+ "mode": "protected",
20
+ "expected_verdict": "block"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "helix-031",
3
+ "title": "shellcheck SC1078 false positive on cmd-segments.sh closed by removing legacy bash module (0.27.0)",
4
+ "introduced_in": "0.26.1",
5
+ "closed_in": "0.27.0",
6
+ "summary": "0.26.1 added an ANSI-C handling path inside hooks/_lib/cmd-segments.sh that confused shellcheck SC1078 (mismatched single-quote in unclosed-string). 0.27.0 removed the legacy bash cmd-segments path entirely (the parser-backed Node scanner has owned the segmentation work since 0.23.0). This claim verifies that shellcheck runs clean on every shipped hook.",
7
+ "pocs": [
8
+ {
9
+ "id": "SC1078-clean-protected-paths-bash-gate",
10
+ "type": "shellcheck",
11
+ "target": "hooks/protected-paths-bash-gate.sh",
12
+ "expected_verdict": "clean"
13
+ },
14
+ {
15
+ "id": "SC1078-clean-blocked-paths-bash-gate",
16
+ "type": "shellcheck",
17
+ "target": "hooks/blocked-paths-bash-gate.sh",
18
+ "expected_verdict": "clean"
19
+ },
20
+ {
21
+ "id": "SC1078-clean-local-review-gate",
22
+ "type": "shellcheck",
23
+ "target": "hooks/local-review-gate.sh",
24
+ "expected_verdict": "clean"
25
+ }
26
+ ]
27
+ }
package/dist/cli/index.js CHANGED
@@ -12,6 +12,7 @@ import { runServe } from './serve.js';
12
12
  import { runStatus } from './status.js';
13
13
  import { runTofuAccept, runTofuList } from './tofu.js';
14
14
  import { runUpgrade } from './upgrade.js';
15
+ import { registerVerifyClaimCommand } from './verify-claim.js';
15
16
  import { err, getPkgVersion } from './utils.js';
16
17
  async function main() {
17
18
  const program = new Command();
@@ -115,6 +116,11 @@ async function main() {
115
116
  // `local-review-gate.sh` hook both delegate to `rea preflight --strict`.
116
117
  registerReviewCommand(program);
117
118
  registerPreflightCommand(program);
119
+ // 0.28.0 — `rea verify-claim <claim-id>` replays recorded
120
+ // security-claim PoC batteries against the CLI under test. The
121
+ // centerpiece of 0.28.0 (4th structural pivot — claims as
122
+ // machine-verifiable artifacts).
123
+ registerVerifyClaimCommand(program);
118
124
  const tofu = program
119
125
  .command('tofu')
120
126
  .description('TOFU fingerprint operations (G7) — inspect and rebase `.rea/fingerprints.json` when a legitimate registry edit has triggered drift fail-close. Emits audit records.');
@@ -111,6 +111,18 @@ export interface LocalReviewLookupResult {
111
111
  * (back-compat / fallback).
112
112
  */
113
113
  match_kind?: 'content_token' | 'head_sha';
114
+ /**
115
+ * 0.28.0 round-29 P3 — set when the most recent path-matching audit
116
+ * entry for this HEAD had verdict `blocking` (or `error`) and was
117
+ * therefore skipped as "not coverage". Surfacing this lets the
118
+ * preflight caller render a clearer message than "no recent local-
119
+ * review audit entry covers HEAD" — the operator hasn't forgotten
120
+ * to review, they've already done one and it told them to fix
121
+ * findings.
122
+ */
123
+ last_blocking_verdict?: 'blocking' | 'error';
124
+ /** ISO timestamp of the last blocking entry, when present. */
125
+ last_blocking_timestamp?: string;
114
126
  }
115
127
  export declare function findRecentLocalReview(baseDir: string, headSha: string, maxAgeSeconds: number, now?: Date, contentToken?: string): LocalReviewLookupResult;
116
128
  /**
@@ -124,10 +124,20 @@ export async function computePreflight(baseDir, options, env = process.env) {
124
124
  if (!reviewCheckSkipped) {
125
125
  const lookup = findRecentLocalReview(baseDir, headSha, maxAgeSeconds, new Date(), contentToken);
126
126
  if (!lookup.found) {
127
+ // 0.28.0 round-29 P3: when the most recent path-matching audit
128
+ // entry was blocking/error, the operator HAS reviewed — they
129
+ // just need to address the findings. The original message ("no
130
+ // recent local-review audit entry covers HEAD") makes them
131
+ // think they forgot to review. Distinguish the two cases.
132
+ const reason = lookup.last_blocking_verdict === 'blocking'
133
+ ? 'your last local review was blocking — address findings or override'
134
+ : lookup.last_blocking_verdict === 'error'
135
+ ? 'your last local review errored — re-run `rea review` and address findings'
136
+ : 'no recent local-review audit entry covers HEAD';
127
137
  return {
128
138
  outcome: {
129
139
  status: 'refuse',
130
- reason: 'no recent local-review audit entry covers HEAD',
140
+ reason,
131
141
  exitCode: 2,
132
142
  details: {
133
143
  head_sha: headSha,
@@ -135,6 +145,12 @@ export async function computePreflight(baseDir, options, env = process.env) {
135
145
  max_age_seconds: maxAgeSeconds,
136
146
  bypass_env_var: bypassEnvVar,
137
147
  policy_off_switch: 'policy.review.local_review.mode: off',
148
+ ...(lookup.last_blocking_verdict !== undefined
149
+ ? {
150
+ last_blocking_verdict: lookup.last_blocking_verdict,
151
+ last_blocking_timestamp: lookup.last_blocking_timestamp,
152
+ }
153
+ : {}),
138
154
  },
139
155
  },
140
156
  policy,
@@ -292,6 +308,21 @@ function resolveCommitCountBase(baseDir) {
292
308
  if (resolveRef(baseDir, ref).length > 0)
293
309
  return ref;
294
310
  }
311
+ // 0.28.0 round-29 P3: develop-branch repos sometimes omit
312
+ // `origin/HEAD` entirely (the symbolic ref is unset until a fresh
313
+ // `git remote set-head origin -a`), and `origin/main` /
314
+ // `origin/master` may not exist when the trunk is `origin/develop`.
315
+ // Pre-fix the resolver silently fell through to `null`, disabling
316
+ // the auto-narrow check without telling the operator. Emit a single
317
+ // advisory line on stderr so the failure mode is visible — but do
318
+ // not fail; this path is best-effort and a missing trunk is a
319
+ // recoverable misconfiguration.
320
+ if (resolveRef(baseDir, 'origin/develop').length > 0) {
321
+ process.stderr.write(`rea: preflight commit-count base falling through to origin/develop ` +
322
+ `(origin/HEAD/main/master not resolvable). ` +
323
+ `Consider: \`git remote set-head origin -a\` to seed origin/HEAD.\n`);
324
+ return 'origin/develop';
325
+ }
295
326
  // `@{upstream}` LAST. We additionally probe what it resolves to —
296
327
  // if it's a remote feature-branch ref under `refs/remotes/origin/`
297
328
  // (not a primary trunk ref we already tried), the candidate is
@@ -365,6 +396,13 @@ export function findRecentLocalReview(baseDir, headSha, maxAgeSeconds, now = new
365
396
  }
366
397
  const lines = raw.split(/\r?\n/);
367
398
  const cutoffMs = now.getTime() - maxAgeSeconds * 1000;
399
+ // 0.28.0 round-29 P3: track the most recent blocking/error entry that
400
+ // path-matched HEAD even though it didn't qualify as coverage. The
401
+ // not-found return path consumes this so the operator-facing message
402
+ // distinguishes "you haven't reviewed" from "your review found
403
+ // problems".
404
+ let lastBlockingVerdict;
405
+ let lastBlockingTimestamp;
368
406
  // Walk in reverse — most recent first.
369
407
  for (let i = lines.length - 1; i >= 0; i--) {
370
408
  const line = lines[i];
@@ -421,8 +459,19 @@ export function findRecentLocalReview(baseDir, headSha, maxAgeSeconds, now = new
421
459
  if (matchKind === null)
422
460
  continue;
423
461
  const verdict = typeof metadata.verdict === 'string' ? metadata.verdict : '';
424
- if (verdict === 'error' || verdict === 'blocking')
462
+ if (verdict === 'error' || verdict === 'blocking') {
463
+ // 0.28.0 round-29 P3 — capture the FIRST (i.e., most-recent in
464
+ // reverse walk) blocking/error entry that path-matched HEAD so
465
+ // the not-found path can render a better message. Don't
466
+ // overwrite a later catch — we want the most-recent one.
467
+ if (lastBlockingVerdict === undefined) {
468
+ lastBlockingVerdict = verdict;
469
+ const ts = typeof record.timestamp === 'string' ? record.timestamp : '';
470
+ if (ts.length > 0)
471
+ lastBlockingTimestamp = ts;
472
+ }
425
473
  continue;
474
+ }
426
475
  const timestamp = typeof record.timestamp === 'string' ? record.timestamp : '';
427
476
  if (timestamp.length > 0) {
428
477
  const ts = Date.parse(timestamp);
@@ -430,7 +479,13 @@ export function findRecentLocalReview(baseDir, headSha, maxAgeSeconds, now = new
430
479
  // Older than max_age_seconds — keep walking; a more recent valid
431
480
  // record may exist further back? No: we walk newest-to-oldest so
432
481
  // anything older from here on is also stale. Stop early.
433
- return { found: false };
482
+ return {
483
+ found: false,
484
+ ...(lastBlockingVerdict !== undefined ? { last_blocking_verdict: lastBlockingVerdict } : {}),
485
+ ...(lastBlockingTimestamp !== undefined
486
+ ? { last_blocking_timestamp: lastBlockingTimestamp }
487
+ : {}),
488
+ };
434
489
  }
435
490
  }
436
491
  return {
@@ -441,7 +496,13 @@ export function findRecentLocalReview(baseDir, headSha, maxAgeSeconds, now = new
441
496
  match_kind: matchKind,
442
497
  };
443
498
  }
444
- return { found: false };
499
+ return {
500
+ found: false,
501
+ ...(lastBlockingVerdict !== undefined ? { last_blocking_verdict: lastBlockingVerdict } : {}),
502
+ ...(lastBlockingTimestamp !== undefined
503
+ ? { last_blocking_timestamp: lastBlockingTimestamp }
504
+ : {}),
505
+ };
445
506
  }
446
507
  async function safeAudit(baseDir, toolName, status, metadata, policy) {
447
508
  try {
@@ -31,6 +31,8 @@
31
31
  * the push-gate's review.
32
32
  */
33
33
  import type { Command } from 'commander';
34
+ import { type LocalReviewVerdict } from '../audit/local-review-event.js';
35
+ import { type Finding } from '../hooks/push-gate/findings.js';
34
36
  export interface RunReviewOptions {
35
37
  /** Optional explicit base ref. Defaults to upstream-ladder resolution. */
36
38
  base?: string;
@@ -42,13 +44,65 @@ export interface RunReviewOptions {
42
44
  strictFailOn?: 'concerns' | 'blocking';
43
45
  /** Emit a single JSON line on stdout instead of pretty output. */
44
46
  json?: boolean;
47
+ /**
48
+ * 0.28.1 defect-V: when true, after the human-readable summary line
49
+ * (or alongside the JSON payload), emit the finding bodies grouped by
50
+ * severity. Default off — preserves backward-compatible single-line
51
+ * stdout for existing CI consumers.
52
+ */
53
+ withFindings?: boolean;
54
+ }
55
+ /**
56
+ * Exported so tests can construct fake outcomes for the seam in
57
+ * `runReview`. Production callers don't reference this directly.
58
+ */
59
+ export interface ReviewOutcome {
60
+ verdict: LocalReviewVerdict;
61
+ findingCount: number;
62
+ baseRef: string;
63
+ headSha: string;
64
+ /**
65
+ * 0.26.0 helix-026 finding-1: tree SHA of HEAD at review time. The
66
+ * deterministic content fingerprint `rea preflight` matches coverage
67
+ * on. Empty string when not resolvable (no HEAD, no git repo) — the
68
+ * audit writer omits `content_token` from metadata in that case.
69
+ */
70
+ contentToken: string;
71
+ durationSeconds: number;
72
+ model: string;
73
+ reasoningEffort: string;
74
+ /**
75
+ * 0.28.1 defect-V: structured findings produced by the review. Pre-fix
76
+ * the CLI threw these away after counting; agents could not remediate
77
+ * blocking verdicts because the bodies were unreadable through any
78
+ * documented surface.
79
+ */
80
+ findings: Finding[];
81
+ /**
82
+ * 0.28.1 defect-V: full agent-prose review text. Persisted to
83
+ * `.rea/last-review.json` (post-redaction) so consumers have a
84
+ * machine-readable transcript for parser-miss debugging.
85
+ */
86
+ reviewText: string;
87
+ /** Count of raw JSONL events from codex — recorded in last-review.json. */
88
+ eventCount: number;
89
+ }
90
+ /**
91
+ * 0.28.1 defect-V — narrow test seam. Production callers never set this;
92
+ * tests inject a fake to drive `runReview` deterministically without
93
+ * spawning codex. The seam matches `executeCodexReview`'s signature so
94
+ * the production path and the test path go through the same downstream
95
+ * wiring (audit append, last-review.json, exit code, output).
96
+ */
97
+ export interface RunReviewDeps {
98
+ executeCodexReview?: (baseDir: string, options: RunReviewOptions) => Promise<ReviewOutcome>;
45
99
  }
46
100
  /**
47
101
  * Public runner — exposed so tests can drive the function in-process and
48
102
  * the commander binding can stay thin. Throws via `process.exit` (CLI
49
103
  * convention across `src/cli/`).
50
104
  */
51
- export declare function runReview(options: RunReviewOptions): Promise<void>;
105
+ export declare function runReview(options: RunReviewOptions, deps?: RunReviewDeps): Promise<void>;
52
106
  /**
53
107
  * Attach `rea review` to a commander Program.
54
108
  */