@massu/core 1.10.7 → 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.
- package/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +559 -400
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1205
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- 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();
|