@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,413 +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-002: PostToolUse Observation Hook
7
- // Captures tool usage as observations (lightweight, no AI needed).
8
- // Must complete in <500ms.
9
- // ============================================================
10
-
11
- import { getMemoryDb, addObservation, createSession, deduplicateFailedAttempt, addSummary } from '../memory-db.ts';
12
- import { classifyRealTimeToolCall, detectPlanProgress } from '../observation-extractor.ts';
13
- import { logAuditEntry } from '../audit-trail.ts';
14
- import { trackModification, recordTestResult } from '../regression-detector.ts';
15
- import { validateFile, storeValidationResult } from '../validation-engine.ts';
16
- import { scoreFileSecurity, storeSecurityScore } from '../security-scorer.ts';
17
- import { readFileSync, existsSync } from 'fs';
18
- import { join } from 'path';
19
- import { parse as parseYaml } from 'yaml';
20
- import { ingestMemoryFile } from '../memory-file-ingest.ts';
21
-
22
- interface HookInput {
23
- session_id: string;
24
- transcript_path: string;
25
- cwd: string;
26
- hook_event_name: string;
27
- tool_name: string;
28
- tool_input: Record<string, unknown>;
29
- tool_response: string;
30
- }
31
-
32
- // In-memory dedup for Read calls within this session
33
- const seenReads = new Set<string>();
34
- let currentSessionId: string | null = null;
35
-
36
- async function main(): Promise<void> {
37
- try {
38
- const input = await readStdin();
39
- const hookInput = JSON.parse(input) as HookInput;
40
- const { session_id, tool_name, tool_input, tool_response } = hookInput;
41
-
42
- // Reset seen reads if session changed
43
- if (currentSessionId !== session_id) {
44
- seenReads.clear();
45
- currentSessionId = session_id;
46
- }
47
-
48
- const db = getMemoryDb();
49
- try {
50
- // Ensure session exists
51
- createSession(db, session_id);
52
-
53
- // Classify and filter
54
- const observation = classifyRealTimeToolCall(tool_name, tool_input, tool_response, seenReads);
55
- if (!observation) {
56
- process.exit(0);
57
- return;
58
- }
59
-
60
- // Deduplicate failed attempts
61
- if (observation.type === 'failed_attempt') {
62
- deduplicateFailedAttempt(db, session_id, observation.title, observation.detail, observation.opts);
63
- } else {
64
- addObservation(db, session_id, observation.type, observation.title, observation.detail, observation.opts);
65
- }
66
-
67
- // Auto-detect plan progress
68
- if (tool_response) {
69
- const progress = detectPlanProgress(tool_response);
70
- if (progress.length > 0) {
71
- // Update plan_progress in session summary
72
- updatePlanProgress(db, session_id, progress);
73
- }
74
- }
75
-
76
- // Audit trail logging for file changes
77
- try {
78
- if (tool_name === 'Edit' || tool_name === 'Write') {
79
- const filePath = (tool_input.file_path as string) ?? '';
80
- logAuditEntry(db, {
81
- sessionId: session_id,
82
- eventType: 'code_change',
83
- actor: 'ai',
84
- filePath,
85
- changeType: tool_name === 'Write' ? 'create' : 'edit',
86
- });
87
-
88
- // Track modification for regression detection
89
- if (filePath) {
90
- const featureMatch = filePath.match(/(?:routers|components|app\/\(([^)]+)\))\/([^/.]+)/);
91
- if (featureMatch) {
92
- const featureKey = featureMatch[1] ?? featureMatch[2];
93
- trackModification(db, featureKey);
94
- }
95
- }
96
- }
97
- } catch (_auditErr) {
98
- // Best-effort: never block post-tool-use
99
- }
100
-
101
- // Real-time validation for Edit/Write
102
- try {
103
- if (tool_name === 'Edit' || tool_name === 'Write') {
104
- const filePath = (tool_input.file_path as string) ?? '';
105
- if (filePath && (filePath.endsWith('.ts') || filePath.endsWith('.tsx'))) {
106
- const projectRoot = hookInput.cwd;
107
- const checks = validateFile(filePath, projectRoot);
108
- const violations = checks.filter(c => c.severity === 'error' || c.severity === 'critical');
109
- if (violations.length > 0) {
110
- storeValidationResult(db, filePath, checks, session_id);
111
- }
112
- }
113
- }
114
- } catch (_validationErr) {
115
- // Best-effort: never block post-tool-use
116
- }
117
-
118
- // Auto-security scoring for router/API files
119
- try {
120
- if (tool_name === 'Edit' || tool_name === 'Write') {
121
- const filePath = (tool_input.file_path as string) ?? '';
122
- if (filePath && (filePath.includes('routers/') || filePath.includes('api/'))) {
123
- const projectRoot = hookInput.cwd;
124
- const { riskScore, findings } = scoreFileSecurity(filePath, projectRoot);
125
- if (findings.length > 0) {
126
- storeSecurityScore(db, session_id, filePath, riskScore, findings);
127
- }
128
- }
129
- }
130
- } catch (_securityErr) {
131
- // Best-effort: never block post-tool-use
132
- }
133
-
134
- // P-H029 (plan-stage-c-high-batch): wire recordTestResult() into the
135
- // post-tool-use hook so `feature_health` dashboard reflects real test
136
- // deltas. Pre-fix: `trackModification` fired but `recordTestResult` was
137
- // unit-tested yet never called; dashboard showed tests_passing=0 for
138
- // every feature.
139
- //
140
- // Strategy: when a Bash tool call runs a test runner AND the output
141
- // includes parseable pass/fail counts, call recordTestResult for every
142
- // feature with `modifications_since_test > 0`. This resets their counter
143
- // and updates pass/fail tallies based on the run.
144
- try {
145
- if (tool_name === 'Bash') {
146
- const command = (tool_input.command as string) ?? '';
147
- if (isTestRunnerCommand(command)) {
148
- const counts = parseTestRunOutput(tool_response ?? '');
149
- if (counts) {
150
- const modifiedFeatures = db
151
- .prepare(
152
- 'SELECT feature_key FROM feature_health WHERE modifications_since_test > 0',
153
- )
154
- .all() as Array<{ feature_key: string }>;
155
- for (const row of modifiedFeatures) {
156
- recordTestResult(db, row.feature_key, counts.passing, counts.failing);
157
- }
158
- // Also record a session-level aggregate so the dashboard has at
159
- // least one row even when no features were modified.
160
- if (modifiedFeatures.length === 0) {
161
- recordTestResult(db, '_session_test_run', counts.passing, counts.failing);
162
- }
163
- }
164
- }
165
- }
166
- } catch (_testResultErr) {
167
- // Best-effort: never block post-tool-use
168
- }
169
-
170
- // MEMORY.md integrity check on write
171
- try {
172
- if (tool_name === 'Edit' || tool_name === 'Write') {
173
- const filePath = (tool_input.file_path as string) ?? '';
174
- if (filePath && filePath.endsWith('MEMORY.md') && filePath.includes('/memory/')) {
175
- const issues = checkMemoryFileIntegrity(filePath);
176
- if (issues.length > 0) {
177
- addObservation(db, session_id, 'incident_near_miss',
178
- 'MEMORY.md integrity issue detected',
179
- issues.join('; '),
180
- { importance: 4 }
181
- );
182
- }
183
- }
184
- }
185
- } catch (_memoryErr) {
186
- // Best-effort: never block post-tool-use
187
- }
188
-
189
- // Memory file auto-ingest: when Claude writes a memory/*.md file,
190
- // parse frontmatter and ingest into observations table
191
- try {
192
- if (tool_name === 'Edit' || tool_name === 'Write') {
193
- const filePath = (tool_input.file_path as string) ?? '';
194
- if (filePath && filePath.includes('/memory/') && filePath.endsWith('.md')) {
195
- const basename = filePath.split('/').pop() ?? '';
196
- if (basename !== 'MEMORY.md') {
197
- ingestMemoryFile(db, session_id, filePath);
198
- }
199
- }
200
- }
201
- } catch (_memoryIngestErr) {
202
- // Best-effort: never block post-tool-use
203
- }
204
-
205
- // Knowledge index staleness check on knowledge file edits
206
- try {
207
- if (tool_name === 'Edit' || tool_name === 'Write') {
208
- const filePath = (tool_input.file_path as string) ?? '';
209
- if (filePath && isKnowledgeSourceFile(filePath)) {
210
- addObservation(db, session_id, 'discovery',
211
- 'Knowledge source file modified - index may be stale',
212
- `Edited ${filePath.split('/').pop() ?? filePath}. Run knowledge re-index to update.`,
213
- { importance: 3 }
214
- );
215
- }
216
- }
217
- } catch (_knowledgeErr) {
218
- // Best-effort: never block post-tool-use
219
- }
220
- } finally {
221
- db.close();
222
- }
223
- } catch (_e) {
224
- // Best-effort: never block Claude Code
225
- }
226
- process.exit(0);
227
- }
228
-
229
- function updatePlanProgress(db: import('better-sqlite3').Database, sessionId: string, progress: Array<{ planItem: string; status: string }>): void {
230
- // Get or create latest summary's plan_progress
231
- const existing = db.prepare(
232
- 'SELECT id, plan_progress FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1'
233
- ).get(sessionId) as { id: number; plan_progress: string } | undefined;
234
-
235
- if (existing) {
236
- try {
237
- const currentProgress = JSON.parse(existing.plan_progress) as Record<string, string>;
238
- for (const p of progress) {
239
- currentProgress[p.planItem] = p.status;
240
- }
241
- db.prepare('UPDATE session_summaries SET plan_progress = ? WHERE id = ?')
242
- .run(JSON.stringify(currentProgress), existing.id);
243
- } catch (_e) {
244
- // Skip if JSON parse fails
245
- }
246
- } else {
247
- // Create a minimal summary with plan progress
248
- const progressMap: Record<string, string> = {};
249
- for (const p of progress) {
250
- progressMap[p.planItem] = p.status;
251
- }
252
- addSummary(db, sessionId, { planProgress: progressMap });
253
- }
254
- }
255
-
256
- /**
257
- * P-H029: Detect test-runner commands. Conservative match — only commands that
258
- * START with a recognized test runner so a build script invoking `npm run test`
259
- * is included but a script that merely mentions "test" in a filename is not.
260
- */
261
- function isTestRunnerCommand(command: string): boolean {
262
- const trimmed = command.trim().toLowerCase();
263
- // Strip leading `cd <dir> && ` or `(cd <dir> && ...)` prefix so we match
264
- // the actual test command.
265
- const stripped = trimmed
266
- .replace(/^cd\s+\S+\s*(&&|;)\s*/, '')
267
- .replace(/^\(\s*cd\s+\S+\s*(&&|;)\s*/, '');
268
- const testRunnerPrefixes = [
269
- 'npm test', 'npm run test', 'npx vitest', 'npx jest', 'vitest', 'jest',
270
- 'pnpm test', 'pnpm run test', 'yarn test', 'pytest', 'go test', 'cargo test',
271
- ];
272
- return testRunnerPrefixes.some((prefix) => stripped.startsWith(prefix));
273
- }
274
-
275
- /**
276
- * P-H029: Parse test-run output for pass/fail counts. Supports vitest
277
- * (`Tests N passed (N)`, `Tests X failed | Y passed (Z)`), jest
278
- * (`Tests: X failed, Y passed, Z total`), and pytest (`X passed, Y failed`).
279
- * Returns null if no parseable summary line found.
280
- */
281
- function parseTestRunOutput(output: string): { passing: number; failing: number } | null {
282
- // vitest: " Tests 439 passed (439)" or " Tests 3 failed | 436 passed (439)"
283
- const vitestSplit = output.match(/Tests?\s+(?:(\d+)\s+failed\s+\|\s+)?(\d+)\s+passed/);
284
- if (vitestSplit) {
285
- return {
286
- passing: parseInt(vitestSplit[2], 10),
287
- failing: vitestSplit[1] ? parseInt(vitestSplit[1], 10) : 0,
288
- };
289
- }
290
- // jest: "Tests: 1 failed, 5 passed, 6 total"
291
- const jest = output.match(/Tests?:\s+(?:(\d+)\s+failed,\s+)?(\d+)\s+passed/);
292
- if (jest) {
293
- return {
294
- passing: parseInt(jest[2], 10),
295
- failing: jest[1] ? parseInt(jest[1], 10) : 0,
296
- };
297
- }
298
- // pytest: "5 passed, 2 failed in 1.23s" or "5 passed in 1.23s"
299
- const pytestPassed = output.match(/(\d+)\s+passed/);
300
- const pytestFailed = output.match(/(\d+)\s+failed/);
301
- if (pytestPassed) {
302
- return {
303
- passing: parseInt(pytestPassed[1], 10),
304
- failing: pytestFailed ? parseInt(pytestFailed[1], 10) : 0,
305
- };
306
- }
307
- return null;
308
- }
309
-
310
- function readStdin(): Promise<string> {
311
- return new Promise((resolve) => {
312
- let data = '';
313
- process.stdin.setEncoding('utf-8');
314
- process.stdin.on('data', (chunk: string) => { data += chunk; });
315
- process.stdin.on('end', () => resolve(data));
316
- setTimeout(() => resolve(data), 3000);
317
- });
318
- }
319
-
320
- /**
321
- * Read the conventions section from massu.config.yaml directly.
322
- * Hooks are compiled with esbuild and cannot use getConfig() from config.ts.
323
- * Falls back to sensible defaults if the config file is not found.
324
- */
325
- function readConventions(cwd?: string): {
326
- knowledgeSourceFiles: string[];
327
- claudeDirName: string;
328
- } {
329
- const defaults = {
330
- knowledgeSourceFiles: ['CLAUDE.md', 'MEMORY.md', 'corrections.md'],
331
- claudeDirName: '.claude',
332
- };
333
- try {
334
- const projectRoot = cwd ?? process.cwd();
335
- const configPath = join(projectRoot, 'massu.config.yaml');
336
- if (!existsSync(configPath)) return defaults;
337
- const content = readFileSync(configPath, 'utf-8');
338
- // 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.
339
- const parsed = parseYaml(content) as Record<string, unknown> | null;
340
- if (!parsed || typeof parsed !== 'object') return defaults;
341
- const conventions = parsed.conventions as Record<string, unknown> | undefined;
342
- if (!conventions || typeof conventions !== 'object') return defaults;
343
- return {
344
- knowledgeSourceFiles: Array.isArray(conventions.knowledgeSourceFiles)
345
- ? conventions.knowledgeSourceFiles as string[]
346
- : defaults.knowledgeSourceFiles,
347
- claudeDirName: typeof conventions.claudeDirName === 'string'
348
- ? conventions.claudeDirName
349
- : defaults.claudeDirName,
350
- };
351
- } catch {
352
- return defaults;
353
- }
354
- }
355
-
356
- /**
357
- * Check if a file path is a knowledge source file (CLAUDE.md, corrections.md,
358
- * memory files, or knowledge system source files).
359
- * When these are edited, the knowledge index may become stale.
360
- */
361
- function isKnowledgeSourceFile(filePath: string): boolean {
362
- const basename = filePath.split('/').pop() ?? '';
363
- const conventions = readConventions();
364
- const knowledgeSourcePatterns = [
365
- ...conventions.knowledgeSourceFiles,
366
- 'file-index.md',
367
- 'knowledge-db.ts',
368
- 'knowledge-indexer.ts',
369
- 'knowledge-tools.ts',
370
- ];
371
- return knowledgeSourcePatterns.some(p => basename === p) ||
372
- filePath.includes('/memory/') ||
373
- filePath.includes(conventions.claudeDirName + '/');
374
- }
375
-
376
- /**
377
- * Check MEMORY.md file integrity after a write.
378
- * Verifies: file exists, has expected structure, and is under line limit.
379
- * Returns array of issue descriptions (empty = all good).
380
- */
381
- function checkMemoryFileIntegrity(filePath: string): string[] {
382
- const issues: string[] = [];
383
-
384
- try {
385
- if (!existsSync(filePath)) {
386
- issues.push('MEMORY.md file does not exist after write');
387
- return issues;
388
- }
389
-
390
- const content = readFileSync(filePath, 'utf-8');
391
- const lines = content.split('\n');
392
-
393
- // Check line count (CLAUDE.md truncates after ~200 lines)
394
- const MAX_LINES = 200;
395
- if (lines.length > MAX_LINES) {
396
- issues.push(`MEMORY.md exceeds ${MAX_LINES} lines (currently ${lines.length}). Consider archiving old entries.`);
397
- }
398
-
399
- // Check required structure sections
400
- const requiredSections = ['# Massu Memory', '## Key Learnings', '## Common Gotchas'];
401
- for (const section of requiredSections) {
402
- if (!content.includes(section)) {
403
- issues.push(`Missing required section: "${section}"`);
404
- }
405
- }
406
- } catch (_e) {
407
- // Graceful degradation: don't report issues if we can't check
408
- }
409
-
410
- return issues;
411
- }
412
-
413
- main();
@@ -1,168 +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-006: PreCompact State Snapshot Hook
7
- // Captures current session state into DB before compaction destroys context.
8
- // ============================================================
9
-
10
- import { getMemoryDb, addSummary, createSession, addObservation } from '../memory-db.ts';
11
- import { logAuditEntry } from '../audit-trail.ts';
12
- import type { SessionSummary } from '../memory-db.ts';
13
-
14
- interface HookInput {
15
- session_id: string;
16
- transcript_path: string;
17
- cwd: string;
18
- hook_event_name: string;
19
- }
20
-
21
- async function main(): Promise<void> {
22
- try {
23
- const input = await readStdin();
24
- const hookInput = JSON.parse(input) as HookInput;
25
- const { session_id } = hookInput;
26
-
27
- const db = getMemoryDb();
28
- try {
29
- // Ensure session exists
30
- createSession(db, session_id);
31
-
32
- // 1. Get all observations for this session
33
- const observations = db.prepare(
34
- 'SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC'
35
- ).all(session_id) as Array<Record<string, unknown>>;
36
-
37
- // 2. Get user prompts
38
- const prompts = db.prepare(
39
- 'SELECT prompt_text FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC'
40
- ).all(session_id) as Array<{ prompt_text: string }>;
41
-
42
- // 3. Generate mid-session summary snapshot
43
- const summary = buildSnapshotSummary(observations, prompts);
44
-
45
- // 4. Store with pre_compact marker in plan_progress
46
- addSummary(db, session_id, summary);
47
-
48
- // 5. Preserve knowledge system state for post-compaction context
49
- try {
50
- const knowledgeObs = observations.filter(
51
- o => (o.title as string)?.includes('knowledge') ||
52
- (o.title as string)?.includes('Knowledge') ||
53
- (o.detail as string)?.includes('knowledge')
54
- );
55
- if (knowledgeObs.length > 0) {
56
- const knowledgeContext = knowledgeObs
57
- .map(o => `[${o.type}] ${o.title}`)
58
- .join('; ');
59
- // Store as a high-importance observation so session-start picks it up
60
- addObservation(db, session_id, 'discovery',
61
- 'Knowledge context preserved before compaction',
62
- knowledgeContext,
63
- { importance: 4 }
64
- );
65
- }
66
- } catch (_knowledgeErr) {
67
- // Best-effort: never block compaction
68
- }
69
-
70
- // Log compaction event for audit trail continuity
71
- try {
72
- logAuditEntry(db, {
73
- sessionId: session_id,
74
- eventType: 'compaction',
75
- actor: 'hook',
76
- metadata: { observations_count: observations.length, prompts_count: prompts.length },
77
- });
78
- } catch (_auditErr) {
79
- // Best-effort: never block compaction
80
- }
81
- } finally {
82
- db.close();
83
- }
84
- } catch (_e) {
85
- // Best-effort: never block Claude Code
86
- }
87
- process.exit(0);
88
- }
89
-
90
- function buildSnapshotSummary(
91
- observations: Array<Record<string, unknown>>,
92
- prompts: Array<{ prompt_text: string }>
93
- ): SessionSummary {
94
- const request = prompts[0]?.prompt_text?.slice(0, 500) ?? undefined;
95
-
96
- const completed = observations
97
- .filter(o => ['feature', 'bugfix', 'refactor'].includes(o.type as string))
98
- .map(o => `- ${o.title}`)
99
- .join('\n');
100
-
101
- const failedAttempts = observations
102
- .filter(o => o.type === 'failed_attempt')
103
- .map(o => `- ${o.title}`)
104
- .join('\n');
105
-
106
- const decisions = observations
107
- .filter(o => o.type === 'decision')
108
- .map(o => `- ${o.title}`)
109
- .join('\n');
110
-
111
- // Collect file changes
112
- const filesCreated: string[] = [];
113
- const filesModified: string[] = [];
114
- for (const o of observations) {
115
- if (o.type !== 'file_change') continue;
116
- const files = safeParseJson(o.files_involved as string, []) as string[];
117
- const title = o.title as string;
118
- if (title.startsWith('Created')) filesCreated.push(...files);
119
- else if (title.startsWith('Edited')) filesModified.push(...files);
120
- }
121
-
122
- // Collect plan progress
123
- const planProgress: Record<string, string> = { snapshot_type: 'pre_compact' };
124
- for (const o of observations) {
125
- if (!o.plan_item) continue;
126
- planProgress[o.plan_item as string] = 'in_progress';
127
- }
128
-
129
- // Verification results
130
- const verificationResults: Record<string, string> = {};
131
- for (const o of observations) {
132
- if (o.type !== 'vr_check') continue;
133
- const vrType = o.vr_type as string;
134
- const passed = (o.title as string).includes('PASS');
135
- if (vrType) verificationResults[vrType] = passed ? 'PASS' : 'FAIL';
136
- }
137
-
138
- return {
139
- request,
140
- completed: completed || undefined,
141
- failedAttempts: failedAttempts || undefined,
142
- decisions: decisions || undefined,
143
- filesCreated: [...new Set(filesCreated)],
144
- filesModified: [...new Set(filesModified)],
145
- verificationResults,
146
- planProgress,
147
- };
148
- }
149
-
150
- function safeParseJson(json: string, fallback: unknown): unknown {
151
- try {
152
- return JSON.parse(json);
153
- } catch (_e) {
154
- return fallback;
155
- }
156
- }
157
-
158
- function readStdin(): Promise<string> {
159
- return new Promise((resolve) => {
160
- let data = '';
161
- process.stdin.setEncoding('utf-8');
162
- process.stdin.on('data', (chunk: string) => { data += chunk; });
163
- process.stdin.on('end', () => resolve(data));
164
- setTimeout(() => resolve(data), 3000);
165
- });
166
- }
167
-
168
- main();