@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
package/src/config.ts CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { resolve, dirname } from 'path';
5
5
  import { existsSync, readFileSync } from 'fs';
6
+ import { homedir } from 'os';
6
7
  import { parse as parseYaml } from 'yaml';
7
8
  import { z } from 'zod';
8
9
 
@@ -159,6 +160,45 @@ const CloudConfigSchema = z.object({
159
160
  }).optional();
160
161
  export type CloudConfig = z.infer<typeof CloudConfigSchema>;
161
162
 
163
+ // --- Conventions Config ---
164
+ const ConventionsConfigSchema = z.object({
165
+ claudeDirName: z.string().default('.claude').refine(
166
+ s => !s.includes('..') && !s.startsWith('/'),
167
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
168
+ ),
169
+ sessionStatePath: z.string().default('.claude/session-state/CURRENT.md').refine(
170
+ s => !s.includes('..') && !s.startsWith('/'),
171
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
172
+ ),
173
+ sessionArchivePath: z.string().default('.claude/session-state/archive').refine(
174
+ s => !s.includes('..') && !s.startsWith('/'),
175
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
176
+ ),
177
+ knowledgeCategories: z.array(z.string()).default([
178
+ 'patterns', 'commands', 'incidents', 'reference', 'protocols',
179
+ 'checklists', 'playbooks', 'critical', 'scripts', 'status',
180
+ 'templates', 'loop-state', 'session-state', 'agents',
181
+ ]),
182
+ knowledgeSourceFiles: z.array(z.string()).default(['CLAUDE.md', 'MEMORY.md', 'corrections.md']),
183
+ excludePatterns: z.array(z.string()).default(['/ARCHIVE/', '/SESSION-HISTORY/']),
184
+ }).optional();
185
+ export type ConventionsConfig = z.infer<typeof ConventionsConfigSchema>;
186
+
187
+ // --- Python Config ---
188
+ const PythonDomainConfigSchema = z.object({
189
+ name: z.string(),
190
+ packages: z.array(z.string()),
191
+ allowed_imports_from: z.array(z.string()).default([]),
192
+ });
193
+
194
+ const PythonConfigSchema = z.object({
195
+ root: z.string(),
196
+ alembic_dir: z.string().optional(),
197
+ domains: z.array(PythonDomainConfigSchema).default([]),
198
+ exclude_dirs: z.array(z.string()).default(['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache']),
199
+ }).optional();
200
+ export type PythonConfig = z.infer<typeof PythonConfigSchema>;
201
+
162
202
  // --- Paths Config ---
163
203
  const PathsConfigSchema = z.object({
164
204
  source: z.string().default('src'),
@@ -198,6 +238,8 @@ const RawConfigSchema = z.object({
198
238
  team: TeamConfigSchema,
199
239
  regression: RegressionConfigSchema,
200
240
  cloud: CloudConfigSchema,
241
+ conventions: ConventionsConfigSchema,
242
+ python: PythonConfigSchema,
201
243
  }).passthrough();
202
244
 
203
245
  // --- Final Config interface (derived from Zod) ---
@@ -217,6 +259,8 @@ export interface Config {
217
259
  team?: TeamConfig;
218
260
  regression?: RegressionConfig;
219
261
  cloud?: CloudConfig;
262
+ conventions?: ConventionsConfig;
263
+ python?: PythonConfig;
220
264
  }
221
265
 
222
266
  let _config: Config | null = null;
@@ -312,8 +356,20 @@ export function getConfig(): Config {
312
356
  team: parsed.team,
313
357
  regression: parsed.regression,
314
358
  cloud: parsed.cloud,
359
+ conventions: parsed.conventions,
360
+ python: parsed.python,
315
361
  };
316
362
 
363
+ // Allow environment variable override for API key (security best practice)
364
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
365
+ _config.cloud = {
366
+ enabled: true,
367
+ sync: { memory: true, analytics: true, audit: true },
368
+ ..._config.cloud,
369
+ apiKey: process.env.MASSU_API_KEY,
370
+ };
371
+ }
372
+
317
373
  return _config;
318
374
  }
