@massu/core 0.1.2 → 0.4.1

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 (84) hide show
  1. package/commands/_shared-preamble.md +76 -0
  2. package/commands/massu-audit-deps.md +211 -0
  3. package/commands/massu-changelog.md +174 -0
  4. package/commands/massu-cleanup.md +315 -0
  5. package/commands/massu-commit.md +481 -0
  6. package/commands/massu-create-plan.md +752 -0
  7. package/commands/massu-dead-code.md +131 -0
  8. package/commands/massu-debug.md +484 -0
  9. package/commands/massu-deploy.md +91 -0
  10. package/commands/massu-deps.md +374 -0
  11. package/commands/massu-doc-gen.md +279 -0
  12. package/commands/massu-docs.md +364 -0
  13. package/commands/massu-estimate.md +313 -0
  14. package/commands/massu-golden-path.md +973 -0
  15. package/commands/massu-guide.md +167 -0
  16. package/commands/massu-hotfix.md +480 -0
  17. package/commands/massu-loop-playwright.md +837 -0
  18. package/commands/massu-loop.md +775 -0
  19. package/commands/massu-new-feature.md +511 -0
  20. package/commands/massu-parity.md +214 -0
  21. package/commands/massu-plan.md +456 -0
  22. package/commands/massu-push-light.md +207 -0
  23. package/commands/massu-push.md +434 -0
  24. package/commands/massu-refactor.md +410 -0
  25. package/commands/massu-release.md +363 -0
  26. package/commands/massu-review.md +238 -0
  27. package/commands/massu-simplify.md +281 -0
  28. package/commands/massu-status.md +278 -0
  29. package/commands/massu-tdd.md +201 -0
  30. package/commands/massu-test.md +516 -0
  31. package/commands/massu-verify-playwright.md +281 -0
  32. package/commands/massu-verify.md +667 -0
  33. package/dist/cli.js +12521 -0
  34. package/dist/hooks/cost-tracker.js +80 -5
  35. package/dist/hooks/post-edit-context.js +72 -6
  36. package/dist/hooks/post-tool-use.js +234 -57
  37. package/dist/hooks/pre-compact.js +144 -5
  38. package/dist/hooks/pre-delete-check.js +141 -11
  39. package/dist/hooks/quality-event.js +80 -5
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +83 -8
  42. package/dist/hooks/session-start.js +153 -7
  43. package/dist/hooks/user-prompt.js +166 -5
  44. package/package.json +6 -5
  45. package/src/backfill-sessions.ts +5 -4
  46. package/src/cli.ts +6 -1
  47. package/src/commands/doctor.ts +193 -6
  48. package/src/commands/init.ts +235 -6
  49. package/src/commands/install-commands.ts +137 -0
  50. package/src/config.ts +68 -2
  51. package/src/db.ts +115 -2
  52. package/src/docs-tools.ts +8 -6
  53. package/src/hooks/post-edit-context.ts +1 -1
  54. package/src/hooks/post-tool-use.ts +130 -0
  55. package/src/hooks/pre-compact.ts +23 -1
  56. package/src/hooks/pre-delete-check.ts +92 -4
  57. package/src/hooks/security-gate.ts +32 -0
  58. package/src/hooks/session-start.ts +97 -4
  59. package/src/hooks/user-prompt.ts +46 -1
  60. package/src/import-resolver.ts +2 -1
  61. package/src/knowledge-db.ts +169 -0
  62. package/src/knowledge-indexer.ts +704 -0
  63. package/src/knowledge-tools.ts +1413 -0
  64. package/src/license.ts +482 -0
  65. package/src/memory-db.ts +14 -1
  66. package/src/observation-extractor.ts +11 -4
  67. package/src/page-deps.ts +3 -2
  68. package/src/python/coupling-detector.ts +124 -0
  69. package/src/python/domain-enforcer.ts +83 -0
  70. package/src/python/impact-analyzer.ts +95 -0
  71. package/src/python/import-parser.ts +244 -0
  72. package/src/python/import-resolver.ts +135 -0
  73. package/src/python/migration-indexer.ts +115 -0
  74. package/src/python/migration-parser.ts +332 -0
  75. package/src/python/model-indexer.ts +70 -0
  76. package/src/python/model-parser.ts +279 -0
  77. package/src/python/route-indexer.ts +58 -0
  78. package/src/python/route-parser.ts +317 -0
  79. package/src/python-tools.ts +629 -0
  80. package/src/sentinel-db.ts +2 -1
  81. package/src/server.ts +29 -6
  82. package/src/session-archiver.ts +4 -5
  83. package/src/tools.ts +283 -31
  84. package/README.md +0 -40
