@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,128 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- // ============================================================
5
- // Memory File Auto-Ingest
6
- // Shared module for parsing memory/*.md files and ingesting
7
- // their YAML frontmatter + content into the observations table.
8
- // Used by: post-tool-use.ts, memory-tools.ts, init.ts
9
- // ============================================================
10
-
11
- import type Database from 'better-sqlite3';
12
- import { readFileSync, existsSync, readdirSync } from 'fs';
13
- import { join } from 'path';
14
- import { parse as parseYaml } from 'yaml';
15
- import { addObservation } from './memory-db.ts';
16
-
17
- export type IngestResult = 'inserted' | 'updated' | 'skipped';
18
-
19
- /**
20
- * Parse a memory/*.md file's YAML frontmatter and ingest it into the
21
- * observations table. Deduplicates by title prefix `[memory-file] {name}`.
22
- *
23
- * @returns 'inserted' | 'updated' | 'skipped'
24
- */
25
- export function ingestMemoryFile(
26
- db: Database.Database,
27
- sessionId: string,
28
- filePath: string,
29
- ): IngestResult {
30
- if (!existsSync(filePath)) return 'skipped';
31
-
32
- const content = readFileSync(filePath, 'utf-8');
33
- const basename = (filePath.split('/').pop() ?? '').replace('.md', '');
34
-
35
- // Parse YAML frontmatter (between first --- and second ---)
36
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
37
-
38
- let name = basename;
39
- let description = '';
40
- let type = 'discovery';
41
- let confidence: number | undefined;
42
-
43
- if (frontmatterMatch) {
44
- try {
45
- // pattern-scanner-allow: yaml-parse — reason: parses YAML FRONTMATTER from markdown memory files (NOT massu.config.yaml). This is document metadata parsing, not application config access; getConfig() does not apply.
46
- const fm = parseYaml(frontmatterMatch[1]) as Record<string, unknown>;
47
- name = (fm.name as string) ?? basename;
48
- description = (fm.description as string) ?? '';
49
- type = (fm.type as string) ?? 'discovery';
50
- confidence = fm.confidence != null ? Number(fm.confidence) : undefined;
51
- } catch {
52
- // Use defaults if YAML parsing fails
53
- }
54
- }
55
-
56
- // Map memory types to observation types
57
- const obsType = mapMemoryTypeToObservationType(type);
58
-
59
- // Calculate importance from confidence (0.0-1.0 -> 1-5)
60
- const importance = confidence != null
61
- ? Math.max(1, Math.min(5, Math.round(confidence * 4 + 1)))
62
- : 4;
63
-
64
- // Extract body (after second ---)
65
- const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
66
- const body = bodyMatch ? bodyMatch[1].trim().slice(0, 500) : '';
67
-
68
- const title = `[memory-file] ${name}`;
69
- const detail = description ? `${description}\n\n${body}` : body;
70
-
71
- // Deduplicate: check if this exact title exists
72
- const existing = db.prepare(
73
- 'SELECT id FROM observations WHERE title = ? LIMIT 1'
74
- ).get(title) as { id: number } | undefined;
75
-
76
- if (existing) {
77
- db.prepare('UPDATE observations SET detail = ?, importance = ? WHERE id = ?')
78
- .run(detail, importance, existing.id);
79
- return 'updated';
80
- } else {
81
- addObservation(db, sessionId, obsType, title, detail, { importance });
82
- return 'inserted';
83
- }
84
- }
85
-
86
- /**
87
- * Bulk-ingest all memory/*.md files from a directory.
88
- * Skips MEMORY.md (the index file).
89
- *
90
- * @returns { inserted, updated, skipped, total }
91
- */
92
- export function backfillMemoryFiles(
93
- db: Database.Database,
94
- memoryDir: string,
95
- sessionId?: string,
96
- ): { inserted: number; updated: number; skipped: number; total: number } {
97
- const stats = { inserted: 0, updated: 0, skipped: 0, total: 0 };
98
-
99
- if (!existsSync(memoryDir)) return stats;
100
-
101
- const files = readdirSync(memoryDir).filter(
102
- f => f.endsWith('.md') && f !== 'MEMORY.md'
103
- );
104
- stats.total = files.length;
105
-
106
- const sid = sessionId ?? `backfill-${Date.now()}`;
107
-
108
- for (const file of files) {
109
- const result = ingestMemoryFile(db, sid, join(memoryDir, file));
110
- stats[result]++;
111
- }
112
-
113
- return stats;
114
- }
115
-
116
- function mapMemoryTypeToObservationType(memoryType: string): string {
117
- switch (memoryType) {
118
- case 'user':
119
- case 'feedback':
120
- return 'decision';
121
- case 'project':
122
- return 'feature';
123
- case 'reference':
124
- return 'discovery';
125
- default:
126
- return 'discovery';
127
- }
128
- }
@@ -1,424 +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 {
7
- searchObservations,
8
- getRecentObservations,
9
- getSessionSummaries,
10
- getSessionTimeline,
11
- getFailedAttempts,
12
- addObservation,
13
- assignImportance,
14
- createSession,
15
- } from './memory-db.ts';
16
- import { getConfig, getResolvedPaths } from './config.ts';
17
- import { backfillMemoryFiles } from './memory-file-ingest.ts';
18
-
19
- /** Prefix a base tool name with the configured tool prefix. */
20
- function p(baseName: string): string {
21
- return `${getConfig().toolPrefix}_${baseName}`;
22
- }
23
-
24
- // ============================================================
25
- // P4-001 through P4-006: MCP Memory Tools
26
- // ============================================================
27
-
28
- /**
29
- * Get all memory tool definitions.
30
- */
31
- export function getMemoryToolDefinitions(): ToolDefinition[] {
32
- return [
33
- // P4-001: memory_search
34
- {
35
- name: p('memory_search'),
36
- description: 'Search past session observations and decisions using full-text search. Returns compact index of matching observations.',
37
- inputSchema: {
38
- type: 'object',
39
- properties: {
40
- query: { type: 'string', description: 'Search text (FTS5 query syntax supported)' },
41
- type: { type: 'string', description: 'Filter by observation type (decision, bugfix, feature, failed_attempt, cr_violation, vr_check, etc.)' },
42
- cr_rule: { type: 'string', description: 'Filter by CR rule (e.g., CR-9)' },
43
- date_from: { type: 'string', description: 'Start date (ISO format)' },
44
- limit: { type: 'number', description: 'Max results (default: 20)' },
45
- },
46
- required: ['query'],
47
- },
48
- },
49
- // P4-002: memory_timeline
50
- {
51
- name: p('memory_timeline'),
52
- description: 'Retrieve episodic memory - chronological context around a specific event. Shows observations before and after the anchor point to reconstruct what happened and why.',
53
- inputSchema: {
54
- type: 'object',
55
- properties: {
56
- observation_id: { type: 'number', description: 'Anchor observation ID' },
57
- depth_before: { type: 'number', description: 'Items before (default: 5)' },
58
- depth_after: { type: 'number', description: 'Items after (default: 5)' },
59
- },
60
- required: ['observation_id'],
61
- },
62
- },
63
- // P4-003: memory_detail
64
- {
65
- name: p('memory_detail'),
66
- description: 'Get full observation details by IDs (batch). Includes evidence, files, plan items.',
67
- inputSchema: {
68
- type: 'object',
69
- properties: {
70
- ids: {
71
- type: 'array',
72
- items: { type: 'number' },
73
- description: 'Observation IDs to retrieve',
74
- },
75
- },
76
- required: ['ids'],
77
- },
78
- },
79
- // P4-004: memory_sessions
80
- {
81
- name: p('memory_sessions'),
82
- description: 'List recent sessions with summaries.',
83
- inputSchema: {
84
- type: 'object',
85
- properties: {
86
- limit: { type: 'number', description: 'Max sessions (default: 10)' },
87
- status: { type: 'string', description: 'Filter by status (active, completed, abandoned)' },
88
- },
89
- required: [],
90
- },
91
- },
92
- // P4-005: memory_failures
93
- {
94
- name: p('memory_failures'),
95
- description: 'Get all failed attempts (DON\'T RETRY warnings). Check before attempting a fix to see if it was already tried.',
96
- inputSchema: {
97
- type: 'object',
98
- properties: {
99
- query: { type: 'string', description: 'Filter by keyword' },
100
- limit: { type: 'number', description: 'Max results (default: 20)' },
101
- },
102
- required: [],
103
- },
104
- },
105
- // P4-007: memory_backfill
106
- {
107
- name: p('memory_backfill'),
108
- description: 'Scan all memory/*.md files and ingest into database. Run after massu init or to recover from DB loss. Parses YAML frontmatter and deduplicates by title.',
109
- inputSchema: {
110
- type: 'object',
111
- properties: {},
112
- required: [],
113
- },
114
- },
115
- // P4-006: memory_ingest
116
- {
117
- name: p('memory_ingest'),
118
- description: 'Manually record an observation that hooks cannot auto-detect. Use for significant decisions, discoveries, or failed attempts mid-session.',
119
- inputSchema: {
120
- type: 'object',
121
- properties: {
122
- type: {
123
- type: 'string',
124
- description: 'Observation type: decision, bugfix, feature, refactor, discovery, cr_violation, vr_check, pattern_compliance, failed_attempt, file_change, incident_near_miss',
125
- },
126
- title: { type: 'string', description: 'Short description' },
127
- detail: { type: 'string', description: 'Full context' },
128
- importance: { type: 'number', description: 'Override importance (1-5, default: auto-assigned)' },
129
- cr_rule: { type: 'string', description: 'Link to CR rule (e.g., CR-9)' },
130
- plan_item: { type: 'string', description: 'Link to plan item (e.g., P2-003)' },
131
- files: {
132
- type: 'array',
133
- items: { type: 'string' },
134
- description: 'Files involved',
135
- },
136
- },
137
- required: ['type', 'title'],
138
- },
139
- },
140
- ];
141
- }
142
-
143
- /**
144
- * Handle a memory tool call.
145
- */
146
- export function handleMemoryToolCall(
147
- name: string,
148
- args: Record<string, unknown>,
149
- memoryDb: Database.Database
150
- ): ToolResult {
151
- try {
152
- const prefix = getConfig().toolPrefix + '_';
153
- const baseName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
154
-
155
- switch (baseName) {
156
- case 'memory_search':
157
- return handleSearch(args, memoryDb);
158
- case 'memory_timeline':
159
- return handleTimeline(args, memoryDb);
160
- case 'memory_detail':
161
- return handleDetail(args, memoryDb);
162
- case 'memory_sessions':
163
- return handleSessions(args, memoryDb);
164
- case 'memory_failures':
165
- return handleFailures(args, memoryDb);
166
- case 'memory_ingest':
167
- return handleIngest(args, memoryDb);
168
- case 'memory_backfill':
169
- return handleBackfill(memoryDb);
170
- default:
171
- return text(`Unknown memory tool: ${name}`);
172
- }
173
- } catch (error) {
174
- return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}`);
175
- }
176
- }
177
-
178
- // ============================================================
179
- // Tool Handlers
180
- // ============================================================
181
-
182
- function handleSearch(args: Record<string, unknown>, db: Database.Database): ToolResult {
183
- const query = args.query as string;
184
- if (!query) return text('Error: query is required');
185
-
186
- const results = searchObservations(db, query, {
187
- type: args.type as string | undefined,
188
- crRule: args.cr_rule as string | undefined,
189
- dateFrom: args.date_from as string | undefined,
190
- limit: args.limit as number | undefined,
191
- });
192
-
193
- if (results.length === 0) {
194
- return text(`No observations found for "${query}".`);
195
- }
196
-
197
- const lines = [`## Search Results for "${query}" (${results.length} matches)`, ''];
198
- lines.push('| ID | Type | Title | Date | Importance |');
199
- lines.push('|----|------|-------|------|------------|');
200
-
201
- for (const r of results) {
202
- lines.push(`| ${r.id} | ${r.type} | ${r.title.slice(0, 80)} | ${r.created_at.split('T')[0]} | ${r.importance} |`);
203
- }
204
-
205
- lines.push('');
206
- lines.push(`Use ${p('memory_detail')} with IDs for full details.`);
207
- lines.push(`Use ${p('memory_timeline')} with an ID for chronological context.`);
208
-
209
- return text(lines.join('\n'));
210
- }
211
-
212
- function handleTimeline(args: Record<string, unknown>, db: Database.Database): ToolResult {
213
- const observationId = args.observation_id as number;
214
- if (!observationId) return text('Error: observation_id is required');
215
-
216
- const depthBefore = (args.depth_before as number) ?? 5;
217
- const depthAfter = (args.depth_after as number) ?? 5;
218
-
219
- // Get the anchor observation
220
- const anchor = db.prepare('SELECT * FROM observations WHERE id = ?').get(observationId) as Record<string, unknown> | undefined;
221
- if (!anchor) return text(`Observation ${observationId} not found.`);
222
-
223
- // Get observations before
224
- const before = db.prepare(
225
- 'SELECT id, type, title, created_at, importance FROM observations WHERE session_id = ? AND created_at_epoch < ? ORDER BY created_at_epoch DESC LIMIT ?'
226
- ).all(anchor.session_id, anchor.created_at_epoch, depthBefore) as Array<Record<string, unknown>>;
227
-
228
- // Get observations after
229
- const after = db.prepare(
230
- 'SELECT id, type, title, created_at, importance FROM observations WHERE session_id = ? AND created_at_epoch > ? ORDER BY created_at_epoch ASC LIMIT ?'
231
- ).all(anchor.session_id, anchor.created_at_epoch, depthAfter) as Array<Record<string, unknown>>;
232
-
233
- const lines = [`## Timeline around observation #${observationId}`, ''];
234
-
235
- // Before (reversed to chronological order)
236
- for (const o of before.reverse()) {
237
- lines.push(` ${o.id} | ${o.type} | ${(o.title as string).slice(0, 60)} | ${(o.created_at as string).split('T')[0]}`);
238
- }
239
-
240
- // Anchor
241
- lines.push(`> ${anchor.id} | ${anchor.type} | ${(anchor.title as string).slice(0, 60)} | ${(anchor.created_at as string).split('T')[0]} <-- ANCHOR`);
242
-
243
- // After
244
- for (const o of after) {
245
- lines.push(` ${o.id} | ${o.type} | ${(o.title as string).slice(0, 60)} | ${(o.created_at as string).split('T')[0]}`);
246
- }
247
-
248
- return text(lines.join('\n'));
249
- }
250
-
251
- function handleDetail(args: Record<string, unknown>, db: Database.Database): ToolResult {
252
- const ids = args.ids as number[];
253
- if (!ids || ids.length === 0) return text('Error: ids array is required');
254
-
255
- const placeholders = ids.map(() => '?').join(',');
256
- const observations = db.prepare(
257
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch ASC`
258
- ).all(...ids) as Array<Record<string, unknown>>;
259
-
260
- if (observations.length === 0) {
261
- return text('No observations found for the given IDs.');
262
- }
263
-
264
- const lines: string[] = [];
265
- for (const o of observations) {
266
- lines.push(`## Observation #${o.id} [${o.type}] (importance: ${o.importance})`);
267
- lines.push(`**Title**: ${o.title}`);
268
- lines.push(`**Session**: ${o.session_id}`);
269
- lines.push(`**Date**: ${o.created_at}`);
270
-
271
- if (o.detail) lines.push(`**Detail**: ${o.detail}`);
272
- if (o.files_involved && o.files_involved !== '[]') {
273
- const files = safeParseJson(o.files_involved as string, []) as string[];
274
- if (files.length > 0) lines.push(`**Files**: ${files.join(', ')}`);
275
- }
276
- if (o.plan_item) lines.push(`**Plan Item**: ${o.plan_item}`);
277
- if (o.cr_rule) lines.push(`**CR Rule**: ${o.cr_rule}`);
278
- if (o.vr_type) lines.push(`**VR Type**: ${o.vr_type}`);
279
- if (o.evidence) lines.push(`**Evidence**: ${(o.evidence as string).slice(0, 500)}`);
280
- if ((o.recurrence_count as number) > 1) lines.push(`**Recurrence**: ${o.recurrence_count}x`);
281
- lines.push('');
282
- }
283
-
284
- return text(lines.join('\n'));
285
- }
286
-
287
- function handleSessions(args: Record<string, unknown>, db: Database.Database): ToolResult {
288
- const limit = (args.limit as number) ?? 10;
289
- const status = args.status as string | undefined;
290
-
291
- let sql = 'SELECT * FROM sessions';
292
- const params: (string | number)[] = [];
293
-
294
- if (status) {
295
- sql += ' WHERE status = ?';
296
- params.push(status);
297
- }
298
-
299
- sql += ' ORDER BY started_at_epoch DESC LIMIT ?';
300
- params.push(limit);
301
-
302
- const sessions = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
303
-
304
- if (sessions.length === 0) {
305
- return text('No sessions found.');
306
- }
307
-
308
- const lines = ['## Recent Sessions', ''];
309
- lines.push('| Session ID | Status | Branch | Started | Plan |');
310
- lines.push('|------------|--------|--------|---------|------|');
311
-
312
- for (const s of sessions) {
313
- const started = (s.started_at as string).split('T')[0];
314
- const plan = s.plan_file ? (s.plan_file as string).split('/').pop() : '-';
315
- lines.push(`| ${(s.session_id as string).slice(0, 8)}... | ${s.status} | ${s.git_branch ?? '-'} | ${started} | ${plan} |`);
316
- }
317
-
318
- // Add summaries for each session
319
- for (const s of sessions) {
320
- const summaries = getSessionSummaries(db, 1);
321
- const summary = summaries.find(sm => sm.session_id === s.session_id);
322
- if (summary) {
323
- lines.push('');
324
- lines.push(`### ${(s.session_id as string).slice(0, 8)}...`);
325
- if (summary.request) lines.push(`**Task**: ${summary.request.slice(0, 200)}`);
326
- if (summary.completed) lines.push(`**Completed**: ${summary.completed.slice(0, 200)}`);
327
- }
328
- }
329
-
330
- return text(lines.join('\n'));
331
- }
332
-
333
- function handleFailures(args: Record<string, unknown>, db: Database.Database): ToolResult {
334
- const query = args.query as string | undefined;
335
- const limit = (args.limit as number) ?? 20;
336
-
337
- const failures = getFailedAttempts(db, query, limit);
338
-
339
- if (failures.length === 0) {
340
- return text(query ? `No failed attempts found for "${query}".` : 'No failed attempts recorded.');
341
- }
342
-
343
- const lines = ['## Failed Attempts (DO NOT RETRY)', ''];
344
-
345
- for (const f of failures) {
346
- const recurrence = f.recurrence_count > 1 ? ` (occurred ${f.recurrence_count}x across sessions)` : '';
347
- lines.push(`### #${f.id}: ${f.title}${recurrence}`);
348
- if (f.detail) lines.push(f.detail.slice(0, 500));
349
- lines.push(`Session: ${f.session_id.slice(0, 8)}... | Date: ${f.created_at.split('T')[0]}`);
350
- lines.push('');
351
- }
352
-
353
- return text(lines.join('\n'));
354
- }
355
-
356
- function handleIngest(args: Record<string, unknown>, db: Database.Database): ToolResult {
357
- const type = args.type as string;
358
- const title = args.title as string;
359
-
360
- if (!type || !title) return text('Error: type and title are required');
361
-
362
- const validTypes = ['decision', 'bugfix', 'feature', 'refactor', 'discovery',
363
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
364
- 'file_change', 'incident_near_miss'];
365
-
366
- if (!validTypes.includes(type)) {
367
- return text(`Error: invalid type "${type}". Valid types: ${validTypes.join(', ')}`);
368
- }
369
-
370
- // We need a session_id - get the most recent active session
371
- const activeSession = db.prepare(
372
- "SELECT session_id FROM sessions WHERE status = 'active' ORDER BY started_at_epoch DESC LIMIT 1"
373
- ).get() as { session_id: string } | undefined;
374
-
375
- if (!activeSession) {
376
- return text('Error: no active session found. Start a session first.');
377
- }
378
-
379
- const importance = (args.importance as number) ?? assignImportance(type);
380
- const id = addObservation(db, activeSession.session_id, type, title, (args.detail as string) ?? null, {
381
- importance,
382
- crRule: args.cr_rule as string | undefined,
383
- planItem: args.plan_item as string | undefined,
384
- filesInvolved: args.files as string[] | undefined,
385
- });
386
-
387
- return text(`Observation #${id} recorded successfully.\nType: ${type}\nTitle: ${title}\nImportance: ${importance}\nSession: ${activeSession.session_id.slice(0, 8)}...`);
388
- }
389
-
390
- function handleBackfill(db: Database.Database): ToolResult {
391
- const memoryDir = getResolvedPaths().memoryDir;
392
- const stats = backfillMemoryFiles(db, memoryDir);
393
-
394
- const lines = [
395
- '## Memory Backfill Results',
396
- '',
397
- `- **Total files scanned**: ${stats.total}`,
398
- `- **Inserted (new)**: ${stats.inserted}`,
399
- `- **Updated (existing)**: ${stats.updated}`,
400
- `- **Skipped (not found)**: ${stats.skipped}`,
401
- '',
402
- stats.total === 0
403
- ? 'No memory files found in memory directory.'
404
- : `Successfully processed ${stats.inserted + stats.updated} of ${stats.total} memory files.`,
405
- ];
406
-
407
- return text(lines.join('\n'));
408
- }
409
-
410
- // ============================================================
411
- // Helpers
412
- // ============================================================
413
-
414
- function text(content: string): ToolResult {
415
- return { content: [{ type: 'text', text: content }] };
416
- }
417
-
418
- function safeParseJson(json: string, fallback: unknown): unknown {
419
- try {
420
- return JSON.parse(json);
421
- } catch (_e) {
422
- return fallback;
423
- }
424
- }
@@ -1,71 +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 { getConfig } from './config.ts';
6
- import { existsSync } from 'fs';
7
- import { resolve } from 'path';
8
- import { t } from './lib/sql-table-names.ts';
9
-
10
- /**
11
- * Build the middleware import tree by tracing all transitive imports
12
- * from the middleware entry point. Any file in this tree is subject to
13
- * Edge Runtime restrictions (no Node.js deps).
14
- */
15
- export function buildMiddlewareTree(dataDb: Database.Database): number {
16
- // Clear existing data
17
- dataDb.exec(`DELETE FROM ${t('middleware_tree')}`);
18
-
19
- const config = getConfig();
20
- const middlewareFile = config.paths.middleware ?? 'src/middleware.ts';
21
-
22
- // Only build if the middleware file path is configured and exists conceptually
23
- if (!middlewareFile) return 0;
24
-
25
- // BFS through import edges starting from middleware.ts
26
- const visited = new Set<string>();
27
- const queue: string[] = [middlewareFile];
28
- visited.add(middlewareFile);
29
-
30
- while (queue.length > 0) {
31
- const current = queue.shift()!;
32
-
33
- const imports = dataDb.prepare(
34
- `SELECT target_file FROM ${t('imports')} WHERE source_file = ?`
35
- ).all(current) as { target_file: string }[];
36
-
37
- for (const imp of imports) {
38
- if (!visited.has(imp.target_file) && imp.target_file.startsWith('src/')) {
39
- visited.add(imp.target_file);
40
- queue.push(imp.target_file);
41
- }
42
- }
43
- }
44
-
45
- // Store the tree
46
- const insertStmt = dataDb.prepare(`INSERT INTO ${t('middleware_tree')} (file) VALUES (?)`);
47
- const insertAll = dataDb.transaction(() => {
48
- for (const file of visited) {
49
- insertStmt.run(file);
50
- }
51
- });
52
- insertAll();
53
-
54
- return visited.size;
55
- }
56
-
57
- /**
58
- * Check if a file is in the middleware import tree.
59
- */
60
- export function isInMiddlewareTree(dataDb: Database.Database, file: string): boolean {
61
- const result = dataDb.prepare(`SELECT 1 FROM ${t('middleware_tree')} WHERE file = ?`).get(file);
62
- return result !== undefined;
63
- }
64
-
65
- /**
66
- * Get all files in the middleware import tree.
67
- */
68
- export function getMiddlewareTree(dataDb: Database.Database): string[] {
69
- const rows = dataDb.prepare(`SELECT file FROM ${t('middleware_tree')} ORDER BY file`).all() as { file: string }[];
70
- return rows.map(r => r.file);
71
- }