@massu/core 0.1.1 → 0.4.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 (151) 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 +7772 -3140
  34. package/dist/hooks/cost-tracker.js +103 -40
  35. package/dist/hooks/post-edit-context.js +74 -8
  36. package/dist/hooks/post-tool-use.js +268 -106
  37. package/dist/hooks/pre-compact.js +167 -43
  38. package/dist/hooks/pre-delete-check.js +159 -42
  39. package/dist/hooks/quality-event.js +103 -40
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +143 -84
  42. package/dist/hooks/session-start.js +186 -49
  43. package/dist/hooks/user-prompt.js +189 -43
  44. package/package.json +10 -15
  45. package/src/adr-generator.ts +9 -2
  46. package/src/analytics.ts +9 -3
  47. package/src/audit-trail.ts +10 -3
  48. package/src/backfill-sessions.ts +5 -4
  49. package/src/cli.ts +6 -0
  50. package/src/cloud-sync.ts +14 -18
  51. package/src/commands/doctor.ts +193 -6
  52. package/src/commands/init.ts +230 -5
  53. package/src/commands/install-commands.ts +137 -0
  54. package/src/config.ts +68 -2
  55. package/src/cost-tracker.ts +11 -6
  56. package/src/db.ts +115 -2
  57. package/src/dependency-scorer.ts +9 -2
  58. package/src/docs-tools.ts +21 -16
  59. package/src/hooks/post-edit-context.ts +4 -4
  60. package/src/hooks/post-tool-use.ts +130 -0
  61. package/src/hooks/pre-compact.ts +23 -1
  62. package/src/hooks/pre-delete-check.ts +92 -4
  63. package/src/hooks/security-gate.ts +32 -0
  64. package/src/hooks/session-end.ts +3 -3
  65. package/src/hooks/session-start.ts +99 -6
  66. package/src/hooks/user-prompt.ts +46 -1
  67. package/src/import-resolver.ts +2 -1
  68. package/src/knowledge-db.ts +169 -0
  69. package/src/knowledge-indexer.ts +704 -0
  70. package/src/knowledge-tools.ts +1413 -0
  71. package/src/license.ts +482 -0
  72. package/src/memory-db.ts +1364 -23
  73. package/src/memory-tools.ts +14 -15
  74. package/src/observability-tools.ts +13 -2
  75. package/src/observation-extractor.ts +11 -4
  76. package/src/page-deps.ts +3 -2
  77. package/src/prompt-analyzer.ts +9 -2
  78. package/src/python/coupling-detector.ts +124 -0
  79. package/src/python/domain-enforcer.ts +83 -0
  80. package/src/python/impact-analyzer.ts +95 -0
  81. package/src/python/import-parser.ts +244 -0
  82. package/src/python/import-resolver.ts +135 -0
  83. package/src/python/migration-indexer.ts +115 -0
  84. package/src/python/migration-parser.ts +332 -0
  85. package/src/python/model-indexer.ts +70 -0
  86. package/src/python/model-parser.ts +279 -0
  87. package/src/python/route-indexer.ts +58 -0
  88. package/src/python/route-parser.ts +317 -0
  89. package/src/python-tools.ts +629 -0
  90. package/src/regression-detector.ts +9 -3
  91. package/src/security-scorer.ts +9 -2
  92. package/src/sentinel-db.ts +45 -89
  93. package/src/sentinel-tools.ts +8 -11
  94. package/src/server.ts +29 -7
  95. package/src/session-archiver.ts +4 -5
  96. package/src/team-knowledge.ts +9 -2
  97. package/src/tools.ts +1032 -44
  98. package/src/validate-features-runner.ts +0 -1
  99. package/src/validation-engine.ts +9 -2
  100. package/README.md +0 -40
  101. package/dist/server.js +0 -7008
  102. package/src/__tests__/adr-generator.test.ts +0 -260
  103. package/src/__tests__/analytics.test.ts +0 -282
  104. package/src/__tests__/audit-trail.test.ts +0 -382
  105. package/src/__tests__/backfill-sessions.test.ts +0 -690
  106. package/src/__tests__/cli.test.ts +0 -290
  107. package/src/__tests__/cloud-sync.test.ts +0 -261
  108. package/src/__tests__/config-sections.test.ts +0 -359
  109. package/src/__tests__/config.test.ts +0 -732
  110. package/src/__tests__/cost-tracker.test.ts +0 -348
  111. package/src/__tests__/db.test.ts +0 -177
  112. package/src/__tests__/dependency-scorer.test.ts +0 -325
  113. package/src/__tests__/docs-integration.test.ts +0 -178
  114. package/src/__tests__/docs-tools.test.ts +0 -199
  115. package/src/__tests__/domains.test.ts +0 -236
  116. package/src/__tests__/hooks.test.ts +0 -221
  117. package/src/__tests__/import-resolver.test.ts +0 -95
  118. package/src/__tests__/integration/path-traversal.test.ts +0 -134
  119. package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
  120. package/src/__tests__/integration/tool-registration.test.ts +0 -146
  121. package/src/__tests__/memory-db.test.ts +0 -404
  122. package/src/__tests__/memory-enhancements.test.ts +0 -316
  123. package/src/__tests__/memory-tools.test.ts +0 -199
  124. package/src/__tests__/middleware-tree.test.ts +0 -177
  125. package/src/__tests__/observability-tools.test.ts +0 -595
  126. package/src/__tests__/observability.test.ts +0 -437
  127. package/src/__tests__/observation-extractor.test.ts +0 -167
  128. package/src/__tests__/page-deps.test.ts +0 -60
  129. package/src/__tests__/prompt-analyzer.test.ts +0 -298
  130. package/src/__tests__/regression-detector.test.ts +0 -295
  131. package/src/__tests__/rules.test.ts +0 -87
  132. package/src/__tests__/schema-mapper.test.ts +0 -29
  133. package/src/__tests__/security-scorer.test.ts +0 -238
  134. package/src/__tests__/security-utils.test.ts +0 -175
  135. package/src/__tests__/sentinel-db.test.ts +0 -491
  136. package/src/__tests__/sentinel-scanner.test.ts +0 -750
  137. package/src/__tests__/sentinel-tools.test.ts +0 -324
  138. package/src/__tests__/sentinel-types.test.ts +0 -750
  139. package/src/__tests__/server.test.ts +0 -452
  140. package/src/__tests__/session-archiver.test.ts +0 -524
  141. package/src/__tests__/session-state-generator.test.ts +0 -900
  142. package/src/__tests__/team-knowledge.test.ts +0 -327
  143. package/src/__tests__/tools.test.ts +0 -340
  144. package/src/__tests__/transcript-parser.test.ts +0 -195
  145. package/src/__tests__/trpc-index.test.ts +0 -25
  146. package/src/__tests__/validate-features-runner.test.ts +0 -517
  147. package/src/__tests__/validation-engine.test.ts +0 -300
  148. package/src/core-tools.ts +0 -685
  149. package/src/memory-queries.ts +0 -804
  150. package/src/memory-schema.ts +0 -546
  151. package/src/tool-helpers.ts +0 -41
