@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,1414 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import Database from 'better-sqlite3';
5
- import type { ToolDefinition, ToolResult } from './tools.ts';
6
- import { indexIfStale } from './knowledge-indexer.ts';
7
- import { readFileSync, writeFileSync, appendFileSync, existsSync, readdirSync } from 'fs';
8
- import { resolve, basename } from 'path';
9
- import { getConfig, getResolvedPaths } from './config.ts';
10
- import { getDataDb } from './db.ts';
11
- import { getMemoryDb, sanitizeFts5Query } from './memory-db.ts';
12
- import { t } from './lib/sql-table-names.ts';
13
-
14
- // ============================================================
15
- // Massu Knowledge: MCP Tool Definitions & Handlers
16
- // ============================================================
17
-
18
- /** Prefix a base tool name with the configured tool prefix. */
19
- function p(baseName: string): string {
20
- return `${getConfig().toolPrefix}_${baseName}`;
21
- }
22
-
23
- /** Strip the configured prefix from a tool name to get the base name. */
24
- function stripPrefix(name: string): string {
25
- const pfx = getConfig().toolPrefix + '_';
26
- if (name.startsWith(pfx)) {
27
- return name.slice(pfx.length);
28
- }
29
- return name;
30
- }
31
-
32
- function text(content: string): ToolResult {
33
- return { content: [{ type: 'text', text: content }] };
34
- }
35
-
36
- /**
37
- * Ensure knowledge DB is indexed before queries (P4-002: lazy re-index).
38
- * Non-fatal: returns false if indexing fails, tools should degrade gracefully.
39
- */
40
- function ensureKnowledgeIndexed(db: Database.Database): boolean {
41
- try {
42
- indexIfStale(db);
43
- return true;
44
- } catch {
45
- return false;
46
- }
47
- }
48
-
49
- function hasData(db: Database.Database): boolean {
50
- const row = db.prepare('SELECT COUNT(*) as cnt FROM knowledge_documents').get() as { cnt: number } | undefined;
51
- return (row?.cnt ?? 0) > 0;
52
- }
53
-
54
- // ============================================================
55
- // Tool Definitions (12 tools)
56
- // ============================================================
57
-
58
- export function getKnowledgeToolDefinitions(): ToolDefinition[] {
59
- const claudeDir = getConfig().conventions?.claudeDirName ?? '.claude';
60
- return [
61
- // P2-001: knowledge_search
62
- {
63
- name: p('knowledge_search'),
64
- description: `Full-text + structured search across all ${claudeDir}/ knowledge (rules, patterns, incidents, commands, protocols). Returns matched chunks with file path, heading, and content preview.`,
65
- inputSchema: {
66
- type: 'object',
67
- properties: {
68
- query: { type: 'string', description: 'FTS5 search term (e.g., "BigInt serialization", "schema mismatch")' },
69
- category: { type: 'string', description: 'Filter by category: patterns, commands, incidents, reference, protocols, memory, checklists, playbooks, critical, root' },
70
- chunk_type: { type: 'string', description: 'Filter by chunk type: section, rule, incident, pattern, command, mismatch, code_block, table_row' },
71
- limit: { type: 'number', description: 'Max results (default 10)' },
72
- },
73
- required: ['query'],
74
- },
75
- },
76
- // P2-002: knowledge_rule
77
- {
78
- name: p('knowledge_rule'),
79
- description: 'Look up a specific CR rule, its VR verification, linked incidents, and prevention. Returns rule text, reference file, and connected entities.',
80
- inputSchema: {
81
- type: 'object',
82
- properties: {
83
- rule_id: { type: 'string', description: 'Exact CR rule ID (e.g., "CR-6")' },
84
- keyword: { type: 'string', description: 'Search rule text (e.g., "database schema")' },
85
- with_incidents: { type: 'boolean', description: 'Include linked incidents (default true)' },
86
- },
87
- required: [],
88
- },
89
- },
90
- // P2-003: knowledge_incident
91
- {
92
- name: p('knowledge_incident'),
93
- description: 'Look up a specific incident or search incidents by type/keyword. Returns full incident detail with date, type, gap, prevention, CR added, root cause.',
94
- inputSchema: {
95
- type: 'object',
96
- properties: {
97
- incident_num: { type: 'number', description: 'Exact incident number' },
98
- keyword: { type: 'string', description: 'Search gap_found + prevention + root_cause text' },
99
- type: { type: 'string', description: 'Filter by type (e.g., "Schema Migration", "Plan Verification")' },
100
- },
101
- required: [],
102
- },
103
- },
104
- // P2-004: knowledge_schema_check
105
- {
106
- name: p('knowledge_schema_check'),
107
- description: `Check if a table/column name has a known mismatch documented in ${claudeDir}/. Instant warning for common pitfalls.`,
108
- inputSchema: {
109
- type: 'object',
110
- properties: {
111
- table: { type: 'string', description: 'Table name to check' },
112
- column: { type: 'string', description: 'Column name to check if known-wrong anywhere' },
113
- },
114
- required: [],
115
- },
116
- },
117
- // P2-005: knowledge_pattern
118
- {
119
- name: p('knowledge_pattern'),
120
- description: 'Get relevant pattern guidance for a domain/topic without loading entire pattern files. Returns relevant sections with code examples and anti-patterns.',
121
- inputSchema: {
122
- type: 'object',
123
- properties: {
124
- domain: { type: 'string', description: 'Domain: database, auth, ui, build, security, realtime, form, etc.' },
125
- topic: { type: 'string', description: 'Narrow to specific topic (e.g., "BigInt", "RLS", "Select.Item", "ctx.db")' },
126
- },
127
- required: ['domain'],
128
- },
129
- },
130
- // P2-006: knowledge_verification
131
- {
132
- name: p('knowledge_verification'),
133
- description: 'Look up which VR-* check to run for a given situation. Returns VR type, command, expected output, when to use.',
134
- inputSchema: {
135
- type: 'object',
136
- properties: {
137
- vr_type: { type: 'string', description: 'Exact VR type (e.g., "VR-BUILD", "VR-SCHEMA-PRE")' },
138
- situation: { type: 'string', description: 'Describe the situation (e.g., "claiming production ready", "schema change")' },
139
- },
140
- required: [],
141
- },
142
- },
143
- // P2-007: knowledge_graph
144
- {
145
- name: p('knowledge_graph'),
146
- description: 'Traverse the knowledge cross-reference graph. Find everything connected to a given entity (CR, VR, incident, pattern, command) at configurable depth.',
147
- inputSchema: {
148
- type: 'object',
149
- properties: {
150
- entity_type: { type: 'string', description: 'Type: cr, vr, incident, pattern, command, chunk' },
151
- entity_id: { type: 'string', description: 'ID: "CR-6", "VR-SCHEMA", "3", "database-patterns"' },
152
- depth: { type: 'number', description: 'Traversal depth (default 1, max 3)' },
153
- },
154
- required: ['entity_type', 'entity_id'],
155
- },
156
- },
157
- // P2-008: knowledge_command
158
- {
159
- name: p('knowledge_command'),
160
- description: 'Get summary of a slash command\'s purpose, workflow position, and key rules. Search commands by name or keyword.',
161
- inputSchema: {
162
- type: 'object',
163
- properties: {
164
- command: { type: 'string', description: 'Command name (e.g., "massu-loop", "massu-plan")' },
165
- keyword: { type: 'string', description: 'Search command descriptions (e.g., "audit", "implement")' },
166
- },
167
- required: [],
168
- },
169
- },
170
- // P2-009: knowledge_correct
171
- {
172
- name: p('knowledge_correct'),
173
- description: 'Record a user correction for persistent behavioral learning. Appends structured entry to corrections.md and indexes into knowledge DB.',
174
- inputSchema: {
175
- type: 'object',
176
- properties: {
177
- wrong: { type: 'string', description: 'What the model did incorrectly' },
178
- correction: { type: 'string', description: 'The correct behavior (user feedback)' },
179
- rule: { type: 'string', description: 'Prevention rule in imperative form' },
180
- cr_rule: { type: 'string', description: 'Linked canonical rule ID (e.g., CR-28). Optional.' },
181
- },
182
- required: ['wrong', 'correction', 'rule'],
183
- },
184
- },
185
- // P2-010: knowledge_plan
186
- {
187
- name: p('knowledge_plan'),
188
- description: 'Find plans related to a file, feature, or keyword. Answers "which plan created/modified this file?" and "which files does plan X touch?"',
189
- inputSchema: {
190
- type: 'object',
191
- properties: {
192
- file: { type: 'string', description: 'File path to find related plans for (e.g., "src/server/api/routers/orders.ts")' },
193
- keyword: { type: 'string', description: 'Search plan titles and content by keyword' },
194
- plan_name: { type: 'string', description: 'Plan filename or slug to get details for' },
195
- status: { type: 'string', description: 'Filter by implementation status: COMPLETE, IN_PROGRESS, NOT_STARTED' },
196
- },
197
- required: [],
198
- },
199
- },
200
- // P2-011: knowledge_gaps
201
- {
202
- name: p('knowledge_gaps'),
203
- description: `Detect documentation blind spots: features, routers, or domains with no corresponding ${claudeDir}/ documentation coverage. Cross-references sentinel features against knowledge documents.`,
204
- inputSchema: {
205
- type: 'object',
206
- properties: {
207
- domain: { type: 'string', description: 'Check gaps for specific domain' },
208
- check_type: { type: 'string', enum: ['features', 'routers', 'patterns', 'incidents'], description: 'Type of gap check. Default: features' },
209
- },
210
- required: [],
211
- },
212
- },
213
- // P2-012: knowledge_effectiveness
214
- {
215
- name: p('knowledge_effectiveness'),
216
- description: 'Track which CR rules and VR checks are most frequently violated or triggered. Cross-references rules against memory observations to rank by effectiveness.',
217
- inputSchema: {
218
- type: 'object',
219
- properties: {
220
- rule_id: { type: 'string', description: 'Check effectiveness of a specific rule (e.g., CR-28)' },
221
- top_n: { type: 'number', description: 'Show top N most/least violated rules. Default: 10' },
222
- mode: { type: 'string', enum: ['most_violated', 'least_violated', 'most_effective', 'detail'], description: 'Ranking mode. Default: most_violated' },
223
- },
224
- required: [],
225
- },
226
- },
227
- ];
228
- }
229
-
230
- // ============================================================
231
- // Tool Name Matching (3-function pattern)
232
- // ============================================================
233
-
234
- const KNOWLEDGE_BASE_NAMES = new Set([
235
- 'knowledge_search',
236
- 'knowledge_rule',
237
- 'knowledge_incident',
238
- 'knowledge_schema_check',
239
- 'knowledge_pattern',
240
- 'knowledge_verification',
241
- 'knowledge_graph',
242
- 'knowledge_command',
243
- 'knowledge_correct',
244
- 'knowledge_plan',
245
- 'knowledge_gaps',
246
- 'knowledge_effectiveness',
247
- ]);
248
-
249
- export function isKnowledgeTool(name: string): boolean {
250
- const pfx = getConfig().toolPrefix + '_';
251
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
252
- return KNOWLEDGE_BASE_NAMES.has(baseName);
253
- }
254
-
255
- // ============================================================
256
- // Tool Handler Router
257
- // ============================================================
258
-
259
- export function handleKnowledgeToolCall(
260
- name: string,
261
- args: Record<string, unknown>,
262
- db: Database.Database
263
- ): ToolResult {
264
- // P4-002: Lazy re-index before any query
265
- ensureKnowledgeIndexed(db);
266
-
267
- if (!hasData(db)) {
268
- return text('No knowledge indexed yet. The knowledge DB will be populated automatically on next MCP server restart.');
269
- }
270
-
271
- const baseName = stripPrefix(name);
272
-
273
- switch (baseName) {
274
- case 'knowledge_search':
275
- return handleSearch(db, args);
276
- case 'knowledge_rule':
277
- return handleRule(db, args);
278
- case 'knowledge_incident':
279
- return handleIncident(db, args);
280
- case 'knowledge_schema_check':
281
- return handleSchemaCheck(db, args);
282
- case 'knowledge_pattern':
283
- return handlePattern(db, args);
284
- case 'knowledge_verification':
285
- return handleVerification(db, args);
286
- case 'knowledge_graph':
287
- return handleGraph(db, args);
288
- case 'knowledge_command':
289
- return handleCommand(db, args);
290
- case 'knowledge_correct':
291
- return handleCorrect(db, args);
292
- case 'knowledge_plan':
293
- return handlePlan(db, args);
294
- case 'knowledge_gaps':
295
- return handleGaps(db, args);
296
- case 'knowledge_effectiveness':
297
- return handleEffectiveness(db, args);
298
- default:
299
- return text(`Unknown knowledge tool: ${name}`);
300
- }
301
- }
302
-
303
- // ============================================================
304
- // P2-001: knowledge_search
305
- // ============================================================
306
-
307
- function handleSearch(db: Database.Database, args: Record<string, unknown>): ToolResult {
308
- const claudeDir = getConfig().conventions?.claudeDirName ?? '.claude';
309
- const query = args.query as string;
310
- const category = args.category as string | undefined;
311
- const chunkType = args.chunk_type as string | undefined;
312
- const limit = Math.min((args.limit as number) || 10, 50);
313
-
314
- if (!query) return text('Error: query is required');
315
-
316
- const lines: string[] = [];
317
- lines.push(`## Knowledge Search: "${query}"`);
318
- lines.push('');
319
-
320
- try {
321
- // Build FTS5 query with optional filters
322
- let sql = `
323
- SELECT kc.id, kc.heading, kc.content, kc.chunk_type, kc.metadata,
324
- kd.file_path, kd.category, kd.title,
325
- rank
326
- FROM knowledge_fts
327
- JOIN knowledge_chunks kc ON kc.id = knowledge_fts.rowid
328
- JOIN knowledge_documents kd ON kd.id = kc.document_id
329
- WHERE knowledge_fts MATCH ?
330
- `;
331
- const params: (string | number)[] = [sanitizeFts5Query(query)];
332
-
333
- if (category) {
334
- sql += ' AND kd.category = ?';
335
- params.push(category);
336
- }
337
- if (chunkType) {
338
- sql += ' AND kc.chunk_type = ?';
339
- params.push(chunkType);
340
- }
341
-
342
- sql += ' ORDER BY rank LIMIT ?';
343
- params.push(limit);
344
-
345
- const results = db.prepare(sql).all(...params) as {
346
- id: number; heading: string; content: string; chunk_type: string;
347
- metadata: string; file_path: string; category: string; title: string; rank: number;
348
- }[];
349
-
350
- if (results.length === 0) {
351
- lines.push('No matches found.');
352
- lines.push('');
353
- lines.push('Tips: Try broader terms, check spelling, or remove filters.');
354
- } else {
355
- lines.push(`Found ${results.length} result(s):`);
356
- lines.push('');
357
-
358
- for (const r of results) {
359
- const preview = r.content.length > 500 ? r.content.substring(0, 500) + '...' : r.content;
360
- lines.push(`### ${r.heading || r.title} [${r.chunk_type}]`);
361
- lines.push(`**File**: ${claudeDir}/${r.file_path} | **Category**: ${r.category}`);
362
- lines.push('');
363
- lines.push(preview);
364
- lines.push('');
365
- lines.push('---');
366
- lines.push('');
367
- }
368
- }
369
- } catch (error) {
370
- lines.push(`Search error: ${error instanceof Error ? error.message : String(error)}`);
371
- lines.push('');
372
- lines.push('Tip: FTS5 uses match syntax. For exact phrase, wrap in double quotes: "BigInt serialization"');
373
- }
374
-
375
- return text(lines.join('\n'));
376
- }
377
-
378
- // ============================================================
379
- // P2-002: knowledge_rule
380
- // ============================================================
381
-
382
- function handleRule(db: Database.Database, args: Record<string, unknown>): ToolResult {
383
- const ruleId = args.rule_id as string | undefined;
384
- const keyword = args.keyword as string | undefined;
385
- const withIncidents = args.with_incidents !== false; // default true
386
-
387
- const lines: string[] = [];
388
-
389
- if (ruleId) {
390
- // Exact lookup
391
- const rule = db.prepare('SELECT * FROM knowledge_rules WHERE rule_id = ?').get(ruleId) as {
392
- rule_id: string; rule_text: string; vr_type: string; reference_path: string;
393
- severity: string; prevention_summary: string;
394
- } | undefined;
395
-
396
- if (!rule) {
397
- return text(`Rule ${ruleId} not found in knowledge base. Try ${p('knowledge_search')} with query: "${ruleId}" or ${p('knowledge_rule')} with keyword to search rule text.`);
398
- }
399
-
400
- lines.push(`## ${rule.rule_id}: ${rule.rule_text}`);
401
- lines.push('');
402
- lines.push(`| Field | Value |`);
403
- lines.push(`|-------|-------|`);
404
- lines.push(`| **Verification** | ${rule.vr_type || 'N/A'} |`);
405
- lines.push(`| **Reference** | ${rule.reference_path || 'N/A'} |`);
406
- lines.push(`| **Severity** | ${rule.severity || 'HIGH'} |`);
407
- if (rule.prevention_summary) {
408
- lines.push(`| **Prevention** | ${rule.prevention_summary} |`);
409
- }
410
- lines.push('');
411
-
412
- // Get linked VR details
413
- if (rule.vr_type) {
414
- const vrTypes = rule.vr_type.split(/[,\s]+/).filter(v => v.startsWith('VR-'));
415
- for (const vrType of vrTypes) {
416
- const vr = db.prepare('SELECT * FROM knowledge_verifications WHERE vr_type = ?').get(vrType) as {
417
- vr_type: string; command: string; expected: string; use_when: string;
418
- } | undefined;
419
- if (vr) {
420
- lines.push(`### Verification: ${vr.vr_type}`);
421
- lines.push(`- **Command**: \`${vr.command}\``);
422
- lines.push(`- **Expected**: ${vr.expected}`);
423
- lines.push(`- **Use When**: ${vr.use_when}`);
424
- lines.push('');
425
- }
426
- }
427
- }
428
-
429
- // Get linked incidents
430
- if (withIncidents) {
431
- const edges = db.prepare(
432
- "SELECT source_id FROM knowledge_edges WHERE target_type = 'cr' AND target_id = ? AND source_type = 'incident'"
433
- ).all(ruleId) as { source_id: string }[];
434
-
435
- if (edges.length > 0) {
436
- lines.push('### Related Incidents');
437
- for (const edge of edges) {
438
- const incident = db.prepare('SELECT * FROM knowledge_incidents WHERE incident_num = ?').get(parseInt(edge.source_id, 10)) as {
439
- incident_num: number; date: string; type: string; gap_found: string; prevention: string;
440
- } | undefined;
441
- if (incident) {
442
- lines.push(`- **Incident #${incident.incident_num}** (${incident.date}): ${incident.type}`);
443
- lines.push(` Gap: ${incident.gap_found}`);
444
- lines.push(` Prevention: ${incident.prevention}`);
445
- }
446
- }
447
- lines.push('');
448
- }
449
- }
450
- } else if (keyword) {
451
- // Search by keyword
452
- const rules = db.prepare(
453
- 'SELECT * FROM knowledge_rules WHERE rule_text LIKE ? OR rule_id LIKE ? ORDER BY rule_id'
454
- ).all(`%${keyword}%`, `%${keyword}%`) as {
455
- rule_id: string; rule_text: string; vr_type: string; reference_path: string;
456
- }[];
457
-
458
- lines.push(`## Rules matching "${keyword}" (${rules.length} found)`);
459
- lines.push('');
460
-
461
- for (const rule of rules) {
462
- lines.push(`- **${rule.rule_id}**: ${rule.rule_text} | VR: ${rule.vr_type || 'N/A'} | Ref: ${rule.reference_path || 'N/A'}`);
463
- }
464
- } else {
465
- // List all rules
466
- const rules = db.prepare('SELECT * FROM knowledge_rules ORDER BY rule_id LIMIT 10000').all() as {
467
- rule_id: string; rule_text: string; vr_type: string;
468
- }[];
469
-
470
- lines.push(`## All Canonical Rules (${rules.length} total)`);
471
- lines.push('');
472
-
473
- for (const rule of rules) {
474
- lines.push(`- **${rule.rule_id}**: ${rule.rule_text} (${rule.vr_type || 'N/A'})`);
475
- }
476
- }
477
-
478
- return text(lines.join('\n'));
479
- }
480
-
481
- // ============================================================
482
- // P2-003: knowledge_incident
483
- // ============================================================
484
-
485
- function handleIncident(db: Database.Database, args: Record<string, unknown>): ToolResult {
486
- const incidentNum = args.incident_num as number | undefined;
487
- const keyword = args.keyword as string | undefined;
488
- const type = args.type as string | undefined;
489
-
490
- const lines: string[] = [];
491
-
492
- if (incidentNum) {
493
- // Exact lookup
494
- const incident = db.prepare('SELECT * FROM knowledge_incidents WHERE incident_num = ?').get(incidentNum) as {
495
- incident_num: number; date: string; type: string; gap_found: string;
496
- prevention: string; cr_added: string; root_cause: string; user_quote: string;
497
- } | undefined;
498
-
499
- if (!incident) {
500
- return text(`Incident #${incidentNum} not found in knowledge base. Try ${p('knowledge_incident')} with keyword to search incident descriptions.`);
501
- }
502
-
503
- lines.push(`## Incident #${incident.incident_num}`);
504
- lines.push('');
505
- lines.push(`| Field | Value |`);
506
- lines.push(`|-------|-------|`);
507
- lines.push(`| **Date** | ${incident.date} |`);
508
- lines.push(`| **Type** | ${incident.type} |`);
509
- lines.push(`| **Gap Found** | ${incident.gap_found} |`);
510
- lines.push(`| **Prevention** | ${incident.prevention} |`);
511
- if (incident.cr_added) lines.push(`| **CR Added** | ${incident.cr_added} |`);
512
- if (incident.root_cause) lines.push(`| **Root Cause** | ${incident.root_cause} |`);
513
- if (incident.user_quote) lines.push(`| **User Quote** | ${incident.user_quote.replace(/\|/g, '\\|')} |`);
514
- lines.push('');
515
-
516
- // Show linked CRs
517
- const edges = db.prepare(
518
- "SELECT target_id FROM knowledge_edges WHERE source_type = 'incident' AND source_id = ? AND target_type = 'cr'"
519
- ).all(String(incidentNum)) as { target_id: string }[];
520
-
521
- if (edges.length > 0) {
522
- lines.push('### Linked Rules');
523
- for (const edge of edges) {
524
- const rule = db.prepare('SELECT rule_id, rule_text FROM knowledge_rules WHERE rule_id = ?').get(edge.target_id) as { rule_id: string; rule_text: string } | undefined;
525
- if (rule) {
526
- lines.push(`- **${rule.rule_id}**: ${rule.rule_text}`);
527
- }
528
- }
529
- lines.push('');
530
- }
531
- } else {
532
- // Search or filter
533
- let sql = 'SELECT * FROM knowledge_incidents WHERE 1=1';
534
- const params: string[] = [];
535
-
536
- if (keyword) {
537
- sql += ' AND (gap_found LIKE ? OR prevention LIKE ? OR root_cause LIKE ? OR type LIKE ?)';
538
- const kw = `%${keyword}%`;
539
- params.push(kw, kw, kw, kw);
540
- }
541
- if (type) {
542
- sql += ' AND type LIKE ?';
543
- params.push(`%${type}%`);
544
- }
545
-
546
- sql += ' ORDER BY incident_num';
547
-
548
- const incidents = db.prepare(sql).all(...params) as {
549
- incident_num: number; date: string; type: string; gap_found: string;
550
- prevention: string; cr_added: string;
551
- }[];
552
-
553
- const desc = keyword ? `matching "${keyword}"` : type ? `type "${type}"` : 'all';
554
- lines.push(`## Incidents ${desc} (${incidents.length} found)`);
555
- lines.push('');
556
-
557
- for (const inc of incidents) {
558
- lines.push(`### #${inc.incident_num} (${inc.date}) — ${inc.type}`);
559
- lines.push(`- **Gap**: ${inc.gap_found}`);
560
- lines.push(`- **Prevention**: ${inc.prevention}`);
561
- if (inc.cr_added) lines.push(`- **CR Added**: ${inc.cr_added}`);
562
- lines.push('');
563
- }
564
- }
565
-
566
- return text(lines.join('\n'));
567
- }
568
-
569
- // ============================================================
570
- // P2-004: knowledge_schema_check
571
- // ============================================================
572
-
573
- function handleSchemaCheck(db: Database.Database, args: Record<string, unknown>): ToolResult {
574
- const table = args.table as string | undefined;
575
- const column = args.column as string | undefined;
576
-
577
- const lines: string[] = [];
578
-
579
- if (table) {
580
- const mismatches = db.prepare(
581
- 'SELECT * FROM knowledge_schema_mismatches WHERE table_name = ?'
582
- ).all(table) as { table_name: string; wrong_column: string; correct_column: string; source: string }[];
583
-
584
- if (mismatches.length > 0) {
585
- lines.push(`## Schema Mismatches for \`${table}\``);
586
- lines.push('');
587
- lines.push('| WRONG Column | CORRECT Column | Source |');
588
- lines.push('|-------------|----------------|--------|');
589
- for (const m of mismatches) {
590
- lines.push(`| \`${m.wrong_column}\` | \`${m.correct_column}\` | ${m.source} |`);
591
- }
592
- lines.push('');
593
- lines.push('**WARNING**: Using the WRONG column will cause runtime errors. Always use the CORRECT column.');
594
- } else {
595
- lines.push(`No known schema mismatches for table \`${table}\`.`);
596
- }
597
- }
598
-
599
- if (column) {
600
- const mismatches = db.prepare(
601
- 'SELECT * FROM knowledge_schema_mismatches WHERE wrong_column = ?'
602
- ).all(column) as { table_name: string; wrong_column: string; correct_column: string; source: string }[];
603
-
604
- if (mismatches.length > 0) {
605
- if (lines.length > 0) lines.push('');
606
- lines.push(`## Column \`${column}\` is KNOWN WRONG in:`);
607
- lines.push('');
608
- for (const m of mismatches) {
609
- lines.push(`- **${m.table_name}**.${m.wrong_column} → should be **${m.correct_column}**`);
610
- }
611
- } else if (!table) {
612
- lines.push(`Column \`${column}\` has no known mismatches.`);
613
- }
614
- }
615
-
616
- if (!table && !column) {
617
- // Show all mismatches
618
- const all = db.prepare('SELECT * FROM knowledge_schema_mismatches ORDER BY table_name LIMIT 10000').all() as {
619
- table_name: string; wrong_column: string; correct_column: string;
620
- }[];
621
-
622
- lines.push(`## All Known Schema Mismatches (${all.length} total)`);
623
- lines.push('');
624
- lines.push('| Table | WRONG | CORRECT |');
625
- lines.push('|-------|-------|---------|');
626
- for (const m of all) {
627
- lines.push(`| ${m.table_name} | \`${m.wrong_column}\` | \`${m.correct_column}\` |`);
628
- }
629
- }
630
-
631
- return text(lines.join('\n'));
632
- }
633
-
634
- // ============================================================
635
- // P2-005: knowledge_pattern
636
- // ============================================================
637
-
638
- function handlePattern(db: Database.Database, args: Record<string, unknown>): ToolResult {
639
- const claudeDir = getConfig().conventions?.claudeDirName ?? '.claude';
640
- const domain = args.domain as string;
641
- const topic = args.topic as string | undefined;
642
-
643
- if (!domain) return text('Error: domain is required');
644
-
645
- const lines: string[] = [];
646
- lines.push(`## Pattern Guidance: ${domain}${topic ? ` / ${topic}` : ''}`);
647
- lines.push('');
648
-
649
- // Find pattern documents matching the domain
650
- const domainPatterns = [
651
- `${domain}-patterns`,
652
- `${domain}`,
653
- `patterns-quickref`,
654
- ];
655
-
656
- // First: get chunks from domain-specific pattern files
657
- const docs = db.prepare(
658
- "SELECT id, file_path, title FROM knowledge_documents WHERE category = 'patterns' OR category = 'reference' OR category = 'root'"
659
- ).all() as { id: number; file_path: string; title: string }[];
660
-
661
- const relevantDocs = docs.filter(d =>
662
- domainPatterns.some(pat => d.file_path.toLowerCase().includes(pat.toLowerCase()))
663
- );
664
-
665
- if (relevantDocs.length === 0) {
666
- // Fall back to FTS search
667
- lines.push(`No dedicated pattern file found for domain "${domain}". Searching all knowledge...`);
668
- lines.push('');
669
- }
670
-
671
- if (topic && relevantDocs.length > 0) {
672
- // Search within domain-specific docs for the topic
673
- const docIds = relevantDocs.map(d => d.id);
674
- const placeholders = docIds.map(() => '?').join(',');
675
-
676
- try {
677
- const topicResults = db.prepare(`
678
- SELECT kc.heading, kc.content, kc.chunk_type, kd.file_path
679
- FROM knowledge_fts
680
- JOIN knowledge_chunks kc ON kc.id = knowledge_fts.rowid
681
- JOIN knowledge_documents kd ON kd.id = kc.document_id
682
- WHERE knowledge_fts MATCH ?
683
- AND kc.document_id IN (${placeholders})
684
- ORDER BY rank
685
- LIMIT 10
686
- `).all(sanitizeFts5Query(topic), ...docIds) as {
687
- heading: string; content: string; chunk_type: string; file_path: string;
688
- }[];
689
-
690
- if (topicResults.length > 0) {
691
- for (const r of topicResults) {
692
- lines.push(`### ${r.heading || '(section)'} [${r.chunk_type}]`);
693
- lines.push(`*Source: ${claudeDir}/${r.file_path}*`);
694
- lines.push('');
695
- lines.push(r.content.length > 800 ? r.content.substring(0, 800) + '...' : r.content);
696
- lines.push('');
697
- lines.push('---');
698
- lines.push('');
699
- }
700
- } else {
701
- lines.push(`No specific sections found for topic "${topic}" in ${domain} patterns.`);
702
- lines.push('');
703
- }
704
- } catch {
705
- // FTS5 syntax error — fall through to direct search
706
- lines.push(`FTS5 search failed for topic "${topic}". Showing all ${domain} sections instead.`);
707
- lines.push('');
708
- }
709
- }
710
-
711
- if (!topic && relevantDocs.length > 0) {
712
- // Show all sections from the domain pattern file
713
- for (const doc of relevantDocs.slice(0, 2)) {
714
- const chunks = db.prepare(
715
- "SELECT heading, content, chunk_type FROM knowledge_chunks WHERE document_id = ? AND chunk_type = 'section' ORDER BY line_start"
716
- ).all(doc.id) as { heading: string; content: string; chunk_type: string }[];
717
-
718
- lines.push(`### From: ${claudeDir}/${doc.file_path}`);
719
- lines.push('');
720
- lines.push(`**Sections** (${chunks.length} total):`);
721
- for (const c of chunks) {
722
- const preview = c.content.length > 100 ? c.content.substring(0, 100) + '...' : c.content;
723
- lines.push(`- **${c.heading || '(untitled)'}**: ${preview}`);
724
- }
725
- lines.push('');
726
- }
727
- }
728
-
729
- // If no results from domain docs, try broad FTS
730
- if (relevantDocs.length === 0 || lines.length < 5) {
731
- const searchTerm = topic ? `${domain} ${topic}` : domain;
732
- try {
733
- const ftsResults = db.prepare(`
734
- SELECT kc.heading, kc.content, kc.chunk_type, kd.file_path
735
- FROM knowledge_fts
736
- JOIN knowledge_chunks kc ON kc.id = knowledge_fts.rowid
737
- JOIN knowledge_documents kd ON kd.id = kc.document_id
738
- WHERE knowledge_fts MATCH ?
739
- ORDER BY rank
740
- LIMIT 5
741
- `).all(sanitizeFts5Query(searchTerm)) as {
742
- heading: string; content: string; chunk_type: string; file_path: string;
743
- }[];
744
-
745
- if (ftsResults.length > 0) {
746
- lines.push('### Broad Search Results');
747
- lines.push('');
748
- for (const r of ftsResults) {
749
- lines.push(`- **${r.heading || '(section)'}** (${claudeDir}/${r.file_path}): ${r.content.substring(0, 200)}...`);
750
- }
751
- }
752
- } catch {
753
- // FTS syntax error — skip
754
- }
755
- }
756
-
757
- return text(lines.join('\n'));
758
- }
759
-
760
- // ============================================================
761
- // P2-006: knowledge_verification
762
- // ============================================================
763
-
764
- function handleVerification(db: Database.Database, args: Record<string, unknown>): ToolResult {
765
- const vrType = args.vr_type as string | undefined;
766
- const situation = args.situation as string | undefined;
767
-
768
- const lines: string[] = [];
769
-
770
- if (vrType) {
771
- const vr = db.prepare('SELECT * FROM knowledge_verifications WHERE vr_type = ?').get(vrType) as {
772
- vr_type: string; command: string; expected: string; use_when: string; catches: string; category: string;
773
- } | undefined;
774
-
775
- if (!vr) {
776
- return text(`Verification type ${vrType} not found. Use ${p('knowledge_search')} to find related checks.`);
777
- }
778
-
779
- lines.push(`## ${vr.vr_type}`);
780
- lines.push('');
781
- lines.push(`| Field | Value |`);
782
- lines.push(`|-------|-------|`);
783
- lines.push(`| **Command** | \`${vr.command}\` |`);
784
- lines.push(`| **Expected** | ${vr.expected} |`);
785
- lines.push(`| **Use When** | ${vr.use_when} |`);
786
- if (vr.catches) lines.push(`| **Catches** | ${vr.catches} |`);
787
- if (vr.category) lines.push(`| **Category** | ${vr.category} |`);
788
- } else if (situation) {
789
- // Search use_when field
790
- const results = db.prepare(
791
- 'SELECT * FROM knowledge_verifications WHERE use_when LIKE ? OR vr_type LIKE ? OR catches LIKE ? ORDER BY vr_type'
792
- ).all(`%${situation}%`, `%${situation}%`, `%${situation}%`) as {
793
- vr_type: string; command: string; expected: string; use_when: string; catches: string;
794
- }[];
795
-
796
- lines.push(`## Verifications for "${situation}" (${results.length} found)`);
797
- lines.push('');
798
-
799
- for (const vr of results) {
800
- lines.push(`### ${vr.vr_type}`);
801
- lines.push(`- **Command**: \`${vr.command}\``);
802
- lines.push(`- **Expected**: ${vr.expected}`);
803
- lines.push(`- **Use When**: ${vr.use_when}`);
804
- lines.push('');
805
- }
806
- } else {
807
- // List all
808
- const all = db.prepare('SELECT vr_type, command, use_when FROM knowledge_verifications ORDER BY vr_type LIMIT 10000').all() as {
809
- vr_type: string; command: string; use_when: string;
810
- }[];
811
-
812
- lines.push(`## All Verification Types (${all.length} total)`);
813
- lines.push('');
814
-
815
- for (const vr of all) {
816
- lines.push(`- **${vr.vr_type}**: \`${vr.command}\` — ${vr.use_when}`);
817
- }
818
- }
819
-
820
- return text(lines.join('\n'));
821
- }
822
-
823
- // ============================================================
824
- // P2-007: knowledge_graph
825
- // ============================================================
826
-
827
- function handleGraph(db: Database.Database, args: Record<string, unknown>): ToolResult {
828
- const entityType = args.entity_type as string;
829
- const entityId = args.entity_id as string;
830
- const maxDepth = Math.min((args.depth as number) || 1, 3);
831
-
832
- if (!entityType || !entityId) return text('Error: entity_type and entity_id are required');
833
-
834
- const lines: string[] = [];
835
- lines.push(`## Knowledge Graph: ${entityType}/${entityId} (depth ${maxDepth})`);
836
- lines.push('');
837
-
838
- // BFS traversal
839
- type Entity = { type: string; id: string };
840
- const visited = new Set<string>();
841
- let currentLevel: Entity[] = [{ type: entityType, id: entityId }];
842
- visited.add(`${entityType}:${entityId}`);
843
-
844
- for (let depth = 0; depth < maxDepth && currentLevel.length > 0; depth++) {
845
- const nextLevel: Entity[] = [];
846
-
847
- lines.push(`### Depth ${depth + 1}`);
848
- lines.push('');
849
-
850
- for (const entity of currentLevel) {
851
- // Find outgoing edges
852
- const outgoing = db.prepare(
853
- 'SELECT target_type, target_id, edge_type FROM knowledge_edges WHERE source_type = ? AND source_id = ?'
854
- ).all(entity.type, entity.id) as { target_type: string; target_id: string; edge_type: string }[];
855
-
856
- // Find incoming edges
857
- const incoming = db.prepare(
858
- 'SELECT source_type, source_id, edge_type FROM knowledge_edges WHERE target_type = ? AND target_id = ?'
859
- ).all(entity.type, entity.id) as { source_type: string; source_id: string; edge_type: string }[];
860
-
861
- for (const edge of outgoing) {
862
- const key = `${edge.target_type}:${edge.target_id}`;
863
- if (!visited.has(key)) {
864
- visited.add(key);
865
- lines.push(`- ${entity.type}/${entity.id} —[${edge.edge_type}]→ **${edge.target_type}/${edge.target_id}**`);
866
- nextLevel.push({ type: edge.target_type, id: edge.target_id });
867
- }
868
- }
869
-
870
- for (const edge of incoming) {
871
- const key = `${edge.source_type}:${edge.source_id}`;
872
- if (!visited.has(key)) {
873
- visited.add(key);
874
- lines.push(`- **${edge.source_type}/${edge.source_id}** —[${edge.edge_type}]→ ${entity.type}/${entity.id}`);
875
- nextLevel.push({ type: edge.source_type, id: edge.source_id });
876
- }
877
- }
878
- }
879
-
880
- if (nextLevel.length === 0) {
881
- lines.push('(no further connections)');
882
- }
883
- lines.push('');
884
-
885
- currentLevel = nextLevel;
886
- }
887
-
888
- // Summary
889
- lines.push(`**Total connected entities**: ${visited.size - 1}`);
890
-
891
- return text(lines.join('\n'));
892
- }
893
-
894
- // ============================================================
895
- // P2-008: knowledge_command
896
- // ============================================================
897
-
898
- function handleCommand(db: Database.Database, args: Record<string, unknown>): ToolResult {
899
- const claudeDir = getConfig().conventions?.claudeDirName ?? '.claude';
900
- const command = args.command as string | undefined;
901
- const keyword = args.keyword as string | undefined;
902
-
903
- const lines: string[] = [];
904
-
905
- if (command) {
906
- // Exact command lookup — find the command chunk
907
- // Support both prefixed (massu-xxx) and unprefixed (xxx) lookups
908
- const chunks = db.prepare(`
909
- SELECT kc.heading, kc.content, kd.file_path, kd.title, kd.description
910
- FROM knowledge_chunks kc
911
- JOIN knowledge_documents kd ON kd.id = kc.document_id
912
- WHERE kc.chunk_type = 'command' AND (kc.heading = ? OR kc.heading = ?)
913
- LIMIT 1
914
- `).all(command, command.replace('massu-', '')) as {
915
- heading: string; content: string; file_path: string; title: string; description: string;
916
- }[];
917
-
918
- if (chunks.length === 0) {
919
- // Try searching by file path
920
- const doc = db.prepare(
921
- "SELECT id, file_path, title, description FROM knowledge_documents WHERE category = 'commands' AND file_path LIKE ?"
922
- ).get(`%${command}%`) as { id: number; file_path: string; title: string; description: string } | undefined;
923
-
924
- if (!doc) {
925
- return text(`Command "${command}" not found. Use keyword search to find related commands.`);
926
- }
927
-
928
- // Get the first section of this command's document
929
- const sections = db.prepare(
930
- "SELECT heading, content FROM knowledge_chunks WHERE document_id = ? AND chunk_type = 'section' ORDER BY line_start LIMIT 3"
931
- ).all(doc.id) as { heading: string; content: string }[];
932
-
933
- lines.push(`## Command: ${command}`);
934
- lines.push(`**File**: ${claudeDir}/${doc.file_path}`);
935
- if (doc.description) lines.push(`**Description**: ${doc.description}`);
936
- lines.push('');
937
-
938
- for (const s of sections) {
939
- lines.push(`### ${s.heading}`);
940
- lines.push(s.content.length > 500 ? s.content.substring(0, 500) + '...' : s.content);
941
- lines.push('');
942
- }
943
- } else {
944
- const chunk = chunks[0];
945
- lines.push(`## Command: ${chunk.heading}`);
946
- lines.push(`**File**: ${claudeDir}/${chunk.file_path}`);
947
- if (chunk.description) lines.push(`**Description**: ${chunk.description}`);
948
- lines.push('');
949
- lines.push(chunk.content);
950
- }
951
- } else if (keyword) {
952
- // Search commands
953
- const results = db.prepare(`
954
- SELECT kc.heading, kc.content, kd.file_path, kd.title
955
- FROM knowledge_chunks kc
956
- JOIN knowledge_documents kd ON kd.id = kc.document_id
957
- WHERE kc.chunk_type = 'command' AND (kc.content LIKE ? OR kc.heading LIKE ?)
958
- ORDER BY kc.heading
959
- `).all(`%${keyword}%`, `%${keyword}%`) as {
960
- heading: string; content: string; file_path: string; title: string;
961
- }[];
962
-
963
- lines.push(`## Commands matching "${keyword}" (${results.length} found)`);
964
- lines.push('');
965
-
966
- for (const r of results) {
967
- const preview = r.content.substring(0, 200).replace(/\n/g, ' ');
968
- lines.push(`- **${r.heading}** (${claudeDir}/${r.file_path}): ${preview}...`);
969
- }
970
- } else {
971
- // List all commands
972
- const commands = db.prepare(`
973
- SELECT kc.heading, kd.file_path, kd.description
974
- FROM knowledge_chunks kc
975
- JOIN knowledge_documents kd ON kd.id = kc.document_id
976
- WHERE kc.chunk_type = 'command'
977
- ORDER BY kc.heading
978
- `).all() as { heading: string; file_path: string; description: string }[];
979
-
980
- lines.push(`## All Commands (${commands.length} total)`);
981
- lines.push('');
982
-
983
- for (const cmd of commands) {
984
- lines.push(`- **${cmd.heading}** (${claudeDir}/${cmd.file_path})${cmd.description ? ': ' + cmd.description : ''}`);
985
- }
986
- }
987
-
988
- return text(lines.join('\n'));
989
- }
990
-
991
- // ============================================================
992
- // P2-009: knowledge_correct
993
- // ============================================================
994
-
995
- function handleCorrect(db: Database.Database, args: Record<string, unknown>): ToolResult {
996
- const wrong = args.wrong as string;
997
- const correction = args.correction as string;
998
- const rule = args.rule as string;
999
- const crRule = args.cr_rule as string | undefined;
1000
-
1001
- if (!wrong || !correction || !rule) {
1002
- return text('Error: wrong, correction, and rule are all required.');
1003
- }
1004
-
1005
- // 1. Append to corrections.md
1006
- const correctionsPath = resolve(getResolvedPaths().memoryDir, 'corrections.md');
1007
- const today = new Date().toISOString().split('T')[0];
1008
- const title = rule.slice(0, 60);
1009
-
1010
- const entry = `\n### ${today} - ${title}\n- **Wrong**: ${wrong}\n- **Correction**: ${correction}\n- **Rule**: ${rule}\n${crRule ? `- **CR**: ${crRule}\n` : ''}\n`;
1011
-
1012
- // Read existing content, find insertion point (before ## Archived if present, else append)
1013
- let existing = '';
1014
- try { existing = readFileSync(correctionsPath, 'utf-8'); } catch { /* new file */ }
1015
-
1016
- const archiveIdx = existing.indexOf('## Archived');
1017
- if (archiveIdx > 0) {
1018
- const before = existing.slice(0, archiveIdx);
1019
- const after = existing.slice(archiveIdx);
1020
- writeFileSync(correctionsPath, before + entry + after);
1021
- } else {
1022
- appendFileSync(correctionsPath, entry);
1023
- }
1024
-
1025
- // 2. Index into knowledge DB immediately (insert chunk + edge)
1026
- const doc = db.prepare('SELECT id FROM knowledge_documents WHERE file_path LIKE ?').get('%corrections.md') as { id: number } | undefined;
1027
- if (doc) {
1028
- db.prepare('INSERT INTO knowledge_chunks (document_id, chunk_type, heading, content, metadata) VALUES (?, ?, ?, ?, ?)')
1029
- .run(doc.id, 'section', `Correction: ${title}`,
1030
- `Wrong: ${wrong}\nCorrection: ${correction}\nRule: ${rule}`,
1031
- JSON.stringify({ is_correction: true, date: today, cr_rule: crRule }));
1032
-
1033
- if (crRule) {
1034
- db.prepare('INSERT OR IGNORE INTO knowledge_edges (source_type, source_id, target_type, target_id, edge_type) VALUES (?, ?, ?, ?, ?)')
1035
- .run('correction', title, 'cr', crRule, 'enforces');
1036
- }
1037
- }
1038
-
1039
- return text(`Correction recorded:\n- **Date**: ${today}\n- **Wrong**: ${wrong}\n- **Rule**: ${rule}\n${crRule ? `- **CR**: ${crRule}\n` : ''}File: corrections.md updated`);
1040
- }
1041
-
1042
- // ============================================================
1043
- // P2-010: knowledge_plan
1044
- // ============================================================
1045
-
1046
- function handlePlan(db: Database.Database, args: Record<string, unknown>): ToolResult {
1047
- const file = args.file as string | undefined;
1048
- const keyword = args.keyword as string | undefined;
1049
- const planName = args.plan_name as string | undefined;
1050
- const status = args.status as string | undefined;
1051
-
1052
- const lines: string[] = [];
1053
-
1054
- if (file) {
1055
- // Find plans that reference this file
1056
- const results = db.prepare(`
1057
- SELECT kd.file_path, kd.title, kc.content
1058
- FROM knowledge_chunks kc
1059
- JOIN knowledge_documents kd ON kd.id = kc.document_id
1060
- WHERE kd.category = 'plan'
1061
- AND kc.heading = 'Referenced Files'
1062
- AND kc.content LIKE ?
1063
- ORDER BY kd.file_path DESC
1064
- LIMIT 20
1065
- `).all(`%${basename(file)}%`) as { file_path: string; title: string; content: string }[];
1066
-
1067
- lines.push(`## Plans referencing \`${file}\` (${results.length} found)`);
1068
- lines.push('');
1069
-
1070
- for (const r of results) {
1071
- lines.push(`- **${r.title}** (${r.file_path})`);
1072
- }
1073
- } else if (keyword) {
1074
- // FTS search scoped to plan documents
1075
- try {
1076
- const results = db.prepare(`
1077
- SELECT DISTINCT kd.file_path, kd.title
1078
- FROM knowledge_fts kf
1079
- JOIN knowledge_chunks kc ON kc.id = kf.rowid
1080
- JOIN knowledge_documents kd ON kd.id = kc.document_id
1081
- WHERE kf.content MATCH ? AND kd.category = 'plan'
1082
- ORDER BY rank
1083
- LIMIT 20
1084
- `).all(sanitizeFts5Query(keyword)) as { file_path: string; title: string }[];
1085
-
1086
- lines.push(`## Plans matching "${keyword}" (${results.length} found)`);
1087
- lines.push('');
1088
-
1089
- for (const r of results) {
1090
- lines.push(`- **${r.title}** (${r.file_path})`);
1091
- }
1092
- } catch {
1093
- lines.push(`FTS search error for "${keyword}". Try simpler terms.`);
1094
- }
1095
- } else if (planName) {
1096
- // Find specific plan by filename
1097
- const doc = db.prepare(
1098
- "SELECT id, file_path, title, description FROM knowledge_documents WHERE category = 'plan' AND file_path LIKE ? LIMIT 1"
1099
- ).get(`%${planName}%`) as { id: number; file_path: string; title: string; description: string } | undefined;
1100
-
1101
- if (!doc) {
1102
- return text(`Plan "${planName}" not found. Try keyword search.`);
1103
- }
1104
-
1105
- lines.push(`## Plan: ${doc.title}`);
1106
- lines.push(`**File**: ${doc.file_path}`);
1107
- if (doc.description) lines.push(`**Description**: ${doc.description}`);
1108
- lines.push('');
1109
-
1110
- // Get plan items
1111
- const items = db.prepare(
1112
- "SELECT heading, content FROM knowledge_chunks WHERE document_id = ? AND metadata LIKE '%plan_item_id%' ORDER BY heading"
1113
- ).all(doc.id) as { heading: string; content: string }[];
1114
-
1115
- if (items.length > 0) {
1116
- lines.push(`### Plan Items (${items.length})`);
1117
- for (const item of items) {
1118
- lines.push(`- ${item.content}`);
1119
- }
1120
- lines.push('');
1121
- }
1122
-
1123
- // Get implementation status
1124
- const statusChunk = db.prepare(
1125
- "SELECT content FROM knowledge_chunks WHERE document_id = ? AND heading = 'IMPLEMENTATION STATUS' LIMIT 1"
1126
- ).get(doc.id) as { content: string } | undefined;
1127
-
1128
- if (statusChunk) {
1129
- lines.push('### Implementation Status');
1130
- lines.push(statusChunk.content.substring(0, 1000));
1131
- lines.push('');
1132
- }
1133
-
1134
- // Get referenced files
1135
- const fileRefs = db.prepare(
1136
- "SELECT content FROM knowledge_chunks WHERE document_id = ? AND heading = 'Referenced Files' LIMIT 1"
1137
- ).get(doc.id) as { content: string } | undefined;
1138
-
1139
- if (fileRefs) {
1140
- lines.push('### Referenced Files');
1141
- lines.push(fileRefs.content.substring(0, 500));
1142
- }
1143
- } else if (status) {
1144
- // Filter plans by status
1145
- const docs = db.prepare(
1146
- "SELECT kd.file_path, kd.title, kc.content FROM knowledge_chunks kc JOIN knowledge_documents kd ON kd.id = kc.document_id WHERE kd.category = 'plan' AND kc.heading = 'IMPLEMENTATION STATUS' AND kc.content LIKE ? ORDER BY kd.file_path DESC"
1147
- ).all(`%${status}%`) as { file_path: string; title: string; content: string }[];
1148
-
1149
- lines.push(`## Plans with status "${status}" (${docs.length} found)`);
1150
- lines.push('');
1151
-
1152
- for (const d of docs) {
1153
- lines.push(`- **${d.title}** (${d.file_path})`);
1154
- }
1155
- } else {
1156
- // List all plans
1157
- const plans = db.prepare(
1158
- "SELECT file_path, title FROM knowledge_documents WHERE category = 'plan' ORDER BY file_path DESC LIMIT 50"
1159
- ).all() as { file_path: string; title: string }[];
1160
-
1161
- lines.push(`## All Plans (${plans.length} indexed)`);
1162
- lines.push('');
1163
-
1164
- for (const plan of plans) {
1165
- lines.push(`- **${plan.title}** (${plan.file_path})`);
1166
- }
1167
- }
1168
-
1169
- return text(lines.join('\n'));
1170
- }
1171
-
1172
- // ============================================================
1173
- // P2-011: knowledge_gaps
1174
- // ============================================================
1175
-
1176
- function handleGaps(db: Database.Database, args: Record<string, unknown>): ToolResult {
1177
- const domain = args.domain as string | undefined;
1178
- const checkType = (args.check_type as string) || 'features';
1179
-
1180
- const lines: string[] = [];
1181
- lines.push(`## Knowledge Gap Analysis (${checkType})`);
1182
- lines.push('');
1183
-
1184
- if (checkType === 'features') {
1185
- // Query sentinel features from data DB (massu_sentinel table)
1186
- let dataDb: Database.Database | null = null;
1187
- try {
1188
- dataDb = getDataDb();
1189
-
1190
- let sql = `SELECT feature_key, title, domain, status FROM ${t('sentinel')} WHERE status = 'active'`;
1191
- const params: string[] = [];
1192
- if (domain) {
1193
- sql += ' AND domain LIKE ?';
1194
- params.push(`%${domain}%`);
1195
- }
1196
- sql += ' ORDER BY domain, feature_key';
1197
-
1198
- const features = dataDb.prepare(sql).all(...params) as {
1199
- feature_key: string; title: string; domain: string; status: string;
1200
- }[];
1201
-
1202
- lines.push(`| Feature | Domain | Knowledge Hits | Status |`);
1203
- lines.push(`|---------|--------|----------------|--------|`);
1204
-
1205
- let gapCount = 0;
1206
- let coveredCount = 0;
1207
-
1208
- for (const feat of features) {
1209
- // Search knowledge DB for this feature
1210
- let hits = 0;
1211
- try {
1212
- const searchTerm = feat.title.replace(/['"]/g, '');
1213
- const results = db.prepare(
1214
- `SELECT COUNT(*) as cnt FROM knowledge_fts WHERE content MATCH ?`
1215
- ).get(sanitizeFts5Query(searchTerm)) as { cnt: number };
1216
- hits = results.cnt;
1217
- } catch {
1218
- // FTS error — try LIKE fallback
1219
- const results = db.prepare(
1220
- `SELECT COUNT(*) as cnt FROM knowledge_chunks WHERE content LIKE ?`
1221
- ).get(`%${feat.feature_key}%`) as { cnt: number };
1222
- hits = results.cnt;
1223
- }
1224
-
1225
- const gapStatus = hits === 0 ? 'GAP' : hits < 3 ? 'THIN' : 'COVERED';
1226
- if (gapStatus === 'GAP') gapCount++;
1227
- else coveredCount++;
1228
-
1229
- lines.push(`| ${feat.feature_key} | ${feat.domain} | ${hits} | ${gapStatus} |`);
1230
- }
1231
-
1232
- lines.push('');
1233
- lines.push(`**Summary**: ${coveredCount} covered, ${gapCount} gaps out of ${features.length} features`);
1234
- } catch (error) {
1235
- lines.push(`Error querying sentinel: ${error instanceof Error ? error.message : String(error)}`);
1236
- } finally {
1237
- dataDb?.close();
1238
- }
1239
- } else if (checkType === 'routers') {
1240
- // List router files and check knowledge coverage
1241
- try {
1242
- const routersDir = getResolvedPaths().routersDir;
1243
- const routerFiles = readdirSync(routersDir)
1244
- .filter(f => f.endsWith('.ts') && !f.startsWith('_'));
1245
-
1246
- lines.push(`| Router | Knowledge Hits | Status |`);
1247
- lines.push(`|--------|----------------|--------|`);
1248
-
1249
- for (const file of routerFiles) {
1250
- const routerName = file.replace('.ts', '');
1251
- const results = db.prepare(
1252
- `SELECT COUNT(*) as cnt FROM knowledge_chunks WHERE content LIKE ?`
1253
- ).get(`%${routerName}%`) as { cnt: number };
1254
-
1255
- const routerStatus = results.cnt === 0 ? 'GAP' : results.cnt < 3 ? 'THIN' : 'COVERED';
1256
- lines.push(`| ${routerName} | ${results.cnt} | ${routerStatus} |`);
1257
- }
1258
- } catch (error) {
1259
- lines.push(`Error scanning routers: ${error instanceof Error ? error.message : String(error)}`);
1260
- }
1261
- } else if (checkType === 'patterns') {
1262
- // Check if domains have pattern documentation
1263
- const patternDocs = db.prepare(
1264
- "SELECT file_path, title FROM knowledge_documents WHERE category = 'patterns'"
1265
- ).all() as { file_path: string; title: string }[];
1266
-
1267
- const documentedDomains = new Set(patternDocs.map(d => d.title.replace('-patterns', '').replace(' Patterns', '').toLowerCase()));
1268
-
1269
- lines.push('### Pattern Coverage');
1270
- lines.push(`Documented domains: ${[...documentedDomains].join(', ')}`);
1271
- lines.push('');
1272
-
1273
- // Check sentinel domains against pattern docs
1274
- let dataDb: Database.Database | null = null;
1275
- try {
1276
- dataDb = getDataDb();
1277
-
1278
- const domains = dataDb.prepare(
1279
- `SELECT DISTINCT domain FROM ${t('sentinel')} WHERE status = 'active' ORDER BY domain`
1280
- ).all() as { domain: string }[];
1281
-
1282
- for (const d of domains) {
1283
- const hasPattern = documentedDomains.has(d.domain.toLowerCase());
1284
- lines.push(`- **${d.domain}**: ${hasPattern ? 'COVERED' : 'GAP'}`);
1285
- }
1286
- } catch { /* sentinel not available */ }
1287
- finally { dataDb?.close(); }
1288
- } else if (checkType === 'incidents') {
1289
- // Find domains with incidents but no prevention patterns
1290
- const incidents = db.prepare(
1291
- "SELECT type, COUNT(*) as cnt FROM knowledge_incidents GROUP BY type ORDER BY cnt DESC"
1292
- ).all() as { type: string; cnt: number }[];
1293
-
1294
- lines.push('### Incident-to-Pattern Coverage');
1295
- lines.push(`| Incident Type | Count | Pattern Coverage |`);
1296
- lines.push(`|---------------|-------|-----------------|`);
1297
-
1298
- for (const inc of incidents) {
1299
- const patternHits = db.prepare(
1300
- "SELECT COUNT(*) as cnt FROM knowledge_chunks WHERE content LIKE ? AND document_id IN (SELECT id FROM knowledge_documents WHERE category = 'patterns')"
1301
- ).get(`%${inc.type}%`) as { cnt: number };
1302
-
1303
- const incStatus = patternHits.cnt === 0 ? 'GAP' : 'COVERED';
1304
- lines.push(`| ${inc.type} | ${inc.cnt} | ${incStatus} (${patternHits.cnt} hits) |`);
1305
- }
1306
- }
1307
-
1308
- return text(lines.join('\n'));
1309
- }
1310
-
1311
- // ============================================================
1312
- // P2-012: knowledge_effectiveness
1313
- // ============================================================
1314
-
1315
- function handleEffectiveness(db: Database.Database, args: Record<string, unknown>): ToolResult {
1316
- const ruleId = args.rule_id as string | undefined;
1317
- const topN = Math.min((args.top_n as number) || 10, 50);
1318
- const mode = (args.mode as string) || 'most_violated';
1319
-
1320
- const lines: string[] = [];
1321
-
1322
- // Get all CR rules from knowledge DB
1323
- const allRules = db.prepare('SELECT rule_id, rule_text FROM knowledge_rules ORDER BY rule_id LIMIT 10000').all() as {
1324
- rule_id: string; rule_text: string;
1325
- }[];
1326
-
1327
- // Try to get violation data from memory DB
1328
- let memoryDb: InstanceType<typeof Database> | null = null;
1329
- const violationCounts = new Map<string, number>();
1330
- const lastViolated = new Map<string, string>();
1331
-
1332
- try {
1333
- memoryDb = getMemoryDb();
1334
-
1335
- // Count observations that reference CR rules
1336
- const observations = memoryDb.prepare(`
1337
- SELECT cr_rule, COUNT(*) as cnt, MAX(created_at) as last_at
1338
- FROM observations
1339
- WHERE cr_rule IS NOT NULL AND cr_rule != ''
1340
- GROUP BY cr_rule
1341
- `).all() as { cr_rule: string; cnt: number; last_at: string }[];
1342
-
1343
- for (const obs of observations) {
1344
- violationCounts.set(obs.cr_rule, obs.cnt);
1345
- lastViolated.set(obs.cr_rule, obs.last_at);
1346
- }
1347
- } catch { /* Memory DB not available */ }
1348
- finally { memoryDb?.close(); }
1349
-
1350
- if (ruleId || mode === 'detail') {
1351
- const targetRule = ruleId || '';
1352
- const rule = allRules.find(r => r.rule_id === targetRule);
1353
-
1354
- if (!rule) {
1355
- return text(`Rule ${targetRule} not found.`);
1356
- }
1357
-
1358
- lines.push(`## Rule Effectiveness: ${rule.rule_id}`);
1359
- lines.push(`**Text**: ${rule.rule_text}`);
1360
- lines.push('');
1361
-
1362
- const violations = violationCounts.get(rule.rule_id) || 0;
1363
- const lastAt = lastViolated.get(rule.rule_id) || 'Never';
1364
-
1365
- // Count linked incidents
1366
- const incidentEdges = db.prepare(
1367
- "SELECT COUNT(*) as cnt FROM knowledge_edges WHERE target_type = 'cr' AND target_id = ? AND source_type = 'incident'"
1368
- ).get(rule.rule_id) as { cnt: number };
1369
-
1370
- // Count correction links
1371
- const correctionEdges = db.prepare(
1372
- "SELECT COUNT(*) as cnt FROM knowledge_edges WHERE target_type = 'cr' AND target_id = ? AND source_type = 'correction'"
1373
- ).get(rule.rule_id) as { cnt: number };
1374
-
1375
- lines.push(`| Metric | Value |`);
1376
- lines.push(`|--------|-------|`);
1377
- lines.push(`| Violations Observed | ${violations} |`);
1378
- lines.push(`| Last Violated | ${lastAt} |`);
1379
- lines.push(`| Linked Incidents | ${incidentEdges.cnt} |`);
1380
- lines.push(`| Linked Corrections | ${correctionEdges.cnt} |`);
1381
- lines.push(`| Effectiveness Score | ${violations === 0 ? 'HIGH (never violated)' : violations < 3 ? 'MEDIUM' : 'LOW (frequently violated)'} |`);
1382
- } else {
1383
- // Ranking mode
1384
- const rulesWithCounts = allRules.map(r => ({
1385
- ...r,
1386
- violations: violationCounts.get(r.rule_id) || 0,
1387
- last_at: lastViolated.get(r.rule_id) || 'Never',
1388
- }));
1389
-
1390
- if (mode === 'most_violated') {
1391
- rulesWithCounts.sort((a, b) => b.violations - a.violations);
1392
- lines.push(`## Most Violated Rules (Top ${topN})`);
1393
- } else if (mode === 'least_violated') {
1394
- rulesWithCounts.sort((a, b) => a.violations - b.violations);
1395
- lines.push(`## Least Violated Rules (Top ${topN})`);
1396
- } else if (mode === 'most_effective') {
1397
- // Most effective = most violations observed (frequently caught and prevented)
1398
- rulesWithCounts.sort((a, b) => b.violations - a.violations);
1399
- lines.push(`## Most Effective Rules (Top ${topN})`);
1400
- lines.push('*Rules that catch the most issues*');
1401
- }
1402
-
1403
- lines.push('');
1404
- lines.push(`| Rule | Text | Violations | Last Violated |`);
1405
- lines.push(`|------|------|------------|---------------|`);
1406
-
1407
- for (const r of rulesWithCounts.slice(0, topN)) {
1408
- const ruleText = r.rule_text.length > 40 ? r.rule_text.substring(0, 40) + '...' : r.rule_text;
1409
- lines.push(`| ${r.rule_id} | ${ruleText} | ${r.violations} | ${r.last_at} |`);
1410
- }
1411
- }
1412
-
1413
- return text(lines.join('\n'));
1414
- }