claude-brain 0.5.0 → 0.8.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 (46) hide show
  1. package/VERSION +1 -1
  2. package/assets/CLAUDE-unified.md +11 -0
  3. package/package.json +2 -1
  4. package/packs/backend/node.json +173 -0
  5. package/packs/core/javascript.json +176 -0
  6. package/packs/core/typescript.json +222 -0
  7. package/packs/frontend/react.json +254 -0
  8. package/packs/meta/testing.json +172 -0
  9. package/src/cli/bin.ts +14 -0
  10. package/src/cli/commands/chroma.ts +53 -17
  11. package/src/cli/commands/hooks.ts +214 -0
  12. package/src/cli/commands/pack.ts +197 -0
  13. package/src/cli/commands/serve.ts +34 -0
  14. package/src/config/defaults.ts +1 -1
  15. package/src/config/schema.ts +85 -2
  16. package/src/hooks/brain-hook.ts +110 -0
  17. package/src/hooks/capture.ts +161 -0
  18. package/src/hooks/deduplicator.ts +72 -0
  19. package/src/hooks/index.ts +19 -0
  20. package/src/hooks/installer.ts +181 -0
  21. package/src/hooks/passive-classifier.ts +366 -0
  22. package/src/hooks/queue.ts +122 -0
  23. package/src/hooks/session-tracker.ts +199 -0
  24. package/src/hooks/types.ts +47 -0
  25. package/src/memory/chroma/client.ts +1 -1
  26. package/src/memory/chroma/index.ts +1 -1
  27. package/src/memory/chroma/store.ts +29 -9
  28. package/src/memory/index.ts +1 -0
  29. package/src/memory/store.ts +1 -0
  30. package/src/packs/index.ts +9 -0
  31. package/src/packs/loader.ts +134 -0
  32. package/src/packs/manager.ts +204 -0
  33. package/src/packs/ranker.ts +78 -0
  34. package/src/packs/types.ts +81 -0
  35. package/src/routing/entity-extractor.ts +410 -0
  36. package/src/routing/intent-classifier.ts +229 -0
  37. package/src/routing/response-filter.ts +221 -0
  38. package/src/routing/router.ts +671 -0
  39. package/src/server/handlers/call-tool.ts +7 -0
  40. package/src/server/handlers/list-tools.ts +22 -5
  41. package/src/server/handlers/tools/brain.ts +85 -0
  42. package/src/server/handlers/tools/init-project.ts +47 -0
  43. package/src/server/handlers/tools/schemas.ts +12 -0
  44. package/src/server/http-api.ts +188 -0
  45. package/src/tools/registry.ts +9 -0
  46. package/src/tools/schemas.ts +33 -1