@@ -6,6 +6,7 @@
6
6
  // PreToolUse Hook: Pre-Deletion Feature Impact Check
7
7
  // Detects file deletion patterns (rm, git rm, Write with empty content)
8
8
  // and runs sentinel impact analysis. Blocks if critical features orphaned.
9
+ // Also protects knowledge system files from accidental deletion (P7-005).
9
10
  // Must complete in <500ms.
10
11
  // ============================================================
11
12
 
@@ -27,6 +28,14 @@ interface HookInput {
27
28
 
28
29
  const PROJECT_ROOT = getProjectRoot();
29
30
 
31
+ // Knowledge system files that must never be silently deleted.
32
+ // These files underpin knowledge indexing and memory retrieval.
33
+ const KNOWLEDGE_PROTECTED_FILES = [
34
+ 'knowledge-db.ts',
35
+ 'knowledge-indexer.ts',
36
+ 'knowledge-tools.ts',
37
+ ];
38
+
30
39
  function getDataDb(): Database.Database | null {
31
40
  const dbPath = getResolvedPaths().dataDbPath;
32
41
  if (!existsSync(dbPath)) return null;
@@ -39,6 +48,43 @@ function getDataDb(): Database.Database | null {
39
48
  }
40
49
  }
41
50
 
51
+ /**
52
+ * P7-005: Check if the tool call targets a protected knowledge system file.
53
+ * Returns a warning message if a protected file would be deleted/emptied.
54
+ */
55
+ function checkKnowledgeFileProtection(input: HookInput): string | null {
56
+ const candidateFiles: string[] = [];
57
+
58
+ if (input.tool_name === 'Bash' && input.tool_input.command) {
59
+ const cmd = input.tool_input.command;
60
+ const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
61
+ if (rmMatch) {
62
+ const parts = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
63
+ candidateFiles.push(...parts);
64
+ }
65
+ }
66
+
67
+ if (input.tool_name === 'Write' && input.tool_input.file_path) {
68
+ const content = input.tool_input.content || '';
69
+ if (content.trim().length === 0) {
70
+ candidateFiles.push(input.tool_input.file_path);
71
+ }
72
+ }
73
+
74
+ for (const f of candidateFiles) {
75
+ const basename = f.split('/').pop() ?? f;
76
+ if (KNOWLEDGE_PROTECTED_FILES.includes(basename)) {
77
+ return (
78
+ `KNOWLEDGE SYSTEM PROTECTION: "${basename}" is a core knowledge system file. ` +
79
+ `Deleting it will break knowledge indexing and memory retrieval. ` +
80
+ `Create a replacement before removing.`
81
+ );
82
+ }
83
+ }
84
+
85
+ return null;
86
+ }
87
+
42
88
  function extractDeletedFiles(input: HookInput): string[] {
43
89
  const files: string[] = [];
44
90
 
@@ -51,7 +97,7 @@ function extractDeletedFiles(input: HookInput): string[] {
51
97
  const paths = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
52
98
  for (const p of paths) {
53
99
  const relPath = p.startsWith('src/') ? p : p.replace(PROJECT_ROOT + '/', '');
54
- if (relPath.startsWith('src/')) {
100
+ if (relPath.startsWith('src/') || relPath.endsWith('.py')) {
55
101
  files.push(relPath);
56
102
  }
57
103
  }
@@ -63,7 +109,7 @@ function extractDeletedFiles(input: HookInput): string[] {
63
109
  const content = input.tool_input.content || '';
64
110
  if (content.trim().length === 0) {
65
111
  const relPath = input.tool_input.file_path.replace(PROJECT_ROOT + '/', '');
66
- if (relPath.startsWith('src/')) {
112
+ if (relPath.startsWith('src/') || relPath.endsWith('.py')) {
67
113
  files.push(relPath);
68
114
  }
69
115
  }
@@ -77,6 +123,14 @@ async function main(): Promise<void> {
77
123
  const input = await readStdin();
78
124
  const hookInput = JSON.parse(input) as HookInput;
79
125
 
126
+ // P7-005: Check knowledge file protection before anything else
127
+ const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
128
+ if (knowledgeWarning) {
129
+ process.stdout.write(JSON.stringify({ message: knowledgeWarning }));
130
+ process.exit(0);
131
+ return;
132
+ }
133
+
80
134
  const deletedFiles = extractDeletedFiles(hookInput);
81
135
  if (deletedFiles.length === 0) {
82
136
  process.exit(0);
@@ -90,11 +144,14 @@ async function main(): Promise<void> {
90
144
  return;
91
145
  }
92
146
 
147
+ // The sentinel registry table name (defined by sentinel-db schema)
148
+ const SENTINEL_TABLE = 'massu_sentinel';
149
+
93
150
  try {
94
151
  // Check if any sentinel tables exist
95
152
  const tableExists = db.prepare(
96
- "SELECT name FROM sqlite_master WHERE type='table' AND name='massu_sentinel'"
97
- ).get();
153
+ `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
154
+ ).get(SENTINEL_TABLE);
98
155
 
99
156
  if (!tableExists) {
100
157
  process.exit(0);
@@ -129,6 +186,37 @@ async function main(): Promise<void> {
129
186
  // Output warning but don't block (user can proceed)
130
187
  process.stdout.write(JSON.stringify({ message: msg.join('\n') }));
131
188
  }
189
+ // Check Python import graph for deleted .py files
190
+ const pyFiles = deletedFiles.filter(f => f.endsWith('.py'));
191
+ if (pyFiles.length > 0) {
192
+ try {
193
+ for (const pyFile of pyFiles) {
194
+ const importers = db.prepare(
195
+ 'SELECT source_file FROM massu_py_imports WHERE target_file = ?'
196
+ ).all(pyFile) as { source_file: string }[];
197
+
198
+ const routes = db.prepare(
199
+ 'SELECT method, path FROM massu_py_routes WHERE file = ?'
200
+ ).all(pyFile) as { method: string; path: string }[];
201
+
202
+ const models = db.prepare(
203
+ 'SELECT class_name FROM massu_py_models WHERE file = ?'
204
+ ).all(pyFile) as { class_name: string }[];
205
+
206
+ if (importers.length > 0 || routes.length > 0 || models.length > 0) {
207
+ const parts: string[] = [];
208
+ if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
209
+ if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
210
+ if (models.length > 0) parts.push(`defines ${models.length} models`);
211
+
212
+ const msg = `PYTHON IMPACT: "${pyFile}" ${parts.join(', ')}. Check dependents before deleting.`;
213
+ process.stdout.write(JSON.stringify({ message: msg }));
214
+ }
215
+ }
216
+ } catch {
217
+ // Python tables may not exist yet
218
+ }
219
+ }
132
220
  } finally {
133
221
  db.close();
134
222
  }
@@ -20,6 +20,7 @@ interface HookInput {
20
20
  command?: string;
21
21
  file_path?: string;
22
22
  content?: string;
23
+ new_string?: string;
23
24
  };
24
25
  }
25
26
 
@@ -77,6 +78,26 @@ function checkFilePath(filePath: string): string | null {
77
78
  return null;
78
79
  }
79
80
 
81
+ const DANGEROUS_PYTHON_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
82
+ { pattern: /\beval\s*\(/, label: 'Python eval() — arbitrary code execution' },
83
+ { pattern: /\bexec\s*\(/, label: 'Python exec() — arbitrary code execution' },
84
+ { pattern: /\b__import__\s*\(/, label: 'Python __import__() — dynamic import (potential code injection)' },
85
+ { pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: 'subprocess.call(shell=True) — shell injection risk' },
86
+ { pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: 'subprocess.Popen(shell=True) — shell injection risk' },
87
+ { pattern: /os\.system\s*\(/, label: 'os.system() — shell injection risk' },
88
+ { pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: 'f-string in SQL — SQL injection risk' },
89
+ { pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: 'String formatting in SQL — SQL injection risk' },
90
+ ];
91
+
92
+ function checkPythonContent(content: string): string | null {
93
+ for (const { pattern, label } of DANGEROUS_PYTHON_PATTERNS) {
94
+ if (pattern.test(content)) {
95
+ return label;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
80
101
  async function main(): Promise<void> {
81
102
  try {
82
103
  const input = await readStdin();
@@ -100,6 +121,17 @@ async function main(): Promise<void> {
100
121
  }));
101
122
  }
102
123
  }
124
+
125
+ // Check Python file content for dangerous patterns (Write uses content, Edit uses new_string)
126
+ const pyContent = tool_input.content || tool_input.new_string;
127
+ if ((tool_name === 'Write' || tool_name === 'Edit') && tool_input.file_path?.endsWith('.py') && pyContent) {
128
+ const pyViolation = checkPythonContent(pyContent);
129
+ if (pyViolation) {
130
+ process.stdout.write(JSON.stringify({
131
+ message: `SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}\nFile: ${tool_input.file_path}\nReview carefully before proceeding.`,
132
+ }));
133
+ }
134
+ }
103
135
  } catch {
104
136
  // Hooks must never crash
105
137
  }
@@ -8,10 +8,10 @@
8
8
  // Dependencies: P1-002, P5-001, P5-002
9
9
  // ============================================================
10
10
 
11
- import { getMemoryDb, endSession, addSummary, getRecentObservations, createSession, addConversationTurn, addToolCallDetail, getLastProcessedLine, setLastProcessedLine } from '../memory-db.ts';
11
+ import { getMemoryDb, endSession, addSummary, createSession, addConversationTurn, addToolCallDetail, getLastProcessedLine, setLastProcessedLine } from '../memory-db.ts';
12
12
  import { generateCurrentMd } from '../session-state-generator.ts';
13
13
  import { archiveAndRegenerate } from '../session-archiver.ts';
14
- import { parseTranscriptFrom, extractUserMessages, extractAssistantMessages, extractToolCalls, estimateTokens } from '../transcript-parser.ts';
14
+ import { parseTranscriptFrom, estimateTokens } from '../transcript-parser.ts';
15
15
  import { syncToCloud, drainSyncQueue } from '../cloud-sync.ts';
16
16
  import { calculateQualityScore, storeQualityScore, backfillQualityScores } from '../analytics.ts';
17
17
  import { extractTokenUsage, calculateCost, storeSessionCost } from '../cost-tracker.ts';
@@ -64,7 +64,7 @@ async function main(): Promise<void> {
64
64
  // 4.6. Calculate and store quality score
65
65
  try {
66
66
  const { score, breakdown } = calculateQualityScore(db, session_id);
67
- if (score > 0) {
67
+ if (score !== 50) {
68
68
  storeQualityScore(db, session_id, score, breakdown);
69
69
  }
70
70
  backfillQualityScores(db);
@@ -9,7 +9,9 @@
9
9
  // ============================================================
10
10
 
11
11
  import { getMemoryDb, getSessionSummaries, getRecentObservations, getFailedAttempts, getCrossTaskProgress, autoDetectTaskId, linkSessionToTask, createSession } from '../memory-db.ts';
12
- import { getConfig } from '../config.ts';
12
+ import { getConfig, getResolvedPaths } from '../config.ts';
13
+ import { readFileSync, existsSync } from 'fs';
14
+ import { join } from 'path';
13
15
  import type Database from 'better-sqlite3';
14
16
 
15
17
  interface HookInput {
@@ -56,7 +58,7 @@ async function main(): Promise<void> {
56
58
  }
57
59
 
58
60
  // Build context
59
- const context = buildContext(db, session_id, source ?? 'startup', tokenBudget, session?.task_id ?? null);
61
+ const context = await buildContext(db, session_id, source ?? 'startup', tokenBudget, session?.task_id ?? null);
60
62
 
61
63
  if (context.trim()) {
62
64
  process.stdout.write(context);
@@ -80,7 +82,7 @@ function getTokenBudget(source: string): number {
80
82
  }
81
83
  }
82
84
 
83
- function buildContext(db: Database.Database, sessionId: string, source: string, tokenBudget: number, taskId: string | null): string {
85
+ async function buildContext(db: Database.Database, sessionId: string, source: string, tokenBudget: number, taskId: string | null): Promise<string> {
84
86
  const sections: Array<{ text: string; importance: number }> = [];
85
87
 
86
88
  // 1. Failed attempts (highest priority - DON'T RETRY warnings)
@@ -138,7 +140,50 @@ function buildContext(db: Database.Database, sessionId: string, source: string,
138
140
  }
139
141
  }
140
142
 
141
- // 5. Recent observations sorted by importance
143
+ // 5. Prevention rules from corrections.md
144
+ const preventionRules = loadCorrectionsPreventionRules();
145
+ if (preventionRules.length > 0) {
146
+ let rulesText = '### Active Prevention Rules (from corrections.md)\n';
147
+ for (const rule of preventionRules) {
148
+ rulesText += `- ${rule}\n`;
149
+ }
150
+ sections.push({ text: rulesText, importance: 9 });
151
+ }
152
+
153
+ // 6. Knowledge index status (warm-up check)
154
+ try {
155
+ const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
156
+ if (existsSync(knowledgeDbPath)) {
157
+ const Database = (await import('better-sqlite3')).default;
158
+ const kdb = new Database(knowledgeDbPath, { readonly: true });
159
+ try {
160
+ const stats = kdb.prepare(
161
+ 'SELECT COUNT(*) as doc_count, MAX(indexed_at) as last_indexed FROM knowledge_documents'
162
+ ).get() as { doc_count: number; last_indexed: string | null };
163
+ if (stats.doc_count > 0 && stats.last_indexed) {
164
+ const ageMs = Date.now() - new Date(stats.last_indexed).getTime();
165
+ const ageHours = Math.round(ageMs / 3600000);
166
+ if (ageHours > 24) {
167
+ sections.push({
168
+ text: `### Knowledge Index Status\nIndex has ${stats.doc_count} documents, last indexed ${ageHours}h ago. Consider re-indexing.\n`,
169
+ importance: 3,
170
+ });
171
+ }
172
+ } else if (stats.doc_count === 0) {
173
+ sections.push({
174
+ text: '### Knowledge Index Status\nKnowledge index is empty. Run knowledge indexing to populate it.\n',
175
+ importance: 2,
176
+ });
177
+ }
178
+ } finally {
179
+ kdb.close();
180
+ }
181
+ }
182
+ } catch (_knowledgeErr) {
183
+ // Best-effort: never block session start
184
+ }
185
+
186
+ // 7. Recent observations sorted by importance
142
187
  const recentObs = getRecentObservations(db, 20);
143
188
  if (recentObs.length > 0) {
144
189
  let obsText = '### Recent Observations\n';
@@ -153,7 +198,7 @@ function buildContext(db: Database.Database, sessionId: string, source: string,
153
198
  sections.sort((a, b) => b.importance - a.importance);
154
199
 
155
200
  let usedTokens = 0;
156
- const headerTokens = estimateTokens('=== CS MEMORY: Previous Session Context ===\n\n=== END CS MEMORY ===\n');
201
+ const headerTokens = estimateTokens('=== Massu Memory: Previous Session Context ===\n\n=== END Massu Memory ===\n');
157
202
  usedTokens += headerTokens;
158
203
 
159
204
  const includedSections: string[] = [];
@@ -167,7 +212,7 @@ function buildContext(db: Database.Database, sessionId: string, source: string,
167
212
 
168
213
  if (includedSections.length === 0) return '';
169
214
 
170
- return `=== CS MEMORY: Previous Session Context ===\n\n${includedSections.join('\n')}\n=== END CS MEMORY ===\n`;
215
+ return `=== Massu Memory: Previous Session Context ===\n\n${includedSections.join('\n')}\n=== END Massu Memory ===\n`;
171
216
  }
172
217
 
173
218
  function estimateTokens(text: string): number {
@@ -207,4 +252,52 @@ function safeParseJson(json: string): Record<string, string> | null {
207
252
  }
208
253
  }
209
254
 
255
+ /**
256
+ * Load prevention rules from corrections.md in the memory directory.
257
+ * Parses the markdown table format: | Date | Wrong Behavior | Correction | Prevention Rule |
258
+ * Returns only the prevention rule column values.
259
+ * Graceful degradation: returns empty array if file doesn't exist or can't be parsed.
260
+ */
261
+ function loadCorrectionsPreventionRules(): string[] {
262
+ try {
263
+ // Memory path follows Claude's project directory convention
264
+ const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? '';
265
+ const cwd = process.cwd();
266
+ const config = getConfig();
267
+ const claudeDirName = config.conventions?.claudeDirName ?? '.claude';
268
+ // Convert cwd to Claude's directory format: /Users/x/project -> -Users-x-project
269
+ const projectDirName = cwd.replace(/\//g, '-').replace(/^-/, '');
270
+ const correctionsPath = join(homeDir, claudeDirName, 'projects', projectDirName, 'memory', 'corrections.md');
271
+
272
+ if (!existsSync(correctionsPath)) return [];
273
+
274
+ const content = readFileSync(correctionsPath, 'utf-8');
275
+ const lines = content.split('\n');
276
+ const rules: string[] = [];
277
+
278
+ for (const line of lines) {
279
+ // Match table rows: | date | wrong | correction | prevention |
280
+ // Skip header row and separator row
281
+ const trimmed = line.trim();
282
+ if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) continue;
283
+
284
+ const cells = trimmed.split('|').map(c => c.trim()).filter(c => c.length > 0);
285
+ if (cells.length < 4) continue;
286
+
287
+ // Skip header and separator rows
288
+ if (cells[0] === 'Date' || cells[0].startsWith('-')) continue;
289
+
290
+ const preventionRule = cells[3];
291
+ if (preventionRule && !preventionRule.startsWith('-') && !preventionRule.startsWith('<!--')) {
292
+ rules.push(preventionRule);
293
+ }
294
+ }
295
+
296
+ return rules;
297
+ } catch (_e) {
298
+ // Graceful degradation: never block session start
299
+ return [];
300
+ }
301
+ }
302
+
210
303
  main();
@@ -7,7 +7,9 @@
7
7
  // Captures user prompts for search and context.
8
8
  // ============================================================
9
9
 
10
- import { getMemoryDb, createSession, addUserPrompt, linkSessionToTask, autoDetectTaskId } from '../memory-db.ts';
10
+ import { getMemoryDb, createSession, addUserPrompt, linkSessionToTask, autoDetectTaskId, addObservation } from '../memory-db.ts';
11
+ import { existsSync } from 'fs';
12
+ import { getResolvedPaths } from '../config.ts';
11
13
 
12
14
  interface HookInput {
13
15
  session_id: string;
@@ -55,6 +57,35 @@ async function main(): Promise<void> {
55
57
 
56
58
  // 4. Insert prompt
57
59
  addUserPrompt(db, session_id, prompt.trim(), promptNumber);
60
+
61
+ // 5. Knowledge-aware prompt enrichment: detect file references and check knowledge index
62
+ try {
63
+ const fileRefs = extractFileReferences(prompt);
64
+ if (fileRefs.length > 0) {
65
+ const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
66
+ if (knowledgeDbPath && existsSync(knowledgeDbPath)) {
67
+ const Database = (await import('better-sqlite3')).default;
68
+ const kdb = new Database(knowledgeDbPath, { readonly: true });
69
+ try {
70
+ const placeholders = fileRefs.map(() => '?').join(',');
71
+ const matches = kdb.prepare(
72
+ `SELECT DISTINCT file_path FROM knowledge_documents WHERE file_path IN (${placeholders})`
73
+ ).all(...fileRefs) as Array<{ file_path: string }>;
74
+ if (matches.length > 0) {
75
+ addObservation(db, session_id, 'discovery',
76
+ `Knowledge entries exist for referenced files`,
77
+ `Files with knowledge context: ${matches.map(m => m.file_path).join(', ')}`,
78
+ { importance: 2 }
79
+ );
80
+ }
81
+ } finally {
82
+ kdb.close();
83
+ }
84
+ }
85
+ }
86
+ } catch (_knowledgeErr) {
87
+ // Best-effort: never block prompt capture
88
+ }
58
89
  } finally {
59
90
  db.close();
60
91
  }
@@ -64,6 +95,20 @@ async function main(): Promise<void> {
64
95
  process.exit(0);
65
96
  }
66
97
 
98
+ /**
99
+ * Extract file path references from user prompt text.
100
+ * Matches patterns like src/foo/bar.ts, packages/core/src/x.ts, etc.
101
+ */
102
+ function extractFileReferences(prompt: string): string[] {
103
+ const filePattern = /(?:^|\s)((?:src|packages|lib)\/[\w./-]+\.(?:ts|tsx|js|jsx|md))/g;
104
+ const matches: string[] = [];
105
+ let match: RegExpExecArray | null;
106
+ while ((match = filePattern.exec(prompt)) !== null) {
107
+ matches.push(match[1]);
108
+ }
109
+ return [...new Set(matches)];
110
+ }
111
+
67
112
  async function getGitBranch(): Promise<string | undefined> {
68
113
  try {
69
114
  const { spawnSync } = await import('child_process');
@@ -5,6 +5,7 @@ import { readFileSync, existsSync, statSync } from 'fs';
5
5
  import { resolve, dirname, join } from 'path';
6
6
  import type Database from 'better-sqlite3';
7
7
  import { getResolvedPaths, getProjectRoot } from './config.ts';
8
+ import { ensureWithinRoot } from './security-utils.ts';
8
9
 
9
10
  interface ImportEdge {
10
11
  source_file: string;
@@ -182,7 +183,7 @@ export function buildImportIndex(dataDb: Database.Database, codegraphDb: Databas
182
183
  let batch: ImportEdge[] = [];
183
184
 
184
185
  for (const file of files) {
185
- const absPath = resolve(projectRoot, file.path);
186
+ const absPath = ensureWithinRoot(resolve(projectRoot, file.path), projectRoot);
186
187
  if (!existsSync(absPath)) continue;
187
188
 
188
189
  let source: string;
@@ -0,0 +1,169 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import Database from 'better-sqlite3';
5
+ import { dirname } from 'path';
6
+ import { existsSync, mkdirSync } from 'fs';
7
+ import { getConfig, getResolvedPaths } from './config.ts';
8
+
9
+ /**
10
+ * Connection to Massu Knowledge's SQLite database.
11
+ * Stores indexed .claude/ knowledge: rules, patterns, incidents, verifications, cross-references.
12
+ * Separate from codegraph.db (CodeGraph data) and memory.db (session memory).
13
+ */
14
+ export function getKnowledgeDb(): Database.Database {
15
+ const dbPath = getResolvedPaths().knowledgeDbPath;
16
+ const dir = dirname(dbPath);
17
+ if (!existsSync(dir)) {
18
+ mkdirSync(dir, { recursive: true });
19
+ }
20
+ const db = new Database(dbPath);
21
+ db.pragma('journal_mode = WAL');
22
+ db.pragma('foreign_keys = ON');
23
+ initKnowledgeSchema(db);
24
+ return db;
25
+ }
26
+
27
+ export function initKnowledgeSchema(db: Database.Database): void {
28
+ db.exec(`
29
+ -- Core document chunks (parsed from .claude/**/*.md)
30
+ CREATE TABLE IF NOT EXISTS knowledge_documents (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ file_path TEXT NOT NULL,
33
+ category TEXT NOT NULL,
34
+ title TEXT NOT NULL,
35
+ description TEXT,
36
+ content_hash TEXT NOT NULL,
37
+ indexed_at TEXT NOT NULL,
38
+ indexed_at_epoch INTEGER NOT NULL
39
+ );
40
+
41
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_kd_filepath ON knowledge_documents(file_path);
42
+ CREATE INDEX IF NOT EXISTS idx_kd_category ON knowledge_documents(category);
43
+
44
+ -- Structured chunks within documents
45
+ CREATE TABLE IF NOT EXISTS knowledge_chunks (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ document_id INTEGER NOT NULL REFERENCES knowledge_documents(id) ON DELETE CASCADE,
48
+ chunk_type TEXT NOT NULL CHECK(chunk_type IN (
49
+ 'section', 'table_row', 'code_block', 'rule', 'incident', 'pattern', 'command', 'mismatch'
50
+ )),
51
+ heading TEXT,
52
+ content TEXT NOT NULL,
53
+ line_start INTEGER,
54
+ line_end INTEGER,
55
+ metadata TEXT DEFAULT '{}'
56
+ );
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_kc_doc ON knowledge_chunks(document_id);
59
+ CREATE INDEX IF NOT EXISTS idx_kc_type ON knowledge_chunks(chunk_type);
60
+ CREATE INDEX IF NOT EXISTS idx_kc_heading ON knowledge_chunks(heading);
61
+
62
+ -- Canonical Rules index
63
+ CREATE TABLE IF NOT EXISTS knowledge_rules (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ rule_id TEXT UNIQUE NOT NULL,
66
+ rule_text TEXT NOT NULL,
67
+ vr_type TEXT,
68
+ reference_path TEXT,
69
+ severity TEXT DEFAULT 'HIGH',
70
+ prevention_summary TEXT
71
+ );
72
+
73
+ CREATE INDEX IF NOT EXISTS idx_kr_id ON knowledge_rules(rule_id);
74
+
75
+ -- Verification Requirements index
76
+ CREATE TABLE IF NOT EXISTS knowledge_verifications (
77
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ vr_type TEXT UNIQUE NOT NULL,
79
+ command TEXT NOT NULL,
80
+ expected TEXT NOT NULL,
81
+ use_when TEXT NOT NULL,
82
+ catches TEXT,
83
+ category TEXT
84
+ );
85
+
86
+ CREATE INDEX IF NOT EXISTS idx_kv_type ON knowledge_verifications(vr_type);
87
+ CREATE INDEX IF NOT EXISTS idx_kv_category ON knowledge_verifications(category);
88
+
89
+ -- Incident index
90
+ CREATE TABLE IF NOT EXISTS knowledge_incidents (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ incident_num INTEGER UNIQUE NOT NULL,
93
+ date TEXT NOT NULL,
94
+ type TEXT NOT NULL,
95
+ gap_found TEXT NOT NULL,
96
+ prevention TEXT NOT NULL,
97
+ cr_added TEXT,
98
+ root_cause TEXT,
99
+ user_quote TEXT
100
+ );
101
+
102
+ CREATE INDEX IF NOT EXISTS idx_ki_num ON knowledge_incidents(incident_num);
103
+ CREATE INDEX IF NOT EXISTS idx_ki_type ON knowledge_incidents(type);
104
+ CREATE INDEX IF NOT EXISTS idx_ki_cr ON knowledge_incidents(cr_added);
105
+
106
+ -- Schema mismatch quick lookup
107
+ CREATE TABLE IF NOT EXISTS knowledge_schema_mismatches (
108
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
109
+ table_name TEXT NOT NULL,
110
+ wrong_column TEXT NOT NULL,
111
+ correct_column TEXT NOT NULL,
112
+ source TEXT DEFAULT '${getConfig().conventions?.knowledgeSourceFiles?.[0] ?? 'CLAUDE.md'}'
113
+ );
114
+
115
+ CREATE INDEX IF NOT EXISTS idx_ksm_table ON knowledge_schema_mismatches(table_name);
116
+
117
+ -- Cross-reference graph
118
+ CREATE TABLE IF NOT EXISTS knowledge_edges (
119
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
120
+ source_type TEXT NOT NULL,
121
+ source_id TEXT NOT NULL,
122
+ target_type TEXT NOT NULL,
123
+ target_id TEXT NOT NULL,
124
+ edge_type TEXT NOT NULL
125
+ );
126
+
127
+ CREATE INDEX IF NOT EXISTS idx_ke_source ON knowledge_edges(source_type, source_id);
128
+ CREATE INDEX IF NOT EXISTS idx_ke_target ON knowledge_edges(target_type, target_id);
129
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ke_unique ON knowledge_edges(source_type, source_id, target_type, target_id, edge_type);
130
+
131
+ -- Staleness tracking
132
+ CREATE TABLE IF NOT EXISTS knowledge_meta (
133
+ key TEXT PRIMARY KEY,
134
+ value TEXT NOT NULL
135
+ );
136
+ `);
137
+
138
+ // FTS5 in separate exec (can fail if schema mismatch on existing table)
139
+ try {
140
+ db.exec(`
141
+ CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
142
+ heading, content, chunk_type, file_path
143
+ );
144
+
145
+ CREATE TRIGGER IF NOT EXISTS kc_fts_insert AFTER INSERT ON knowledge_chunks BEGIN
146
+ INSERT INTO knowledge_fts(rowid, heading, content, chunk_type, file_path)
147
+ SELECT new.id, new.heading, new.content, new.chunk_type, kd.file_path
148
+ FROM knowledge_documents kd WHERE kd.id = new.document_id;
149
+ END;
150
+
151
+ CREATE TRIGGER IF NOT EXISTS kc_fts_delete AFTER DELETE ON knowledge_chunks BEGIN
152
+ INSERT INTO knowledge_fts(knowledge_fts, rowid, heading, content, chunk_type, file_path)
153
+ SELECT 'delete', old.id, old.heading, old.content, old.chunk_type, kd.file_path
154
+ FROM knowledge_documents kd WHERE kd.id = old.document_id;
155
+ END;
156
+
157
+ CREATE TRIGGER IF NOT EXISTS kc_fts_update AFTER UPDATE ON knowledge_chunks BEGIN
158
+ INSERT INTO knowledge_fts(knowledge_fts, rowid, heading, content, chunk_type, file_path)
159
+ SELECT 'delete', old.id, old.heading, old.content, old.chunk_type, kd.file_path
160
+ FROM knowledge_documents kd WHERE kd.id = old.document_id;
161
+ INSERT INTO knowledge_fts(rowid, heading, content, chunk_type, file_path)
162
+ SELECT new.id, new.heading, new.content, new.chunk_type, kd.file_path
163
+ FROM knowledge_documents kd WHERE kd.id = new.document_id;
164
+ END;
165
+ `);
166
+ } catch {
167
+ // FTS5 table may already exist with different schema — not fatal
168
+ }
169
+ }