@massu/core 0.1.1 → 0.4.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 (151) hide show
  1. package/commands/_shared-preamble.md +76 -0
  2. package/commands/massu-audit-deps.md +211 -0
  3. package/commands/massu-changelog.md +174 -0
  4. package/commands/massu-cleanup.md +315 -0
  5. package/commands/massu-commit.md +481 -0
  6. package/commands/massu-create-plan.md +752 -0
  7. package/commands/massu-dead-code.md +131 -0
  8. package/commands/massu-debug.md +484 -0
  9. package/commands/massu-deploy.md +91 -0
  10. package/commands/massu-deps.md +374 -0
  11. package/commands/massu-doc-gen.md +279 -0
  12. package/commands/massu-docs.md +364 -0
  13. package/commands/massu-estimate.md +313 -0
  14. package/commands/massu-golden-path.md +973 -0
  15. package/commands/massu-guide.md +167 -0
  16. package/commands/massu-hotfix.md +480 -0
  17. package/commands/massu-loop-playwright.md +837 -0
  18. package/commands/massu-loop.md +775 -0
  19. package/commands/massu-new-feature.md +511 -0
  20. package/commands/massu-parity.md +214 -0
  21. package/commands/massu-plan.md +456 -0
  22. package/commands/massu-push-light.md +207 -0
  23. package/commands/massu-push.md +434 -0
  24. package/commands/massu-refactor.md +410 -0
  25. package/commands/massu-release.md +363 -0
  26. package/commands/massu-review.md +238 -0
  27. package/commands/massu-simplify.md +281 -0
  28. package/commands/massu-status.md +278 -0
  29. package/commands/massu-tdd.md +201 -0
  30. package/commands/massu-test.md +516 -0
  31. package/commands/massu-verify-playwright.md +281 -0
  32. package/commands/massu-verify.md +667 -0
  33. package/dist/cli.js +7772 -3140
  34. package/dist/hooks/cost-tracker.js +103 -40
  35. package/dist/hooks/post-edit-context.js +74 -8
  36. package/dist/hooks/post-tool-use.js +268 -106
  37. package/dist/hooks/pre-compact.js +167 -43
  38. package/dist/hooks/pre-delete-check.js +159 -42
  39. package/dist/hooks/quality-event.js +103 -40
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +143 -84
  42. package/dist/hooks/session-start.js +186 -49
  43. package/dist/hooks/user-prompt.js +189 -43
  44. package/package.json +10 -15
  45. package/src/adr-generator.ts +9 -2
  46. package/src/analytics.ts +9 -3
  47. package/src/audit-trail.ts +10 -3
  48. package/src/backfill-sessions.ts +5 -4
  49. package/src/cli.ts +6 -0
  50. package/src/cloud-sync.ts +14 -18
  51. package/src/commands/doctor.ts +193 -6
  52. package/src/commands/init.ts +230 -5
  53. package/src/commands/install-commands.ts +137 -0
  54. package/src/config.ts +68 -2
  55. package/src/cost-tracker.ts +11 -6
  56. package/src/db.ts +115 -2
  57. package/src/dependency-scorer.ts +9 -2
  58. package/src/docs-tools.ts +21 -16
  59. package/src/hooks/post-edit-context.ts +4 -4
  60. package/src/hooks/post-tool-use.ts +130 -0
  61. package/src/hooks/pre-compact.ts +23 -1
  62. package/src/hooks/pre-delete-check.ts +92 -4
  63. package/src/hooks/security-gate.ts +32 -0
  64. package/src/hooks/session-end.ts +3 -3
  65. package/src/hooks/session-start.ts +99 -6
  66. package/src/hooks/user-prompt.ts +46 -1
  67. package/src/import-resolver.ts +2 -1
  68. package/src/knowledge-db.ts +169 -0
  69. package/src/knowledge-indexer.ts +704 -0
  70. package/src/knowledge-tools.ts +1413 -0
  71. package/src/license.ts +482 -0
  72. package/src/memory-db.ts +1364 -23
  73. package/src/memory-tools.ts +14 -15
  74. package/src/observability-tools.ts +13 -2
  75. package/src/observation-extractor.ts +11 -4
  76. package/src/page-deps.ts +3 -2
  77. package/src/prompt-analyzer.ts +9 -2
  78. package/src/python/coupling-detector.ts +124 -0
  79. package/src/python/domain-enforcer.ts +83 -0
  80. package/src/python/impact-analyzer.ts +95 -0
  81. package/src/python/import-parser.ts +244 -0
  82. package/src/python/import-resolver.ts +135 -0
  83. package/src/python/migration-indexer.ts +115 -0
  84. package/src/python/migration-parser.ts +332 -0
  85. package/src/python/model-indexer.ts +70 -0
  86. package/src/python/model-parser.ts +279 -0
  87. package/src/python/route-indexer.ts +58 -0
  88. package/src/python/route-parser.ts +317 -0
  89. package/src/python-tools.ts +629 -0
  90. package/src/regression-detector.ts +9 -3
  91. package/src/security-scorer.ts +9 -2
  92. package/src/sentinel-db.ts +45 -89
  93. package/src/sentinel-tools.ts +8 -11
  94. package/src/server.ts +29 -7
  95. package/src/session-archiver.ts +4 -5
  96. package/src/team-knowledge.ts +9 -2
  97. package/src/tools.ts +1032 -44
  98. package/src/validate-features-runner.ts +0 -1
  99. package/src/validation-engine.ts +9 -2
  100. package/README.md +0 -40
  101. package/dist/server.js +0 -7008
  102. package/src/__tests__/adr-generator.test.ts +0 -260
  103. package/src/__tests__/analytics.test.ts +0 -282
  104. package/src/__tests__/audit-trail.test.ts +0 -382
  105. package/src/__tests__/backfill-sessions.test.ts +0 -690
  106. package/src/__tests__/cli.test.ts +0 -290
  107. package/src/__tests__/cloud-sync.test.ts +0 -261
  108. package/src/__tests__/config-sections.test.ts +0 -359
  109. package/src/__tests__/config.test.ts +0 -732
  110. package/src/__tests__/cost-tracker.test.ts +0 -348
  111. package/src/__tests__/db.test.ts +0 -177
  112. package/src/__tests__/dependency-scorer.test.ts +0 -325
  113. package/src/__tests__/docs-integration.test.ts +0 -178
  114. package/src/__tests__/docs-tools.test.ts +0 -199
  115. package/src/__tests__/domains.test.ts +0 -236
  116. package/src/__tests__/hooks.test.ts +0 -221
  117. package/src/__tests__/import-resolver.test.ts +0 -95
  118. package/src/__tests__/integration/path-traversal.test.ts +0 -134
  119. package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
  120. package/src/__tests__/integration/tool-registration.test.ts +0 -146
  121. package/src/__tests__/memory-db.test.ts +0 -404
  122. package/src/__tests__/memory-enhancements.test.ts +0 -316
  123. package/src/__tests__/memory-tools.test.ts +0 -199
  124. package/src/__tests__/middleware-tree.test.ts +0 -177
  125. package/src/__tests__/observability-tools.test.ts +0 -595
  126. package/src/__tests__/observability.test.ts +0 -437
  127. package/src/__tests__/observation-extractor.test.ts +0 -167
  128. package/src/__tests__/page-deps.test.ts +0 -60
  129. package/src/__tests__/prompt-analyzer.test.ts +0 -298
  130. package/src/__tests__/regression-detector.test.ts +0 -295
  131. package/src/__tests__/rules.test.ts +0 -87
  132. package/src/__tests__/schema-mapper.test.ts +0 -29
  133. package/src/__tests__/security-scorer.test.ts +0 -238
  134. package/src/__tests__/security-utils.test.ts +0 -175
  135. package/src/__tests__/sentinel-db.test.ts +0 -491
  136. package/src/__tests__/sentinel-scanner.test.ts +0 -750
  137. package/src/__tests__/sentinel-tools.test.ts +0 -324
  138. package/src/__tests__/sentinel-types.test.ts +0 -750
  139. package/src/__tests__/server.test.ts +0 -452
  140. package/src/__tests__/session-archiver.test.ts +0 -524
  141. package/src/__tests__/session-state-generator.test.ts +0 -900
  142. package/src/__tests__/team-knowledge.test.ts +0 -327
  143. package/src/__tests__/tools.test.ts +0 -340
  144. package/src/__tests__/transcript-parser.test.ts +0 -195
  145. package/src/__tests__/trpc-index.test.ts +0 -25
  146. package/src/__tests__/validate-features-runner.test.ts +0 -517
  147. package/src/__tests__/validation-engine.test.ts +0 -300
  148. package/src/core-tools.ts +0 -685
  149. package/src/memory-queries.ts +0 -804
  150. package/src/memory-schema.ts +0 -546
  151. package/src/tool-helpers.ts +0 -41
