@massu/core 1.10.8 → 1.12.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 (178) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +517 -397
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -0,0 +1,962 @@
1
+ #!/usr/bin/env node
2
+ import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
3
+
4
+ // src/hooks/lib/write-hook-message.ts
5
+ function writeHookMessage(message) {
6
+ process.stdout.write(JSON.stringify({ message }) + "\n");
7
+ }
8
+
9
+ // src/hooks/security-gate.ts
10
+ var DANGEROUS_BASH_PATTERNS = [
11
+ { pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /" },
12
+ { pattern: /rm\s+-[a-z]*f[a-z]*r[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /" },
13
+ { pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/, label: "curl | bash (remote code execution)" },
14
+ { pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/, label: "wget | bash (remote code execution)" },
15
+ { pattern: /chmod\s+777/, label: "chmod 777 (world-writable permissions)" },
16
+ { pattern: /chmod\s+-R\s+777/, label: "chmod -R 777 (world-writable permissions)" },
17
+ { pattern: />\s*\/etc\/passwd/, label: "write to /etc/passwd" },
18
+ { pattern: />\s*\/etc\/shadow/, label: "write to /etc/shadow" },
19
+ { pattern: />\s*\/etc\/sudoers/, label: "write to /etc/sudoers" },
20
+ { pattern: /dd\s+if=.*of=\/dev\/(?:sda|sdb|hda|hdb|nvme)/, label: "dd to raw device" },
21
+ { pattern: /mkfs\s+\/dev\//, label: "format disk device" },
22
+ { pattern: /:\(\)\s*\{\s*:\|:\s*&\s*\}/, label: "fork bomb" },
23
+ { pattern: /eval\s+.*\$\(.*curl/, label: "eval with remote curl" },
24
+ { pattern: /base64\s+-d\s+.*\|\s*(?:bash|sh|zsh)/, label: "base64 decoded shell exec" }
25
+ ];
26
+ var PROTECTED_FILE_PATTERNS = [
27
+ { pattern: /\.env$/, label: ".env file" },
28
+ { pattern: /\.env\./, label: ".env.* file" },
29
+ { pattern: /credentials(?:\.json)?$/, label: "credentials file" },
30
+ { pattern: /\.pem$/, label: ".pem certificate/key file" },
31
+ { pattern: /\.key$/, label: ".key file" },
32
+ { pattern: /\.p12$/, label: ".p12 keystore file" },
33
+ { pattern: /\.pfx$/, label: ".pfx keystore file" },
34
+ { pattern: /id_rsa$/, label: "RSA private key" },
35
+ { pattern: /id_ed25519$/, label: "Ed25519 private key" },
36
+ { pattern: /id_ecdsa$/, label: "ECDSA private key" },
37
+ { pattern: /\.ssh\/config$/, label: "SSH config file" },
38
+ { pattern: /secrets\.yaml$/, label: "secrets.yaml file" },
39
+ { pattern: /secrets\.yml$/, label: "secrets.yml file" },
40
+ { pattern: /\.netrc$/, label: ".netrc credentials file" },
41
+ { pattern: /aws\/credentials$/, label: "AWS credentials file" },
42
+ { pattern: /kubeconfig$/, label: "Kubernetes config file" }
43
+ ];
44
+ function checkBashCommand(command) {
45
+ for (const { pattern, label } of DANGEROUS_BASH_PATTERNS) {
46
+ if (pattern.test(command)) {
47
+ return label;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ function checkFilePath(filePath) {
53
+ for (const { pattern, label } of PROTECTED_FILE_PATTERNS) {
54
+ if (pattern.test(filePath)) {
55
+ return label;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ var DANGEROUS_PYTHON_PATTERNS = [
61
+ { pattern: /\beval\s*\(/, label: "Python eval() \u2014 arbitrary code execution" },
62
+ { pattern: /\bexec\s*\(/, label: "Python exec() \u2014 arbitrary code execution" },
63
+ { pattern: /\b__import__\s*\(/, label: "Python __import__() \u2014 dynamic import (potential code injection)" },
64
+ { pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: "subprocess.call(shell=True) \u2014 shell injection risk" },
65
+ { pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: "subprocess.Popen(shell=True) \u2014 shell injection risk" },
66
+ { pattern: /os\.system\s*\(/, label: "os.system() \u2014 shell injection risk" },
67
+ { pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: "f-string in SQL \u2014 SQL injection risk" },
68
+ { pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: "String formatting in SQL \u2014 SQL injection risk" }
69
+ ];
70
+ function checkPythonContent(content) {
71
+ for (const { pattern, label } of DANGEROUS_PYTHON_PATTERNS) {
72
+ if (pattern.test(content)) {
73
+ return label;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ function runSecurityGateChecks(hookInput) {
79
+ const messages = [];
80
+ try {
81
+ const { tool_name, tool_input } = hookInput;
82
+ if (tool_name === "Bash" && tool_input.command) {
83
+ const violation = checkBashCommand(tool_input.command);
84
+ if (violation) {
85
+ messages.push(`SECURITY GATE: Dangerous command pattern detected: ${violation}
86
+ Command: ${tool_input.command.slice(0, 200)}
87
+ Review carefully before proceeding.`);
88
+ }
89
+ }
90
+ if ((tool_name === "Write" || tool_name === "Edit") && tool_input.file_path) {
91
+ const violation = checkFilePath(tool_input.file_path);
92
+ if (violation) {
93
+ messages.push(`SECURITY GATE: Attempt to write to protected file: ${violation}
94
+ Path: ${tool_input.file_path}
95
+ Ensure this is intentional and no secrets will be exposed.`);
96
+ }
97
+ }
98
+ const pyContent = tool_input.content || tool_input.new_string;
99
+ if ((tool_name === "Write" || tool_name === "Edit") && tool_input.file_path?.endsWith(".py") && pyContent) {
100
+ const pyViolation = checkPythonContent(pyContent);
101
+ if (pyViolation) {
102
+ messages.push(`SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}
103
+ File: ${tool_input.file_path}
104
+ Review carefully before proceeding.`);
105
+ }
106
+ }
107
+ } catch {
108
+ }
109
+ return messages;
110
+ }
111
+ async function main() {
112
+ try {
113
+ const input = await readStdin();
114
+ const hookInput = JSON.parse(input);
115
+ const messages = runSecurityGateChecks(hookInput);
116
+ for (const msg of messages) writeHookMessage(msg);
117
+ } catch {
118
+ }
119
+ process.exit(0);
120
+ }
121
+ function readStdin() {
122
+ return new Promise((resolve2) => {
123
+ let data = "";
124
+ process.stdin.setEncoding("utf-8");
125
+ process.stdin.on("data", (chunk) => {
126
+ data += chunk;
127
+ });
128
+ process.stdin.on("end", () => resolve2(data));
129
+ setTimeout(() => resolve2(data), 400);
130
+ });
131
+ }
132
+ if (process.argv[1]?.endsWith("security-gate.js") || process.argv[1]?.endsWith("security-gate")) {
133
+ main();
134
+ }
135
+
136
+ // src/hooks/pre-delete-check.ts
137
+ import Database2 from "better-sqlite3";
138
+ import { existsSync as existsSync2 } from "fs";
139
+
140
+ // src/config.ts
141
+ import { resolve, dirname } from "path";
142
+ import { existsSync, readFileSync } from "fs";
143
+ import { homedir } from "os";
144
+ import { parse as parseYaml } from "yaml";
145
+ import { z } from "zod";
146
+
147
+ // src/lib/memory-path.ts
148
+ function encodeMemoryDirName(projectRoot) {
149
+ return projectRoot.replace(/\//g, "-");
150
+ }
151
+
152
+ // src/config.ts
153
+ var DomainConfigSchema = z.object({
154
+ name: z.string().default("Unknown"),
155
+ routers: z.array(z.string()).default([]),
156
+ pages: z.array(z.string()).default([]),
157
+ tables: z.array(z.string()).default([]),
158
+ allowedImportsFrom: z.array(z.string()).default([])
159
+ });
160
+ var PatternRuleConfigSchema = z.object({
161
+ pattern: z.string().default("**"),
162
+ rules: z.array(z.string()).default([]),
163
+ language: z.string().optional()
164
+ });
165
+ var CostModelSchema = z.object({
166
+ input_per_million: z.number(),
167
+ output_per_million: z.number(),
168
+ cache_read_per_million: z.number().optional(),
169
+ cache_write_per_million: z.number().optional()
170
+ });
171
+ var AnalyticsConfigSchema = z.object({
172
+ quality: z.object({
173
+ weights: z.record(z.string(), z.number()).default({
174
+ bug_found: -5,
175
+ vr_failure: -10,
176
+ incident: -20,
177
+ cr_violation: -3,
178
+ vr_pass: 2,
179
+ clean_commit: 5,
180
+ successful_verification: 3
181
+ }),
182
+ categories: z.array(z.string()).default(["security", "architecture", "coupling", "tests", "rule_compliance"])
183
+ }).optional(),
184
+ cost: z.object({
185
+ models: z.record(z.string(), CostModelSchema).default({}),
186
+ currency: z.string().default("USD")
187
+ }).optional(),
188
+ prompts: z.object({
189
+ success_indicators: z.array(z.string()).default(["committed", "approved", "looks good", "perfect", "great", "thanks"]),
190
+ failure_indicators: z.array(z.string()).default(["revert", "wrong", "that's not", "undo", "incorrect"]),
191
+ max_turns_for_success: z.number().default(2)
192
+ }).optional()
193
+ }).optional();
194
+ var CustomPatternSchema = z.object({
195
+ pattern: z.string(),
196
+ severity: z.string(),
197
+ message: z.string()
198
+ });
199
+ var GovernanceConfigSchema = z.object({
200
+ audit: z.object({
201
+ formats: z.array(z.string()).default(["summary", "detailed", "soc2"]),
202
+ retention_days: z.number().default(365),
203
+ auto_log: z.record(z.string(), z.boolean()).default({
204
+ code_changes: true,
205
+ rule_enforcement: true,
206
+ approvals: true,
207
+ commits: true
208
+ })
209
+ }).optional(),
210
+ validation: z.object({
211
+ realtime: z.boolean().default(true),
212
+ checks: z.record(z.string(), z.boolean()).default({
213
+ rule_compliance: true,
214
+ import_existence: true,
215
+ naming_conventions: true
216
+ }),
217
+ custom_patterns: z.array(CustomPatternSchema).default([])
218
+ }).optional(),
219
+ adr: z.object({
220
+ detection_phrases: z.array(z.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
221
+ template: z.string().default("default"),
222
+ storage: z.string().default("database"),
223
+ output_dir: z.string().default("docs/adr")
224
+ }).optional()
225
+ }).optional();
226
+ var SecurityPatternSchema = z.object({
227
+ pattern: z.string(),
228
+ severity: z.string(),
229
+ category: z.string(),
230
+ description: z.string()
231
+ });
232
+ var SecurityConfigSchema = z.object({
233
+ patterns: z.array(SecurityPatternSchema).default([]),
234
+ auto_score_on_edit: z.boolean().default(true),
235
+ score_threshold_alert: z.number().default(50),
236
+ severity_weights: z.record(z.string(), z.number()).optional(),
237
+ restrictive_licenses: z.array(z.string()).optional(),
238
+ dep_alternatives: z.record(z.string(), z.array(z.string())).optional(),
239
+ dependencies: z.object({
240
+ package_manager: z.string().default("npm"),
241
+ blocked_packages: z.array(z.string()).default([]),
242
+ preferred_packages: z.record(z.string(), z.string()).default({}),
243
+ max_bundle_size_kb: z.number().default(500)
244
+ }).optional()
245
+ }).optional();
246
+ var TeamConfigSchema = z.object({
247
+ enabled: z.boolean().default(false),
248
+ sync_backend: z.string().default("local"),
249
+ developer_id: z.string().default("auto"),
250
+ share_by_default: z.boolean().default(false),
251
+ expertise_weights: z.object({
252
+ session: z.number().default(20),
253
+ observation: z.number().default(10)
254
+ }).optional(),
255
+ privacy: z.object({
256
+ share_file_paths: z.boolean().default(true),
257
+ share_code_snippets: z.boolean().default(false),
258
+ share_observations: z.boolean().default(true)
259
+ }).optional()
260
+ }).optional();
261
+ var RegressionConfigSchema = z.object({
262
+ test_patterns: z.array(z.string()).default([
263
+ "{dir}/__tests__/{name}.test.{ext}",
264
+ "{dir}/{name}.spec.{ext}",
265
+ "tests/{path}.test.{ext}"
266
+ ]),
267
+ test_runner: z.string().default("npm test"),
268
+ health_thresholds: z.object({
269
+ healthy: z.number().default(80),
270
+ warning: z.number().default(50)
271
+ }).optional()
272
+ }).optional();
273
+ var AutoLearningConfigSchema = z.object({
274
+ enabled: z.boolean().default(true),
275
+ incidentDir: z.string().default("docs/incidents"),
276
+ memoryDir: z.string().default("memory"),
277
+ memoryIndexFile: z.string().default("MEMORY.md"),
278
+ enforcementHooksDir: z.string().default("scripts/hooks"),
279
+ fixDetection: z.object({
280
+ enabled: z.boolean().default(true),
281
+ lookbackDays: z.number().default(7),
282
+ signals: z.array(z.string()).default([
283
+ "removed_broken_code",
284
+ "added_error_handling",
285
+ "method_name_correction",
286
+ "auth_fix",
287
+ "nil_handling_fix",
288
+ "concurrency_fix",
289
+ "async_pattern_fix",
290
+ "added_missing_import"
291
+ ])
292
+ }).default({}),
293
+ failureClassification: z.object({
294
+ enabled: z.boolean().default(true),
295
+ thresholds: z.object({
296
+ known: z.number().default(5),
297
+ similar: z.number().default(3)
298
+ }).default({}),
299
+ scoring: z.object({
300
+ diffPatternWeight: z.number().default(3),
301
+ filePatternWeight: z.number().default(2),
302
+ promptKeywordWeight: z.number().default(2)
303
+ }).default({})
304
+ }).default({}),
305
+ pipeline: z.object({
306
+ requireIncidentReport: z.boolean().default(true),
307
+ requirePreventionRule: z.boolean().default(true),
308
+ requireEnforcement: z.boolean().default(true)
309
+ }).default({})
310
+ }).optional();
311
+ var CloudConfigSchema = z.object({
312
+ enabled: z.boolean().default(false),
313
+ apiKey: z.string().optional(),
314
+ endpoint: z.string().optional(),
315
+ sync: z.object({
316
+ memory: z.boolean().default(true),
317
+ analytics: z.boolean().default(true),
318
+ audit: z.boolean().default(true)
319
+ }).default({ memory: true, analytics: true, audit: true })
320
+ }).optional();
321
+ var ConventionsConfigSchema = z.object({
322
+ claudeDirName: z.string().default(".claude").refine(
323
+ (s) => !s.includes("..") && !s.startsWith("/"),
324
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
325
+ ),
326
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
327
+ (s) => !s.includes("..") && !s.startsWith("/"),
328
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
329
+ ),
330
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
331
+ (s) => !s.includes("..") && !s.startsWith("/"),
332
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
333
+ ),
334
+ knowledgeCategories: z.array(z.string()).default([
335
+ "patterns",
336
+ "commands",
337
+ "incidents",
338
+ "reference",
339
+ "protocols",
340
+ "checklists",
341
+ "playbooks",
342
+ "critical",
343
+ "scripts",
344
+ "status",
345
+ "templates",
346
+ "loop-state",
347
+ "session-state",
348
+ "agents"
349
+ ]),
350
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
351
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
352
+ }).optional();
353
+ var PythonDomainConfigSchema = z.object({
354
+ name: z.string(),
355
+ packages: z.array(z.string()),
356
+ allowed_imports_from: z.array(z.string()).default([])
357
+ });
358
+ var PythonConfigSchema = z.object({
359
+ root: z.string(),
360
+ alembic_dir: z.string().optional(),
361
+ domains: z.array(PythonDomainConfigSchema).default([]),
362
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
363
+ }).optional();
364
+ var PathsConfigSchema = z.object({
365
+ source: z.string().default("src"),
366
+ aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
367
+ monorepo_roots: z.array(z.string()).optional(),
368
+ routers: z.string().optional(),
369
+ routerRoot: z.string().optional(),
370
+ pages: z.string().optional(),
371
+ middleware: z.string().optional(),
372
+ schema: z.string().optional(),
373
+ components: z.string().optional(),
374
+ hooks: z.string().optional()
375
+ });
376
+ var LanguageFrameworkEntrySchema = z.object({
377
+ framework: z.string().optional(),
378
+ test_framework: z.string().optional(),
379
+ test: z.string().optional(),
380
+ runtime: z.string().optional(),
381
+ orm: z.string().optional(),
382
+ router: z.string().optional(),
383
+ ui: z.string().optional()
384
+ }).passthrough();
385
+ var FrameworkConfigSchema = z.object({
386
+ type: z.string().default("typescript"),
387
+ primary: z.string().optional(),
388
+ router: z.string().default("none"),
389
+ orm: z.string().default("none"),
390
+ ui: z.string().default("none"),
391
+ languages: z.record(z.string(), LanguageFrameworkEntrySchema).optional()
392
+ }).passthrough();
393
+ var DetectedConfigSchema = z.object({}).passthrough().optional();
394
+ var VerificationEntrySchema = z.object({
395
+ type: z.string().optional(),
396
+ test: z.string().optional(),
397
+ syntax: z.string().optional(),
398
+ lint: z.string().optional(),
399
+ build: z.string().optional()
400
+ }).passthrough();
401
+ var VerificationConfigSchema = z.record(z.string(), VerificationEntrySchema).optional();
402
+ var CanonicalPathsSchema = z.record(z.string(), z.string()).optional();
403
+ var VerificationTypesSchema = z.record(z.string(), z.string()).optional();
404
+ var DetectionRuleEntrySchema = z.object({
405
+ signals: z.array(z.string()).default([]),
406
+ priority: z.number().optional()
407
+ }).passthrough();
408
+ var DetectionConfigSchema = z.object({
409
+ rules: z.record(
410
+ z.string(),
411
+ // language
412
+ z.record(z.string(), DetectionRuleEntrySchema)
413
+ // framework -> rule entry
414
+ ).optional(),
415
+ signal_weights: z.record(z.string(), z.number()).optional(),
416
+ disable_builtin: z.boolean().optional()
417
+ }).passthrough().optional();
418
+ var WatchConfigSchema = z.object({
419
+ debounce_ms: z.number().int().positive().default(3e3),
420
+ storm_threshold: z.number().int().positive().default(50),
421
+ deep_storm_threshold: z.number().int().positive().default(500),
422
+ hard_timeout_ms: z.number().int().positive().default(3e5),
423
+ scope: z.enum(["paths", "full"]).default("paths"),
424
+ // Plan 3a hotfix 2026-05-02: refuse to start if the watch surface
425
+ // exceeds this many files. Prevents the misconfig pattern where
426
+ // `paths.source_dirs` includes `.` or otherwise expands to a 60K+
427
+ // file tree, producing 30-100% steady CPU. Override via
428
+ // `paths_full_root_opt_in: true` for users on small repos who genuinely
429
+ // need root-level watching.
430
+ max_watched_files: z.number().int().positive().default(1e4),
431
+ paths_full_root_opt_in: z.boolean().default(false)
432
+ }).passthrough().optional();
433
+ var AdapterLocalPathSchema = z.string().refine((s) => !/^([A-Za-z]:[\\/]|[\\/])/.test(s), {
434
+ message: "absolute paths are rejected; adapters.local entries must be relative to the massu.config.yaml directory"
435
+ }).refine((s) => !s.split(/[\\/]/).includes(".."), {
436
+ message: "parent-directory traversal (`..`) is rejected; adapters.local entries must stay inside the project tree"
437
+ }).transform((s) => s.split(/[\\/]/).filter((part) => part !== "" && part !== ".").join("/"));
438
+ var AdaptersConfigSchema = z.object({
439
+ enabled: z.boolean().default(false),
440
+ local: z.array(AdapterLocalPathSchema).default([])
441
+ }).passthrough().optional();
442
+ var TelemetryConfigSchema = z.object({
443
+ adapters: z.boolean().default(false)
444
+ }).passthrough().optional();
445
+ var LSPConfigSchema = z.object({
446
+ enabled: z.boolean().default(false),
447
+ servers: z.array(z.object({
448
+ language: z.string(),
449
+ command: z.string(),
450
+ // F-014 (closed 2026-05-06): explicit opt-in to spawn SUID/SGID
451
+ // binaries. Default false — argv[0] with the SUID bit is rejected
452
+ // unless this is true. Decision is auditable in the YAML.
453
+ allow_setuid: z.boolean().default(false),
454
+ // F-015 (closed 2026-05-06): per-server RSS budget (MB). Watchdog
455
+ // SIGKILLs the server after sustained breach. Default 1024 MB.
456
+ // Set to 0 to disable the watchdog for this server.
457
+ max_rss_mb: z.number().int().nonnegative().default(1024)
458
+ })).default([]),
459
+ autoDetect: z.object({
460
+ viaPortScan: z.boolean().default(false)
461
+ }).optional()
462
+ }).passthrough();
463
+ var RawConfigSchema = z.object({
464
+ schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
465
+ project: z.object({
466
+ name: z.string().default("my-project"),
467
+ root: z.string().default("auto")
468
+ }).default({ name: "my-project", root: "auto" }),
469
+ framework: FrameworkConfigSchema.default({
470
+ type: "typescript",
471
+ router: "none",
472
+ orm: "none",
473
+ ui: "none"
474
+ }),
475
+ paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
476
+ toolPrefix: z.string().default("massu"),
477
+ dbAccessPattern: z.string().optional(),
478
+ knownMismatches: z.record(z.string(), z.record(z.string(), z.string())).optional(),
479
+ accessScopes: z.array(z.string()).optional(),
480
+ domains: z.array(DomainConfigSchema).default([]),
481
+ rules: z.array(PatternRuleConfigSchema).default([]),
482
+ // P-M-036 (plan-stage-d-medium-sweep): customer-authored CR-style
483
+ // governance rules. DISTINCT from `rules:` above (path-scoped lint hints
484
+ // used by pattern-scanner). At config-refresh time these entries are
485
+ // loaded into the `knowledge_rules` SQLite table with
486
+ // `source = 'customer-config'` so `massu_knowledge_rule` and the
487
+ // governance docs surface customer-defined rules alongside framework CRs.
488
+ governance_rules: z.array(
489
+ z.object({
490
+ id: z.string().min(1, "governance_rules[].id is required"),
491
+ title: z.string().min(1, "governance_rules[].title is required"),
492
+ description: z.string().min(1, "governance_rules[].description is required"),
493
+ vr_type: z.string().default("VR-CUSTOM"),
494
+ reference_path: z.string().optional(),
495
+ severity: z.enum(["critical", "high", "medium", "low", "info"]).default("medium")
496
+ }).passthrough()
497
+ ).default([]),
498
+ analytics: AnalyticsConfigSchema,
499
+ governance: GovernanceConfigSchema,
500
+ security: SecurityConfigSchema,
501
+ team: TeamConfigSchema,
502
+ regression: RegressionConfigSchema,
503
+ cloud: CloudConfigSchema,
504
+ conventions: ConventionsConfigSchema,
505
+ autoLearning: AutoLearningConfigSchema,
506
+ python: PythonConfigSchema,
507
+ // P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
508
+ verification: VerificationConfigSchema,
509
+ canonical_paths: CanonicalPathsSchema,
510
+ verification_types: VerificationTypesSchema,
511
+ detection: DetectionConfigSchema,
512
+ // Plan #2: detector-owned per-language conventions (free-form passthrough)
513
+ detected: DetectedConfigSchema,
514
+ // Plan 3a: file-watcher daemon tunables
515
+ watch: WatchConfigSchema,
516
+ // Plan 3c: third-party adapter registry kill-switch + signing override + local-path opt-in.
517
+ adapters: AdaptersConfigSchema,
518
+ // Plan 3c: anonymous adapter-discovery telemetry opt-in (default off).
519
+ telemetry: TelemetryConfigSchema,
520
+ // Plan 3b Phase 4: optional LSP enrichment of AST adapter results.
521
+ lsp: LSPConfigSchema.optional()
522
+ }).passthrough();
523
+ var _config = null;
524
+ var _projectRoot = null;
525
+ function findProjectRoot() {
526
+ const cwd = process.cwd();
527
+ let dir = cwd;
528
+ while (true) {
529
+ if (existsSync(resolve(dir, "massu.config.yaml"))) {
530
+ return dir;
531
+ }
532
+ const parent = dirname(dir);
533
+ if (parent === dir) break;
534
+ dir = parent;
535
+ }
536
+ dir = cwd;
537
+ while (true) {
538
+ if (existsSync(resolve(dir, "package.json"))) {
539
+ return dir;
540
+ }
541
+ if (existsSync(resolve(dir, ".git"))) {
542
+ return dir;
543
+ }
544
+ const parent = dirname(dir);
545
+ if (parent === dir) break;
546
+ dir = parent;
547
+ }
548
+ return cwd;
549
+ }
550
+ function getProjectRoot() {
551
+ if (!_projectRoot) {
552
+ _projectRoot = findProjectRoot();
553
+ }
554
+ return _projectRoot;
555
+ }
556
+ function getConfig() {
557
+ if (_config) return _config;
558
+ const root = getProjectRoot();
559
+ const configPath = resolve(root, "massu.config.yaml");
560
+ let rawYaml = {};
561
+ if (existsSync(configPath)) {
562
+ const content = readFileSync(configPath, "utf-8");
563
+ rawYaml = parseYaml(content) ?? {};
564
+ }
565
+ const result = RawConfigSchema.safeParse(rawYaml);
566
+ if (!result.success) {
567
+ const issues = result.error.issues.map((i) => {
568
+ const path = i.path.length > 0 ? i.path.join(".") : "(root)";
569
+ const received = "received" in i && i.received !== void 0 ? ` (received ${JSON.stringify(i.received)})` : "";
570
+ return ` - ${path}: ${i.message}${received}`;
571
+ }).join("\n");
572
+ throw new Error(
573
+ `Invalid massu.config.yaml at ${configPath}:
574
+ ${issues}
575
+ Hint: run \`massu config refresh\` to regenerate a valid config or fix the listed fields manually.`
576
+ );
577
+ }
578
+ const parsed = result.data;
579
+ const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve(root, parsed.project.root);
580
+ const fw = parsed.framework;
581
+ let router = fw.router;
582
+ let orm = fw.orm;
583
+ let ui = fw.ui;
584
+ if (fw.type === "multi" && fw.primary && fw.languages) {
585
+ const primaryEntry = fw.languages[fw.primary];
586
+ if (primaryEntry) {
587
+ if (router === "none" && primaryEntry.router) router = primaryEntry.router;
588
+ if (orm === "none" && primaryEntry.orm) orm = primaryEntry.orm;
589
+ if (ui === "none" && primaryEntry.ui) ui = primaryEntry.ui;
590
+ }
591
+ }
592
+ _config = {
593
+ schema_version: parsed.schema_version,
594
+ project: {
595
+ name: parsed.project.name,
596
+ root: projectRoot
597
+ },
598
+ // Spread `fw` first so zod-`.passthrough()` extras (e.g., `framework.swift`,
599
+ // `framework.python`) survive into the consumer-visible Config. Then override
600
+ // the v2-backcompat-mirrored router/orm/ui values. Without the spread, the
601
+ // variant-resolution `pickVariant` (install-commands.ts) cannot see the
602
+ // top-level passthrough language blocks.
603
+ framework: {
604
+ ...fw,
605
+ router,
606
+ orm,
607
+ ui
608
+ },
609
+ paths: parsed.paths,
610
+ toolPrefix: parsed.toolPrefix,
611
+ dbAccessPattern: parsed.dbAccessPattern,
612
+ knownMismatches: parsed.knownMismatches,
613
+ accessScopes: parsed.accessScopes,
614
+ domains: parsed.domains,
615
+ rules: parsed.rules,
616
+ // P-M-036: customer-authored CR-style governance rules.
617
+ governance_rules: parsed.governance_rules,
618
+ analytics: parsed.analytics,
619
+ governance: parsed.governance,
620
+ security: parsed.security,
621
+ team: parsed.team,
622
+ regression: parsed.regression,
623
+ cloud: parsed.cloud,
624
+ conventions: parsed.conventions,
625
+ autoLearning: parsed.autoLearning,
626
+ python: parsed.python,
627
+ verification: parsed.verification,
628
+ canonical_paths: parsed.canonical_paths,
629
+ verification_types: parsed.verification_types,
630
+ detection: parsed.detection,
631
+ detected: parsed.detected,
632
+ watch: parsed.watch,
633
+ adapters: parsed.adapters,
634
+ telemetry: parsed.telemetry,
635
+ lsp: parsed.lsp
636
+ };
637
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
638
+ _config.cloud = {
639
+ enabled: true,
640
+ sync: { memory: true, analytics: true, audit: true },
641
+ ..._config.cloud,
642
+ apiKey: process.env.MASSU_API_KEY
643
+ };
644
+ }
645
+ return _config;
646
+ }
647
+ function getResolvedPaths() {
648
+ const config = getConfig();
649
+ const root = getProjectRoot();
650
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
651
+ return {
652
+ codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
653
+ dataDbPath: resolve(root, ".massu/data.db"),
654
+ prismaSchemaPath: resolve(root, config.paths.schema ?? "prisma/schema.prisma"),
655
+ rootRouterPath: resolve(root, config.paths.routerRoot ?? "src/server/api/root.ts"),
656
+ routersDir: resolve(root, config.paths.routers ?? "src/server/api/routers"),
657
+ srcDir: resolve(root, config.paths.source),
658
+ pathAlias: Object.fromEntries(
659
+ Object.entries(config.paths.aliases).map(([alias, target]) => [
660
+ alias,
661
+ resolve(root, target)
662
+ ])
663
+ ),
664
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
665
+ indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
666
+ patternsDir: resolve(root, claudeDirName, "patterns"),
667
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
668
+ docsMapPath: resolve(root, ".massu/docs-map.json"),
669
+ helpSitePath: resolve(root, "../" + config.project.name + "-help"),
670
+ memoryDbPath: resolve(root, ".massu/memory.db"),
671
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
672
+ plansDir: resolve(root, "docs/plans"),
673
+ docsDir: resolve(root, "docs"),
674
+ claudeDir: resolve(root, claudeDirName),
675
+ memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
676
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
677
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
678
+ mcpJsonPath: resolve(root, ".mcp.json"),
679
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
680
+ };
681
+ }
682
+
683
+ // src/memory-db.ts
684
+ import Database from "better-sqlite3";
685
+
686
+ // src/lib/sql-table-names.ts
687
+ function t(suffix) {
688
+ return `${getConfig().toolPrefix}_${suffix}`;
689
+ }
690
+
691
+ // src/sentinel-db.ts
692
+ var PROJECT_ROOT = getProjectRoot();
693
+ function parsePortalScope(raw) {
694
+ if (!raw) return [];
695
+ try {
696
+ return JSON.parse(raw);
697
+ } catch {
698
+ return [];
699
+ }
700
+ }
701
+ function toFeature(row) {
702
+ return {
703
+ id: row.id,
704
+ feature_key: row.feature_key,
705
+ domain: row.domain,
706
+ subdomain: row.subdomain || null,
707
+ title: row.title,
708
+ description: row.description || null,
709
+ status: row.status,
710
+ priority: row.priority,
711
+ portal_scope: parsePortalScope(row.portal_scope),
712
+ created_at: row.created_at,
713
+ updated_at: row.updated_at,
714
+ removed_at: row.removed_at || null,
715
+ removed_reason: row.removed_reason || null
716
+ };
717
+ }
718
+ function getFeatureById(db, id) {
719
+ const row = db.prepare(`SELECT * FROM ${t("sentinel")} WHERE id = ?`).get(id);
720
+ return row ? toFeature(row) : null;
721
+ }
722
+ function getFeatureImpact(db, filePaths) {
723
+ const fileSet = new Set(filePaths);
724
+ const affectedFeatureIds = /* @__PURE__ */ new Set();
725
+ for (const filePath of filePaths) {
726
+ const links = db.prepare(
727
+ `SELECT feature_id FROM ${t("sentinel_components")} WHERE component_file = ?`
728
+ ).all(filePath);
729
+ for (const link of links) {
730
+ affectedFeatureIds.add(link.feature_id);
731
+ }
732
+ }
733
+ const orphaned = [];
734
+ const degraded = [];
735
+ const unaffected = [];
736
+ for (const featureId of affectedFeatureIds) {
737
+ const feature = getFeatureById(db, featureId);
738
+ if (!feature || feature.status !== "active") continue;
739
+ const allComponents = db.prepare(
740
+ `SELECT component_file, is_primary FROM ${t("sentinel_components")} WHERE feature_id = ?`
741
+ ).all(featureId);
742
+ const affected = allComponents.filter((c) => fileSet.has(c.component_file));
743
+ const remaining = allComponents.filter((c) => !fileSet.has(c.component_file));
744
+ const primaryAffected = affected.some((c) => c.is_primary);
745
+ const item = {
746
+ feature,
747
+ affected_files: affected.map((c) => c.component_file),
748
+ remaining_files: remaining.map((c) => c.component_file),
749
+ status: "unaffected"
750
+ };
751
+ if (primaryAffected && remaining.filter((c) => c.is_primary).length === 0) {
752
+ item.status = "orphaned";
753
+ orphaned.push(item);
754
+ } else if (affected.length > 0) {
755
+ item.status = "degraded";
756
+ degraded.push(item);
757
+ } else {
758
+ unaffected.push(item);
759
+ }
760
+ }
761
+ const hasCriticalOrphans = orphaned.some((o) => o.feature.priority === "critical");
762
+ const hasStandardOrphans = orphaned.some((o) => o.feature.priority === "standard");
763
+ return {
764
+ files_analyzed: filePaths,
765
+ orphaned,
766
+ degraded,
767
+ unaffected,
768
+ blocked: hasCriticalOrphans || hasStandardOrphans,
769
+ block_reason: hasCriticalOrphans ? `BLOCKED: ${orphaned.length} features would be orphaned (includes critical features). Create migration plan first.` : hasStandardOrphans ? `BLOCKED: ${orphaned.length} standard features would be orphaned. Create migration plan first.` : null
770
+ };
771
+ }
772
+
773
+ // src/hooks/pre-delete-check.ts
774
+ var PROJECT_ROOT2 = getProjectRoot();
775
+ var KNOWLEDGE_PROTECTED_FILES = [
776
+ "knowledge-db.ts",
777
+ "knowledge-indexer.ts",
778
+ "knowledge-tools.ts"
779
+ ];
780
+ function getDataDb() {
781
+ const dbPath = getResolvedPaths().dataDbPath;
782
+ if (!existsSync2(dbPath)) return null;
783
+ try {
784
+ const db = new Database2(dbPath, { readonly: true });
785
+ db.pragma("journal_mode = WAL");
786
+ return db;
787
+ } catch {
788
+ return null;
789
+ }
790
+ }
791
+ function checkKnowledgeFileProtection(input) {
792
+ const candidateFiles = [];
793
+ if (input.tool_name === "Bash" && input.tool_input.command) {
794
+ const cmd = input.tool_input.command;
795
+ const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
796
+ if (rmMatch) {
797
+ const parts = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
798
+ candidateFiles.push(...parts);
799
+ }
800
+ }
801
+ if (input.tool_name === "Write" && input.tool_input.file_path) {
802
+ const content = input.tool_input.content || "";
803
+ if (content.trim().length === 0) {
804
+ candidateFiles.push(input.tool_input.file_path);
805
+ }
806
+ }
807
+ for (const f of candidateFiles) {
808
+ const basename = f.split("/").pop() ?? f;
809
+ if (KNOWLEDGE_PROTECTED_FILES.includes(basename)) {
810
+ return `KNOWLEDGE SYSTEM PROTECTION: "${basename}" is a core knowledge system file. Deleting it will break knowledge indexing and memory retrieval. Create a replacement before removing.`;
811
+ }
812
+ }
813
+ return null;
814
+ }
815
+ function extractDeletedFiles(input) {
816
+ const files = [];
817
+ if (input.tool_name === "Bash" && input.tool_input.command) {
818
+ const cmd = input.tool_input.command;
819
+ const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
820
+ if (rmMatch) {
821
+ const paths = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
822
+ for (const p of paths) {
823
+ const relPath = p.startsWith("src/") ? p : p.replace(PROJECT_ROOT2 + "/", "");
824
+ if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
825
+ files.push(relPath);
826
+ }
827
+ }
828
+ }
829
+ }
830
+ if (input.tool_name === "Write" && input.tool_input.file_path) {
831
+ const content = input.tool_input.content || "";
832
+ if (content.trim().length === 0) {
833
+ const relPath = input.tool_input.file_path.replace(PROJECT_ROOT2 + "/", "");
834
+ if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
835
+ files.push(relPath);
836
+ }
837
+ }
838
+ }
839
+ return files;
840
+ }
841
+ function runPreDeleteChecks(hookInput) {
842
+ const messages = [];
843
+ try {
844
+ const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
845
+ if (knowledgeWarning) {
846
+ messages.push(knowledgeWarning);
847
+ return messages;
848
+ }
849
+ const deletedFiles = extractDeletedFiles(hookInput);
850
+ if (deletedFiles.length === 0) return messages;
851
+ const db = getDataDb();
852
+ if (!db) return messages;
853
+ const SENTINEL_TABLE = t("sentinel");
854
+ try {
855
+ const tableExists = db.prepare(
856
+ `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
857
+ ).get(SENTINEL_TABLE);
858
+ if (!tableExists) return messages;
859
+ const impact = getFeatureImpact(db, deletedFiles);
860
+ if (impact.blocked) {
861
+ const msg = [
862
+ `SENTINEL IMPACT WARNING: Deleting ${deletedFiles.length} file(s) would affect features:`,
863
+ ""
864
+ ];
865
+ if (impact.orphaned.length > 0) {
866
+ msg.push(`ORPHANED (${impact.orphaned.length} features - no primary components left):`);
867
+ for (const item of impact.orphaned) {
868
+ msg.push(` - ${item.feature.feature_key} [${item.feature.priority}]: ${item.feature.title}`);
869
+ }
870
+ }
871
+ if (impact.degraded.length > 0) {
872
+ msg.push(`DEGRADED (${impact.degraded.length} features - some components removed):`);
873
+ for (const item of impact.degraded) {
874
+ msg.push(` - ${item.feature.feature_key}: ${item.feature.title}`);
875
+ }
876
+ }
877
+ msg.push("");
878
+ msg.push("Create a migration plan before deleting these files.");
879
+ messages.push(msg.join("\n"));
880
+ }
881
+ const pyFiles = deletedFiles.filter((f) => f.endsWith(".py"));
882
+ if (pyFiles.length > 0) {
883
+ try {
884
+ for (const pyFile of pyFiles) {
885
+ const importers = db.prepare(
886
+ `SELECT source_file FROM ${t("py_imports")} WHERE target_file = ?`
887
+ ).all(pyFile);
888
+ const routes = db.prepare(
889
+ `SELECT method, path FROM ${t("py_routes")} WHERE file = ?`
890
+ ).all(pyFile);
891
+ const models = db.prepare(
892
+ `SELECT class_name FROM ${t("py_models")} WHERE file = ?`
893
+ ).all(pyFile);
894
+ if (importers.length > 0 || routes.length > 0 || models.length > 0) {
895
+ const parts = [];
896
+ if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
897
+ if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
898
+ if (models.length > 0) parts.push(`defines ${models.length} models`);
899
+ messages.push(`PYTHON IMPACT: "${pyFile}" ${parts.join(", ")}. Check dependents before deleting.`);
900
+ }
901
+ }
902
+ } catch {
903
+ }
904
+ }
905
+ } finally {
906
+ db.close();
907
+ }
908
+ } catch {
909
+ }
910
+ return messages;
911
+ }
912
+ async function main2() {
913
+ try {
914
+ const input = await readStdin2();
915
+ const hookInput = JSON.parse(input);
916
+ const messages = runPreDeleteChecks(hookInput);
917
+ for (const msg of messages) writeHookMessage(msg);
918
+ } catch {
919
+ }
920
+ process.exit(0);
921
+ }
922
+ function readStdin2() {
923
+ return new Promise((resolve2) => {
924
+ let data = "";
925
+ process.stdin.setEncoding("utf-8");
926
+ process.stdin.on("data", (chunk) => {
927
+ data += chunk;
928
+ });
929
+ process.stdin.on("end", () => resolve2(data));
930
+ setTimeout(() => resolve2(data), 400);
931
+ });
932
+ }
933
+ if (process.argv[1]?.endsWith("pre-delete-check.js") || process.argv[1]?.endsWith("pre-delete-check")) {
934
+ main2();
935
+ }
936
+
937
+ // src/hooks/pre-tool-use-gate.ts
938
+ async function main3() {
939
+ try {
940
+ const input = await readStdin3();
941
+ const hookInput = JSON.parse(input);
942
+ const securityMessages = runSecurityGateChecks(hookInput);
943
+ const deleteMessages = runPreDeleteChecks(hookInput);
944
+ for (const msg of [...securityMessages, ...deleteMessages]) {
945
+ writeHookMessage(msg);
946
+ }
947
+ } catch {
948
+ }
949
+ process.exit(0);
950
+ }
951
+ function readStdin3() {
952
+ return new Promise((resolve2) => {
953
+ let data = "";
954
+ process.stdin.setEncoding("utf-8");
955
+ process.stdin.on("data", (chunk) => {
956
+ data += chunk;
957
+ });
958
+ process.stdin.on("end", () => resolve2(data));
959
+ setTimeout(() => resolve2(data), 400);
960
+ });
961
+ }
962
+ main3();