@@ -9,6 +9,7 @@ import { existsSync as existsSync2, mkdirSync } from "fs";
9
9
  // src/config.ts
10
10
  import { resolve, dirname } from "path";
11
11
  import { existsSync, readFileSync } from "fs";
12
+ import { homedir } from "os";
12
13
  import { parse as parseYaml } from "yaml";
13
14
  import { z } from "zod";
14
15
  var DomainConfigSchema = z.object({
@@ -140,6 +141,49 @@ var CloudConfigSchema = z.object({
140
141
  audit: z.boolean().default(true)
141
142
  }).default({ memory: true, analytics: true, audit: true })
142
143
  }).optional();
144
+ var ConventionsConfigSchema = z.object({
145
+ claudeDirName: z.string().default(".claude").refine(
146
+ (s) => !s.includes("..") && !s.startsWith("/"),
147
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
148
+ ),
149
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
150
+ (s) => !s.includes("..") && !s.startsWith("/"),
151
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
152
+ ),
153
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
154
+ (s) => !s.includes("..") && !s.startsWith("/"),
155
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
156
+ ),
157
+ knowledgeCategories: z.array(z.string()).default([
158
+ "patterns",
159
+ "commands",
160
+ "incidents",
161
+ "reference",
162
+ "protocols",
163
+ "checklists",
164
+ "playbooks",
165
+ "critical",
166
+ "scripts",
167
+ "status",
168
+ "templates",
169
+ "loop-state",
170
+ "session-state",
171
+ "agents"
172
+ ]),
173
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
174
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
175
+ }).optional();
176
+ var PythonDomainConfigSchema = z.object({
177
+ name: z.string(),
178
+ packages: z.array(z.string()),
179
+ allowed_imports_from: z.array(z.string()).default([])
180
+ });
181
+ var PythonConfigSchema = z.object({
182
+ root: z.string(),
183
+ alembic_dir: z.string().optional(),
184
+ domains: z.array(PythonDomainConfigSchema).default([]),
185
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
186
+ }).optional();
143
187
  var PathsConfigSchema = z.object({
144
188
  source: z.string().default("src"),
145
189
  aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
@@ -174,7 +218,9 @@ var RawConfigSchema = z.object({
174
218
  security: SecurityConfigSchema,
175
219
  team: TeamConfigSchema,
176
220
  regression: RegressionConfigSchema,
177
- cloud: CloudConfigSchema
221
+ cloud: CloudConfigSchema,
222
+ conventions: ConventionsConfigSchema,
223
+ python: PythonConfigSchema
178
224
  }).passthrough();
179
225
  var _config = null;
180
226
  var _projectRoot = null;
@@ -238,13 +284,24 @@ function getConfig() {
238
284
  security: parsed.security,
239
285
  team: parsed.team,
240
286
  regression: parsed.regression,
241
- cloud: parsed.cloud
287
+ cloud: parsed.cloud,
288
+ conventions: parsed.conventions,
289
+ python: parsed.python
242
290
  };
291
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
292
+ _config.cloud = {
293
+ enabled: true,
294
+ sync: { memory: true, analytics: true, audit: true },
295
+ ..._config.cloud,
296
+ apiKey: process.env.MASSU_API_KEY
297
+ };
298
+ }
243
299
  return _config;
244
300
  }
245
301
  function getResolvedPaths() {
246
302
  const config = getConfig();
247
303
  const root = getProjectRoot();
304
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
248
305
  return {
249
306
  codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
250
307
  dataDbPath: resolve(root, ".massu/data.db"),
@@ -260,11 +317,20 @@ function getResolvedPaths() {
260
317
  ),
261
318
  extensions: [".ts", ".tsx", ".js", ".jsx"],
262
319
  indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
263
- patternsDir: resolve(root, ".claude/patterns"),
264
- claudeMdPath: resolve(root, ".claude/CLAUDE.md"),
320
+ patternsDir: resolve(root, claudeDirName, "patterns"),
321
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
265
322
  docsMapPath: resolve(root, ".massu/docs-map.json"),
266
323
  helpSitePath: resolve(root, "../" + config.project.name + "-help"),
267
- memoryDbPath: resolve(root, ".massu/memory.db")
324
+ memoryDbPath: resolve(root, ".massu/memory.db"),
325
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
326
+ plansDir: resolve(root, "docs/plans"),
327
+ docsDir: resolve(root, "docs"),
328
+ claudeDir: resolve(root, claudeDirName),
329
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
330
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
331
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
332
+ mcpJsonPath: resolve(root, ".mcp.json"),
333
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
268
334
  };
269
335
  }
