@bookedsolid/rea 0.30.1 → 0.32.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 (51) hide show
  1. package/.husky/prepare-commit-msg +80 -6
  2. package/MIGRATING.md +24 -15
  3. package/dist/cli/audit-specialists.d.ts +106 -24
  4. package/dist/cli/audit-specialists.js +239 -64
  5. package/dist/cli/delegation-advisory.d.ts +161 -0
  6. package/dist/cli/delegation-advisory.js +433 -0
  7. package/dist/cli/doctor.d.ts +110 -39
  8. package/dist/cli/doctor.js +302 -90
  9. package/dist/cli/hook.d.ts +6 -0
  10. package/dist/cli/hook.js +45 -22
  11. package/dist/cli/index.js +1 -1
  12. package/dist/cli/install/settings-merge.js +25 -0
  13. package/dist/cli/roster.d.ts +119 -0
  14. package/dist/cli/roster.js +141 -0
  15. package/dist/hooks/_lib/halt-check.d.ts +78 -0
  16. package/dist/hooks/_lib/halt-check.js +106 -0
  17. package/dist/hooks/_lib/payload.d.ts +86 -0
  18. package/dist/hooks/_lib/payload.js +166 -0
  19. package/dist/hooks/_lib/segments.d.ts +100 -0
  20. package/dist/hooks/_lib/segments.js +444 -0
  21. package/dist/hooks/attribution-advisory/index.d.ts +72 -0
  22. package/dist/hooks/attribution-advisory/index.js +233 -0
  23. package/dist/hooks/bash-scanner/protected-scan.js +14 -2
  24. package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
  25. package/dist/hooks/pr-issue-link-gate/index.js +127 -0
  26. package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
  27. package/dist/hooks/security-disclosure-gate/index.js +502 -0
  28. package/dist/policy/loader.d.ts +23 -0
  29. package/dist/policy/loader.js +46 -0
  30. package/dist/policy/profiles.d.ts +23 -0
  31. package/dist/policy/profiles.js +16 -0
  32. package/dist/policy/types.d.ts +61 -0
  33. package/hooks/_lib/protected-paths.sh +10 -3
  34. package/hooks/attribution-advisory.sh +139 -131
  35. package/hooks/delegation-advisory.sh +162 -0
  36. package/hooks/pr-issue-link-gate.sh +114 -45
  37. package/hooks/security-disclosure-gate.sh +148 -316
  38. package/hooks/settings-protection.sh +13 -9
  39. package/package.json +1 -1
  40. package/profiles/bst-internal-no-codex.yaml +12 -0
  41. package/profiles/bst-internal.yaml +13 -0
  42. package/profiles/client-engagement.yaml +11 -0
  43. package/profiles/lit-wc.yaml +10 -0
  44. package/profiles/minimal.yaml +11 -0
  45. package/profiles/open-source-no-codex.yaml +11 -0
  46. package/profiles/open-source.yaml +11 -0
  47. package/templates/attribution-advisory.dogfood-staged.sh +170 -0
  48. package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
  49. package/templates/prepare-commit-msg.husky.sh +80 -6
  50. package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
  51. package/templates/settings-protection.dogfood.patch +58 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Quote-aware shell-segment splitter for the Node-binary hook tier.