@@ -0,0 +1,671 @@
1
+ /**
2
+ * Brain Router
3
+ * Phase 16: Core orchestrator for the unified brain() tool
4
+ *
5
+ * Routes classified intents to internal service calls and
6
+ * returns unified BrainResponse objects.
7
+ */
8
+
9
+ import type { Logger } from 'pino'
10
+ import { IntentClassifier, type ClassificationResult } from './intent-classifier'
11
+ import { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
12
+ import { ResponseFilter, type BrainResponse, type TierResults } from './response-filter'
13
+ import {
14
+ getMemoryService,
15
+ getVaultService,
16
+ getContextService,
17
+ getPhase12Service,
18
+ getKnowledgeGraphService,
19
+ isServicesInitialized
20
+ } from '@/server/services'
21
+
22
+ export interface BrainInput {
23
+ message: string
24
+ project?: string
25
+ }
26
+
27
+ export class BrainRouter {
28
+ private classifier: IntentClassifier
29
+ private entityExtractor: BrainEntityExtractor
30
+ private responseFilter: ResponseFilter
31
+ private logger: Logger
32
+
33
+ constructor(logger: Logger) {
34
+ this.classifier = new IntentClassifier()
35
+ this.entityExtractor = new BrainEntityExtractor()
36
+ this.responseFilter = new ResponseFilter()
37
+ this.logger = logger.child({ component: 'brain-router' })
38
+ }
39
+
40
+ async route(input: BrainInput): Promise<BrainResponse> {
41
+ const { message, project: inputProject } = input
42
+
43
+ // Classify intent
44
+ const classification = this.classifier.classify(message)
45
+ this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
46
+
47
+ // Extract entities
48
+ const entities = await this.entityExtractor.extract(message, inputProject)
49
+ const project = entities.project || inputProject
50
+ this.logger.debug({ project, technologies: entities.technologies }, 'Entities extracted')
51
+
52
+ // Route to handler
53
+ try {
54
+ switch (classification.primary) {
55
+ case 'no_action':
56
+ return this.handleNoAction(message)
57
+
58
+ case 'session_start':
59
+ return this.handleSessionStart(message, project, entities)
60
+
61
+ case 'context_needed':
62
+ return this.handleContextNeeded(message, project, entities)
63
+
64
+ case 'decision_made':
65
+ return this.handleDecisionMade(message, project, entities)
66
+
67
+ case 'pattern_found':
68
+ return this.handlePatternFound(message, project, entities)
69
+
70
+ case 'mistake_learned':
71
+ return this.handleMistakeLearned(message, project, entities)
72
+
73
+ case 'progress_update':
74
+ return this.handleProgressUpdate(message, project, entities)
75
+
76
+ case 'question':
77
+ return this.handleQuestion(message, project, entities, classification)
78
+
79
+ case 'comparison':
80
+ return this.handleComparison(message, project, entities)
81
+
82
+ case 'exploration':
83
+ return this.handleExploration(message, project, entities)
84
+
85
+ default:
86
+ return this.handleContextNeeded(message, project, entities)
87
+ }
88
+ } catch (error) {
89
+ this.logger.error({ error, intent: classification.primary }, 'Router handler error')
90
+ return {
91
+ action: 'none',
92
+ summary: `Error processing request`,
93
+ content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
94
+ relevantItems: 0
95
+ }
96
+ }
97
+ }
98
+
99
+ // ===== Intent Handlers =====
100
+
101
+ private handleNoAction(_message: string): BrainResponse {
102
+ return {
103
+ action: 'none',
104
+ summary: 'No action needed',
105
+ content: '',
106
+ relevantItems: 0
107
+ }
108
+ }
109
+
110
+ private async handleSessionStart(
111
+ message: string,
112
+ project: string | undefined,
113
+ entities: BrainExtractedEntities
114
+ ): Promise<BrainResponse> {
115
+ if (!project) {
116
+ return {
117
+ action: 'none',
118
+ summary: 'No project detected',
119
+ content: 'Could not determine project. Please specify which project you are working on.',
120
+ relevantItems: 0
121
+ }
122
+ }
123
+
124
+ if (!isServicesInitialized()) {
125
+ return this.servicesNotReady()
126
+ }
127
+
128
+ const contextService = getContextService()
129
+ const phase12 = getPhase12Service()
130
+
131
+ // Get project context
132
+ const context = await contextService.getContext(project, {
133
+ includeMemories: false,
134
+ includeProgress: true,
135
+ includeStandards: true,
136
+ maxTokens: 6000,
137
+ relevanceThreshold: 0.5
138
+ })
139
+
140
+ const formattedContext = contextService.formatter.format(context)
141
+
142
+ // Process with Phase 12 for proactive recall
143
+ const phase12Result = await phase12.processMessage(
144
+ entities.topic || message,
145
+ project
146
+ )
147
+
148
+ // Build response
149
+ const parts: string[] = [formattedContext]
150
+
151
+ if (phase12Result.recalledMemories?.memories.length) {
152
+ parts.push('\n---\n## Relevant Past Decisions\n')
153
+ for (const mem of phase12Result.recalledMemories.memories) {
154
+ const similarity = Math.round(mem.similarity * 100)
155
+ parts.push(`**[${similarity}%]** ${mem.decision?.decision || mem.memory?.content?.slice(0, 100) || ''}`)
156
+ if (mem.decision?.reasoning) {
157
+ parts.push(` _${mem.decision.reasoning}_`)
158
+ }
159
+ }
160
+ }
161
+
162
+ // If Phase 12 found nothing, do a direct search fallback
163
+ if (!phase12Result.recalledMemories?.memories.length) {
164
+ try {
165
+ const memory = getMemoryService()
166
+ const directResults = await memory.searchRaw(entities.topic || message, {
167
+ project,
168
+ limit: 5,
169
+ minSimilarity: 0.3
170
+ })
171
+ if (directResults.length > 0) {
172
+ parts.push('\n---\n## Related Memories\n')
173
+ for (const r of directResults) {
174
+ const similarity = Math.round((r.similarity || 0) * 100)
175
+ parts.push(`**[${similarity}%]** ${r.decision?.decision || r.content?.slice(0, 100) || ''}`)
176
+ }
177
+ }
178
+ } catch {
179
+ // Direct search failed, continue without
180
+ }
181
+ }
182
+
183
+ const totalRecalled = phase12Result.recalledMemories?.memories.length || 0
184
+ return {
185
+ action: 'retrieved',
186
+ summary: `Session context for ${project}${totalRecalled ? ` (${totalRecalled} memories)` : ''}`,
187
+ content: parts.join('\n'),
188
+ relevantItems: totalRecalled
189
+ }
190
+ }
191
+
192
+ private async handleContextNeeded(
193
+ message: string,
194
+ project: string | undefined,
195
+ entities: BrainExtractedEntities
196
+ ): Promise<BrainResponse> {
197
+ if (!isServicesInitialized()) {
198
+ return this.servicesNotReady()
199
+ }
200
+
201
+ const memory = getMemoryService()
202
+ const query = entities.topic || message
203
+
204
+ // Search for relevant memories
205
+ const tiers: TierResults[] = []
206
+
207
+ try {
208
+ const rawResults = await memory.searchRaw(query, {
209
+ project,
210
+ limit: 5,
211
+ minSimilarity: 0.3
212
+ })
213
+
214
+ tiers.push({
215
+ label: 'Memories',
216
+ results: rawResults.map(r => ({
217
+ content: r.decision?.decision
218
+ ? `**${r.decision.decision}**\n${r.decision.reasoning || ''}\n_Context: ${r.decision.context || ''}_`
219
+ : r.content?.slice(0, 300) || '',
220
+ score: r.similarity || 0,
221
+ source: 'Past Decision',
222
+ metadata: r.metadata
223
+ }))
224
+ })
225
+ } catch {
226
+ // Search failed, continue
227
+ }
228
+
229
+ // Also search patterns if project is available
230
+ try {
231
+ const patternResults = await memory.searchPatterns(query, {
232
+ project,
233
+ limit: 3,
234
+ minSimilarity: 0.3
235
+ })
236
+
237
+ if (patternResults.length > 0) {
238
+ tiers.push({
239
+ label: 'Patterns',
240
+ results: patternResults.map(p => ({
241
+ content: p.metadata?.description || p.content || '',
242
+ score: p.similarity || 0,
243
+ source: `Pattern (${p.metadata?.pattern_type || 'general'})`,
244
+ metadata: p.metadata
245
+ }))
246
+ })
247
+ }
248
+ } catch {
249
+ // Pattern search not available
250
+ }
251
+
252
+ return this.responseFilter.synthesize(tiers, message, project)
253
+ }
254
+
255
+ private async handleDecisionMade(
256
+ message: string,
257
+ project: string | undefined,
258
+ entities: BrainExtractedEntities
259
+ ): Promise<BrainResponse> {
260
+ if (!project) {
261
+ return {
262
+ action: 'none',
263
+ summary: 'Cannot store decision without a project',
264
+ content: 'Please specify which project this decision relates to.',
265
+ relevantItems: 0
266
+ }
267
+ }
268
+
269
+ if (!isServicesInitialized()) {
270
+ return this.servicesNotReady()
271
+ }
272
+
273
+ const memory = getMemoryService()
274
+ const vault = getVaultService()
275
+
276
+ // Extract decision components (use entities or fall back to raw message)
277
+ const decision = entities.decision || message
278
+ const reasoning = entities.reasoning || ''
279
+ const context = entities.topic || message.slice(0, 200)
280
+ const alternatives = entities.alternatives
281
+
282
+ const decisionId = await memory.rememberDecision(
283
+ project,
284
+ context,
285
+ decision,
286
+ reasoning,
287
+ {
288
+ alternatives,
289
+ tags: entities.technologies.length > 0 ? entities.technologies : ['auto-detected']
290
+ }
291
+ )
292
+
293
+ // Also write to vault
294
+ try {
295
+ const projectPaths = vault.getProjectPaths(project)
296
+ const date = new Date().toISOString().split('T')[0]
297
+ const entry = `### Decision: ${decision.slice(0, 100)}\n\n**Date:** ${date}\n**Context:** ${context}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}\n${alternatives ? `**Alternatives:** ${alternatives}\n` : ''}\n---\n\n`
298
+ await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
299
+ } catch {
300
+ // Vault write failed — memory storage still succeeded
301
+ }
302
+
303
+ return {
304
+ action: 'stored',
305
+ summary: `Stored decision: ${decision.slice(0, 60)}`,
306
+ content: `Decision stored (ID: ${decisionId})\n\n**Project:** ${project}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}`,
307
+ relevantItems: 1
308
+ }
309
+ }
310
+
311
+ private async handlePatternFound(
312
+ message: string,
313
+ project: string | undefined,
314
+ entities: BrainExtractedEntities
315
+ ): Promise<BrainResponse> {
316
+ if (!project) {
317
+ return {
318
+ action: 'none',
319
+ summary: 'Cannot store pattern without a project',
320
+ content: 'Please specify which project this pattern relates to.',
321
+ relevantItems: 0
322
+ }
323
+ }
324
+
325
+ if (!isServicesInitialized()) {
326
+ return this.servicesNotReady()
327
+ }
328
+
329
+ const memory = getMemoryService()
330
+ const patternType = entities.patternType || 'solution'
331
+ const description = entities.topic || message
332
+
333
+ const patternId = await memory.storePattern({
334
+ project,
335
+ pattern_type: patternType,
336
+ description,
337
+ confidence: 0.8,
338
+ context: message.slice(0, 300)
339
+ })
340
+
341
+ return {
342
+ action: 'stored',
343
+ summary: `Stored ${patternType}: ${description.slice(0, 60)}`,
344
+ content: `Pattern stored (ID: ${patternId})\n\n**Type:** ${patternType}\n**Project:** ${project}\n**Description:** ${description}`,
345
+ relevantItems: 1
346
+ }
347
+ }
348
+
349
+ private async handleMistakeLearned(
350
+ message: string,
351
+ project: string | undefined,
352
+ entities: BrainExtractedEntities
353
+ ): Promise<BrainResponse> {
354
+ if (!project) {
355
+ return {
356
+ action: 'none',
357
+ summary: 'Cannot store correction without a project',
358
+ content: 'Please specify which project this lesson relates to.',
359
+ relevantItems: 0
360
+ }
361
+ }
362
+
363
+ if (!isServicesInitialized()) {
364
+ return this.servicesNotReady()
365
+ }
366
+
367
+ const memory = getMemoryService()
368
+
369
+ const original = entities.original || message
370
+ const correction = entities.correction || ''
371
+ const reasoning = entities.reasoning || 'Lesson learned from experience'
372
+
373
+ const correctionId = await memory.storeCorrection({
374
+ project,
375
+ original,
376
+ correction: correction || message,
377
+ reasoning,
378
+ context: entities.topic || '',
379
+ confidence: 0.9
380
+ })
381
+
382
+ return {
383
+ action: 'stored',
384
+ summary: `Stored correction: ${original.slice(0, 60)}`,
385
+ content: `Correction stored (ID: ${correctionId})\n\n**Project:** ${project}\n**Original:** ${original}\n**Correction:** ${correction || '(see original message)'}`,
386
+ relevantItems: 1
387
+ }
388
+ }
389
+
390
+ private async handleProgressUpdate(
391
+ message: string,
392
+ project: string | undefined,
393
+ entities: BrainExtractedEntities
394
+ ): Promise<BrainResponse> {
395
+ if (!project) {
396
+ return {
397
+ action: 'none',
398
+ summary: 'Cannot update progress without a project',
399
+ content: 'Please specify which project to update progress for.',
400
+ relevantItems: 0
401
+ }
402
+ }
403
+
404
+ if (!isServicesInitialized()) {
405
+ return this.servicesNotReady()
406
+ }
407
+
408
+ const contextService = getContextService()
409
+
410
+ const completedTask = entities.completedTask || message
411
+ const nextSteps = entities.nextSteps || 'Continue development'
412
+
413
+ try {
414
+ await contextService.progress.addCompletedTask(project, {
415
+ id: this.generateTaskId(completedTask),
416
+ title: completedTask,
417
+ status: 'done',
418
+ completedAt: new Date()
419
+ })
420
+ } catch {
421
+ // Progress update failed — still report what we found
422
+ }
423
+
424
+ return {
425
+ action: 'stored',
426
+ summary: `Progress: ${completedTask.slice(0, 60)}`,
427
+ content: `Progress updated for ${project}\n\n**Completed:** ${completedTask}\n**Next:** ${nextSteps}`,
428
+ relevantItems: 1
429
+ }
430
+ }
431
+
432
+ private async handleQuestion(
433
+ message: string,
434
+ project: string | undefined,
435
+ entities: BrainExtractedEntities,
436
+ classification: ClassificationResult
437
+ ): Promise<BrainResponse> {
438
+ if (!isServicesInitialized()) {
439
+ return this.servicesNotReady()
440
+ }
441
+
442
+ const memory = getMemoryService()
443
+ const query = entities.topic || message
444
+ const tiers: TierResults[] = []
445
+
446
+ // Parallel search: raw memories + patterns + corrections
447
+ const [rawResults, patternResults, correctionResults] = await Promise.all([
448
+ memory.searchRaw(query, {
449
+ project,
450
+ limit: 5,
451
+ minSimilarity: 0.3
452
+ }).catch(() => [] as any[]),
453
+ memory.searchPatterns(query, {
454
+ project,
455
+ limit: 3,
456
+ minSimilarity: 0.3
457
+ }).catch(() => [] as any[]),
458
+ memory.searchCorrections(query, {
459
+ project,
460
+ limit: 3,
461
+ minSimilarity: 0.3
462
+ }).catch(() => [] as any[])
463
+ ])
464
+
465
+ if (rawResults.length > 0) {
466
+ tiers.push({
467
+ label: 'Memories',
468
+ results: rawResults.map((r: any) => ({
469
+ content: r.decision?.decision
470
+ ? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
471
+ : r.content?.slice(0, 300) || '',
472
+ score: r.similarity || 0,
473
+ source: 'Past Decision',
474
+ metadata: r.metadata
475
+ }))
476
+ })
477
+ }
478
+
479
+ if (patternResults.length > 0) {
480
+ tiers.push({
481
+ label: 'Patterns',
482
+ results: patternResults.map((p: any) => ({
483
+ content: p.metadata?.description || p.content || '',
484
+ score: p.similarity || 0,
485
+ source: `Pattern (${p.metadata?.pattern_type || 'general'})`,
486
+ metadata: p.metadata
487
+ }))
488
+ })
489
+ }
490
+
491
+ if (correctionResults.length > 0) {
492
+ tiers.push({
493
+ label: 'Corrections',
494
+ results: correctionResults.map((c: any) => ({
495
+ content: `Original: ${c.metadata?.original || ''}\nFix: ${c.metadata?.correction || c.content || ''}`,
496
+ score: c.similarity || 0,
497
+ source: 'Lesson Learned',
498
+ metadata: c.metadata
499
+ }))
500
+ })
501
+ }
502
+
503
+ // If secondary intent is exploration, also try graph search
504
+ if (classification.secondary.includes('exploration')) {
505
+ await this.addExplorationResults(query, project, tiers)
506
+ }
507
+
508
+ return this.responseFilter.synthesize(tiers, message, project)
509
+ }
510
+
511
+ private async handleComparison(
512
+ message: string,
513
+ project: string | undefined,
514
+ entities: BrainExtractedEntities
515
+ ): Promise<BrainResponse> {
516
+ if (!isServicesInitialized()) {
517
+ return this.servicesNotReady()
518
+ }
519
+
520
+ const memory = getMemoryService()
521
+ const query = entities.topic || message
522
+ const tiers: TierResults[] = []
523
+
524
+ // Search for related decisions
525
+ try {
526
+ const rawResults = await memory.searchRaw(query, {
527
+ project,
528
+ limit: 5,
529
+ minSimilarity: 0.2
530
+ })
531
+
532
+ if (rawResults.length > 0) {
533
+ tiers.push({
534
+ label: 'Related Decisions',
535
+ results: rawResults.map((r: any) => ({
536
+ content: r.decision?.decision
537
+ ? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
538
+ : r.content?.slice(0, 300) || '',
539
+ score: r.similarity || 0,
540
+ source: 'Past Decision'
541
+ }))
542
+ })
543
+ }
544
+ } catch {
545
+ // Search failed
546
+ }
547
+
548
+ // Try what-if analysis if knowledge graph is available
549
+ try {
550
+ const kgService = getKnowledgeGraphService()
551
+ if (kgService?.search) {
552
+ const graphResults = kgService.search.search({ query, limit: 5 })
553
+ if (graphResults?.nodes?.length) {
554
+ tiers.push({
555
+ label: 'Knowledge Graph',
556
+ results: graphResults.nodes.map((n: any) => ({
557
+ content: `**${n.label || n.name}** (${n.type})\n${n.metadata?.description || ''}`,
558
+ score: n.score || 0.5,
559
+ source: 'Knowledge Graph'
560
+ }))
561
+ })
562
+ }
563
+ }
564
+ } catch {
565
+ // Knowledge graph not available
566
+ }
567
+
568
+ return this.responseFilter.synthesize(tiers, message, project, 'analyzed')
569
+ }
570
+
571
+ private async handleExploration(
572
+ message: string,
573
+ project: string | undefined,
574
+ entities: BrainExtractedEntities
575
+ ): Promise<BrainResponse> {
576
+ if (!isServicesInitialized()) {
577
+ return this.servicesNotReady()
578
+ }
579
+
580
+ const query = entities.topic || message
581
+ const tiers: TierResults[] = []
582
+
583
+ await this.addExplorationResults(query, project, tiers)
584
+
585
+ // Also do a basic memory search
586
+ try {
587
+ const memory = getMemoryService()
588
+ const rawResults = await memory.searchRaw(query, {
589
+ project,
590
+ limit: 5,
591
+ minSimilarity: 0.2
592
+ })
593
+
594
+ if (rawResults.length > 0) {
595
+ tiers.push({
596
+ label: 'Memories',
597
+ results: rawResults.map((r: any) => ({
598
+ content: r.decision?.decision
599
+ ? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
600
+ : r.content?.slice(0, 300) || '',
601
+ score: r.similarity || 0,
602
+ source: 'Past Decision'
603
+ }))
604
+ })
605
+ }
606
+ } catch {
607
+ // Search failed
608
+ }
609
+
610
+ return this.responseFilter.synthesize(tiers, message, project, 'analyzed')
611
+ }
612
+
613
+ // ===== Helpers =====
614
+
615
+ private async addExplorationResults(
616
+ query: string,
617
+ _project: string | undefined,
618
+ tiers: TierResults[]
619
+ ): Promise<void> {
620
+ try {
621
+ const kgService = getKnowledgeGraphService()
622
+ if (kgService?.search) {
623
+ const graphResults = kgService.search.search({ query, limit: 10 })
624
+ if (graphResults?.nodes?.length) {
625
+ tiers.push({
626
+ label: 'Knowledge Graph',
627
+ results: graphResults.nodes.map((n: any) => ({
628
+ content: `**${n.label || n.name}** (${n.type})${n.metadata?.description ? `\n${n.metadata.description}` : ''}`,
629
+ score: n.score || 0.5,
630
+ source: 'Knowledge Graph'
631
+ }))
632
+ })
633
+ }
634
+ }
635
+ } catch {
636
+ // Knowledge graph not available
637
+ }
638
+ }
639
+
640
+ private servicesNotReady(): BrainResponse {
641
+ return {
642
+ action: 'none',
643
+ summary: 'Services not initialized',
644
+ content: 'Claude Brain services are not ready. The server may still be starting up.',
645
+ relevantItems: 0
646
+ }
647
+ }
648
+
649
+ private generateTaskId(title: string): string {
650
+ return title
651
+ .toLowerCase()
652
+ .replace(/[^a-z0-9]+/g, '-')
653
+ .replace(/^-+|-+$/g, '')
654
+ .slice(0, 50)
655
+ }
656
+ }
657
+
658
+ // Lazy singleton
659
+ let routerInstance: BrainRouter | null = null
660
+
661
+ export function getBrainRouter(logger: Logger): BrainRouter {
662
+ if (!routerInstance) {
663
+ routerInstance = new BrainRouter(logger)
664
+ }
665
+ return routerInstance
666
+ }
667
+
668
+ /** Reset singleton for testing */
669
+ export function _resetBrainRouterForTesting(): void {
670
+ routerInstance = null
671
+ }
@@ -47,6 +47,9 @@ import { handleWhatIfAnalysis } from './tools/what-if-analysis'
47
47
  import { handleGetRecommendations } from './tools/get-recommendations'
48
48
  import { handleFindCrossProjectPatterns } from './tools/find-cross-project-patterns'
49
49
 
50
+ // Phase 16 Unified Brain Tool
51
+ import { handleBrain } from './tools/brain'
52
+
50
53
  /**
51
54
  * Handle tools/call request
52
55
  * Validates that tool exists and routes to the appropriate handler
@@ -164,6 +167,10 @@ export async function handleCallTool(
164
167
  case 'find_cross_project_patterns':
165
168
  return await handleFindCrossProjectPatterns(args, logger)
166
169
 
170
+ // Phase 16 Unified Brain Tool
171
+ case 'brain':
172
+ return await handleBrain(args, logger)
173
+
167
174
  default:
168
175
  // This should never happen if validateToolExists works correctly
169
176
  throw new McpError(