270
336
 
@@ -746,6 +812,39 @@ function initMemorySchema(db) {
746
812
  );
747
813
  CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
748
814
  `);
815
+ db.exec(`
816
+ CREATE TABLE IF NOT EXISTS license_cache (
817
+ api_key_hash TEXT PRIMARY KEY,
818
+ tier TEXT NOT NULL,
819
+ valid_until TEXT NOT NULL,
820
+ last_validated TEXT NOT NULL,
821
+ features TEXT DEFAULT '[]'
822
+ );
823
+ `);
824
+ }
825
+ function assignImportance(type, vrResult) {
826
+ switch (type) {
827
+ case "decision":
828
+ case "failed_attempt":
829
+ return 5;
830
+ case "cr_violation":
831
+ case "incident_near_miss":
832
+ return 4;
833
+ case "vr_check":
834
+ return vrResult === "PASS" ? 2 : 4;
835
+ case "pattern_compliance":
836
+ return vrResult === "PASS" ? 2 : 4;
837
+ case "feature":
838
+ case "bugfix":
839
+ return 3;
840
+ case "refactor":
841
+ return 2;
842
+ case "file_change":
843
+ case "discovery":
844
+ return 1;
845
+ default:
846
+ return 3;
847
+ }
749
848
  }
750
849
  function autoDetectTaskId(planFile) {
751
850
  if (!planFile) return null;
@@ -760,6 +859,29 @@ function createSession(db, sessionId, opts) {
760
859
  VALUES (?, ?, ?, ?, ?, ?)
761
860
  `).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1e3));
762
861
  }