3
+ *
4
+ * 0.32.0 — port of the relevant primitives in
5
+ * `hooks/_lib/cmd-segments.sh`. The bash helper is 1002 LOC of
6
+ * defense-in-depth (heredoc unwrapping, nested-shell recursion,
7
+ * env-var-assignment stripping, etc.) — most of those branches exist
8
+ * to defend against bypass attempts in WRITE-tier gates (`dangerous-
9
+ * bash-interceptor`, `dependency-audit-gate`). The Phase 1 pilots
10
+ * landing in 0.32.0 (`security-disclosure-gate`,
11
+ * `attribution-advisory`) only need the SUBSET of segment behavior
12
+ * those two hooks actually exercise:
13
+ *
14
+ * 1. Split the input on shell command separators (`;`, `&&`, `||`,
15
+ * `|`, `&`, newline) while masking separators that appear inside
16
+ * matched `"..."` and `'...'` quote spans.
17
+ * 2. For each segment, strip leading `sudo`, `exec`, `time`, `then`,
18
+ * `do`, `else`, `fi`, and `VAR=value` env-prefixes so the
19
+ * caller's regex can anchor at the segment's actual command head.
20
+ * 3. Expose two query primitives:
21
+ * - `anySegmentStartsWith(cmd, regexHead)`
22
+ * true if any segment's prefix-stripped head matches the
23
+ * head-anchored regex.
24
+ * - `anySegmentMatches(cmd, regex)`
25
+ * true if any segment's raw (non-stripped) text contains a
26
+ * match for the regex (used for content scans like
27
+ * `Co-Authored-By:` markers inside `git commit -m "..."`).
28
+ *
29
+ * Out-of-scope vs. the bash helper:
30
+ *
31
+ * - No heredoc body extraction. The pilots match on the command
32
+ * line, not on heredoc contents. (Body-file resolution in
33
+ * `security-disclosure-gate` is done separately by reading the
34
+ * file path off the command.)
35
+ * - No nested-shell unwrapping (`bash -c 'PAYLOAD'`). The
36
+ * bash-scanner walker already handles that for the WRITE gates;
37
+ * the Phase 1 pilots inherit the SECURITY guarantee that any
38
+ * hostile nested shell would have been refused by the bash-scanner
39
+ * tier BEFORE this advisory tier ran.
40
+ * - No backtick/command-substitution recursion.
41
+ *
42
+ * If a future pilot needs those branches, port them here in a
43
+ * subsequent release. The CURRENT pilots' bash counterparts call only
44
+ * `any_segment_starts_with` and `any_segment_matches` against
45
+ * direct-stdin commands.
46
+ *
47
+ * Quote-handling parity with cmd-segments.sh:
48
+ *
49
+ * - Double-quoted spans (`"..."`): `\"` and `\\` are literal escapes;
50
+ * all other characters are literal.
51
+ * - Single-quoted spans (`'...'`): no escape semantics; every
52
+ * character is literal until the next `'`.
53
+ * - Unterminated quote spans extend to end-of-input (caller's bug —
54
+ * we still emit a single segment for it rather than throwing).
55
+ * - Backslash outside quotes escapes the following character (so
56
+ * `git commit \&\& foo` parses as a single segment, matching
57
+ * bash's behavior).
58
+ */
59
+ /**
60
+ * Sentinel bytes used to mask separators that appear inside quote
61
+ * spans before splitting. Multi-byte and not legal in shell command
62
+ * input — collisions are impossible for any realistic payload.
63
+ *
64
+ * The byte choices (0x1c – 0x1f are ASCII file-separator / group-
65
+ * separator / record-separator / unit-separator) are the same range
66
+ * `cmd-segments.sh` uses for its in-quote masking. We never expose
67
+ * them externally; they exist only during the split and are restored
68
+ * verbatim in the emitted segment text.
69
+ */
70
+ const MASK = {
71
+ SEMI: '\x1c\x10S\x1d',
72
+ AMP_AMP: '\x1c\x10A\x10A\x1d',
73
+ PIPE_PIPE: '\x1c\x10P\x10P\x1d',
74
+ PIPE: '\x1c\x10P\x1d',
75
+ AMP: '\x1c\x10A\x1d',
76
+ NEWLINE: '\x1c\x10N\x1d',
77
+ };
78
+ /**
79
+ * Replace separators inside quote spans with sentinels so the split
80
+ * walker doesn't see them. After splitting, the sentinels are
81
+ * unmasked back to their literal characters in each emitted segment.
82
+ */
83
+ function maskQuotedSeparators(cmd) {
84
+ let out = '';
85
+ let i = 0;
86
+ const n = cmd.length;
87
+ let mode = 'plain';
88
+ while (i < n) {
89
+ const ch = cmd[i];
90
+ if (mode === 'plain') {
91
+ if (ch === '\\' && i + 1 < n) {
92
+ // Backslash escapes the next character — emit both verbatim;
93
+ // the split walker treats `\` as not-a-separator so escaped
94
+ // `\&\&` etc. survive into the segment.
95
+ out += ch + cmd[i + 1];
96
+ i += 2;
97
+ continue;
98
+ }
99
+ if (ch === '"') {
100
+ mode = 'dquote';
101
+ out += ch;
102
+ i += 1;
103
+ continue;
104
+ }
105
+ if (ch === "'") {
106
+ mode = 'squote';
107
+ out += ch;
108
+ i += 1;
109
+ continue;
110
+ }
111
+ out += ch;
112
+ i += 1;
113
+ continue;
114
+ }
115
+ if (mode === 'dquote') {
116
+ if (ch === '\\' && i + 1 < n) {
117
+ out += ch + cmd[i + 1];
118
+ i += 2;
119
+ continue;
120
+ }
121
+ if (ch === '"') {
122
+ mode = 'plain';
123
+ out += ch;
124
+ i += 1;
125
+ continue;
126
+ }
127
+ // Mask separators inside double-quoted spans.
128
+ if (ch === ';') {
129
+ out += MASK.SEMI;
130
+ i += 1;
131
+ continue;
132
+ }
133
+ if (ch === '&' && cmd[i + 1] === '&') {
134
+ out += MASK.AMP_AMP;
135
+ i += 2;
136
+ continue;
137
+ }
138
+ if (ch === '|' && cmd[i + 1] === '|') {
139
+ out += MASK.PIPE_PIPE;
140
+ i += 2;
141
+ continue;
142
+ }
143
+ if (ch === '|') {
144
+ out += MASK.PIPE;
145
+ i += 1;
146
+ continue;
147
+ }
148
+ if (ch === '&') {
149
+ out += MASK.AMP;
150
+ i += 1;
151
+ continue;
152
+ }
153
+ if (ch === '\n') {
154
+ out += MASK.NEWLINE;
155
+ i += 1;
156
+ continue;
157
+ }
158
+ out += ch;
159
+ i += 1;
160
+ continue;
161
+ }
162
+ // mode === 'squote' — no escape semantics; mask separators verbatim.
163
+ if (ch === "'") {
164
+ mode = 'plain';
165
+ out += ch;
166
+ i += 1;
167
+ continue;
168
+ }
169
+ if (ch === ';') {
170
+ out += MASK.SEMI;
171
+ i += 1;
172
+ continue;
173
+ }
174
+ if (ch === '&' && cmd[i + 1] === '&') {
175
+ out += MASK.AMP_AMP;
176
+ i += 2;
177
+ continue;
178
+ }
179
+ if (ch === '|' && cmd[i + 1] === '|') {
180
+ out += MASK.PIPE_PIPE;
181
+ i += 2;
182
+ continue;
183
+ }
184
+ if (ch === '|') {
185
+ out += MASK.PIPE;
186
+ i += 1;
187
+ continue;
188
+ }
189
+ if (ch === '&') {
190
+ out += MASK.AMP;
191
+ i += 1;
192
+ continue;
193
+ }
194
+ if (ch === '\n') {
195
+ out += MASK.NEWLINE;
196
+ i += 1;
197
+ continue;
198
+ }
199
+ out += ch;
200
+ i += 1;
201
+ }
202
+ return out;
203
+ }
204
+ /**
205
+ * Reverse the masking. Sentinels become their literal separator
206
+ * character again so the emitted segment text reads as the caller
207
+ * authored it.
208
+ */
209
+ function unmask(text) {
210
+ return text
211
+ .replace(/\x1c\x10S\x1d/g, ';')
212
+ .replace(/\x1c\x10A\x10A\x1d/g, '&&')
213
+ .replace(/\x1c\x10P\x10P\x1d/g, '||')
214
+ .replace(/\x1c\x10P\x1d/g, '|')
215
+ .replace(/\x1c\x10A\x1d/g, '&')
216
+ .replace(/\x1c\x10N\x1d/g, '\n');
217
+ }
218
+ /**
219
+ * Split the masked command on UNQUOTED separators. The masking pass
220
+ * already replaced in-quote separators with sentinels, so a plain
221
+ * regex split is now safe.
222
+ *
223
+ * The split pattern matches any of: `;`, `&&`, `||`, `|`, `&` (when
224
+ * not part of `&&`), newline. We use a single regex with a lookbehind
225
+ * to avoid splitting `&&` as two `&`s.
226
+ *
227
+ * `\\` escapes the next character — we don't want to split on `\;`
228
+ * either. Handled by checking the preceding character is NOT `\`
229
+ * (lookbehind).
230
+ */
231
+ function splitOnUnquotedSeparators(masked) {
232
+ // Negative lookbehind for `\` — `git commit \; foo` shouldn't split.
233
+ // JS regex supports lookbehind in V8 / Node 12+.
234
+ const splitter = /(?<!\\)(\&\&|\|\||;|\||\&|\n)/g;
235
+ // We split AND consume the separator (capture group above). The
236
+ // result interleaves segment, separator, segment, separator, …; we
237
+ // keep only the even-indexed entries (the segments).
238
+ const parts = masked.split(splitter);
239
+ const segments = [];
240
+ for (let i = 0; i < parts.length; i += 2) {
241
+ const raw = parts[i];
242
+ if (raw === undefined)
243
+ continue;
244
+ const trimmed = raw.trim();
245
+ if (trimmed.length === 0)
246
+ continue;
247
+ segments.push(trimmed);
248
+ }
249
+ return segments;
250
+ }
251
+ /**
252
+ * Patterns that may precede a real command head in a segment. Mirrors
253
+ * the catalog in `cmd-segments.sh#strip_segment_prefix`. Order matters
254
+ * — env-var-assignment must come AFTER `sudo` because `sudo VAR=x cmd`
255
+ * is a real shape.
256
+ *
257
+ * `--<flag>=<value>` is NOT stripped — those are part of the command.
258
+ */
259
+ const LEADING_KEYWORDS = ['sudo', 'exec', 'time', 'then', 'do', 'else', 'fi'];
260
+ /**
261
+ * Match an env-var assignment at the head of a segment, INCLUDING
262
+ * quoted and ANSI-C values. Codex round 1 P1 (2026-05-15): the
263
+ * pre-fix pattern was `^[A-Za-z_][A-Za-z0-9_]*=\S*\s+` which only
264
+ * matched unquoted single-token values. The bash helper this
265
+ * replaces handles five shapes the prior regex missed:
266
+ *
267
+ * 1. `KEY="value with spaces" cmd` (double-quoted)
268
+ * 2. `KEY='value with spaces' cmd` (single-quoted)
269
+ * 3. `KEY=$'ANSI-C\\nvalue' cmd` (ANSI-C escape form)
270
+ * 4. `KEY=` (empty value)
271
+ * 5. `KEY=value cmd` (unquoted, the old form)
272
+ *
273
+ * Without coverage of (1)-(3), an attacker could hide a relevant
274
+ * command head behind `REA_SKIP="urgent" gh issue create …` and
275
+ * the `gh issue create` head would never reach the matcher in
276
+ * `runSecurityDisclosureGate` / `runAttributionAdvisory`.
277
+ *
278
+ * Returns the consumed prefix length, or 0 if no env assignment.
279
+ */
280
+ function matchEnvAssignLength(seg) {
281
+ // Variable-name prefix: `[A-Za-z_][A-Za-z0-9_]*=`. Strict POSIX
282
+ // identifier — bash itself rejects names starting with a digit.
283
+ const namePrefix = /^[A-Za-z_][A-Za-z0-9_]*=/.exec(seg);
284
+ if (namePrefix === null)
285
+ return 0;
286
+ let i = namePrefix[0].length;
287
+ const n = seg.length;
288
+ if (i >= n)
289
+ return 0; // `KEY=` followed by nothing — not a prefix.
290
+ // Determine the value-form by the first character after `=`.
291
+ const ch = seg[i];
292
+ // 3. ANSI-C form: `$'…'`. Consume up to the matching `'`,
293
+ // honoring backslash escapes (so `$'a\\'b'` → contents are
294
+ // `a\'b`, terminator is the third `'`). Bash forbids the
295
+ // closing quote from being escaped — the `$'` shape uses C
296
+ // string conventions, not shell-quote conventions.
297
+ if (ch === '$' && i + 1 < n && seg[i + 1] === "'") {
298
+ i += 2; // consume `$'`
299
+ while (i < n && seg[i] !== "'") {
300
+ if (seg[i] === '\\' && i + 1 < n) {
301
+ i += 2;
302
+ continue;
303
+ }
304
+ i += 1;
305
+ }
306
+ if (i >= n)
307
+ return 0; // unterminated — not a clean prefix.
308
+ i += 1; // consume closing `'`
309
+ }
310
+ else if (ch === '"') {
311
+ // 1. Double-quoted form. `\"` and `\\` are escapes.
312
+ i += 1;
313
+ while (i < n && seg[i] !== '"') {
314
+ if (seg[i] === '\\' && i + 1 < n) {
315
+ i += 2;
316
+ continue;
317
+ }
318
+ i += 1;
319
+ }
320
+ if (i >= n)
321
+ return 0;
322
+ i += 1;
323
+ }
324
+ else if (ch === "'") {
325
+ // 2. Single-quoted form. No escapes — consume until next `'`.
326
+ i += 1;
327
+ while (i < n && seg[i] !== "'")
328
+ i += 1;
329
+ if (i >= n)
330
+ return 0;
331
+ i += 1;
332
+ }
333
+ else {
334
+ // 5. Unquoted form. Consume contiguous non-whitespace.
335
+ while (i < n && seg[i] !== ' ' && seg[i] !== '\t')
336
+ i += 1;
337
+ }
338
+ // Require at least one whitespace after the value so we don't
339
+ // strip `FOO=barbaz` (no command following).
340
+ if (i >= n || (seg[i] !== ' ' && seg[i] !== '\t'))
341
+ return 0;
342
+ // Consume trailing whitespace before yielding the new segment.
343
+ while (i < n && (seg[i] === ' ' || seg[i] === '\t'))
344
+ i += 1;
345
+ return i;
346
+ }
347
+ /**
348
+ * Strip leading shell keywords and env-var assignments from a segment
349
+ * so the caller's head-anchored regex sees the actual command first.
350
+ *
351
+ * Examples:
352
+ * `sudo gh pr create` → `gh pr create`
353
+ * `CI=1 pnpm add foo` → `pnpm add foo`
354
+ * `sudo CI=1 pnpm add foo` → `pnpm add foo`
355
+ * `REA_SKIP="urgent fix" gh issue create x` → `gh issue create x`
356
+ * `KEY=$'a\\nb' git commit` → `git commit`
357
+ * `then git push --force` → `git push --force`
358
+ *
359
+ * The bash counterpart loops until no more prefix matches. We mirror
360
+ * that with an iteration cap of 32 (was 8; raised to support deeply
361
+ * stacked env prefixes — bash itself has no limit so 8 was a per-
362
+ * advisory-pilot bypass surface).
363
+ */
364
+ function stripSegmentPrefix(seg) {
365
+ let current = seg;
366
+ for (let iter = 0; iter < 32; iter += 1) {
367
+ let changed = false;
368
+ for (const kw of LEADING_KEYWORDS) {
369
+ const re = new RegExp(`^${kw}\\s+`);
370
+ if (re.test(current)) {
371
+ current = current.replace(re, '');
372
+ changed = true;
373
+ break;
374
+ }
375
+ }
376
+ if (changed)
377
+ continue;
378
+ const envLen = matchEnvAssignLength(current);
379
+ if (envLen > 0) {
380
+ current = current.slice(envLen);
381
+ changed = true;
382
+ }
383
+ if (!changed)
384
+ break;
385
+ }
386
+ return current;
387
+ }
388
+ /**
389
+ * Split `cmd` into segments using the quote-aware masking → split →
390
+ * unmask pipeline. Returns an array of `{ raw, head }` tuples in the
391
+ * order they appeared in the original command.
392
+ */
393
+ export function splitSegments(cmd) {
394
+ if (cmd.length === 0)
395
+ return [];
396
+ const masked = maskQuotedSeparators(cmd);
397
+ const rawSegs = splitOnUnquotedSeparators(masked);
398
+ return rawSegs.map((raw) => {
399
+ const unmaskedRaw = unmask(raw);
400
+ return { raw: unmaskedRaw, head: stripSegmentPrefix(unmaskedRaw) };
401
+ });
402
+ }
403
+ /**
404
+ * Returns true if any segment's prefix-stripped head matches the
405
+ * head-anchored regex. The regex must NOT include a `^` anchor —
406
+ * we anchor by testing against the head of the segment via
407
+ * `regex.test(head.slice(0, match.length))` simulation. In practice
408
+ * we just run the regex against the head with the regex already
409
+ * head-anchored by virtue of `head` containing only the prefix-
410
+ * stripped form.
411
+ *
412
+ * The bash counterpart uses `grep -qiE PATTERN <<<"$head"` so we
413
+ * match the same posture: case-INSENSITIVE, extended regex.
414
+ *
415
+ * @param regexSource ERE source. We compile with case-insensitive
416
+ * flag. Caller passes the same string they would
417
+ * have passed to `any_segment_starts_with` in bash.
418
+ * The regex is internally anchored with `^`.
419
+ */
420
+ export function anySegmentStartsWith(cmd, regexSource) {
421
+ // Compile once. `^` anchor + `i` flag.
422
+ const re = new RegExp(`^${regexSource}`, 'i');
423
+ for (const seg of splitSegments(cmd)) {
424
+ if (re.test(seg.head))
425
+ return true;
426
+ }
427
+ return false;
428
+ }
429
+ /**
430
+ * Returns true if any segment's RAW text contains a match for the
431
+ * regex (no head anchoring). Mirrors `any_segment_matches` — used for
432
+ * content-scan patterns like `Co-Authored-By:` markers inside
433
+ * quoted `git commit -m "..."` arguments.
434
+ *
435
+ * Case-INSENSITIVE, extended regex. Same posture as the bash helper.
436
+ */
437
+ export function anySegmentMatches(cmd, regexSource) {
438
+ const re = new RegExp(regexSource, 'i');
439
+ for (const seg of splitSegments(cmd)) {
440
+ if (re.test(seg.raw))
441
+ return true;
442
+ }
443
+ return false;
444
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Node-binary port of `hooks/attribution-advisory.sh`.
3
+ *
4
+ * 0.32.0 Phase 1 Pilot #3 — opt-in policy-gated AI-attribution
5
+ * detector for `git commit` / `gh pr create|edit` commands.
6
+ *
7
+ * Why pilot #3 (and not #1): pilot #1 was the smallest port surface
8
+ * (no segments, no body-file resolution). Pilot #3 introduces the
9
+ * FULL `splitSegments` + `anySegmentStartsWith` + `anySegmentMatches`
10
+ * API surface. Pilot #2 (`security-disclosure-gate`) layers the
11
+ * file-IO body-file resolver on top of this same segment primitive.
12
+ *
13
+ * Behavioral contract — preserves bash hook byte-for-byte:
14
+ *
15
+ * 1. HALT check → exit 2 with shared banner. (Same as pilot 1.)
16
+ * 2. Read stdin payload. When `tool_input.command` is missing /
17
+ * empty, exit 0 silently.
18
+ * 3. Read `<reaRoot>/.rea/policy.yaml` and check for the line
19
+ * `block_ai_attribution: true`. The bash original used `grep -qE
20
+ * '^block_ai_attribution:[[:space:]]*true'` against the file
21
+ * directly; the Node port preserves the EXACT same regex against
22
+ * the file contents. NOT a YAML parse — the bash hook ran before
23
+ * we had a CLI-mediated `policy-get` read, and consumers may
24
+ * authored the line in either block or inline form. Matching the
25
+ * regex behavior preserves all the edge cases the bash hook
26
+ * shipped with.
27
+ * 4. Identify whether the command is RELEVANT — a `git commit` or
28
+ * `gh pr create|edit` invocation at the head of any segment.
29
+ * Uses `anySegmentStartsWith` (head-anchored, post-prefix-strip)
30
+ * so a quoted-body mention like `gh pr edit --body "ref: git
31
+ * commit earlier"` does NOT count as relevant.
32
+ * 5. Scan for FIVE attribution-marker classes, each via
33
+ * `anySegmentMatches` so the match has to live in the same
34
+ * segment as the relevant command head:
35
+ * a. `Co-Authored-By:` with an AI vendor noreply@ domain
36
+ * b. `Co-Authored-By:` with a known AI tool name
37
+ * c. `Generated|Created|Built|… with|by <AI Tool>`
38
+ * d. Markdown-linked tool name (`[Claude Code](`)
39
+ * e. Robot-emoji + Generated marker
40
+ * 6. Any match → exit 2 with the banner. No match → exit 0.
41
+ *
42
+ * Wider-net pattern choice: the bash hook used `[[:space:]]+` for
43
+ * `\s+` equivalents. JS regex uses `\s+` which is broader (includes
44
+ * vertical tab / form feed). For the ASCII payloads `gh` and `git`
45
+ * actually accept, the behavior is identical.
46
+ */
47
+ import type { Buffer } from 'node:buffer';
48
+ export interface AttributionAdvisoryOptions {
49
+ reaRoot?: string;
50
+ stdinOverride?: string | Buffer;
51
+ stderrWrite?: (s: string) => void;
52
+ }
53
+ export interface AttributionAdvisoryResult {
54
+ exitCode: number;
55
+ stderr: string;
56
+ }
57
+ /**
58
+ * Pure executor — returns `{ exitCode, stderr }`.
59
+ */
60
+ export declare function runAttributionAdvisory(options?: AttributionAdvisoryOptions): Promise<AttributionAdvisoryResult>;
61
+ /**
62
+ * CLI entry — `rea hook attribution-advisory`.
63
+ */
64
+ export declare function runHookAttributionAdvisory(options?: AttributionAdvisoryOptions): Promise<void>;
65
+ export declare const __INTERNAL_BLOCK_BANNER_FOR_TESTS: string;
66
+ export declare const __INTERNAL_PATTERNS_FOR_TESTS: {
67
+ PATTERN_NOREPLY_AI: string;
68
+ PATTERN_COAUTH_AI_NAME: string;
69
+ PATTERN_GENERATED_WITH: string;
70
+ PATTERN_MD_LINK: string;
71
+ PATTERN_EMOJI: string;
72
+ };