@bookedsolid/rea 0.32.0 → 0.34.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 (34) hide show
  1. package/dist/cli/hook.js +49 -0
  2. package/dist/hooks/_lib/payload.d.ts +38 -0
  3. package/dist/hooks/_lib/payload.js +79 -0
  4. package/dist/hooks/_lib/segments.d.ts +127 -0
  5. package/dist/hooks/_lib/segments.js +628 -16
  6. package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
  7. package/dist/hooks/architecture-review-gate/index.js +250 -0
  8. package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
  9. package/dist/hooks/changeset-security-gate/index.js +330 -0
  10. package/dist/hooks/dangerous-bash-interceptor/index.d.ts +103 -0
  11. package/dist/hooks/dangerous-bash-interceptor/index.js +669 -0
  12. package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
  13. package/dist/hooks/dependency-audit-gate/index.js +294 -0
  14. package/dist/hooks/env-file-protection/index.d.ts +55 -0
  15. package/dist/hooks/env-file-protection/index.js +159 -0
  16. package/dist/hooks/local-review-gate/index.d.ts +145 -0
  17. package/dist/hooks/local-review-gate/index.js +374 -0
  18. package/dist/hooks/secret-scanner/index.d.ts +143 -0
  19. package/dist/hooks/secret-scanner/index.js +404 -0
  20. package/hooks/architecture-review-gate.sh +92 -77
  21. package/hooks/changeset-security-gate.sh +114 -149
  22. package/hooks/dangerous-bash-interceptor.sh +168 -386
  23. package/hooks/dependency-audit-gate.sh +115 -156
  24. package/hooks/env-file-protection.sh +130 -97
  25. package/hooks/local-review-gate.sh +523 -410
  26. package/hooks/secret-scanner.sh +210 -200
  27. package/package.json +1 -1
  28. package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
  29. package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
  30. package/templates/dangerous-bash-interceptor.dogfood-staged.sh +196 -0
  31. package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
  32. package/templates/env-file-protection.dogfood-staged.sh +157 -0
  33. package/templates/local-review-gate.dogfood-staged.sh +573 -0
  34. package/templates/secret-scanner.dogfood-staged.sh +240 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Node-binary port of `hooks/architecture-review-gate.sh`.