319
375
 
@@ -324,6 +380,7 @@ export function getConfig(): Config {
324
380
  export function getResolvedPaths() {
325
381
  const config = getConfig();
326
382
  const root = getProjectRoot();
383
+ const claudeDirName = config.conventions?.claudeDirName ?? '.claude';
327
384
 
328
385
  return {
329
386
  codegraphDbPath: resolve(root, '.codegraph/codegraph.db'),
@@ -340,11 +397,20 @@ export function getResolvedPaths() {
340
397
  ) as Record<string, string>,
341
398
  extensions: ['.ts', '.tsx', '.js', '.jsx'] as const,
342
399
  indexFiles: ['index.ts', 'index.tsx', 'index.js', 'index.jsx'] as const,
343
- patternsDir: resolve(root, '.claude/patterns'),
344
- claudeMdPath: resolve(root, '.claude/CLAUDE.md'),
400
+ patternsDir: resolve(root, claudeDirName, 'patterns'),
401
+ claudeMdPath: resolve(root, claudeDirName, 'CLAUDE.md'),
345
402
  docsMapPath: resolve(root, '.massu/docs-map.json'),
346
403
  helpSitePath: resolve(root, '../' + config.project.name + '-help'),
347
404
  memoryDbPath: resolve(root, '.massu/memory.db'),
405
+ knowledgeDbPath: resolve(root, '.massu/knowledge.db'),
406
+ plansDir: resolve(root, 'docs/plans'),
407
+ docsDir: resolve(root, 'docs'),
408
+ claudeDir: resolve(root, claudeDirName),
409
+ memoryDir: resolve(homedir(), claudeDirName, 'projects', root.replace(/\//g, '-'), 'memory'),
410
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
411
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
412
+ mcpJsonPath: resolve(root, '.mcp.json'),
413
+ settingsLocalPath: resolve(root, claudeDirName, 'settings.local.json'),
348
414
  };
349
415
  }
350
416
 
@@ -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 type { TranscriptEntry } from './transcript-parser.ts';
8
7
  import { getConfig } from './config.ts';
9
8
 
@@ -11,13 +10,17 @@ import { getConfig } from './config.ts';
11
10
  // Cost Attribution Tracking
12
11
  // ============================================================
13
12
 
13
+ /** Prefix a base tool name with the configured tool prefix. */
14
+ function p(baseName: string): string {
15
+ return `${getConfig().toolPrefix}_${baseName}`;
16
+ }
17
+
14
18
  /** Default model pricing (Claude models). Can be overridden via config.analytics.cost.models */
15
19
  const DEFAULT_MODEL_PRICING: Record<string, { input_per_million: number; output_per_million: number; cache_read_per_million?: number; cache_write_per_million?: number }> = {
16
- 'claude-opus-4-6': { input_per_million: 5.00, output_per_million: 25.00, cache_read_per_million: 0.50, cache_write_per_million: 6.25 },
20
+ 'claude-opus-4-6': { input_per_million: 15.00, output_per_million: 75.00, cache_read_per_million: 1.50, cache_write_per_million: 18.75 },
17
21
  'claude-sonnet-4-6': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
18
22
  'claude-sonnet-4-5': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
19
- 'claude-3-5-haiku-20241022': { input_per_million: 0.80, output_per_million: 4.00, cache_read_per_million: 0.08, cache_write_per_million: 1.00 },
20
- 'claude-haiku-4-5-20251001': { input_per_million: 1.00, output_per_million: 5.00, cache_read_per_million: 0.10, cache_write_per_million: 1.25 },
23
+ 'claude-haiku-4-5-20251001': { input_per_million: 0.80, output_per_million: 4.00, cache_read_per_million: 0.08, cache_write_per_million: 1.00 },
21
24
  'default': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
22
25
  };
23
26
 
@@ -134,7 +137,6 @@ export function backfillSessionCosts(db: Database.Database): number {
134
137
  FROM sessions s
135
138
  LEFT JOIN session_costs c ON s.session_id = c.session_id
136
139
  WHERE c.session_id IS NULL
137
- LIMIT 1000
138
140
  `).all() as Array<{ session_id: string }>;
139
141
 
140
142
  // Backfilling requires transcript data which may not be available
@@ -348,3 +350,6 @@ function handleCostFeature(args: Record<string, unknown>, db: Database.Database)
348
350
  return text(lines.join('\n'));
349
351
  }
350
352
 
353
+ function text(content: string): ToolResult {
354
+ return { content: [{ type: 'text', text: content }] };
355
+ }
package/src/db.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  // Licensed under BSL 1.1 - see LICENSE file for details.
3
3
 
4
4
  import Database from 'better-sqlite3';
5
- import { dirname } from 'path';
6
- import { existsSync, mkdirSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+ import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
7
7
  import { getResolvedPaths } from './config.ts';
8
8
 
9
9
  /**
@@ -96,6 +96,84 @@ function initDataSchema(db: Database.Database): void {
96
96
  value TEXT NOT NULL
97
97
  );
98
98
 
99
+ -- ============================================================
100
+ -- Python Code Intelligence Tables
101
+ -- ============================================================
102
+
103
+ CREATE TABLE IF NOT EXISTS massu_py_imports (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ source_file TEXT NOT NULL,
106
+ target_file TEXT NOT NULL,
107
+ import_type TEXT NOT NULL CHECK(import_type IN ('absolute', 'relative', 'from_absolute', 'from_relative')),
108
+ imported_names TEXT NOT NULL DEFAULT '[]',
109
+ line INTEGER NOT NULL DEFAULT 0
110
+ );
111
+ CREATE INDEX IF NOT EXISTS idx_massu_py_imports_source ON massu_py_imports(source_file);
112
+ CREATE INDEX IF NOT EXISTS idx_massu_py_imports_target ON massu_py_imports(target_file);
113
+
114
+ CREATE TABLE IF NOT EXISTS massu_py_meta (
115
+ key TEXT PRIMARY KEY,
116
+ value TEXT NOT NULL
117
+ );
118
+
119
+ CREATE TABLE IF NOT EXISTS massu_py_routes (
120
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
121
+ file TEXT NOT NULL,
122
+ method TEXT NOT NULL,
123
+ path TEXT NOT NULL,
124
+ function_name TEXT NOT NULL,
125
+ dependencies TEXT NOT NULL DEFAULT '[]',
126
+ request_model TEXT,
127
+ response_model TEXT,
128
+ is_authenticated INTEGER NOT NULL DEFAULT 0,
129
+ line INTEGER NOT NULL DEFAULT 0
130
+ );
131
+ CREATE INDEX IF NOT EXISTS idx_massu_py_routes_path ON massu_py_routes(path);
132
+ CREATE INDEX IF NOT EXISTS idx_massu_py_routes_file ON massu_py_routes(file);
133
+
134
+ CREATE TABLE IF NOT EXISTS massu_py_route_callers (
135
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
136
+ route_id INTEGER NOT NULL REFERENCES massu_py_routes(id) ON DELETE CASCADE,
137
+ frontend_file TEXT NOT NULL,
138
+ line INTEGER NOT NULL DEFAULT 0,
139
+ call_pattern TEXT NOT NULL
140
+ );
141
+ CREATE INDEX IF NOT EXISTS idx_massu_py_route_callers_route ON massu_py_route_callers(route_id);
142
+
143
+ CREATE TABLE IF NOT EXISTS massu_py_models (
144
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
145
+ class_name TEXT NOT NULL,
146
+ table_name TEXT,
147
+ file TEXT NOT NULL,
148
+ line INTEGER NOT NULL DEFAULT 0,
149
+ columns TEXT NOT NULL DEFAULT '[]',
150
+ relationships TEXT NOT NULL DEFAULT '[]',
151
+ foreign_keys TEXT NOT NULL DEFAULT '[]'
152
+ );
153
+ CREATE INDEX IF NOT EXISTS idx_massu_py_models_file ON massu_py_models(file);
154
+ CREATE INDEX IF NOT EXISTS idx_massu_py_models_table ON massu_py_models(table_name);
155
+
156
+ CREATE TABLE IF NOT EXISTS massu_py_fk_edges (
157
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
158
+ source_table TEXT NOT NULL,
159
+ source_column TEXT NOT NULL,
160
+ target_table TEXT NOT NULL,
161
+ target_column TEXT NOT NULL
162
+ );
163
+ CREATE INDEX IF NOT EXISTS idx_massu_py_fk_source ON massu_py_fk_edges(source_table);
164
+ CREATE INDEX IF NOT EXISTS idx_massu_py_fk_target ON massu_py_fk_edges(target_table);
165
+
166
+ CREATE TABLE IF NOT EXISTS massu_py_migrations (
167
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
168
+ revision TEXT NOT NULL UNIQUE,
169
+ down_revision TEXT,
170
+ file TEXT NOT NULL,
171
+ description TEXT,
172
+ operations TEXT NOT NULL DEFAULT '[]',
173
+ is_head INTEGER NOT NULL DEFAULT 0
174
+ );
175
+ CREATE INDEX IF NOT EXISTS idx_massu_py_migrations_rev ON massu_py_migrations(revision);
176
+
99
177
  -- ============================================================
100
178
  -- Sentinel: Feature Registry Tables
101
179
  -- ============================================================
@@ -231,3 +309,38 @@ export function isDataStale(dataDb: Database.Database, codegraphDb: Database.Dat
231
309
  export function updateBuildTimestamp(dataDb: Database.Database): void {
232
310
  dataDb.prepare("INSERT OR REPLACE INTO massu_meta (key, value) VALUES ('last_build_time', ?)").run(new Date().toISOString());
233
311
  }
312
+
313
+ /**
314
+ * Check if Python indexes are stale based on massu_py_meta.last_build_time.
315
+ */
316
+ export function isPythonDataStale(dataDb: Database.Database, pythonRoot: string): boolean {
317
+ const lastBuild = dataDb.prepare("SELECT value FROM massu_py_meta WHERE key = 'last_build_time'").get() as { value: string } | undefined;
318
+ if (!lastBuild) return true;
319
+
320
+ const lastBuildTime = new Date(lastBuild.value).getTime();
321
+ // Check if any .py file is newer than last build
322
+ function checkDir(dir: string): boolean {
323
+ try {
324
+ const entries = readdirSync(dir, { withFileTypes: true });
325
+ for (const entry of entries) {
326
+ const fullPath = join(dir, entry.name);
327
+ if (entry.isDirectory()) {
328
+ if (['__pycache__', '.venv', 'venv', 'node_modules', '.mypy_cache', '.pytest_cache'].includes(entry.name)) continue;
329
+ if (checkDir(fullPath)) return true;
330
+ } else if (entry.name.endsWith('.py')) {
331
+ if (statSync(fullPath).mtimeMs > lastBuildTime) return true;
332
+ }
333
+ }
334
+ } catch { /* directory may not exist */ }
335
+ return false;
336
+ }
337
+
338
+ return checkDir(pythonRoot);
339
+ }
340
+
341
+ /**
342
+ * Update the Python build timestamp in massu_py_meta.
343
+ */
344
+ export function updatePythonBuildTimestamp(dataDb: Database.Database): void {
345
+ dataDb.prepare("INSERT OR REPLACE INTO massu_py_meta (key, value) VALUES ('last_build_time', ?)").run(new Date().toISOString());
346
+ }
@@ -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 { getConfig } from './config.ts';
8
7
  import { existsSync, readFileSync } from 'fs';
9
8
  import { resolve } from 'path';
@@ -12,6 +11,11 @@ import { resolve } from 'path';
12
11
  // Dependency Risk Scoring
13
12
  // ============================================================
14
13
 
14
+ /** Prefix a base tool name with the configured tool prefix. */
15
+ function p(baseName: string): string {
16
+ return `${getConfig().toolPrefix}_${baseName}`;
17
+ }
18
+
15
19
  export interface DepRiskFactors {
16
20
  vulnerabilities: number;
17
21
  lastPublishDays: number | null;
@@ -328,3 +332,6 @@ function handleDepAlternatives(args: Record<string, unknown>, db: Database.Datab
328
332
  return text(lines.filter(Boolean).join('\n'));
329
333
  }
330
334
 
335
+ function text(content: string): ToolResult {
336
+ return { content: [{ type: 'text', text: content }] };
337
+ }
package/src/docs-tools.ts CHANGED
@@ -3,23 +3,20 @@
3
3
 
4
4
  import { readFileSync, existsSync } from 'fs';
5
5
  import { resolve, basename } from 'path';
6
- import { getConfig, getResolvedPaths } from './config.ts';
7
- import type { ToolDefinition, ToolResult } from './tool-helpers.ts';
8
- import { p, text } from './tool-helpers.ts';
6
+ import { getConfig, getResolvedPaths, getProjectRoot } from './config.ts';
7
+ import { ensureWithinRoot } from './security-utils.ts';
8
+ import type { ToolDefinition, ToolResult } from './tools.ts';
9
+
10
+ /** Prefix a base tool name with the configured tool prefix. */
11
+ function p(baseName: string): string {
12
+ return `${getConfig().toolPrefix}_${baseName}`;
13
+ }
9
14
 
10
15
  // ============================================================
11
16
  // Help Site Auto-Sync: MCP Docs Tools
12
17
  // docs_audit + docs_coverage
13
18
  // ============================================================
14
19
 
15
- const DOCS_BASE_NAMES = new Set(['docs_audit', 'docs_coverage']);
16
-
17
- export function isDocsTool(name: string): boolean {
18
- const pfx = getConfig().toolPrefix + '_';
19
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
20
- return DOCS_BASE_NAMES.has(baseName);
21
- }
22
-
23
20
  interface DocsMapping {
24
21
  id: string;
25
22
  helpPage: string;
@@ -256,10 +253,11 @@ function extractFrontmatter(content: string): Record<string, string> | null {
256
253
  * Extract procedure names from a router file.
257
254
  */
258
255
  function extractProcedureNames(routerPath: string): string[] {
259
- const absPath = resolve(getResolvedPaths().srcDir, '..', routerPath);
256
+ const root = getProjectRoot();
257
+ const absPath = ensureWithinRoot(resolve(getResolvedPaths().srcDir, '..', routerPath), root);
260
258
  if (!existsSync(absPath)) {
261
259
  // Try from project root
262
- const altPath = resolve(getResolvedPaths().srcDir, '../server/api/routers', basename(routerPath));
260
+ const altPath = ensureWithinRoot(resolve(getResolvedPaths().srcDir, '../server/api/routers', basename(routerPath)), root);
263
261
  if (!existsSync(altPath)) return [];
264
262
  return extractProcedureNamesFromContent(readFileSync(altPath, 'utf-8'));
265
263
  }
@@ -329,7 +327,7 @@ function handleDocsAudit(args: Record<string, unknown>): ToolResult {
329
327
  const mapping = docsMap.mappings.find(m => m.id === mappingId);
330
328
  if (!mapping) continue;
331
329
 
332
- const helpPagePath = resolve(getResolvedPaths().helpSitePath, mapping.helpPage);
330
+ const helpPagePath = ensureWithinRoot(resolve(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
333
331
 
334
332
  if (!existsSync(helpPagePath)) {
335
333
  results.push({
@@ -398,7 +396,7 @@ function handleDocsAudit(args: Record<string, unknown>): ToolResult {
398
396
  // Also flag inherited user guides
399
397
  for (const [guideName, parentId] of Object.entries(docsMap.userGuideInheritance.examples)) {
400
398
  if (parentId === mappingId) {
401
- const guidePath = resolve(getResolvedPaths().helpSitePath, `pages/user-guides/${guideName}/index.mdx`);
399
+ const guidePath = ensureWithinRoot(resolve(getResolvedPaths().helpSitePath, `pages/user-guides/${guideName}/index.mdx`), getProjectRoot());
402
400
  if (existsSync(guidePath)) {
403
401
  const guideContent = readFileSync(guidePath, 'utf-8');
404
402
  const guideFrontmatter = extractFrontmatter(guideContent);
@@ -443,7 +441,7 @@ function handleDocsCoverage(args: Record<string, unknown>): ToolResult {
443
441
  : docsMap.mappings;
444
442
 
445
443
  for (const mapping of mappings) {
446
- const helpPagePath = resolve(getResolvedPaths().helpSitePath, mapping.helpPage);
444
+ const helpPagePath = ensureWithinRoot(resolve(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
447
445
  const exists = existsSync(helpPagePath);
448
446
  let hasContent = false;
449
447
  let lineCount = 0;
@@ -512,3 +510,10 @@ function handleDocsCoverage(args: Record<string, unknown>): ToolResult {
512
510
  return text(lines.join('\n'));
513
511
  }
514
512
 
513
+ // ============================================================
514
+ // Utilities
515
+ // ============================================================
516
+
517
+ function text(content: string): ToolResult {
518
+ return { content: [{ type: 'text', text: content }] };
519
+ }
@@ -37,7 +37,7 @@ async function main(): Promise<void> {
37
37
  const rel = filePath.startsWith(root + '/') ? filePath.slice(root.length + 1) : filePath;
38
38
 
39
39
  // Only process src/ files
40
- if (!rel.startsWith('src/')) {
40
+ if (!rel.startsWith('src/') && !rel.endsWith('.py')) {
41
41
  process.exit(0);
42
42
  return;
43
43
  }
@@ -54,12 +54,12 @@ async function main(): Promise<void> {
54
54
  }
55
55
  }
56
56
 
57
- // 2. Check middleware tree membership (CR-16)
57
+ // 2. Check middleware tree membership
58
58
  try {
59
59
  const dataDb = new Database(getResolvedPaths().dataDbPath, { readonly: true });
60
60
  try {
61
61
  if (isInMiddlewareTree(dataDb, rel)) {
62
- warnings.push('[CRITICAL] CR-16: This file is in the middleware import tree. No Node.js deps allowed.');
62
+ warnings.push('[CRITICAL] This file is in the middleware import tree. No Node.js deps allowed.');
63
63
  }
64
64
  } finally {
65
65
  dataDb.close();
@@ -70,7 +70,7 @@ async function main(): Promise<void> {
70
70
 
71
71
  // 3. Output warnings if any
72
72
  if (warnings.length > 0) {
73
- console.log(`[CS CONTEXT] ${warnings.join(' | ')}`);
73
+ console.log(`[Massu] ${warnings.join(' | ')}`);
74
74
  }
75
75
  } catch (_e) {
76
76
  // Best-effort: never block Claude Code
@@ -14,6 +14,9 @@ import { logAuditEntry } from '../audit-trail.ts';
14
14
  import { trackModification } from '../regression-detector.ts';
15
15
  import { validateFile, storeValidationResult } from '../validation-engine.ts';
16
16
  import { scoreFileSecurity, storeSecurityScore } from '../security-scorer.ts';
17
+ import { readFileSync, existsSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { parse as parseYaml } from 'yaml';
17
20
 
18
21
  interface HookInput {
19
22
  session_id: string;
@@ -126,6 +129,41 @@ async function main(): Promise<void> {
126
129
  } catch (_securityErr) {
127
130
  // Best-effort: never block post-tool-use
128
131
  }
132
+
133
+ // MEMORY.md integrity check on write
134
+ try {
135
+ if (tool_name === 'Edit' || tool_name === 'Write') {
136
+ const filePath = (tool_input.file_path as string) ?? '';
137
+ if (filePath && filePath.endsWith('MEMORY.md') && filePath.includes('/memory/')) {
138
+ const issues = checkMemoryFileIntegrity(filePath);
139
+ if (issues.length > 0) {
140
+ addObservation(db, session_id, 'incident_near_miss',
141
+ 'MEMORY.md integrity issue detected',
142
+ issues.join('; '),
143
+ { importance: 4 }
144
+ );
145
+ }
146
+ }
147
+ }
148
+ } catch (_memoryErr) {
149
+ // Best-effort: never block post-tool-use
150
+ }
151
+
152
+ // Knowledge index staleness check on knowledge file edits
153
+ try {
154
+ if (tool_name === 'Edit' || tool_name === 'Write') {
155
+ const filePath = (tool_input.file_path as string) ?? '';
156
+ if (filePath && isKnowledgeSourceFile(filePath)) {
157
+ addObservation(db, session_id, 'discovery',
158
+ 'Knowledge source file modified - index may be stale',
159
+ `Edited ${filePath.split('/').pop() ?? filePath}. Run knowledge re-index to update.`,
160
+ { importance: 3 }
161
+ );
162
+ }
163
+ }
164
+ } catch (_knowledgeErr) {
165
+ // Best-effort: never block post-tool-use
166
+ }
129
167
  } finally {
130
168
  db.close();
131
169
  }
@@ -172,4 +210,96 @@ function readStdin(): Promise<string> {
172
210
  });
173
211
  }
174
212
 
213
+ /**
214
+ * Read the conventions section from massu.config.yaml directly.
215
+ * Hooks are compiled with esbuild and cannot use getConfig() from config.ts.
216
+ * Falls back to sensible defaults if the config file is not found.
217
+ */
218
+ function readConventions(cwd?: string): {
219
+ knowledgeSourceFiles: string[];
220
+ claudeDirName: string;
221
+ } {
222
+ const defaults = {
223
+ knowledgeSourceFiles: ['CLAUDE.md', 'MEMORY.md', 'corrections.md'],
224
+ claudeDirName: '.claude',
225
+ };
226
+ try {
227
+ const projectRoot = cwd ?? process.cwd();
228
+ const configPath = join(projectRoot, 'massu.config.yaml');
229
+ if (!existsSync(configPath)) return defaults;
230
+ const content = readFileSync(configPath, 'utf-8');
231
+ const parsed = parseYaml(content) as Record<string, unknown> | null;
232
+ if (!parsed || typeof parsed !== 'object') return defaults;
233
+ const conventions = parsed.conventions as Record<string, unknown> | undefined;
234
+ if (!conventions || typeof conventions !== 'object') return defaults;
235
+ return {
236
+ knowledgeSourceFiles: Array.isArray(conventions.knowledgeSourceFiles)
237
+ ? conventions.knowledgeSourceFiles as string[]
238
+ : defaults.knowledgeSourceFiles,
239
+ claudeDirName: typeof conventions.claudeDirName === 'string'
240
+ ? conventions.claudeDirName
241
+ : defaults.claudeDirName,
242
+ };
243
+ } catch {
244
+ return defaults;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Check if a file path is a knowledge source file (CLAUDE.md, corrections.md,
250
+ * memory files, or knowledge system source files).
251
+ * When these are edited, the knowledge index may become stale.
252
+ */
253
+ function isKnowledgeSourceFile(filePath: string): boolean {
254
+ const basename = filePath.split('/').pop() ?? '';
255
+ const conventions = readConventions();
256
+ const knowledgeSourcePatterns = [
257
+ ...conventions.knowledgeSourceFiles,
258
+ 'file-index.md',
259
+ 'knowledge-db.ts',
260
+ 'knowledge-indexer.ts',
261
+ 'knowledge-tools.ts',
262
+ ];
263
+ return knowledgeSourcePatterns.some(p => basename === p) ||
264
+ filePath.includes('/memory/') ||
265
+ filePath.includes(conventions.claudeDirName + '/');
266
+ }
267
+
268
+ /**
269
+ * Check MEMORY.md file integrity after a write.
270
+ * Verifies: file exists, has expected structure, and is under line limit.
271
+ * Returns array of issue descriptions (empty = all good).
272
+ */
273
+ function checkMemoryFileIntegrity(filePath: string): string[] {
274
+ const issues: string[] = [];
275
+
276
+ try {
277
+ if (!existsSync(filePath)) {
278
+ issues.push('MEMORY.md file does not exist after write');
279
+ return issues;
280
+ }
281
+
282
+ const content = readFileSync(filePath, 'utf-8');
283
+ const lines = content.split('\n');
284
+
285
+ // Check line count (CLAUDE.md truncates after ~200 lines)
286
+ const MAX_LINES = 200;
287
+ if (lines.length > MAX_LINES) {
288
+ issues.push(`MEMORY.md exceeds ${MAX_LINES} lines (currently ${lines.length}). Consider archiving old entries.`);
289
+ }
290
+
291
+ // Check required structure sections
292
+ const requiredSections = ['# Massu Memory', '## Key Learnings', '## Common Gotchas'];
293
+ for (const section of requiredSections) {
294
+ if (!content.includes(section)) {
295
+ issues.push(`Missing required section: "${section}"`);
296
+ }
297
+ }
298
+ } catch (_e) {
299
+ // Graceful degradation: don't report issues if we can't check
300
+ }
301
+
302
+ return issues;
303
+ }
304
+
175
305
  main();
@@ -7,7 +7,7 @@
7
7
  // Captures current session state into DB before compaction destroys context.
8
8
  // ============================================================
9
9
 
10
- import { getMemoryDb, addSummary, createSession } from '../memory-db.ts';
10
+ import { getMemoryDb, addSummary, createSession, addObservation } from '../memory-db.ts';
11
11
  import { logAuditEntry } from '../audit-trail.ts';
12
12
  import type { SessionSummary } from '../memory-db.ts';
13
13
 
@@ -45,6 +45,28 @@ async function main(): Promise<void> {
45
45
  // 4. Store with pre_compact marker in plan_progress
46
46
  addSummary(db, session_id, summary);
47
47
 
48
+ // 5. Preserve knowledge system state for post-compaction context
49
+ try {
50
+ const knowledgeObs = observations.filter(
51
+ o => (o.title as string)?.includes('knowledge') ||
52
+ (o.title as string)?.includes('Knowledge') ||
53
+ (o.detail as string)?.includes('knowledge')
54
+ );
55
+ if (knowledgeObs.length > 0) {
56
+ const knowledgeContext = knowledgeObs
57
+ .map(o => `[${o.type}] ${o.title}`)
58
+ .join('; ');
59
+ // Store as a high-importance observation so session-start picks it up
60
+ addObservation(db, session_id, 'discovery',
61
+ 'Knowledge context preserved before compaction',
62
+ knowledgeContext,
63
+ { importance: 4 }
64
+ );
65
+ }
66
+ } catch (_knowledgeErr) {
67
+ // Best-effort: never block compaction
68
+ }
69
+
48
70
  // Log compaction event for audit trail continuity
49
71
  try {
50
72
  logAuditEntry(db, {