862
+ function addObservation(db, sessionId, type, title, detail, opts) {
863
+ const now = /* @__PURE__ */ new Date();
864
+ const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes("PASS") ? "PASS" : void 0);
865
+ const result = db.prepare(`
866
+ INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
867
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
868
+ `).run(
869
+ sessionId,
870
+ type,
871
+ title,
872
+ detail,
873
+ JSON.stringify(opts?.filesInvolved ?? []),
874
+ opts?.planItem ?? null,
875
+ opts?.crRule ?? null,
876
+ opts?.vrType ?? null,
877
+ opts?.evidence ?? null,
878
+ importance,
879
+ opts?.originalTokens ?? 0,
880
+ now.toISOString(),
881
+ Math.floor(now.getTime() / 1e3)
882
+ );
883
+ return Number(result.lastInsertRowid);
884
+ }
763
885
  function addSummary(db, sessionId, summary) {
764
886
  const now = /* @__PURE__ */ new Date();
765
887
  db.prepare(`
@@ -818,6 +940,23 @@ async function main() {
818
940
  ).all(session_id);
819
941
  const summary = buildSnapshotSummary(observations, prompts);
820
942
  addSummary(db, session_id, summary);
943
+ try {
944
+ const knowledgeObs = observations.filter(
945
+ (o) => o.title?.includes("knowledge") || o.title?.includes("Knowledge") || o.detail?.includes("knowledge")
946
+ );
947
+ if (knowledgeObs.length > 0) {
948
+ const knowledgeContext = knowledgeObs.map((o) => `[${o.type}] ${o.title}`).join("; ");
949
+ addObservation(
950
+ db,
951
+ session_id,
952
+ "discovery",
953
+ "Knowledge context preserved before compaction",
954
+ knowledgeContext,
955
+ { importance: 4 }
956
+ );
957
+ }
958
+ } catch (_knowledgeErr) {
959
+ }
821
960
  try {
822
961
  logAuditEntry(db, {
823
962
  sessionId: session_id,
@@ -2,12 +2,13 @@
2
2
  import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
3
3
 
4
4
  // src/hooks/pre-delete-check.ts
5
- import Database from "better-sqlite3";
5
+ import Database2 from "better-sqlite3";
6
6
  import { existsSync as existsSync2 } from "fs";
7
7
 
8
8
  // src/config.ts
9
9
  import { resolve, dirname } from "path";
10
10
  import { existsSync, readFileSync } from "fs";
11
+ import { homedir } from "os";
11
12
  import { parse as parseYaml } from "yaml";
12
13
  import { z } from "zod";
13
14
  var DomainConfigSchema = z.object({
@@ -139,6 +140,49 @@ var CloudConfigSchema = z.object({
139
140
  audit: z.boolean().default(true)
140
141
  }).default({ memory: true, analytics: true, audit: true })
141
142
  }).optional();
143
+ var ConventionsConfigSchema = z.object({
144
+ claudeDirName: z.string().default(".claude").refine(
145
+ (s) => !s.includes("..") && !s.startsWith("/"),
146
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
147
+ ),
148
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
149
+ (s) => !s.includes("..") && !s.startsWith("/"),
150
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
151
+ ),
152
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
153
+ (s) => !s.includes("..") && !s.startsWith("/"),
154
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
155
+ ),
156
+ knowledgeCategories: z.array(z.string()).default([
157
+ "patterns",
158
+ "commands",
159
+ "incidents",
160
+ "reference",
161
+ "protocols",
162
+ "checklists",
163
+ "playbooks",
164
+ "critical",
165
+ "scripts",
166
+ "status",
167
+ "templates",
168
+ "loop-state",
169
+ "session-state",
170
+ "agents"
171
+ ]),
172
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
173
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
174
+ }).optional();
175
+ var PythonDomainConfigSchema = z.object({
176
+ name: z.string(),
177
+ packages: z.array(z.string()),
178
+ allowed_imports_from: z.array(z.string()).default([])
179
+ });
180
+ var PythonConfigSchema = z.object({
181
+ root: z.string(),
182
+ alembic_dir: z.string().optional(),
183
+ domains: z.array(PythonDomainConfigSchema).default([]),
184
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
185
+ }).optional();
142
186
  var PathsConfigSchema = z.object({
143
187
  source: z.string().default("src"),
144
188
  aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
@@ -173,7 +217,9 @@ var RawConfigSchema = z.object({
173
217
  security: SecurityConfigSchema,
174
218
  team: TeamConfigSchema,
175
219
  regression: RegressionConfigSchema,
176
- cloud: CloudConfigSchema
220
+ cloud: CloudConfigSchema,
221
+ conventions: ConventionsConfigSchema,
222
+ python: PythonConfigSchema
177
223
  }).passthrough();
178
224
  var _config = null;
179
225
  var _projectRoot = null;
@@ -237,13 +283,24 @@ function getConfig() {
237
283
  security: parsed.security,
238
284
  team: parsed.team,
239
285
  regression: parsed.regression,
240
- cloud: parsed.cloud
286
+ cloud: parsed.cloud,
287
+ conventions: parsed.conventions,
288
+ python: parsed.python
241
289
  };
290
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
291
+ _config.cloud = {
292
+ enabled: true,
293
+ sync: { memory: true, analytics: true, audit: true },
294
+ ..._config.cloud,
295
+ apiKey: process.env.MASSU_API_KEY
296
+ };
297
+ }
242
298
  return _config;
243
299
  }
244
300
  function getResolvedPaths() {
245
301
  const config = getConfig();
246
302
  const root = getProjectRoot();
303
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
247
304
  return {
248
305
  codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
249
306
  dataDbPath: resolve(root, ".massu/data.db"),
@@ -259,14 +316,26 @@ function getResolvedPaths() {
259
316
  ),
260
317
  extensions: [".ts", ".tsx", ".js", ".jsx"],
261
318
  indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
262
- patternsDir: resolve(root, ".claude/patterns"),
263
- claudeMdPath: resolve(root, ".claude/CLAUDE.md"),
319
+ patternsDir: resolve(root, claudeDirName, "patterns"),
320
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
264
321
  docsMapPath: resolve(root, ".massu/docs-map.json"),
265
322
  helpSitePath: resolve(root, "../" + config.project.name + "-help"),
266
- memoryDbPath: resolve(root, ".massu/memory.db")
323
+ memoryDbPath: resolve(root, ".massu/memory.db"),
324
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
325
+ plansDir: resolve(root, "docs/plans"),
326
+ docsDir: resolve(root, "docs"),
327
+ claudeDir: resolve(root, claudeDirName),
328
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
329
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
330
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
331
+ mcpJsonPath: resolve(root, ".mcp.json"),
332
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
267
333
  };
268
334
  }
269
335
 
336
+ // src/memory-db.ts
337
+ import Database from "better-sqlite3";
338
+
270
339
  // src/sentinel-db.ts
271
340
  var PROJECT_ROOT = getProjectRoot();
272
341
  function parsePortalScope(raw) {
@@ -351,17 +420,46 @@ function getFeatureImpact(db, filePaths) {
351
420
 
352
421
  // src/hooks/pre-delete-check.ts
353
422
  var PROJECT_ROOT2 = getProjectRoot();
423
+ var KNOWLEDGE_PROTECTED_FILES = [
424
+ "knowledge-db.ts",
425
+ "knowledge-indexer.ts",
426
+ "knowledge-tools.ts"
427
+ ];
354
428
  function getDataDb() {
355
429
  const dbPath = getResolvedPaths().dataDbPath;
356
430
  if (!existsSync2(dbPath)) return null;
357
431
  try {
358
- const db = new Database(dbPath, { readonly: true });
432
+ const db = new Database2(dbPath, { readonly: true });
359
433
  db.pragma("journal_mode = WAL");
360
434
  return db;
361
435
  } catch {
362
436
  return null;
363
437
  }
364
438
  }
439
+ function checkKnowledgeFileProtection(input) {
440
+ const candidateFiles = [];
441
+ if (input.tool_name === "Bash" && input.tool_input.command) {
442
+ const cmd = input.tool_input.command;
443
+ const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
444
+ if (rmMatch) {
445
+ const parts = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
446
+ candidateFiles.push(...parts);
447
+ }
448
+ }
449
+ if (input.tool_name === "Write" && input.tool_input.file_path) {
450
+ const content = input.tool_input.content || "";
451
+ if (content.trim().length === 0) {
452
+ candidateFiles.push(input.tool_input.file_path);
453
+ }
454
+ }
455
+ for (const f of candidateFiles) {
456
+ const basename = f.split("/").pop() ?? f;
457
+ if (KNOWLEDGE_PROTECTED_FILES.includes(basename)) {
458
+ 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.`;
459
+ }
460
+ }
461
+ return null;
462
+ }
365
463
  function extractDeletedFiles(input) {
366
464
  const files = [];
367
465
  if (input.tool_name === "Bash" && input.tool_input.command) {
@@ -371,7 +469,7 @@ function extractDeletedFiles(input) {
371
469
  const paths = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
372
470
  for (const p of paths) {
373
471
  const relPath = p.startsWith("src/") ? p : p.replace(PROJECT_ROOT2 + "/", "");
374
- if (relPath.startsWith("src/")) {
472
+ if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
375
473
  files.push(relPath);
376
474
  }
377
475
  }
@@ -381,7 +479,7 @@ function extractDeletedFiles(input) {
381
479
  const content = input.tool_input.content || "";
382
480
  if (content.trim().length === 0) {
383
481
  const relPath = input.tool_input.file_path.replace(PROJECT_ROOT2 + "/", "");
384
- if (relPath.startsWith("src/")) {
482
+ if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
385
483
  files.push(relPath);
386
484
  }
387
485
  }
@@ -392,6 +490,12 @@ async function main() {
392
490
  try {
393
491
  const input = await readStdin();
394
492
  const hookInput = JSON.parse(input);
493
+ const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
494
+ if (knowledgeWarning) {
495
+ process.stdout.write(JSON.stringify({ message: knowledgeWarning }));
496
+ process.exit(0);
497
+ return;
498
+ }
395
499
  const deletedFiles = extractDeletedFiles(hookInput);
396
500
  if (deletedFiles.length === 0) {
397
501
  process.exit(0);
@@ -402,10 +506,11 @@ async function main() {
402
506
  process.exit(0);
403
507
  return;
404
508
  }
509
+ const SENTINEL_TABLE = "massu_sentinel";
405
510
  try {
406
511
  const tableExists = db.prepare(
407
- "SELECT name FROM sqlite_master WHERE type='table' AND name='massu_sentinel'"
408
- ).get();
512
+ `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
513
+ ).get(SENTINEL_TABLE);
409
514
  if (!tableExists) {
410
515
  process.exit(0);
411
516
  return;
@@ -432,6 +537,31 @@ async function main() {
432
537
  msg.push("Create a migration plan before deleting these files.");
433
538
  process.stdout.write(JSON.stringify({ message: msg.join("\n") }));
434
539
  }
540
+ const pyFiles = deletedFiles.filter((f) => f.endsWith(".py"));
541
+ if (pyFiles.length > 0) {
542
+ try {
543
+ for (const pyFile of pyFiles) {
544
+ const importers = db.prepare(
545
+ "SELECT source_file FROM massu_py_imports WHERE target_file = ?"
546
+ ).all(pyFile);
547
+ const routes = db.prepare(
548
+ "SELECT method, path FROM massu_py_routes WHERE file = ?"
549
+ ).all(pyFile);
550
+ const models = db.prepare(
551
+ "SELECT class_name FROM massu_py_models WHERE file = ?"
552
+ ).all(pyFile);
553
+ if (importers.length > 0 || routes.length > 0 || models.length > 0) {
554
+ const parts = [];
555
+ if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
556
+ if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
557
+ if (models.length > 0) parts.push(`defines ${models.length} models`);
558
+ const msg = `PYTHON IMPACT: "${pyFile}" ${parts.join(", ")}. Check dependents before deleting.`;
559
+ process.stdout.write(JSON.stringify({ message: msg }));
560
+ }
561
+ }
562
+ } catch {
563
+ }
564
+ }
435
565
  } finally {
436
566
  db.close();
437
567
  }
@@ -9,6 +9,7 @@ import { existsSync as existsSync2, mkdirSync } from "fs";
9
9
  // src/config.ts
10
10
  import { resolve, dirname } from "path";
11
11
  import { existsSync, readFileSync } from "fs";
12
+ import { homedir } from "os";
12
13
  import { parse as parseYaml } from "yaml";
13
14
  import { z } from "zod";
14
15
  var DomainConfigSchema = z.object({
@@ -140,6 +141,49 @@ var CloudConfigSchema = z.object({
140
141
  audit: z.boolean().default(true)
141
142
  }).default({ memory: true, analytics: true, audit: true })
142
143
  }).optional();
144
+ var ConventionsConfigSchema = z.object({
145
+ claudeDirName: z.string().default(".claude").refine(
146
+ (s) => !s.includes("..") && !s.startsWith("/"),
147
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
148
+ ),
149
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
150
+ (s) => !s.includes("..") && !s.startsWith("/"),
151
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
152
+ ),
153
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
154
+ (s) => !s.includes("..") && !s.startsWith("/"),
155
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
156
+ ),
157
+ knowledgeCategories: z.array(z.string()).default([
158
+ "patterns",
159
+ "commands",
160
+ "incidents",
161
+ "reference",
162
+ "protocols",
163
+ "checklists",
164
+ "playbooks",
165
+ "critical",
166
+ "scripts",
167
+ "status",
168
+ "templates",
169
+ "loop-state",
170
+ "session-state",
171
+ "agents"
172
+ ]),
173
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
174
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
175
+ }).optional();
176
+ var PythonDomainConfigSchema = z.object({
177
+ name: z.string(),
178
+ packages: z.array(z.string()),
179
+ allowed_imports_from: z.array(z.string()).default([])
180
+ });
181
+ var PythonConfigSchema = z.object({
182
+ root: z.string(),
183
+ alembic_dir: z.string().optional(),
184
+ domains: z.array(PythonDomainConfigSchema).default([]),
185
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
186
+ }).optional();
143
187
  var PathsConfigSchema = z.object({
144
188
  source: z.string().default("src"),
145
189
  aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
@@ -174,7 +218,9 @@ var RawConfigSchema = z.object({
174
218
  security: SecurityConfigSchema,
175
219
  team: TeamConfigSchema,
176
220
  regression: RegressionConfigSchema,
177
- cloud: CloudConfigSchema
221
+ cloud: CloudConfigSchema,
222
+ conventions: ConventionsConfigSchema,
223
+ python: PythonConfigSchema
178
224
  }).passthrough();
179
225
  var _config = null;
180
226
  var _projectRoot = null;
@@ -238,13 +284,24 @@ function getConfig() {
238
284
  security: parsed.security,
239
285
  team: parsed.team,
240
286
  regression: parsed.regression,
241
- cloud: parsed.cloud
287
+ cloud: parsed.cloud,
288
+ conventions: parsed.conventions,
289
+ python: parsed.python
242
290
  };
291
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
292
+ _config.cloud = {
293
+ enabled: true,
294
+ sync: { memory: true, analytics: true, audit: true },
295
+ ..._config.cloud,
296
+ apiKey: process.env.MASSU_API_KEY
297
+ };
298
+ }
243
299
  return _config;
244
300
  }
245
301
  function getResolvedPaths() {
246
302
  const config = getConfig();
247
303
  const root = getProjectRoot();
304
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
248
305
  return {
249
306
  codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
250
307
  dataDbPath: resolve(root, ".massu/data.db"),
@@ -260,11 +317,20 @@ function getResolvedPaths() {
260
317
  ),
261
318
  extensions: [".ts", ".tsx", ".js", ".jsx"],
262
319
  indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
263
- patternsDir: resolve(root, ".claude/patterns"),
264
- claudeMdPath: resolve(root, ".claude/CLAUDE.md"),
320
+ patternsDir: resolve(root, claudeDirName, "patterns"),
321
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
265
322
  docsMapPath: resolve(root, ".massu/docs-map.json"),
266
323
  helpSitePath: resolve(root, "../" + config.project.name + "-help"),
267
- memoryDbPath: resolve(root, ".massu/memory.db")
324
+ memoryDbPath: resolve(root, ".massu/memory.db"),
325
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
326
+ plansDir: resolve(root, "docs/plans"),
327
+ docsDir: resolve(root, "docs"),
328
+ claudeDir: resolve(root, claudeDirName),
329
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
330
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
331
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
332
+ mcpJsonPath: resolve(root, ".mcp.json"),
333
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
268
334
  };
269
335
  }
270
336
 
@@ -746,6 +812,15 @@ function initMemorySchema(db) {
746
812
  );
747
813
  CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
748
814
  `);
815
+ db.exec(`
816
+ CREATE TABLE IF NOT EXISTS license_cache (
817
+ api_key_hash TEXT PRIMARY KEY,
818
+ tier TEXT NOT NULL,
819
+ valid_until TEXT NOT NULL,
820
+ last_validated TEXT NOT NULL,
821
+ features TEXT DEFAULT '[]'
822
+ );
823
+ `);
749
824
  }
750
825
 
751
826
  // src/hooks/quality-event.ts
@@ -52,6 +52,24 @@ function checkFilePath(filePath) {
52
52
  }
53
53
  return null;
54
54
  }
55
+ var DANGEROUS_PYTHON_PATTERNS = [
56
+ { pattern: /\beval\s*\(/, label: "Python eval() \u2014 arbitrary code execution" },
57
+ { pattern: /\bexec\s*\(/, label: "Python exec() \u2014 arbitrary code execution" },
58
+ { pattern: /\b__import__\s*\(/, label: "Python __import__() \u2014 dynamic import (potential code injection)" },
59
+ { pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: "subprocess.call(shell=True) \u2014 shell injection risk" },
60
+ { pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: "subprocess.Popen(shell=True) \u2014 shell injection risk" },
61
+ { pattern: /os\.system\s*\(/, label: "os.system() \u2014 shell injection risk" },
62
+ { pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: "f-string in SQL \u2014 SQL injection risk" },
63
+ { pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: "String formatting in SQL \u2014 SQL injection risk" }
64
+ ];
65
+ function checkPythonContent(content) {
66
+ for (const { pattern, label } of DANGEROUS_PYTHON_PATTERNS) {
67
+ if (pattern.test(content)) {
68
+ return label;
69
+ }
70
+ }
71
+ return null;
72
+ }
55
73
  async function main() {
56
74
  try {
57
75
  const input = await readStdin();
@@ -77,6 +95,17 @@ Ensure this is intentional and no secrets will be exposed.`
77
95
  }));
78
96
  }
79
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
+ process.stdout.write(JSON.stringify({
103
+ message: `SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}
104
+ File: ${tool_input.file_path}
105
+ Review carefully before proceeding.`
106
+ }));
107
+ }
108
+ }
80
109
  } catch {
81
110
  }
82
111
  process.exit(0);