3
+ *
4
+ * 0.33.0 Phase 1 port #4 — the SIMPLEST tier-1 port.
5
+ *
6
+ * PostToolUse Write/Edit advisory. Reads `policy.architecture_review.
7
+ * patterns` and prints an advisory banner to stderr when the just-
8
+ * written file path begins with one of the configured prefixes.
9
+ * ALWAYS exits 0 — this is a nudge, not a gate.
10
+ *
11
+ * Behavioral contract preserves the bash hook byte-for-byte:
12
+ *
13
+ * 1. HALT check → exit 2 with the shared banner. (Even though the
14
+ * gate is advisory, HALT short-circuits ALL hooks.)
15
+ * 2. `policy.architecture_advisory: false` short-circuit → exit 0
16
+ * silently. The bash hook reads the policy file with a grep
17
+ * `architecture_advisory: false`; we mirror via the canonical
18
+ * YAML loader.
19
+ * 3. Read stdin → `tool_input.file_path` (the bash hook uses
20
+ * `notebook_path` too via fall-through, but the original
21
+ * `jq -r '.tool_input.file_path // empty'` expression does NOT
22
+ * fall through to notebook_path. We preserve that exactly).
23
+ * 4. Empty file_path → exit 0.
24
+ * 5. Path normalization mirrors `_lib/path-normalize.sh::normalize_path`:
25
+ * - Convert backslashes to forward slashes (Windows / Git Bash).
26
+ * - URL-decode `%xx` sequences.
27
+ * - Strip a leading `<REA_ROOT>/` prefix if present so
28
+ * `policy.architecture_review.patterns` can use repo-relative
29
+ * patterns.
30
+ * 6. Read `policy.architecture_review.patterns`. Empty / unset →
31
+ * silent no-op (exit 0). The bst-internal profile pins rea-
32
+ * source patterns; consumer projects opt in by populating their
33
+ * own list.
34
+ * 7. First prefix match wins. Emit the advisory banner to stderr;
35
+ * exit 0.
36
+ *
37
+ * Distinct from the other 0.33.0 ports: this gate is POSTToolUse
38
+ * (fires AFTER the write, advisory only). The shim that invokes it
39
+ * should NOT fail-closed on missing CLI — the pre-0.33.0 bash hook
40
+ * was already a silent no-op when the policy was unset.
41
+ */
42
+ import type { Buffer } from 'node:buffer';
43
+ export interface ArchitectureReviewGateOptions {
44
+ reaRoot?: string;
45
+ stdinOverride?: string | Buffer;
46
+ stderrWrite?: (s: string) => void;
47
+ }
48
+ export interface ArchitectureReviewGateResult {
49
+ exitCode: number;
50
+ stderr: string;
51
+ /** Test seam — the matched pattern (or `null`). */
52
+ matched: string | null;
53
+ }
54
+ export declare function runArchitectureReviewGate(options?: ArchitectureReviewGateOptions): Promise<ArchitectureReviewGateResult>;
55
+ /**
56
+ * CLI entry — `rea hook architecture-review-gate`.
57
+ */
58
+ export declare function runHookArchitectureReviewGate(options?: ArchitectureReviewGateOptions): Promise<void>;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Node-binary port of `hooks/architecture-review-gate.sh`.
3
+ *
4
+ * 0.33.0 Phase 1 port #4 — the SIMPLEST tier-1 port.
5
+ *
6
+ * PostToolUse Write/Edit advisory. Reads `policy.architecture_review.
7
+ * patterns` and prints an advisory banner to stderr when the just-
8
+ * written file path begins with one of the configured prefixes.
9
+ * ALWAYS exits 0 — this is a nudge, not a gate.
10
+ *
11
+ * Behavioral contract preserves the bash hook byte-for-byte:
12
+ *
13
+ * 1. HALT check → exit 2 with the shared banner. (Even though the
14
+ * gate is advisory, HALT short-circuits ALL hooks.)
15
+ * 2. `policy.architecture_advisory: false` short-circuit → exit 0
16
+ * silently. The bash hook reads the policy file with a grep
17
+ * `architecture_advisory: false`; we mirror via the canonical
18
+ * YAML loader.
19
+ * 3. Read stdin → `tool_input.file_path` (the bash hook uses
20
+ * `notebook_path` too via fall-through, but the original
21
+ * `jq -r '.tool_input.file_path // empty'` expression does NOT
22
+ * fall through to notebook_path. We preserve that exactly).
23
+ * 4. Empty file_path → exit 0.
24
+ * 5. Path normalization mirrors `_lib/path-normalize.sh::normalize_path`:
25
+ * - Convert backslashes to forward slashes (Windows / Git Bash).
26
+ * - URL-decode `%xx` sequences.
27
+ * - Strip a leading `<REA_ROOT>/` prefix if present so
28
+ * `policy.architecture_review.patterns` can use repo-relative
29
+ * patterns.
30
+ * 6. Read `policy.architecture_review.patterns`. Empty / unset →
31
+ * silent no-op (exit 0). The bst-internal profile pins rea-
32
+ * source patterns; consumer projects opt in by populating their
33
+ * own list.
34
+ * 7. First prefix match wins. Emit the advisory banner to stderr;
35
+ * exit 0.
36
+ *
37
+ * Distinct from the other 0.33.0 ports: this gate is POSTToolUse
38
+ * (fires AFTER the write, advisory only). The shim that invokes it
39
+ * should NOT fail-closed on missing CLI — the pre-0.33.0 bash hook
40
+ * was already a silent no-op when the policy was unset.
41
+ */
42
+ import fs from 'node:fs';
43
+ import path from 'node:path';
44
+ import { parse as parseYaml } from 'yaml';
45
+ import { checkHalt, formatHaltBanner } from '../_lib/halt-check.js';
46
+ import { parseWriteHookPayload, MalformedPayloadError, TypePayloadError, readStdinWithTimeout, } from '../_lib/payload.js';
47
+ /**
48
+ * Normalize the incoming file path. Mirrors
49
+ * `hooks/_lib/path-normalize.sh::normalize_path`:
50
+ * - backslashes → forward slashes
51
+ * - URL-decoded
52
+ * - leading `<REA_ROOT>/` stripped (when applicable)
53
+ *
54
+ * Pre-0.16.0 the bash hook ONLY stripped the REA_ROOT prefix, which
55
+ * meant Windows / Git Bash backslash paths bypassed advisory.
56
+ */
57
+ function normalizePath(rawPath, reaRoot) {
58
+ let p = rawPath.replace(/\\/g, '/');
59
+ try {
60
+ p = decodeURIComponent(p);
61
+ }
62
+ catch {
63
+ // Malformed % escape — leave the string unchanged. The bash
64
+ // helper's `printf '%b'` behavior is similar (passes through).
65
+ }
66
+ // Strip leading <REA_ROOT>/. Compare normalized forms.
67
+ const normRoot = reaRoot.replace(/\\/g, '/').replace(/\/+$/, '');
68
+ if (normRoot.length > 0) {
69
+ if (p === normRoot)
70
+ return '';
71
+ const withSep = normRoot + '/';
72
+ if (p.startsWith(withSep)) {
73
+ p = p.slice(withSep.length);
74
+ }
75
+ }
76
+ // 2026-05-15 codex round-1 P3 fix: strip chains of leading `./`
77
+ // segments. Mirrors `_lib/path-normalize.sh::path_canonical_form`.
78
+ // Pre-fix `./src/gateway/foo.ts` did NOT match the `src/gateway/`
79
+ // pattern because the leading `./` was preserved. Bash's
80
+ // path_canonical_form collapses `./` chains, so `./src/...`,
81
+ // `././src/...`, etc. all reduce to `src/...`.
82
+ while (p.startsWith('./')) {
83
+ p = p.slice(2);
84
+ }
85
+ return p;
86
+ }
87
+ function buildAdvisoryBanner(filePath, matched) {
88
+ return [
89
+ 'ARCHITECTURE ADVISORY: Sensitive path modified\n',
90
+ '\n',
91
+ ` File: ${filePath}\n`,
92
+ ` Category: ${matched}\n`,
93
+ '\n',
94
+ ' This file is in an architecture-sensitive directory.\n',
95
+ ' Consider: Does this change maintain backward compatibility?\n',
96
+ ' Consider: Should this be reviewed by the principal-engineer agent?\n',
97
+ ].join('');
98
+ }
99
+ /**
100
+ * Read `policy.architecture_review.patterns`. Returns `[]` on:
101
+ * - policy file missing
102
+ * - YAML unparseable
103
+ * - architecture_review unset
104
+ * - architecture_review.patterns unset/empty/non-list
105
+ *
106
+ * 2026-05-15 codex round-1 P3 fix: do NOT use `loadPolicy()` here.
107
+ * The strict zod schema throws on legacy keys / extra fields, which
108
+ * caused the catch to swallow patterns silently — a legacy policy.yaml
109
+ * with one unknown key would disable the advisory entirely, with no
110
+ * indication to the user.
111
+ *
112
+ * The bash original used `policy_list` (a non-strict reader). To match
113
+ * that behavior we read the YAML directly via the same permissive
114
+ * parser that `rea hook policy-get` uses (`yaml` package's `parse`),
115
+ * then pull `architecture_review.patterns` as a list of strings. Any
116
+ * non-string entry is filtered out. Unknown keys ELSEWHERE in the
117
+ * policy are tolerated — only the patterns subset matters for this
118
+ * advisory.
119
+ */
120
+ function loadArchitecturePatterns(reaRoot, onWarning) {
121
+ const policyPath = path.join(reaRoot, '.rea', 'policy.yaml');
122
+ let raw;
123
+ try {
124
+ raw = fs.readFileSync(policyPath, 'utf8');
125
+ }
126
+ catch {
127
+ // File missing — bash hook treats this as "advisory disabled".
128
+ return [];
129
+ }
130
+ let parsed;
131
+ try {
132
+ parsed = parseYaml(raw);
133
+ }
134
+ catch {
135
+ // Unparseable YAML — log to stderr (NOT silent) and return [].
136
+ // The advisory still short-circuits to exit 0 since this is an
137
+ // advisory tier, but the user sees a one-line warning instead of
138
+ // mysterious silence.
139
+ onWarning('architecture-review-gate: policy.yaml is unparseable; advisory disabled\n');
140
+ return [];
141
+ }
142
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
143
+ return [];
144
+ }
145
+ const ar = parsed['architecture_review'];
146
+ if (ar === undefined || ar === null || typeof ar !== 'object' || Array.isArray(ar)) {
147
+ return [];
148
+ }
149
+ const patterns = ar['patterns'];
150
+ if (!Array.isArray(patterns))
151
+ return [];
152
+ const out = [];
153
+ for (const entry of patterns) {
154
+ if (typeof entry === 'string' && entry.length > 0) {
155
+ out.push(entry);
156
+ }
157
+ }
158
+ return out;
159
+ }
160
+ /**
161
+ * Quick policy-disable probe. The bash hook reads
162
+ * `architecture_advisory: false` (legacy key — pre-0.20.1 toggle)
163
+ * directly from policy.yaml via grep. The canonical loader doesn't
164
+ * surface this key (it's not in the strict schema), so we re-read
165
+ * the raw YAML text. Returns true when the key is present and
166
+ * literally `false` (no other value disables the hook in the bash
167
+ * implementation).
168
+ */
169
+ function isAdvisoryDisabled(reaRoot) {
170
+ const policyPath = path.join(reaRoot, '.rea', 'policy.yaml');
171
+ let raw;
172
+ try {
173
+ raw = fs.readFileSync(policyPath, 'utf8');
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ return /^architecture_advisory:\s*false\b/m.test(raw);
179
+ }
180
+ export async function runArchitectureReviewGate(options = {}) {
181
+ const reaRoot = options.reaRoot ?? process.env['CLAUDE_PROJECT_DIR'] ?? process.cwd();
182
+ let stderr = '';
183
+ const writeStderr = (s) => {
184
+ stderr += s;
185
+ if (options.stderrWrite)
186
+ options.stderrWrite(s);
187
+ };
188
+ // 1. HALT.
189
+ const halt = checkHalt(reaRoot);
190
+ if (halt.halted) {
191
+ writeStderr(formatHaltBanner(halt.reason));
192
+ return { exitCode: 2, stderr, matched: null };
193
+ }
194
+ // 2. Disabled?
195
+ if (isAdvisoryDisabled(reaRoot)) {
196
+ return { exitCode: 0, stderr, matched: null };
197
+ }
198
+ // 3. Stdin.
199
+ const stdinRaw = options.stdinOverride !== undefined
200
+ ? options.stdinOverride
201
+ : await readStdinWithTimeout(5_000);
202
+ let filePath = '';
203
+ try {
204
+ const payload = parseWriteHookPayload(stdinRaw);
205
+ filePath = payload.filePath;
206
+ }
207
+ catch (err) {
208
+ if (err instanceof MalformedPayloadError || err instanceof TypePayloadError) {
209
+ // Advisory tier: silently exit 0 on malformed payload. The bash
210
+ // hook used `jq -r '.tool_input.file_path // empty'` which
211
+ // coerces malformed JSON to empty stdout, then exits 0. Mirror
212
+ // that — never refuse on a parse error in the advisory path.
213
+ return { exitCode: 0, stderr, matched: null };
214
+ }
215
+ throw err;
216
+ }
217
+ if (filePath.length === 0) {
218
+ return { exitCode: 0, stderr, matched: null };
219
+ }
220
+ const normalized = normalizePath(filePath, reaRoot);
221
+ if (normalized.length === 0) {
222
+ return { exitCode: 0, stderr, matched: null };
223
+ }
224
+ const patterns = loadArchitecturePatterns(reaRoot, writeStderr);
225
+ if (patterns.length === 0) {
226
+ return { exitCode: 0, stderr, matched: null };
227
+ }
228
+ let matched = null;
229
+ for (const pattern of patterns) {
230
+ if (normalized.startsWith(pattern)) {
231
+ matched = pattern;
232
+ break;
233
+ }
234
+ }
235
+ if (matched === null) {
236
+ return { exitCode: 0, stderr, matched: null };
237
+ }
238
+ writeStderr(buildAdvisoryBanner(normalized, matched));
239
+ return { exitCode: 0, stderr, matched };
240
+ }
241
+ /**
242
+ * CLI entry — `rea hook architecture-review-gate`.
243
+ */
244
+ export async function runHookArchitectureReviewGate(options = {}) {
245
+ const result = await runArchitectureReviewGate({
246
+ ...options,
247
+ stderrWrite: (s) => process.stderr.write(s),
248
+ });
249
+ process.exit(result.exitCode);
250
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Node-binary port of `hooks/changeset-security-gate.sh`.
3
+ *
4
+ * 0.33.0 Phase 1 port #3.
5
+ *
6
+ * Guards `.changeset/*.md` files against two failure modes:
7
+ *
8
+ * 1. SECURITY DISCLOSURE LEAK — a GHSA or CVE identifier in a
9
+ * changeset file becomes public via CHANGELOG.md when the
10
+ * release ships. Block the write.
11
+ * 2. MISSING OR MALFORMED FRONTMATTER — a changeset without a
12
+ * proper frontmatter block is silently ignored by the
13
+ * changesets tool, wasting the release entry. Block the write.
14
+ *
15
+ * Behavioral contract preserves the bash hook byte-for-byte:
16
+ *
17
+ * 1. HALT check → exit 2 with shared banner.
18
+ * 2. Tool filter: only `Write`, `Edit`, `MultiEdit`, `NotebookEdit`.
19
+ * Any other tool exits 0.
20
+ * 3. File-path filter: only `.changeset/*.md` files. The
21
+ * `.changeset/README.md` companion is excluded (it's metadata
22
+ * for the changesets tool itself).
23
+ * 4. Security disclosure scan on the resolved content. The
24
+ * ordered pattern list is reproduced verbatim. First match wins;
25
+ * emit the `MATCHED_PATTERN` placeholder.
26
+ * 5. MultiEdit short-circuit for frontmatter: MultiEdit's
27
+ * `edits[].new_string` is a list of replacement FRAGMENTS, not
28
+ * a full file. Running frontmatter validation against the
29
+ * concatenated fragments would reject every legitimate edit.
30
+ * The bash hook added this exemption in 0.15.0; we mirror it.
31
+ * The disclosure scan still runs on the fragments because
32
+ * GHSA/CVE patterns match per-fragment without structural
33
+ * assumption.
34
+ * 6. Frontmatter validation:
35
+ * a. Must start with `---`.
36
+ * b. Must contain at least one `<pkg>: (patch|minor|major)`
37
+ * entry inside the first `---`/`---` block. Accepts
38
+ * single-quoted, double-quoted, and unquoted package
39
+ * names — same explicit alternation form as the bash hook
40
+ * (0.15.0 codex round-1 P2-1 fix).
41
+ * c. Must have a non-empty description after the closing
42
+ * `---`.
43
+ *
44
+ * Block emissions use the Claude Code PreToolUse JSON-on-stdout
45
+ * protocol via `emitJsonBlock`, mirroring `_lib/common.sh::json_output`
46
+ * — JSON on stdout AND the human reason on stderr, exit 2.
47
+ */
48
+ import type { Buffer } from 'node:buffer';
49
+ export interface ChangesetSecurityGateOptions {
50
+ reaRoot?: string;
51
+ stdinOverride?: string | Buffer;
52
+ stderrWrite?: (s: string) => void;
53
+ stdoutWrite?: (s: string) => void;
54
+ }
55
+ export interface ChangesetSecurityGateResult {
56
+ exitCode: number;
57
+ stderr: string;
58
+ stdout: string;
59
+ }
60
+ export declare function runChangesetSecurityGate(options?: ChangesetSecurityGateOptions): Promise<ChangesetSecurityGateResult>;
61
+ /**
62
+ * CLI entry — `rea hook changeset-security-gate`.
63
+ */
64
+ export declare function runHookChangesetSecurityGate(options?: ChangesetSecurityGateOptions): Promise<void>;
65
+ export declare const __INTERNAL_DISCLOSURE_PATTERNS_FOR_TESTS: readonly string[];
66
+ export declare const __INTERNAL_FRONTMATTER_PATTERN_FOR_TESTS: RegExp;
67
+ export declare const __INTERNAL_BANNERS_FOR_TESTS: {
68
+ MISSING_FRONTMATTER_BANNER: string;
69
+ INVALID_FRONTMATTER_BANNER: string;
70
+ MISSING_DESCRIPTION_BANNER: string;
71
+ };