@massu/core 1.10.7 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  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 +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  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 +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  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/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
@@ -1,450 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import type Database from 'better-sqlite3';
5
- import type { ToolDefinition, ToolResult } from './tools.ts';
6
- import { getConfig } from './config.ts';
7
-
8
- // ============================================================
9
- // Compliance Audit Trail
10
- // ============================================================
11
-
12
- /** Prefix a base tool name with the configured tool prefix. */
13
- function p(baseName: string): string {
14
- return `${getConfig().toolPrefix}_${baseName}`;
15
- }
16
-
17
- export interface AuditEntry {
18
- eventType: 'code_change' | 'rule_enforced' | 'approval' | 'review' | 'commit' | 'compaction';
19
- actor: 'ai' | 'human' | 'hook' | 'agent';
20
- filePath?: string;
21
- changeType?: 'create' | 'edit' | 'delete';
22
- evidence?: string;
23
- metadata?: Record<string, unknown>;
24
- sessionId?: string;
25
- modelId?: string;
26
- approvalStatus?: 'auto_approved' | 'human_approved' | 'pending' | 'denied';
27
- rulesInEffect?: string;
28
- }
29
-
30
- /** Default audit formats */
31
- const DEFAULT_FORMATS = ['summary', 'detailed', 'soc2'];
32
-
33
- /** Default retention days */
34
- const DEFAULT_RETENTION_DAYS = 365;
35
-
36
- /**
37
- * Get configured audit formats.
38
- */
39
- function getAuditFormats(): string[] {
40
- return getConfig().governance?.audit?.formats ?? DEFAULT_FORMATS;
41
- }
42
-
43
- /**
44
- * Log an audit entry.
45
- */
46
- export function logAuditEntry(
47
- db: Database.Database,
48
- entry: AuditEntry
49
- ): void {
50
- db.prepare(`
51
- INSERT INTO audit_log (session_id, event_type, actor, model_id, file_path, change_type, rules_in_effect, approval_status, evidence, metadata)
52
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
53
- `).run(
54
- entry.sessionId ?? null,
55
- entry.eventType,
56
- entry.actor,
57
- entry.modelId ?? null,
58
- entry.filePath ?? null,
59
- entry.changeType ?? null,
60
- entry.rulesInEffect ?? null,
61
- entry.approvalStatus ?? null,
62
- entry.evidence ?? null,
63
- entry.metadata ? JSON.stringify(entry.metadata) : null
64
- );
65
- }
66
-
67
- /**
68
- * Query audit log with filters.
69
- */
70
- export function queryAuditLog(
71
- db: Database.Database,
72
- options: {
73
- eventType?: string;
74
- actor?: string;
75
- days?: number;
76
- limit?: number;
77
- filePath?: string;
78
- changeType?: string;
79
- }
80
- ): Array<Record<string, unknown>> {
81
- let sql = 'SELECT * FROM audit_log WHERE 1=1';
82
- const params: (string | number)[] = [];
83
-
84
- if (options.eventType) {
85
- sql += ' AND event_type = ?';
86
- params.push(options.eventType);
87
- }
88
- if (options.actor) {
89
- sql += ' AND actor = ?';
90
- params.push(options.actor);
91
- }
92
- if (options.days) {
93
- sql += ' AND timestamp >= datetime(\'now\', ?)';
94
- params.push(`-${options.days} days`);
95
- }
96
- if (options.filePath) {
97
- sql += ' AND file_path = ?';
98
- params.push(options.filePath);
99
- }
100
- if (options.changeType) {
101
- sql += ' AND change_type = ?';
102
- params.push(options.changeType);
103
- }
104
-
105
- sql += ' ORDER BY timestamp DESC';
106
-
107
- if (options.limit) {
108
- sql += ' LIMIT ?';
109
- params.push(options.limit);
110
- }
111
-
112
- return db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
113
- }
114
-
115
- /**
116
- * Get audit chain for a file - chronological audit history.
117
- */
118
- export function getFileChain(
119
- db: Database.Database,
120
- filePath: string
121
- ): Array<Record<string, unknown>> {
122
- return db.prepare(
123
- 'SELECT * FROM audit_log WHERE file_path = ? ORDER BY timestamp ASC'
124
- ).all(filePath) as Array<Record<string, unknown>>;
125
- }
126
-
127
- /**
128
- * Backfill audit log from session observations.
129
- */
130
- export function backfillAuditLog(db: Database.Database): number {
131
- const observations = db.prepare(`
132
- SELECT o.id, o.type, o.detail, o.files_involved, o.session_id, o.created_at
133
- FROM observations o
134
- LEFT JOIN audit_log a ON a.evidence = o.detail AND a.session_id = o.session_id
135
- WHERE a.id IS NULL
136
- AND o.type IN ('bugfix', 'cr_violation', 'vr_check', 'incident', 'decision')
137
- ORDER BY o.created_at ASC
138
- LIMIT 1000
139
- `).all() as Array<Record<string, unknown>>;
140
-
141
- let backfilled = 0;
142
- for (const obs of observations) {
143
- const files = obs.files_involved ? JSON.parse(obs.files_involved as string) : [];
144
- const eventType = (obs.type === 'cr_violation') ? 'rule_enforced' as const
145
- : (obs.type === 'vr_check') ? 'review' as const
146
- : 'code_change' as const;
147
-
148
- logAuditEntry(db, {
149
- eventType,
150
- actor: 'ai',
151
- filePath: files[0] ?? undefined,
152
- changeType: 'edit',
153
- evidence: obs.detail as string,
154
- sessionId: obs.session_id as string,
155
- metadata: { original_type: obs.type, backfilled: true },
156
- });
157
- backfilled++;
158
- }
159
-
160
- return backfilled;
161
- }
162
-
163
- // ============================================================
164
- // MCP Tool Definitions & Handlers
165
- // ============================================================
166
-
167
- export function getAuditToolDefinitions(): ToolDefinition[] {
168
- return [
169
- {
170
- name: p('audit_log'),
171
- description: 'Query the audit log. Filter by event type, actor, file, or time range.',
172
- inputSchema: {
173
- type: 'object',
174
- properties: {
175
- event_type: { type: 'string', description: 'Filter by event type: code_change, rule_enforced, approval, review, commit, compaction' },
176
- actor: { type: 'string', description: 'Filter by actor: ai, human, hook, agent' },
177
- file_path: { type: 'string', description: 'Filter by file path' },
178
- change_type: { type: 'string', description: 'Filter by change type: create, edit, delete' },
179
- days: { type: 'number', description: 'Days to look back (default: 30)' },
180
- limit: { type: 'number', description: 'Max results (default: 50)' },
181
- },
182
- required: [],
183
- },
184
- },
185
- {
186
- name: p('audit_report'),
187
- description: 'Generate an audit report in a specified format (summary, detailed, soc2).',
188
- inputSchema: {
189
- type: 'object',
190
- properties: {
191
- format: { type: 'string', description: 'Report format: summary, detailed, soc2 (default: summary)' },
192
- days: { type: 'number', description: 'Days to cover (default: 30)' },
193
- },
194
- required: [],
195
- },
196
- },
197
- {
198
- name: p('audit_chain'),
199
- description: 'Get the complete audit trail for a specific file. Shows all changes, reviews, and decisions chronologically.',
200
- inputSchema: {
201
- type: 'object',
202
- properties: {
203
- file: { type: 'string', description: 'File path to trace' },
204
- },
205
- required: ['file'],
206
- },
207
- },
208
- ];
209
- }
210
-
211
- const AUDIT_BASE_NAMES = new Set(['audit_log', 'audit_report', 'audit_chain']);
212
-
213
- export function isAuditTool(name: string): boolean {
214
- const pfx = getConfig().toolPrefix + '_';
215
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
216
- return AUDIT_BASE_NAMES.has(baseName);
217
- }
218
-
219
- export function handleAuditToolCall(
220
- name: string,
221
- args: Record<string, unknown>,
222
- memoryDb: Database.Database
223
- ): ToolResult {
224
- try {
225
- const pfx = getConfig().toolPrefix + '_';
226
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
227
-
228
- switch (baseName) {
229
- case 'audit_log':
230
- return handleAuditLog(args, memoryDb);
231
- case 'audit_report':
232
- return handleAuditReport(args, memoryDb);
233
- case 'audit_chain':
234
- return handleAuditChain(args, memoryDb);
235
- default:
236
- return text(`Unknown audit tool: ${name}`);
237
- }
238
- } catch (error) {
239
- return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('audit_log')} { severity: "critical" }, ${p('audit_report')} { format: "soc2" }`);
240
- }
241
- }
242
-
243
- function handleAuditLog(args: Record<string, unknown>, db: Database.Database): ToolResult {
244
- const entries = queryAuditLog(db, {
245
- eventType: args.event_type as string | undefined,
246
- actor: args.actor as string | undefined,
247
- filePath: args.file_path as string | undefined,
248
- changeType: args.change_type as string | undefined,
249
- days: (args.days as number) ?? 30,
250
- limit: (args.limit as number) ?? 50,
251
- });
252
-
253
- if (entries.length === 0) {
254
- return text(`No audit log entries found matching the filters. Audit entries are recorded automatically during code changes, rule checks, and commits. Try broadening your search: ${p('audit_log')} { days: 90 } for a longer time range, or ${p('audit_log')} {} with no filters.`);
255
- }
256
-
257
- const lines = [
258
- `## Audit Log (${entries.length} entries)`,
259
- '',
260
- '| Timestamp | Event | Actor | Change | File | Evidence |',
261
- '|-----------|-------|-------|--------|------|----------|',
262
- ];
263
-
264
- for (const entry of entries) {
265
- const file = entry.file_path ? (entry.file_path as string).split('/').pop() : '-';
266
- const evidence = entry.evidence ? (entry.evidence as string).slice(0, 50) : '-';
267
- lines.push(
268
- `| ${(entry.timestamp as string).slice(0, 16)} | ${entry.event_type} | ${entry.actor} | ${entry.change_type ?? '-'} | ${file} | ${evidence} |`
269
- );
270
- }
271
-
272
- return text(lines.join('\n'));
273
- }
274
-
275
- function handleAuditReport(args: Record<string, unknown>, db: Database.Database): ToolResult {
276
- const format = (args.format as string) ?? 'summary';
277
- const days = (args.days as number) ?? 30;
278
-
279
- const availableFormats = getAuditFormats();
280
- if (!availableFormats.includes(format)) {
281
- return text(`Unknown report format: "${format}". Available formats: ${availableFormats.join(', ')}. Example: ${p('audit_report')} { format: "soc2", days: 30 }. Configure additional formats via the governance.audit.formats config key.`);
282
- }
283
-
284
- const entries = queryAuditLog(db, { days });
285
-
286
- if (entries.length === 0) {
287
- return text(`No audit entries found in the last ${days} days. Audit entries are recorded automatically during code changes, reviews, and commits. Try: ${p('audit_log')} { days: 90 } for a longer time range.`);
288
- }
289
-
290
- // Count by event type and actor
291
- const byEventType: Record<string, number> = {};
292
- const byActor: Record<string, number> = {};
293
- for (const e of entries) {
294
- const et = e.event_type as string;
295
- byEventType[et] = (byEventType[et] ?? 0) + 1;
296
- const actor = e.actor as string;
297
- byActor[actor] = (byActor[actor] ?? 0) + 1;
298
- }
299
-
300
- if (format === 'soc2') {
301
- return generateSoc2Report(entries, byEventType, byActor, days);
302
- }
303
-
304
- if (format === 'detailed') {
305
- return generateDetailedReport(entries, byEventType, byActor, days);
306
- }
307
-
308
- // Summary format
309
- const lines = [
310
- `## Audit Summary Report (${days} days)`,
311
- `Generated: ${new Date().toISOString().slice(0, 16)}`,
312
- '',
313
- '### By Event Type',
314
- `| Event Type | Count |`,
315
- `|------------|-------|`,
316
- ];
317
-
318
- for (const [et, count] of Object.entries(byEventType).sort((a, b) => b[1] - a[1])) {
319
- lines.push(`| ${et} | ${count} |`);
320
- }
321
-
322
- lines.push(`| **Total** | **${entries.length}** |`);
323
- lines.push('');
324
- lines.push('### By Actor');
325
- lines.push(`| Actor | Count |`);
326
- lines.push(`|-------|-------|`);
327
-
328
- for (const [actor, count] of Object.entries(byActor).sort((a, b) => b[1] - a[1])) {
329
- lines.push(`| ${actor} | ${count} |`);
330
- }
331
-
332
- return text(lines.join('\n'));
333
- }
334
-
335
- function generateSoc2Report(
336
- entries: Array<Record<string, unknown>>,
337
- byEventType: Record<string, number>,
338
- byActor: Record<string, number>,
339
- days: number
340
- ): ToolResult {
341
- const lines = [
342
- `# SOC 2 Compliance Report`,
343
- `Period: Last ${days} days`,
344
- `Generated: ${new Date().toISOString()}`,
345
- '',
346
- '## 1. Control Environment',
347
- `Total audit events: ${entries.length}`,
348
- `Human-initiated: ${byActor['human'] ?? 0}`,
349
- `AI-initiated: ${byActor['ai'] ?? 0}`,
350
- `Hook-triggered: ${byActor['hook'] ?? 0}`,
351
- '',
352
- '## 2. Change Management',
353
- `Code changes logged: ${byEventType['code_change'] ?? 0}`,
354
- `Rule enforcements: ${byEventType['rule_enforced'] ?? 0}`,
355
- `Approvals recorded: ${byEventType['approval'] ?? 0}`,
356
- `Reviews: ${byEventType['review'] ?? 0}`,
357
- `Commits: ${byEventType['commit'] ?? 0}`,
358
- '',
359
- '## 3. Approval Status',
360
- ];
361
-
362
- const pendingApprovals = entries.filter(e => e.approval_status === 'pending');
363
- const deniedApprovals = entries.filter(e => e.approval_status === 'denied');
364
- lines.push(`Pending approvals: ${pendingApprovals.length}`);
365
- lines.push(`Denied approvals: ${deniedApprovals.length}`);
366
- lines.push('');
367
- lines.push('## 4. Events Requiring Review');
368
-
369
- const reviewEntries = entries.filter(e => e.approval_status === 'denied' || e.event_type === 'rule_enforced');
370
- if (reviewEntries.length > 0) {
371
- for (const e of reviewEntries.slice(0, 20)) {
372
- lines.push(`- [${(e.timestamp as string).slice(0, 16)}] ${e.event_type}: ${((e.evidence as string) ?? '').slice(0, 100)}`);
373
- }
374
- } else {
375
- lines.push('No events requiring review in this period.');
376
- }
377
-
378
- return text(lines.join('\n'));
379
- }
380
-
381
- function generateDetailedReport(
382
- entries: Array<Record<string, unknown>>,
383
- byEventType: Record<string, number>,
384
- byActor: Record<string, number>,
385
- days: number
386
- ): ToolResult {
387
- const lines = [
388
- `## Detailed Audit Report (${days} days)`,
389
- `Total events: ${entries.length}`,
390
- '',
391
- '### Event Type Distribution',
392
- ];
393
-
394
- for (const [et, count] of Object.entries(byEventType).sort((a, b) => b[1] - a[1])) {
395
- lines.push(`- ${et}: ${count}`);
396
- }
397
-
398
- lines.push('');
399
- lines.push('### Actor Distribution');
400
- for (const [actor, count] of Object.entries(byActor).sort((a, b) => b[1] - a[1])) {
401
- lines.push(`- ${actor}: ${count}`);
402
- }
403
-
404
- lines.push('');
405
- lines.push('### All Events');
406
-
407
- for (const e of entries.slice(0, 100)) {
408
- lines.push(`#### ${(e.timestamp as string).slice(0, 16)} [${e.event_type}]`);
409
- lines.push(`Actor: ${e.actor} | Change: ${e.change_type ?? '-'} | Approval: ${e.approval_status ?? '-'}`);
410
- if (e.file_path) lines.push(`File: ${e.file_path}`);
411
- if (e.evidence) lines.push(`Evidence: ${e.evidence}`);
412
- lines.push('');
413
- }
414
-
415
- if (entries.length > 100) {
416
- lines.push(`... and ${entries.length - 100} more entries`);
417
- }
418
-
419
- return text(lines.join('\n'));
420
- }
421
-
422
- function handleAuditChain(args: Record<string, unknown>, db: Database.Database): ToolResult {
423
- const filePath = args.file as string;
424
- if (!filePath) return text(`Usage: ${p('audit_chain')} { file: "src/path/to/file.ts" } - Get complete audit history for a file.`);
425
-
426
- const chain = getFileChain(db, filePath);
427
-
428
- if (chain.length === 0) {
429
- return text(`No audit entries found for "${filePath}". Audit entries are recorded automatically when files are modified, reviewed, or validated. Try: ${p('audit_log')} {} to see all recent audit entries across all files.`);
430
- }
431
-
432
- const lines = [
433
- `## Audit Chain: ${filePath}`,
434
- `Total events: ${chain.length}`,
435
- '',
436
- ];
437
-
438
- for (const entry of chain) {
439
- lines.push(`### ${(entry.timestamp as string).slice(0, 16)} [${entry.event_type}]`);
440
- lines.push(`**${entry.actor}** (${entry.change_type ?? 'unknown'})`);
441
- if (entry.evidence) lines.push(entry.evidence as string);
442
- lines.push('');
443
- }
444
-
445
- return text(lines.join('\n'));
446
- }
447
-
448
- function text(content: string): ToolResult {
449
- return { content: [{ type: 'text', text: content }] };
450
- }
@@ -1,182 +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
- // P6-001: Backfill Tool
7
- // One-time script to parse existing transcript JSONL files and
8
- // populate the memory DB with historical session data.
9
- // ============================================================
10
-
11
- import { readdirSync, statSync, existsSync } from 'fs';
12
- import { resolve, basename } from 'path';
13
- import { getMemoryDb, createSession, addObservation, addSummary, addUserPrompt, deduplicateFailedAttempt } from './memory-db.ts';
14
- import { parseTranscript, extractUserMessages, getLastAssistantMessage } from './transcript-parser.ts';
15
- import { extractObservationsFromEntries } from './observation-extractor.ts';
16
- import { getProjectRoot, getConfig } from './config.ts';
17
- import { encodeMemoryDirName } from './lib/memory-path.ts';
18
-
19
- /**
20
- * Auto-detect the Claude Code project transcript directory.
21
- * Claude Code stores transcripts at ~/.claude/projects/-<escaped-path>/
22
- */
23
- function findTranscriptDir(): string {
24
- const home = process.env.HOME ?? '~';
25
- const projectRoot = getProjectRoot();
26
- const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
27
- // Claude Code escapes the path by replacing / with -. Shared helper is SoT.
28
- const escapedPath = encodeMemoryDirName(projectRoot);
29
- const candidate = resolve(home, `${claudeDirName}/projects`, escapedPath);
30
- if (existsSync(candidate)) return candidate;
31
- // Fallback: scan projects dir for directories matching the project name
32
- const projectsDir = resolve(home, `${claudeDirName}/projects`);
33
- if (existsSync(projectsDir)) {
34
- try {
35
- const entries = readdirSync(projectsDir);
36
- const projectName = basename(projectRoot);
37
- const match = entries.find(e => e.includes(projectName));
38
- if (match) return resolve(projectsDir, match);
39
- } catch {
40
- // Ignore
41
- }
42
- }
43
- return candidate;
44
- }
45
-
46
- const MAX_SESSIONS = 20;
47
-
48
- async function main(): Promise<void> {
49
- console.log('=== Massu Memory Backfill ===');
50
- const TRANSCRIPT_DIR = findTranscriptDir();
51
- console.log(`Transcript directory: ${TRANSCRIPT_DIR}`);
52
-
53
- // 1. List JSONL files
54
- let files: string[];
55
- try {
56
- files = readdirSync(TRANSCRIPT_DIR)
57
- .filter(f => f.endsWith('.jsonl'))
58
- .map(f => ({
59
- name: f,
60
- path: resolve(TRANSCRIPT_DIR, f),
61
- mtime: statSync(resolve(TRANSCRIPT_DIR, f)).mtimeMs,
62
- }))
63
- .sort((a, b) => b.mtime - a.mtime) // Most recent first
64
- .slice(0, MAX_SESSIONS)
65
- .map(f => f.path);
66
- } catch (error) {
67
- console.error(`Failed to read transcript directory: ${error instanceof Error ? error.message : String(error)}`);
68
- process.exit(1);
69
- return;
70
- }
71
-
72
- console.log(`Found ${files.length} transcript files (processing most recent ${MAX_SESSIONS})`);
73
-
74
- const db = getMemoryDb();
75
- let totalObservations = 0;
76
- let totalSessions = 0;
77
-
78
- try {
79
- for (const filePath of files) {
80
- const sessionId = filePath.split('/').pop()?.replace('.jsonl', '') ?? 'unknown';
81
- console.log(`\nProcessing: ${sessionId.slice(0, 8)}...`);
82
-
83
- try {
84
- // Parse transcript
85
- const entries = await parseTranscript(filePath);
86
- if (entries.length === 0) {
87
- console.log(' Skipped: empty transcript');
88
- continue;
89
- }
90
-
91
- // Extract session metadata
92
- const firstEntry = entries.find(e => e.sessionId);
93
- const gitBranch = firstEntry?.gitBranch;
94
- const startTimestamp = firstEntry?.timestamp;
95
-
96
- // Create session (INSERT OR IGNORE for idempotency)
97
- createSession(db, sessionId, { branch: gitBranch });
98
-
99
- // Update started_at if we have a timestamp
100
- if (startTimestamp) {
101
- db.prepare('UPDATE sessions SET started_at = ?, started_at_epoch = ? WHERE session_id = ? AND started_at_epoch = 0')
102
- .run(startTimestamp, Math.floor(new Date(startTimestamp).getTime() / 1000), sessionId);
103
- }
104
-
105
- // Extract user prompts
106
- const userMessages = extractUserMessages(entries);
107
- for (let i = 0; i < Math.min(userMessages.length, 50); i++) {
108
- try {
109
- addUserPrompt(db, sessionId, userMessages[i].text.slice(0, 5000), i + 1);
110
- } catch (_e) {
111
- // Skip duplicate prompts
112
- }
113
- }
114
-
115
- // Extract observations (with noise filtering applied)
116
- const observations = extractObservationsFromEntries(entries);
117
- let sessionObsCount = 0;
118
-
119
- for (const obs of observations) {
120
- try {
121
- if (obs.type === 'failed_attempt') {
122
- deduplicateFailedAttempt(db, sessionId, obs.title, obs.detail, obs.opts);
123
- } else {
124
- addObservation(db, sessionId, obs.type, obs.title, obs.detail, obs.opts);
125
- }
126
- sessionObsCount++;
127
- } catch (_e) {
128
- // Skip on error
129
- }
130
- }
131
-
132
- totalObservations += sessionObsCount;
133
- totalSessions++;
134
- console.log(` Extracted: ${sessionObsCount} observations, ${Math.min(userMessages.length, 50)} prompts`);
135
-
136
- // Generate summary from observations
137
- const completed = observations
138
- .filter(o => ['feature', 'bugfix', 'refactor'].includes(o.type))
139
- .map(o => `- ${o.title}`)
140
- .join('\n');
141
-
142
- const decisions = observations
143
- .filter(o => o.type === 'decision')
144
- .map(o => `- ${o.title}`)
145
- .join('\n');
146
-
147
- const failedAttempts = observations
148
- .filter(o => o.type === 'failed_attempt')
149
- .map(o => `- ${o.title}`)
150
- .join('\n');
151
-
152
- if (observations.length > 0) {
153
- try {
154
- addSummary(db, sessionId, {
155
- request: userMessages[0]?.text?.slice(0, 500),
156
- completed: completed || undefined,
157
- decisions: decisions || undefined,
158
- failedAttempts: failedAttempts || undefined,
159
- });
160
- } catch (_e) {
161
- // Skip summary errors
162
- }
163
- }
164
-
165
- // Mark as completed
166
- db.prepare("UPDATE sessions SET status = 'completed' WHERE session_id = ? AND status = 'active'")
167
- .run(sessionId);
168
-
169
- } catch (error) {
170
- console.log(` Error: ${error instanceof Error ? error.message : String(error)}`);
171
- }
172
- }
173
- } finally {
174
- db.close();
175
- }
176
-
177
- console.log('\n=== Backfill Complete ===');
178
- console.log(`Sessions processed: ${totalSessions}`);
179
- console.log(`Total observations: ${totalObservations}`);
180
- }
181
-
182
- main().catch(console.error);