@massu/core 1.10.8 → 1.12.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 (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,436 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // P3-001: Enhanced SessionStart Hook
7
- // Injects context from previous sessions into new sessions.
8
- // Output: plain text to stdout (auto-injected by Claude Code)
9
- // ============================================================
10
-
11
- import { getMemoryDb, getSessionSummaries, getRecentObservations, getFailedAttempts, getCrossTaskProgress, autoDetectTaskId, linkSessionToTask, createSession } from '../memory-db.ts';
12
- import { getConfig, getResolvedPaths } from '../config.ts';
13
- import { readFileSync, existsSync } from 'fs';
14
- import { join, resolve } from 'path';
15
- import { parse as parseYaml } from 'yaml';
16
- import type Database from 'better-sqlite3';
17
- import { runDetection } from '../detect/index.ts';
18
- import { computeFingerprint } from '../detect/drift.ts';
19
- import { isPidAlive } from '../lib/pidLiveness.ts';
20
-
21
- interface HookInput {
22
- session_id: string;
23
- transcript_path: string;
24
- cwd: string;
25
- hook_event_name: string;
26
- source?: 'startup' | 'resume' | 'clear' | 'compact';
27
- }
28
-
29
- async function main(): Promise<void> {
30
- try {
31
- // Read stdin
32
- const input = await readStdin();
33
- const hookInput = JSON.parse(input) as HookInput;
34
- const { session_id, source } = hookInput;
35
-
36
- const db = getMemoryDb();
37
-
38
- try {
39
- // Create session if not exists
40
- const gitBranch = await getGitBranch();
41
- createSession(db, session_id, { branch: gitBranch });
42
-
43
- // Check if session has a plan_file and link task
44
- const session = db.prepare('SELECT plan_file, task_id FROM sessions WHERE session_id = ?').get(session_id) as { plan_file: string | null; task_id: string | null } | undefined;
45
- if (session?.plan_file && !session.task_id) {
46
- const taskId = autoDetectTaskId(session.plan_file);
47
- if (taskId) linkSessionToTask(db, session_id, taskId);
48
- }
49
-
50
- // Token budget based on source
51
- const tokenBudget = getTokenBudget(source ?? 'startup');
52
-
53
- // Check if this is the very first session (no prior sessions)
54
- const sessionCount = db.prepare('SELECT COUNT(*) as count FROM sessions').get() as { count: number };
55
- if (sessionCount.count <= 1 && (source === 'startup' || !source)) {
56
- process.stdout.write(
57
- '=== MASSU AI: Active ===\n' +
58
- 'Session memory, code intelligence, and governance are now active.\n' +
59
- `11 hooks monitoring this session. Type "${getConfig().toolPrefix ?? 'massu'}_sync" to index your codebase.\n` +
60
- '=== END MASSU ===\n\n'
61
- );
62
- }
63
-
64
- // Build context
65
- const context = await buildContext(db, session_id, source ?? 'startup', tokenBudget, session?.task_id ?? null);
66
-
67
- if (context.trim()) {
68
- process.stdout.write(context);
69
- }
70
-
71
- // P5-001: drift banner (runs after memory context, independent of it).
72
- // Plan 3a Phase 6: when a live watcher daemon exists, the drift banner
73
- // is suppressed in favor of a compact watcher banner.
74
- const driftBanner = await buildDriftBanner();
75
- if (driftBanner) {
76
- process.stdout.write(driftBanner);
77
- } else {
78
- const watcherBanner = buildWatcherBanner();
79
- if (watcherBanner) process.stdout.write(watcherBanner);
80
- }
81
- } finally {
82
- db.close();
83
- }
84
- } catch (_e) {
85
- // Best-effort: never block Claude Code
86
- process.exit(0);
87
- }
88
- }
89
-
90
- function getTokenBudget(source: string): number {
91
- switch (source) {
92
- case 'compact': return 4000;
93
- case 'startup': return 2000;
94
- case 'resume': return 1000;
95
- case 'clear': return 2000;
96
- default: return 2000;
97
- }
98
- }
99
-
100
- async function buildContext(db: Database.Database, sessionId: string, source: string, tokenBudget: number, taskId: string | null): Promise<string> {
101
- const sections: Array<{ text: string; importance: number }> = [];
102
-
103
- // 1. Failed attempts (highest priority - DON'T RETRY warnings)
104
- const failures = getFailedAttempts(db, undefined, 10);
105
- if (failures.length > 0) {
106
- let failText = '### Failed Attempts (DO NOT RETRY)\n';
107
- for (const f of failures) {
108
- const recurrence = f.recurrence_count > 1 ? ` (${f.recurrence_count}x)` : '';
109
- failText += `- ${f.title}${recurrence}\n`;
110
- }
111
- sections.push({ text: failText, importance: 10 });
112
- }
113
-
114
- // 2. For compact: include current session's own observations
115
- if (source === 'compact') {
116
- const currentObs = getRecentObservations(db, 30, sessionId);
117
- if (currentObs.length > 0) {
118
- let currentText = '### Current Session Observations (restored after compaction)\n';
119
- for (const obs of currentObs) {
120
- currentText += `- [${obs.type}] ${obs.title}\n`;
121
- }
122
- sections.push({ text: currentText, importance: 9 });
123
- }
124
- }
125
-
126
- // 3. Recent session summaries
127
- const summaryCount = source === 'compact' ? 5 : 3;
128
- const summaries = getSessionSummaries(db, summaryCount);
129
- if (summaries.length > 0) {
130
- for (const s of summaries) {
131
- let sumText = `### Session (${s.created_at.split('T')[0]})\n`;
132
- if (s.request) sumText += `**Task**: ${s.request.slice(0, 200)}\n`;
133
- if (s.completed) sumText += `**Completed**: ${s.completed.slice(0, 300)}\n`;
134
- if (s.failed_attempts) sumText += `**Failed**: ${s.failed_attempts.slice(0, 200)}\n`;
135
-
136
- const progress = safeParseJson(s.plan_progress);
137
- if (progress && Object.keys(progress).length > 0) {
138
- const total = Object.keys(progress).length;
139
- const complete = Object.values(progress).filter(v => v === 'complete').length;
140
- sumText += `**Plan**: ${complete}/${total} complete\n`;
141
- }
142
- sections.push({ text: sumText, importance: 7 });
143
- }
144
- }
145
-
146
- // 4. Cross-task progress if task_id exists
147
- if (taskId) {
148
- const progress = getCrossTaskProgress(db, taskId);
149
- if (Object.keys(progress).length > 0) {
150
- const total = Object.keys(progress).length;
151
- const complete = Object.values(progress).filter(v => v === 'complete').length;
152
- let progressText = `### Cross-Session Task Progress (${taskId})\n`;
153
- progressText += `${complete}/${total} items complete\n`;
154
- sections.push({ text: progressText, importance: 8 });
155
- }
156
- }
157
-
158
- // 5. Prevention rules from corrections.md
159
- const preventionRules = loadCorrectionsPreventionRules();
160
- if (preventionRules.length > 0) {
161
- let rulesText = '### Active Prevention Rules (from corrections.md)\n';
162
- for (const rule of preventionRules) {
163
- rulesText += `- ${rule}\n`;
164
- }
165
- sections.push({ text: rulesText, importance: 9 });
166
- }
167
-
168
- // 6. Knowledge index status (warm-up check)
169
- try {
170
- const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
171
- if (existsSync(knowledgeDbPath)) {
172
- const Database = (await import('better-sqlite3')).default;
173
- const kdb = new Database(knowledgeDbPath, { readonly: true });
174
- try {
175
- const stats = kdb.prepare(
176
- 'SELECT COUNT(*) as doc_count, MAX(indexed_at) as last_indexed FROM knowledge_documents'
177
- ).get() as { doc_count: number; last_indexed: string | null };
178
- if (stats.doc_count > 0 && stats.last_indexed) {
179
- const ageMs = Date.now() - new Date(stats.last_indexed).getTime();
180
- const ageHours = Math.round(ageMs / 3600000);
181
- if (ageHours > 24) {
182
- sections.push({
183
- text: `### Knowledge Index Status\nIndex has ${stats.doc_count} documents, last indexed ${ageHours}h ago. Consider re-indexing.\n`,
184
- importance: 3,
185
- });
186
- }
187
- } else if (stats.doc_count === 0) {
188
- sections.push({
189
- text: '### Knowledge Index Status\nKnowledge index is empty. Run knowledge indexing to populate it.\n',
190
- importance: 2,
191
- });
192
- }
193
- } finally {
194
- kdb.close();
195
- }
196
- }
197
- } catch (_knowledgeErr) {
198
- // Best-effort: never block session start
199
- }
200
-
201
- // 7. Recent observations sorted by importance
202
- const recentObs = getRecentObservations(db, 20);
203
- if (recentObs.length > 0) {
204
- let obsText = '### Recent Observations\n';
205
- const sorted = [...recentObs].sort((a, b) => b.importance - a.importance);
206
- for (const obs of sorted) {
207
- obsText += `- [${obs.type}|imp:${obs.importance}] ${obs.title} (${obs.created_at.split('T')[0]})\n`;
208
- }
209
- sections.push({ text: obsText, importance: 5 });
210
- }
211
-
212
- // Fill token budget from high-importance to low-importance
213
- sections.sort((a, b) => b.importance - a.importance);
214
-
215
- let usedTokens = 0;
216
- const headerTokens = estimateTokens('=== Massu Memory: Previous Session Context ===\n\n=== END Massu Memory ===\n');
217
- usedTokens += headerTokens;
218
-
219
- const includedSections: string[] = [];
220
- for (const section of sections) {
221
- const sectionTokens = estimateTokens(section.text);
222
- if (usedTokens + sectionTokens <= tokenBudget) {
223
- includedSections.push(section.text);
224
- usedTokens += sectionTokens;
225
- }
226
- }
227
-
228
- if (includedSections.length === 0) return '';
229
-
230
- return `=== Massu Memory: Previous Session Context ===\n\n${includedSections.join('\n')}\n=== END Massu Memory ===\n`;
231
- }
232
-
233
- function estimateTokens(text: string): number {
234
- return Math.ceil(text.length / 4);
235
- }
236
-
237
- async function getGitBranch(): Promise<string | undefined> {
238
- try {
239
- const { spawnSync } = await import('child_process');
240
- const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
241
- encoding: 'utf-8',
242
- timeout: 5000,
243
- });
244
- if (result.status !== 0 || result.error) return undefined;
245
- return result.stdout.trim();
246
- } catch (_e) {
247
- return undefined;
248
- }
249
- }
250
-
251
- function readStdin(): Promise<string> {
252
- return new Promise((resolve) => {
253
- let data = '';
254
- process.stdin.setEncoding('utf-8');
255
- process.stdin.on('data', (chunk: string) => { data += chunk; });
256
- process.stdin.on('end', () => resolve(data));
257
- // Timeout after 3s
258
- setTimeout(() => resolve(data), 3000);
259
- });
260
- }
261
-
262
- /**
263
- * P5-001: compare the fingerprint stored in massu.config.yaml (detection.fingerprint,
264
- * stamped by init/refresh/upgrade) against a freshly-computed fingerprint. If they
265
- * disagree, return a plain-text banner. Returns '' on any error or when the
266
- * config has no fingerprint (back-compat with v1 configs).
267
- */
268
- async function buildDriftBanner(): Promise<string> {
269
- try {
270
- // Plan #2 P4-004: explicit opt-out for users in deliberate mid-migration windows.
271
- // Stays at the top so MASSU_DRIFT_QUIET=1 remains the strongest signal
272
- // (iter-1 G8: env-var override beats watcher-state suppression).
273
- if (process.env.MASSU_DRIFT_QUIET === '1') return '';
274
-
275
- // Plan 3a Phase 6: if a live watcher daemon refreshed within the last 24h,
276
- // suppress this banner — the watcher already keeps the config current.
277
- if (watcherIsLiveAndFresh()) return '';
278
-
279
- const configPath = resolve(process.cwd(), 'massu.config.yaml');
280
- if (!existsSync(configPath)) return '';
281
- const content = readFileSync(configPath, 'utf-8');
282
- // pattern-scanner-allow: yaml-parse — reason: compiled standalone hook (esbuild bundle). Per P2-023a, hooks cannot import getConfig() — they run in the Claude Code subprocess context with no module resolution path back to packages/core. Direct YAML parse is the only available access pattern.
283
- const parsed = parseYaml(content) as Record<string, unknown> | null;
284
- if (!parsed || typeof parsed !== 'object') return '';
285
- const det = parsed.detection as Record<string, unknown> | undefined;
286
- const storedFp = typeof det?.fingerprint === 'string' ? (det.fingerprint as string) : null;
287
- if (!storedFp) return '';
288
- // Plan #2 P4-006: skip the codebase introspector pass — the drift banner
289
- // only needs the fingerprint, not the introspected detail. Saves up to 2s
290
- // wall-clock from the hook's 5-second budget.
291
- const detection = await runDetection(process.cwd(), undefined, { skipIntrospect: true });
292
- const currentFp = computeFingerprint(detection);
293
- if (currentFp === storedFp) return '';
294
- return (
295
- '=== Massu Config Drift ===\n' +
296
- 'Detected stack has changed since last config refresh.\n' +
297
- `Fingerprint: ${storedFp.slice(0, 16)} -> ${currentFp.slice(0, 16)}\n` +
298
- 'Run: npx massu config refresh\n' +
299
- '(this will update massu.config.yaml AND any commands that need\n' +
300
- ' re-templating for your new stack)\n' +
301
- 'Tip: set MASSU_DRIFT_QUIET=1 to suppress this banner during mid-migration.\n' +
302
- '=== END ===\n'
303
- );
304
- } catch (_e) {
305
- // Never block session start on drift-check failure.
306
- return '';
307
- }
308
- }
309
-
310
- function safeParseJson(json: string): Record<string, string> | null {
311
- try {
312
- return JSON.parse(json);
313
- } catch (_e) {
314
- return null;
315
- }
316
- }
317
-
318
- /**
319
- * Load prevention rules from corrections.md in the memory directory.
320
- * Parses the markdown table format: | Date | Wrong Behavior | Correction | Prevention Rule |
321
- * Returns only the prevention rule column values.
322
- * Graceful degradation: returns empty array if file doesn't exist or can't be parsed.
323
- */
324
- function loadCorrectionsPreventionRules(): string[] {
325
- try {
326
- // Memory path follows Claude's project directory convention
327
- const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? '';
328
- const cwd = process.cwd();
329
- const config = getConfig();
330
- const claudeDirName = config.conventions?.claudeDirName ?? '.claude';
331
- // Convert cwd to Claude's directory format: /Users/x/project -> -Users-x-project.
332
- // Match both forward slashes (POSIX) and backslashes (Windows) so the lookup
333
- // works cross-platform — without this, Windows users silently miss prevention
334
- // rules because their projectDirName never resolves.
335
- const projectDirName = cwd.replace(/[/\\]/g, '-').replace(/^-/, '');
336
- const correctionsPath = join(homeDir, claudeDirName, 'projects', projectDirName, 'memory', 'corrections.md');
337
-
338
- if (!existsSync(correctionsPath)) return [];
339
-
340
- const content = readFileSync(correctionsPath, 'utf-8');
341
- const lines = content.split('\n');
342
- const rules: string[] = [];
343
-
344
- for (const line of lines) {
345
- // Match table rows: | date | wrong | correction | prevention |
346
- // Skip header row and separator row
347
- const trimmed = line.trim();
348
- if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) continue;
349
-
350
- const cells = trimmed.split('|').map(c => c.trim()).filter(c => c.length > 0);
351
- if (cells.length < 4) continue;
352
-
353
- // Skip header and separator rows
354
- if (cells[0] === 'Date' || cells[0].startsWith('-')) continue;
355
-
356
- const preventionRule = cells[3];
357
- if (preventionRule && !preventionRule.startsWith('-') && !preventionRule.startsWith('<!--')) {
358
- rules.push(preventionRule);
359
- }
360
- }
361
-
362
- return rules;
363
- } catch (_e) {
364
- // Graceful degradation: never block session start
365
- return [];
366
- }
367
- }
368
-
369
- // ============================================================
370
- // Plan 3a Phase 6: watcher-aware banner support
371
- // ============================================================
372
-
373
- interface WatchStateShape {
374
- schema_version?: number;
375
- daemonPid?: number | null;
376
- lastRefreshAt?: string | null;
377
- startedAt?: string | null;
378
- }
379
-
380
- function readWatchStateRaw(cwd: string): WatchStateShape | null {
381
- try {
382
- const path = resolve(cwd, '.massu', 'watch-state.json');
383
- if (!existsSync(path)) return null;
384
- const obj = JSON.parse(readFileSync(path, 'utf-8'));
385
- if (!obj || typeof obj !== 'object') return null;
386
- return obj as WatchStateShape;
387
- } catch {
388
- return null;
389
- }
390
- }
391
-
392
- function watcherIsLiveAndFresh(): boolean {
393
- // MASSU_DRIFT_QUIET takes precedence (caller already short-circuited).
394
- // Fresh = last refresh within 24h AND daemonPid is alive.
395
- const state = readWatchStateRaw(process.cwd());
396
- if (!state) return false;
397
- if (typeof state.daemonPid !== 'number' || state.daemonPid <= 0) return false;
398
- if (!isPidAlive(state.daemonPid)) return false;
399
- if (typeof state.lastRefreshAt !== 'string') return false;
400
- const last = Date.parse(state.lastRefreshAt);
401
- if (!Number.isFinite(last)) return false;
402
- const ageMs = Date.now() - last;
403
- return ageMs >= 0 && ageMs < 24 * 60 * 60 * 1000;
404
- }
405
-
406
- function buildWatcherBanner(): string {
407
- // P4-004 ordering: MASSU_DRIFT_QUIET wins everywhere.
408
- if (process.env.MASSU_DRIFT_QUIET === '1') return '';
409
- const state = readWatchStateRaw(process.cwd());
410
- if (!state) return '';
411
- if (typeof state.daemonPid !== 'number' || state.daemonPid <= 0) return '';
412
- if (!isPidAlive(state.daemonPid)) return '';
413
- if (typeof state.lastRefreshAt !== 'string') return '';
414
- const last = Date.parse(state.lastRefreshAt);
415
- if (!Number.isFinite(last)) return '';
416
- const ageMs = Date.now() - last;
417
- if (ageMs < 0 || ageMs >= 24 * 60 * 60 * 1000) return '';
418
-
419
- const ageStr = formatAge(ageMs);
420
- return (
421
- '=== Massu Watcher ===\n' +
422
- `[massu] watcher running, last refresh: ${ageStr} ago (pid ${state.daemonPid})\n` +
423
- '=== END ===\n'
424
- );
425
- }
426
-
427
- function formatAge(ms: number): string {
428
- const sec = Math.round(ms / 1000);
429
- if (sec < 60) return `${sec}s`;
430
- const min = Math.round(sec / 60);
431
- if (min < 60) return `${min}m`;
432
- const hr = Math.round(min / 60);
433
- return `${hr}h`;
434
- }
435
-
436
- main();
@@ -1,157 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // P3-004: UserPromptSubmit Hook
7
- // Captures user prompts for search and context.
8
- // ============================================================
9
-
10
- import { getMemoryDb, createSession, addUserPrompt, linkSessionToTask, autoDetectTaskId, addObservation } from '../memory-db.ts';
11
- import { existsSync } from 'fs';
12
- import { getResolvedPaths } from '../config.ts';
13
-
14
- interface HookInput {
15
- session_id: string;
16
- transcript_path: string;
17
- cwd: string;
18
- hook_event_name: string;
19
- prompt: string;
20
- }
21
-
22
- async function main(): Promise<void> {
23
- try {
24
- const input = await readStdin();
25
- const hookInput = JSON.parse(input) as HookInput;
26
- const { session_id, prompt } = hookInput;
27
-
28
- if (!prompt || !prompt.trim()) {
29
- process.exit(0);
30
- return;
31
- }
32
-
33
- const db = getMemoryDb();
34
- try {
35
- // 1. Create session if not exists
36
- const gitBranch = await getGitBranch();
37
- createSession(db, session_id, { branch: gitBranch });
38
-
39
- // 2. Scan prompt for plan file references
40
- const planFileMatch = prompt.match(/([^\s]+docs\/plans\/[^\s]+\.md)/);
41
- if (planFileMatch) {
42
- const planFile = planFileMatch[1];
43
- db.prepare('UPDATE sessions SET plan_file = ? WHERE session_id = ?').run(planFile, session_id);
44
-
45
- // Auto-detect and link task_id
46
- const taskId = autoDetectTaskId(planFile);
47
- if (taskId) {
48
- linkSessionToTask(db, session_id, taskId);
49
- }
50
- }
51
-
52
- // 3. Get current prompt count for this session
53
- const countResult = db.prepare(
54
- 'SELECT COUNT(*) as count FROM user_prompts WHERE session_id = ?'
55
- ).get(session_id) as { count: number };
56
- const promptNumber = countResult.count + 1;
57
-
58
- // 4. Insert prompt
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
- }
89
- // 6. Memory enforcement: nag when significant work detected but no memory ingestion
90
- try {
91
- const significantSignals = ['fix', 'implement', 'migrate', 'refactor', 'debug', 'decision', 'chose', 'architecture', 'redesign', 'rewrite'];
92
- const promptLower = prompt.toLowerCase();
93
- const signalCount = significantSignals.filter(s => promptLower.includes(s)).length;
94
-
95
- if (signalCount >= 2) {
96
- const memoryFileCount = db.prepare(
97
- "SELECT COUNT(*) as count FROM observations WHERE session_id = ? AND title LIKE '[memory-file] %'"
98
- ).get(session_id) as { count: number };
99
-
100
- if (memoryFileCount.count === 0) {
101
- process.stderr.write(
102
- '\n[MEMORY REMINDER] Significant work detected but no memory files have been written.\n' +
103
- 'Consider saving learnings to memory/*.md files for future sessions.\n\n'
104
- );
105
- }
106
- }
107
- } catch (_memoryNagErr) {
108
- // Best-effort: never block prompt capture
109
- }
110
- } finally {
111
- db.close();
112
- }
113
- } catch (_e) {
114
- // Best-effort: never block Claude Code
115
- }
116
- process.exit(0);
117
- }
118
-
119
- /**
120
- * Extract file path references from user prompt text.
121
- * Matches patterns like src/foo/bar.ts, packages/core/src/x.ts, etc.
122
- */
123
- function extractFileReferences(prompt: string): string[] {
124
- const filePattern = /(?:^|\s)((?:src|packages|lib)\/[\w./-]+\.(?:ts|tsx|js|jsx|md))/g;
125
- const matches: string[] = [];
126
- let match: RegExpExecArray | null;
127
- while ((match = filePattern.exec(prompt)) !== null) {
128
- matches.push(match[1]);
129
- }
130
- return [...new Set(matches)];
131
- }
132
-
133
- async function getGitBranch(): Promise<string | undefined> {
134
- try {
135
- const { spawnSync } = await import('child_process');
136
- const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
137
- encoding: 'utf-8',
138
- timeout: 5000,
139
- });
140
- if (result.status !== 0 || result.error) return undefined;
141
- return result.stdout.trim();
142
- } catch (_e) {
143
- return undefined;
144
- }
145
- }
146
-
147
- function readStdin(): Promise<string> {
148
- return new Promise((resolve) => {
149
- let data = '';
150
- process.stdin.setEncoding('utf-8');
151
- process.stdin.on('data', (chunk: string) => { data += chunk; });
152
- process.stdin.on('end', () => resolve(data));
153
- setTimeout(() => resolve(data), 3000);
154
- });
155
- }
156
-
157
- main();