@@ -23,6 +23,7 @@ import type {
23
23
  ComponentRole,
24
24
  } from './sentinel-types.ts';
25
25
  import { getProjectRoot } from './config.ts';
26
+ import { sanitizeFts5Query } from './memory-db.ts';
26
27
 
27
28
  // ============================================================
28
29
  // Sentinel: Feature Registry Data Access Layer
@@ -135,7 +136,7 @@ export function searchFeatures(
135
136
  JOIN massu_sentinel_fts fts ON s.id = fts.rowid
136
137
  WHERE massu_sentinel_fts MATCH ?
137
138
  `;
138
- params.push(query);
139
+ params.push(sanitizeFts5Query(query));
139
140
  } else {
140
141
  sql = `
141
142
  SELECT s.*,
@@ -246,41 +247,17 @@ export function getOrphanedFeatures(db: Database.Database): Feature[] {
246
247
  }
247
248
 
248
249
  export function getFeatureImpact(db: Database.Database, filePaths: string[]): ImpactReport {
249
- if (filePaths.length === 0) {
250
- return { files_analyzed: [], orphaned: [], degraded: [], unaffected: [], blocked: false, block_reason: null };
251
- }
252
-
253
250
  const fileSet = new Set(filePaths);
254
-
255
- // Batch: find all features linked to these files in a single query
256
- const placeholders = filePaths.map(() => '?').join(',');
257
- const featureLinks = db.prepare(
258
- `SELECT DISTINCT feature_id FROM massu_sentinel_components WHERE component_file IN (${placeholders})`
259
- ).all(...filePaths) as { feature_id: number }[];
260
- const affectedFeatureIds = featureLinks.map(l => l.feature_id);
261
-
262
- if (affectedFeatureIds.length === 0) {
263
- return { files_analyzed: filePaths, orphaned: [], degraded: [], unaffected: [], blocked: false, block_reason: null };
264
- }
265
-
266
- // Batch: load all affected features in a single query
267
- const featurePlaceholders = affectedFeatureIds.map(() => '?').join(',');
268
- const featureRows = db.prepare(
269
- `SELECT * FROM massu_sentinel WHERE id IN (${featurePlaceholders}) AND status = 'active'`
270
- ).all(...affectedFeatureIds) as Record<string, unknown>[];
271
- const featuresById = new Map(featureRows.map(r => [r.id as number, toFeature(r)]));
272
-
273
- // Batch: load all components for affected features in a single query
274
- const allComponents = db.prepare(
275
- `SELECT feature_id, component_file, is_primary FROM massu_sentinel_components WHERE feature_id IN (${featurePlaceholders})`
276
- ).all(...affectedFeatureIds) as { feature_id: number; component_file: string; is_primary: number }[];
277
-
278
- // Group components by feature
279
- const componentsByFeature = new Map<number, typeof allComponents>();
280
- for (const comp of allComponents) {
281
- let list = componentsByFeature.get(comp.feature_id);
282
- if (!list) { list = []; componentsByFeature.set(comp.feature_id, list); }
283
- list.push(comp);
251
+ const affectedFeatureIds = new Set<number>();
252
+
253
+ // Find all features linked to these files
254
+ for (const filePath of filePaths) {
255
+ const links = db.prepare(
256
+ 'SELECT feature_id FROM massu_sentinel_components WHERE component_file = ?'
257
+ ).all(filePath) as { feature_id: number }[];
258
+ for (const link of links) {
259
+ affectedFeatureIds.add(link.feature_id);
260
+ }
284
261
  }
285
262
 
286
263
  const orphaned: ImpactItem[] = [];
@@ -288,12 +265,15 @@ export function getFeatureImpact(db: Database.Database, filePaths: string[]): Im
288
265
  const unaffected: ImpactItem[] = [];
289
266
 
290
267
  for (const featureId of affectedFeatureIds) {
291
- const feature = featuresById.get(featureId);
292
- if (!feature) continue;
268
+ const feature = getFeatureById(db, featureId);
269
+ if (!feature || feature.status !== 'active') continue;
293
270
 
294
- const comps = componentsByFeature.get(featureId) ?? [];
295
- const affected = comps.filter(c => fileSet.has(c.component_file));
296
- const remaining = comps.filter(c => !fileSet.has(c.component_file));
271
+ const allComponents = db.prepare(
272
+ 'SELECT component_file, is_primary FROM massu_sentinel_components WHERE feature_id = ?'
273
+ ).all(featureId) as { component_file: string; is_primary: number }[];
274
+
275
+ const affected = allComponents.filter(c => fileSet.has(c.component_file));
276
+ const remaining = allComponents.filter(c => !fileSet.has(c.component_file));
297
277
  const primaryAffected = affected.some(c => c.is_primary);
298
278
 
299
279
  const item: ImpactItem = {
@@ -403,7 +383,7 @@ export function validateFeatures(db: Database.Database, domainFilter?: string):
403
383
  sql += ' AND domain = ?';
404
384
  params.push(domainFilter);
405
385
  }
406
- sql += ' ORDER BY domain, feature_key LIMIT 1000';
386
+ sql += ' ORDER BY domain, feature_key';
407
387
 
408
388
  const features = db.prepare(sql).all(...params) as Record<string, unknown>[];
409
389
  const details: ValidationItem[] = [];
@@ -489,63 +469,39 @@ export function validateFeatures(db: Database.Database, domainFilter?: string):
489
469
  // ============================================================
490
470
 
491
471
  export function checkParity(db: Database.Database, oldFiles: string[], newFiles: string[]): ParityReport {
492
- if (oldFiles.length === 0 && newFiles.length === 0) {
493
- return { done: [], gaps: [], new_features: [], parity_percentage: 100 };
494
- }
472
+ const oldFileSet = new Set(oldFiles);
473
+ const newFileSet = new Set(newFiles);
495
474
 
496
- // Batch: find features linked to old files in a single query
475
+ // Find features linked to old files
497
476
  const oldFeatureIds = new Set<number>();
498
- if (oldFiles.length > 0) {
499
- const ph = oldFiles.map(() => '?').join(',');
500
- const links = db.prepare(`SELECT DISTINCT feature_id FROM massu_sentinel_components WHERE component_file IN (${ph})`).all(...oldFiles) as { feature_id: number }[];
501
- for (const link of links) oldFeatureIds.add(link.feature_id);
502
- }
503
-
504
- // Batch: find features linked to new files in a single query
505
- const newFeatureIds = new Set<number>();
506
- if (newFiles.length > 0) {
507
- const ph = newFiles.map(() => '?').join(',');
508
- const links = db.prepare(`SELECT DISTINCT feature_id FROM massu_sentinel_components WHERE component_file IN (${ph})`).all(...newFiles) as { feature_id: number }[];
509
- for (const link of links) newFeatureIds.add(link.feature_id);
510
- }
511
-
512
- // Batch: load all referenced features in a single query
513
- const allIds = [...new Set([...oldFeatureIds, ...newFeatureIds])];
514
- const featuresById = new Map<number, Feature>();
515
- if (allIds.length > 0) {
516
- const ph = allIds.map(() => '?').join(',');
517
- const rows = db.prepare(`SELECT * FROM massu_sentinel WHERE id IN (${ph})`).all(...allIds) as Record<string, unknown>[];
518
- for (const row of rows) {
519
- const f = toFeature(row);
520
- featuresById.set(f.id, f);
477
+ for (const file of oldFiles) {
478
+ const links = db.prepare('SELECT feature_id FROM massu_sentinel_components WHERE component_file = ?').all(file) as { feature_id: number }[];
479
+ for (const link of links) {
480
+ oldFeatureIds.add(link.feature_id);
521
481
  }
522
482
  }
523
483
 
524
- // Batch: load all components for referenced features
525
- const componentsByFeature = new Map<number, Array<{ component_file: string }>>();
526
- if (allIds.length > 0) {
527
- const ph = allIds.map(() => '?').join(',');
528
- const comps = db.prepare(`SELECT feature_id, component_file FROM massu_sentinel_components WHERE feature_id IN (${ph})`).all(...allIds) as { feature_id: number; component_file: string }[];
529
- for (const comp of comps) {
530
- let list = componentsByFeature.get(comp.feature_id);
531
- if (!list) { list = []; componentsByFeature.set(comp.feature_id, list); }
532
- list.push(comp);
484
+ // Find features linked to new files
485
+ const newFeatureIds = new Set<number>();
486
+ for (const file of newFiles) {
487
+ const links = db.prepare('SELECT feature_id FROM massu_sentinel_components WHERE component_file = ?').all(file) as { feature_id: number }[];
488
+ for (const link of links) {
489
+ newFeatureIds.add(link.feature_id);
533
490
  }
534
491
  }
535
492
 
536
- const oldFileSet = new Set(oldFiles);
537
- const newFileSet = new Set(newFiles);
538
493
  const done: ParityItem[] = [];
539
494
  const gaps: ParityItem[] = [];
540
- const newFeaturesList: ParityItem[] = [];
495
+ const newFeatures: ParityItem[] = [];
541
496
 
497
+ // Features in old that are also in new = DONE
498
+ // Features in old but NOT in new = GAP
542
499
  for (const fId of oldFeatureIds) {
543
- const feature = featuresById.get(fId);
500
+ const feature = getFeatureById(db, fId);
544
501
  if (!feature) continue;
545
502
 
546
- const comps = componentsByFeature.get(fId) ?? [];
547
- const oldComps = comps.filter(c => oldFileSet.has(c.component_file));
548
- const newComps = comps.filter(c => newFileSet.has(c.component_file));
503
+ const oldComps = db.prepare('SELECT component_file FROM massu_sentinel_components WHERE feature_id = ? AND component_file IN (' + oldFiles.map(() => '?').join(',') + ')').all(fId, ...oldFiles) as { component_file: string }[];
504
+ const newComps = db.prepare('SELECT component_file FROM massu_sentinel_components WHERE feature_id = ? AND component_file IN (' + newFiles.map(() => '?').join(',') + ')').all(fId, ...newFiles) as { component_file: string }[];
549
505
 
550
506
  const item: ParityItem = {
551
507
  feature_key: feature.feature_key,
@@ -562,15 +518,15 @@ export function checkParity(db: Database.Database, oldFiles: string[], newFiles:
562
518
  }
563
519
  }
564
520
 
521
+ // Features only in new = NEW
565
522
  for (const fId of newFeatureIds) {
566
523
  if (oldFeatureIds.has(fId)) continue;
567
- const feature = featuresById.get(fId);
524
+ const feature = getFeatureById(db, fId);
568
525
  if (!feature) continue;
569
526
 
570
- const comps = componentsByFeature.get(fId) ?? [];
571
- const newComps = comps.filter(c => newFileSet.has(c.component_file));
527
+ const newComps = db.prepare('SELECT component_file FROM massu_sentinel_components WHERE feature_id = ? AND component_file IN (' + newFiles.map(() => '?').join(',') + ')').all(fId, ...newFiles) as { component_file: string }[];
572
528
 
573
- newFeaturesList.push({
529
+ newFeatures.push({
574
530
  feature_key: feature.feature_key,
575
531
  title: feature.title,
576
532
  status: 'NEW',
@@ -582,7 +538,7 @@ export function checkParity(db: Database.Database, oldFiles: string[], newFiles:
582
538
  const total = done.length + gaps.length;
583
539
  const parityPercentage = total > 0 ? Math.round((done.length / total) * 100) : 100;
584
540
 
585
- return { done, gaps, new_features: newFeaturesList, parity_percentage: parityPercentage };
541
+ return { done, gaps, new_features: newFeatures, parity_percentage: parityPercentage };
586
542
  }
587
543
 
588
544
  // ============================================================
@@ -2,8 +2,7 @@
2
2
  // Licensed under BSL 1.1 - see LICENSE file for details.
3
3
 
4
4
  import type Database from 'better-sqlite3';
5
- import type { ToolDefinition, ToolResult } from './tool-helpers.ts';
6
- import { p, text } from './tool-helpers.ts';
5
+ import type { ToolDefinition, ToolResult } from './tools.ts';
7
6
  import {
8
7
  searchFeatures,
9
8
  getFeatureDetail,
@@ -19,19 +18,17 @@ import {
19
18
  import type { ComponentRole, FeatureStatus, FeaturePriority } from './sentinel-types.ts';
20
19
  import { getConfig } from './config.ts';
21
20
 
21
+ /** Prefix a base tool name with the configured tool prefix. */
22
+ function p(baseName: string): string {
23
+ return `${getConfig().toolPrefix}_${baseName}`;
24
+ }
25
+
22
26
  // ============================================================
23
27
  // Sentinel: MCP Tool Definitions & Handlers
24
28
  // ============================================================
25
29
 
26
- const SENTINEL_BASE_NAMES = new Set([
27
- 'sentinel_search', 'sentinel_detail', 'sentinel_impact',
28
- 'sentinel_validate', 'sentinel_register', 'sentinel_parity',
29
- ]);
30
-
31
- export function isSentinelTool(name: string): boolean {
32
- const pfx = getConfig().toolPrefix + '_';
33
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
34
- return SENTINEL_BASE_NAMES.has(baseName);
30
+ function text(content: string): ToolResult {
31
+ return { content: [{ type: 'text', text: content }] };
35
32
  }
36
33
 
37
34
  export function getSentinelToolDefinitions(): ToolDefinition[] {
package/src/server.ts CHANGED
@@ -11,10 +11,24 @@
11
11
  * Tool names are configurable via massu.config.yaml toolPrefix.
12
12
  */
13
13
 
14
+ import { readFileSync } from 'fs';
15
+ import { resolve, dirname } from 'path';
16
+ import { fileURLToPath } from 'url';
14
17
  import { getCodeGraphDb, getDataDb } from './db.ts';
15
18
  import { getConfig } from './config.ts';
16
19
  import { getToolDefinitions, handleToolCall } from './tools.ts';
17
20
  import { getMemoryDb, pruneOldConversationTurns, pruneOldObservations } from './memory-db.ts';
21
+ import { getCurrentTier } from './license.ts';
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const PKG_VERSION = (() => {
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
27
+ return pkg.version ?? '0.0.0';
28
+ } catch {
29
+ return '0.0.0';
30
+ }
31
+ })();
18
32
 
19
33
  interface JsonRpcRequest {
20
34
  jsonrpc: '2.0';
@@ -40,7 +54,7 @@ function getDb() {
40
54
  return { codegraphDb, dataDb: dataDb };
41
55
  }
42
56
 
43
- function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
57
+ async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
44
58
  const { method, params, id } = request;
45
59
 
46
60
  switch (method) {
@@ -54,8 +68,8 @@ function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
54
68
  tools: {},
55
69
  },
56
70
  serverInfo: {
57
- name: 'massu',
58
- version: '0.1.0',
71
+ name: getConfig().toolPrefix || 'massu',
72
+ version: PKG_VERSION,
59
73
  },
60
74
  },
61
75
  };
@@ -80,7 +94,7 @@ function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
80
94
  const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
81
95
 
82
96
  const { codegraphDb: cgDb, dataDb: lDb } = getDb();
83
- const result = handleToolCall(toolName, toolArgs, lDb, cgDb);
97
+ const result = await handleToolCall(toolName, toolArgs, lDb, cgDb);
84
98
 
85
99
  return {
86
100
  jsonrpc: '2.0',
@@ -133,12 +147,21 @@ function pruneMemoryOnStartup(): void {
133
147
 
134
148
  pruneMemoryOnStartup();
135
149
 
150
+ // === License init: pre-cache tier status ===
151
+ getCurrentTier().then(tier => {
152
+ process.stderr.write(`massu: License tier: ${tier}\n`);
153
+ }).catch(error => {
154
+ process.stderr.write(
155
+ `massu: License check failed (non-fatal): ${error instanceof Error ? error.message : String(error)}\n`
156
+ );
157
+ });
158
+
136
159
  // === stdio JSON-RPC transport ===
137
160
 
138
161
  let buffer = '';
139
162
 
140
163
  process.stdin.setEncoding('utf-8');
141
- process.stdin.on('data', (chunk: string) => {
164
+ process.stdin.on('data', async (chunk: string) => {
142
165
  buffer += chunk;
143
166
 
144
167
  // Process complete messages (newline-delimited JSON-RPC)
@@ -151,7 +174,7 @@ process.stdin.on('data', (chunk: string) => {
151
174
 
152
175
  try {
153
176
  const request = JSON.parse(line) as JsonRpcRequest;
154
- const response = handleRequest(request);
177
+ const response = await handleRequest(request);
155
178
 
156
179
  // Don't send responses for notifications (no id)
157
180
  if (request.id !== undefined) {
@@ -182,7 +205,6 @@ process.stdin.on('end', () => {
182
205
  // Handle errors gracefully
183
206
  process.on('uncaughtException', (error) => {
184
207
  process.stderr.write(`massu: Uncaught exception: ${error.message}\n`);
185
- process.exit(1);
186
208
  });
187
209
 
188
210
  process.on('unhandledRejection', (reason) => {
@@ -5,14 +5,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from '
5
5
  import { resolve, dirname } from 'path';
6
6
  import type Database from 'better-sqlite3';
7
7
  import { generateCurrentMd } from './session-state-generator.ts';
8
- import { getProjectRoot } from './config.ts';
8
+ import { getResolvedPaths } from './config.ts';
9
9
 
10
10
  // ============================================================
11
11
  // P5-002: Session Archiver
12
12
  // ============================================================
13
13
 
14
- const PROJECT_ROOT = getProjectRoot();
15
-
16
14
  /**
17
15
  * Archive the current CURRENT.md and generate a new one from memory DB.
18
16
  */
@@ -21,8 +19,9 @@ export function archiveAndRegenerate(db: Database.Database, sessionId: string):
21
19
  archivePath?: string;
22
20
  newContent: string;
23
21
  } {
24
- const currentMdPath = resolve(PROJECT_ROOT, '.claude/session-state/CURRENT.md');
25
- const archiveDir = resolve(PROJECT_ROOT, '.claude/session-state/archive');
22
+ const resolved = getResolvedPaths();
23
+ const currentMdPath = resolved.sessionStatePath;
24
+ const archiveDir = resolved.sessionArchivePath;
26
25
  let archived = false;
27
26
  let archivePath: string | undefined;
28
27
 
@@ -2,14 +2,18 @@
2
2
  // Licensed under BSL 1.1 - see LICENSE file for details.
3
3
 
4
4
  import type Database from 'better-sqlite3';
5
- import type { ToolDefinition, ToolResult } from './tool-helpers.ts';
6
- import { p, text } from './tool-helpers.ts';
5
+ import type { ToolDefinition, ToolResult } from './tools.ts';
7
6
  import { getConfig } from './config.ts';
8
7
 
9
8
  // ============================================================
10
9
  // Team Knowledge Graph
11
10
  // ============================================================
12
11
 
12
+ /** Prefix a base tool name with the configured tool prefix. */
13
+ function p(baseName: string): string {
14
+ return `${getConfig().toolPrefix}_${baseName}`;
15
+ }
16
+
13
17
  /**
14
18
  * Calculate expertise score for a developer in a module.
15
19
  * Based on session depth (how many sessions) and observation quality.
@@ -398,3 +402,6 @@ function handleTeamConflicts(args: Record<string, unknown>, db: Database.Databas
398
402
  return text(lines.join('\n'));
399
403
  }
400
404
 
405
+ function text(content: string): ToolResult {
406
+ return { content: [{ type: 'text', text: content }] };
407
+ }