@bookedsolid/rea 0.35.0 → 0.37.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.
@@ -169,22 +169,19 @@ export const EXPECTED_HOOKS = [
169
169
  'blocked-paths-enforcer.sh',
170
170
  'changeset-security-gate.sh',
171
171
  'dangerous-bash-interceptor.sh',
172
- // 0.31.0 — `delegation-advisory.sh` is INTENTIONALLY NOT in this list.
173
- // It is the brand-new PostToolUse delegation-nudge hook this release
174
- // ships; adding it to EXPECTED_HOOKS would make `checkHooksInstalled`
175
- // hard-`fail` on every pre-0.31.0 consumer install (and on this repo's
176
- // own dogfood) the instant they upgrade the rea binary but before they
177
- // run `rea upgrade` to lay down the new hook file — a regression that
178
- // turns a green doctor red purely from upgrade lag. This mirrors the
179
- // staged rollout `delegation-capture.sh` itself used: the file-presence
180
- // entry joins EXPECTED_HOOKS in the SAME future minor that promotes
181
- // `checkDelegationAdvisoryHookRegistered` from `warn` to `fail`, once
182
- // consumers have had upgrade-lag time. Until then, a missing
183
- // `delegation-advisory.sh` surfaces only through that `warn`-tier
184
- // registration check — proportionate, since the hook is advisory at
185
- // runtime and never blocks a tool call. See `git blame` on the 0.29.0
186
- // round-2 P2 comment on `checkDelegationHookRegistered` for the prior
187
- // application of this exact discipline.
172
+ // 0.36.0 — `delegation-advisory.sh` PROMOTED to EXPECTED_HOOKS (charter
173
+ // follow-through from 0.31.0). Originally held out in 0.31.0 to give
174
+ // consumers an upgrade-lag window: adding a brand-new hook to
175
+ // EXPECTED_HOOKS would have hard-`fail`ed `checkHooksInstalled` on
176
+ // every pre-0.31.0 install the instant they bumped the rea binary, a
177
+ // regression that turns a green doctor red purely from upgrade lag.
178
+ // After 4 releases of propagation (0.32, 0.33, 0.34, 0.35), the lag
179
+ // window has closed — consumers running `rea upgrade` since 0.31.0
180
+ // have laid the hook down. Same ratchet `delegation-capture.sh` went
181
+ // through 0.29.0 0.30.0. Promotion happens in lockstep with
182
+ // `checkDelegationAdvisoryHookRegistered` flipping `warn` `fail`
183
+ // (see that function for the matching commentary).
184
+ 'delegation-advisory.sh',
188
185
  // 0.29.0 — delegation-telemetry MVP. The PreToolUse hook on
189
186
  // matcher `Agent|Skill` emits a `rea.delegation_signal` audit record
190
187
  // on every subagent / skill dispatch. Observational only — fails
@@ -1126,14 +1123,25 @@ export function checkDelegationHookRegistered(baseDir) {
1126
1123
  */
1127
1124
  export function checkDelegationAdvisoryHookRegistered(baseDir) {
1128
1125
  const label = 'delegation-advisory hook registered';
1129
- const ADVISORY = 'warn';
1126
+ // 0.36.0 promoted from `warn` (advisory in 0.31.0) to `fail` (hard)
1127
+ // per the staged-rollout ratchet. After 4 releases of upgrade-lag
1128
+ // propagation (0.32, 0.33, 0.34, 0.35), consumer installs that have
1129
+ // run `rea upgrade` since 0.31.0 already carry the PostToolUse
1130
+ // `Bash|Edit|Write|MultiEdit|NotebookEdit` group. Any install that
1131
+ // still lacks it after that window is genuinely missing the nudge
1132
+ // and `fail` is the proportionate signal. Companion change:
1133
+ // `delegation-advisory.sh` joined `EXPECTED_HOOKS` in the same
1134
+ // commit, so `checkHooksInstalled` also covers the file-presence +
1135
+ // executability checks now (this function still does both directly
1136
+ // as defense-in-depth, mirroring `checkDelegationHookRegistered`).
1137
+ const REFUSE = 'fail';
1130
1138
  const MATCHER = 'Bash|Edit|Write|MultiEdit|NotebookEdit';
1131
1139
  const settingsPath = path.join(baseDir, '.claude', 'settings.json');
1132
1140
  if (!fs.existsSync(settingsPath)) {
1133
1141
  return {
1134
1142
  label,
1135
- status: ADVISORY,
1136
- detail: `missing: ${settingsPath} — run \`rea upgrade\` or \`rea init\` (advisory in 0.31.0)`,
1143
+ status: REFUSE,
1144
+ detail: `missing: ${settingsPath} — run \`rea upgrade\` or \`rea init\``,
1137
1145
  };
1138
1146
  }
1139
1147
  let parsed;
@@ -1143,7 +1151,7 @@ export function checkDelegationAdvisoryHookRegistered(baseDir) {
1143
1151
  catch (e) {
1144
1152
  return {
1145
1153
  label,
1146
- status: ADVISORY,
1154
+ status: REFUSE,
1147
1155
  detail: e instanceof Error ? e.message : String(e),
1148
1156
  };
1149
1157
  }
@@ -1152,9 +1160,9 @@ export function checkDelegationAdvisoryHookRegistered(baseDir) {
1152
1160
  if (group === undefined) {
1153
1161
  return {
1154
1162
  label,
1155
- status: ADVISORY,
1163
+ status: REFUSE,
1156
1164
  detail: `no PostToolUse group with matcher "${MATCHER}" found in .claude/settings.json — ` +
1157
- 'run `rea upgrade` to install (advisory in 0.31.0; promoted to fail in a later minor). ' +
1165
+ 'run `rea upgrade` to install. ' +
1158
1166
  'NOTE: the matcher INCLUDES Bash — the delegation nudge counts every write-class ' +
1159
1167
  'tool call, not just file edits.',
1160
1168
  };
@@ -1163,16 +1171,16 @@ export function checkDelegationAdvisoryHookRegistered(baseDir) {
1163
1171
  if (!cmds.some((c) => c.includes('delegation-advisory.sh'))) {
1164
1172
  return {
1165
1173
  label,
1166
- status: ADVISORY,
1174
+ status: REFUSE,
1167
1175
  detail: `${MATCHER} matcher exists but no delegation-advisory.sh command found in its hooks list`,
1168
1176
  };
1169
1177
  }
1170
- // Round-2/3 P2: the registration string is present — now confirm the
1171
- // hook file it points at actually exists AND is executable. Because
1172
- // `delegation-advisory.sh` is intentionally out of `EXPECTED_HOOKS`
1173
- // for 0.31.0, `checkHooksInstalled` does NOT cover it; this is the
1174
- // only signal, so it owns both the presence and the `0o111` checks
1175
- // that `checkHooksInstalled` does for every other shipped hook.
1178
+ // 0.31.0 round-2/3 P2: the registration string is present — now
1179
+ // confirm the hook file it points at actually exists AND is
1180
+ // executable. Kept after the 0.36.0 EXPECTED_HOOKS promotion as
1181
+ // defense-in-depth (the same `0o111` check `checkHooksInstalled`
1182
+ // does, scoped to this hook so the failure message can name the
1183
+ // exact remediation rather than a generic "missing X" enumeration).
1176
1184
  const hookFile = path.join(baseDir, '.claude', 'hooks', 'delegation-advisory.sh');
1177
1185
  let hookStat;
1178
1186
  try {
@@ -1181,19 +1189,19 @@ export function checkDelegationAdvisoryHookRegistered(baseDir) {
1181
1189
  catch {
1182
1190
  return {
1183
1191
  label,
1184
- status: ADVISORY,
1192
+ status: REFUSE,
1185
1193
  detail: `${MATCHER} matcher references delegation-advisory.sh but the hook file is missing: ` +
1186
- `${hookFile} — run \`rea upgrade\` to lay it down (advisory in 0.31.0). ` +
1194
+ `${hookFile} — run \`rea upgrade\` to lay it down. ` +
1187
1195
  'Without the file every matching PostToolUse dispatch shells out to a nonexistent path.',
1188
1196
  };
1189
1197
  }
1190
1198
  if ((hookStat.mode & 0o111) === 0) {
1191
1199
  return {
1192
1200
  label,
1193
- status: ADVISORY,
1201
+ status: REFUSE,
1194
1202
  detail: `${MATCHER} matcher references delegation-advisory.sh but the hook file is not executable ` +
1195
1203
  `(mode=${(hookStat.mode & 0o777).toString(8)}): ${hookFile} — ` +
1196
- 'run `rea upgrade` or `chmod +x` it (advisory in 0.31.0). ' +
1204
+ 'run `rea upgrade` or `chmod +x` it. ' +
1197
1205
  'A non-executable hook cannot be launched by Claude Code from settings.json.',
1198
1206
  };
1199
1207
  }
@@ -1469,9 +1477,10 @@ export function collectChecks(baseDir, codexProbeState, prePushState, options =
1469
1477
  checkDelegationHookRegistered(baseDir),
1470
1478
  // 0.31.0 — delegation-telemetry completion. The PostToolUse
1471
1479
  // `Bash|Edit|Write|MultiEdit|NotebookEdit` matcher group drives
1472
- // the delegation-advisory nudge hook. Advisory (warn) for 0.31.0
1473
- // same upgrade-lag ratchet checkDelegationHookRegistered went
1474
- // through in 0.29.0.
1480
+ // the delegation-advisory nudge hook. 0.36.0 — promoted warn →
1481
+ // fail (same upgrade-lag ratchet checkDelegationHookRegistered
1482
+ // went through in 0.29.0 → 0.30.0, after 4 release cycles of
1483
+ // propagation).
1475
1484
  checkDelegationAdvisoryHookRegistered(baseDir),
1476
1485
  ];
1477
1486
  // Non-git escape hatch: when `.git/` is absent, both git-hook checks are
@@ -467,6 +467,50 @@ function splitSegmentsRecursive(cmd, depth) {
467
467
  }
468
468
  return out;
469
469
  }
470
+ /**
471
+ * Test whether a flag token is a `-c`-class introducer.
472
+ *
473
+ * Bash accepts `-c` combined with any other short single-char flags in
474
+ * a single short-flag bundle: `-c`, `-lc`, `-lic`, `-cli`, `-lci`,
475
+ * `-cil`, `-ilc`, etc. The bash cmd-segments.sh `WRAP` regex lists
476
+ * a non-exhaustive defensive subset (`c|lc|lic|ic|cl|cli|li|il`) but
477
+ * bash itself accepts ANY short-flag bundle that contains a `c`.
478
+ *
479
+ * The TS detector mirrors bash semantics: a SHORT-flag bundle
480
+ * (`-letters`, single leading `-`) whose letter set contains `c` is
481
+ * an introducer. The separated `--c` long-flag form is also
482
+ * recognized for parity with the bash WRAP regex's `--c` alternation.
483
+ *
484
+ * Long flags (`--rcfile`, `--noprofile`, `--login`, `--init-file`)
485
+ * are NOT introducers regardless of whether they contain the letter
486
+ * `c` — bash's long-options namespace is disjoint from the `-c`
487
+ * payload-execute semantics.
488
+ *
489
+ * 0.36.0 audit-trail:
490
+ * - Charter item 4 / 0.34.0 codex round-7 P2 #1: pre-fix the test
491
+ * was `/c/i.test(flag.replace(/^--?/, ''))` which over-matched on
492
+ * every flag with a `c` in its name (`--rcfile`, `--noprofile`).
493
+ * - 0.36.0 codex round-1 P1: the first fix attempt used an explicit
494
+ * allowlist `Set` mirroring the bash WRAP regex's explicit
495
+ * alternation, which was a NARROWING vs the pre-fix behavior —
496
+ * valid combined-flag forms like `-lci`, `-cil`, `-ilc` were not
497
+ * in the allowlist and stopped unwrapping, reopening a bypass
498
+ * surface against env-file-protection / dependency-audit-gate /
499
+ * dangerous-bash matchers. This function restores parity with
500
+ * bash itself: any short-flag bundle containing `c` qualifies.
501
+ */
502
+ function isCDashIntroducer(flag) {
503
+ // Separated long-flag form (rare but bash accepts it).
504
+ if (flag === '--c')
505
+ return true;
506
+ // Short-flag bundle: single leading `-`, then one-or-more letters.
507
+ // The bundle is a `-c` introducer iff it contains the letter `c`
508
+ // (any position, any other-letters mix).
509
+ const m = /^-([A-Za-z]+)$/.exec(flag);
510
+ if (m === null)
511
+ return false;
512
+ return /c/i.test(m[1] ?? '');
513
+ }
470
514
  /**
471
515
  * Recognize a nested-shell wrapper segment and return the unquoted
472
516
  * payload string. Returns `null` when the segment is not a wrapper.
@@ -554,15 +598,29 @@ function extractNestedShellPayload(head) {
554
598
  return null;
555
599
  const flag = flagMatch[0] ?? '';
556
600
  cursor += flag.length;
557
- // Recognized flag-token shapes:
558
- // `-c` `-l` `-i` `-e` `-lc` `-lic` `-ic` `-cl` `-cli` `-li` `-il`
559
- // `--c` `--noprofile` (etc.) — we don't enforce the full list,
560
- // just that it's `-<letters>` or `--<letters>`.
601
+ // Recognized flag-token shapes (parity with cmd-segments.sh WRAP):
602
+ // - pre-flags (no `-c` yet): `-l`, `-i`, `-e`, `-li`, `-il`,
603
+ // `--noprofile`, `--rcfile`, `--login` (etc.)
604
+ // - `-c`-class introducer: exactly `-c`, `-lc`, `-lic`, `-cl`,
605
+ // `-cli`, `-li`, `-il`, `-ic` (the bash WRAP regex's
606
+ // `-(c|lc|lic|ic|cl|cli|li|il)` set), OR separated `--c`.
607
+ //
608
+ // 0.36.0 audit-trail (charter item 4 / 0.34.0 codex round-7 P2 #1):
609
+ // pre-fix the test `/c/i.test(flag.replace(/^--?/, ''))` treated
610
+ // ANY flag containing the letter `c` as a `-c` introducer. This
611
+ // false-positived on benign flags like `--rcfile`, `--noprofile`
612
+ // (with `c` in the name), causing the walker to "commit" to a -c
613
+ // unwrap, advance past the flag, and then either fail to find a
614
+ // quoted payload or unwrap something that was never a shell-payload
615
+ // body. Net effect: over-trigger of nested-shell unwrap, with
616
+ // downstream advisory matchers seeing payloads that weren't ever
617
+ // shell-payloads. Fix restricts the introducer set to the exact
618
+ // WRAP-regex shapes; any other flag shape continues the flag walk
619
+ // (still valid — pre-flags before `-c` are accepted) but does NOT
620
+ // mark `sawCFlag = true`.
561
621
  if (!/^--?[A-Za-z]+$/.test(flag))
562
622
  return null;
563
- // Does this flag contain `c` (the -c introducer letter)?
564
- // `--c` also counts (rare but bash accepts).
565
- if (/c/i.test(flag.replace(/^--?/, ''))) {
623
+ if (isCDashIntroducer(flag)) {
566
624
  sawCFlag = true;
567
625
  // Continue the loop — the payload is the NEXT non-flag token.
568
626
  // (Bash's argv parser stops walking flags as soon as it sees -c,
@@ -570,6 +628,8 @@ function extractNestedShellPayload(head) {
570
628
  // safety; the bash WRAP regex similarly tolerates trailing
571
629
  // flag-like tokens before the quoted body.)
572
630
  }
631
+ // Else: a pre-flag (e.g. `-l`, `--rcfile`, `--noprofile`) — keep
632
+ // walking; if a later token IS in `CDASH_INTRODUCERS` we'll fire.
573
633
  }
574
634
  if (!sawCFlag)
575
635
  return null;
@@ -127,7 +127,22 @@ const SECRET_PATTERNS = [
127
127
  {
128
128
  severity: 'HIGH',
129
129
  label: 'Supabase service role key (JWT)',
130
- regex: /SUPABASE_SERVICE_ROLE_KEY\s*=\s*["']?eyJ[A-Za-z0-9._-]{50,}/g,
130
+ // 0.36.0 audit-trail (charter item 5 / 0.34.0 codex round-7 P2 #2):
131
+ // restore byte-parity with the pre-0.34.0 bash body. The bash hook
132
+ // required a quote introducer (`["']`, no `?`); only quoted
133
+ // assignments matched HIGH. Pre-fix this TS regex made the quote
134
+ // OPTIONAL (`["']?`), which upgraded an unquoted `.env` line like
135
+ // `SUPABASE_SERVICE_ROLE_KEY=eyJ...` from MEDIUM advisory (matched
136
+ // by the lower-down `.env credential assignment` pattern) to HIGH
137
+ // blocking. That over-blocks legitimate `.env` files committed to
138
+ // public repos and breaks parity with consumers still on the bash
139
+ // body. Fix: drop the `?` so the quote is required, matching bash
140
+ // exactly. Unquoted assignments continue to fire MEDIUM via the
141
+ // `.env credential assignment` pattern below (line ~190); the only
142
+ // change is that they no longer ALSO fire HIGH here. The `[\"']`
143
+ // character class accepts both single and double quotes
144
+ // (mirroring the bash `["\'"'"']` literal).
145
+ regex: /SUPABASE_SERVICE_ROLE_KEY\s*=\s*["']eyJ[A-Za-z0-9._-]{50,}/g,
131
146
  },
132
147
  // ── MEDIUM severity (advisory) ───────────────────────────────────
133
148
  {
@@ -153,10 +168,57 @@ const SECRET_PATTERNS = [
153
168
  label: 'Hardcoded DB connection string with password',
154
169
  regex: /postgresql:\/\/[^:]+:[^@]{8,}@/g,
155
170
  },
171
+ {
172
+ severity: 'MEDIUM',
173
+ label: 'Supabase service role key (JWT, unquoted non-.env shape)',
174
+ // 0.36.0 codex round-2 P1 → round-3 P3 → round-4 P1 evolution.
175
+ //
176
+ // Background:
177
+ // - Round 1: 0.34.0-introduced HIGH regex had `["']?` (quote
178
+ // optional) which over-blocked unquoted .env lines vs the
179
+ // pre-0.34.0 bash baseline. Charter item 5 dropped the `?`.
180
+ // - Round 2 P1: dropping the `?` left a gap for unquoted forms
181
+ // outside `^FOO=` shape (`export FOO=…` etc.) that ALSO
182
+ // existed in the bash baseline. Added an unquoted-anywhere
183
+ // MEDIUM rule to close it.
184
+ // - Round 3 P3: unquoted-anywhere double-fired with the broader
185
+ // `.env credential assignment` MEDIUM on plain `^FOO=` lines.
186
+ // Narrowed to only fire on 5 specific shell-keyword prefixes
187
+ // (`export`, `readonly`, `declare`, `local`, `typeset`).
188
+ // - Round 4 P1: too narrow — left other unquoted shapes
189
+ // (Dockerfile `ENV FOO=…`, k8s manifests, ad-hoc shell
190
+ // `FOO=…; bar`) entirely unscanned.
191
+ //
192
+ // Round-4 resolution: fire on any unquoted assignment that is
193
+ // NOT at the start of a line (`^`). The `.env credential
194
+ // assignment` MEDIUM pattern owns `^FOO=…`; this rule owns
195
+ // everything else (`ENV FOO=…`, `export FOO=…`, `; FOO=…`,
196
+ // template-string `${FOO=…}`, etc.). Implemented via a
197
+ // multi-line regex with a look-behind for "anything except a
198
+ // line start". JS regex doesn't support a direct "not at line
199
+ // start" assertion, so we require at least one non-newline char
200
+ // before `SUPABASE_SERVICE_ROLE_KEY` on the same line.
201
+ //
202
+ // The `(?!["'])` look-ahead refuses when the value starts with
203
+ // a quote so the same secret isn't double-reported by the HIGH
204
+ // pattern above. Result: each secret produces exactly one
205
+ // MEDIUM finding regardless of shape, and the HIGH rule keeps
206
+ // exclusive ownership of quoted forms.
207
+ regex: /(?<=[^\n\r])\bSUPABASE_SERVICE_ROLE_KEY\s*=\s*(?!["'])eyJ[A-Za-z0-9._-]{50,}/gm,
208
+ },
156
209
  {
157
210
  severity: 'MEDIUM',
158
211
  label: 'Supabase anon key in non-client context',
159
- regex: /SUPABASE_ANON_KEY\s*=\s*["']?eyJ[A-Za-z0-9._-]{50,}/g,
212
+ // 0.36.0 audit-trail (charter item 5 / 0.34.0 codex round-7 P2 #2,
213
+ // sibling fix): same parity restoration as the SUPABASE_SERVICE_ROLE_KEY
214
+ // pattern above — bash hook required a quote introducer, TS pattern
215
+ // had it optional via `?`. Removed for byte-parity. Unquoted .env
216
+ // forms continue to be advisory via the broader `.env credential
217
+ // assignment` MEDIUM pattern (where SUPABASE_ANON_KEY is NOT one of
218
+ // the named keys — anon-key prose in unquoted .env is acceptable
219
+ // since anon keys are public; only QUOTED-in-source matches stay
220
+ // an advisory MEDIUM here).
221
+ regex: /SUPABASE_ANON_KEY\s*=\s*["']eyJ[A-Za-z0-9._-]{50,}/g,
160
222
  },
161
223
  ];
162
224
  /**