@massu/core 0.1.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) 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 +12521 -0
  34. package/dist/hooks/cost-tracker.js +80 -5
  35. package/dist/hooks/post-edit-context.js +72 -6
  36. package/dist/hooks/post-tool-use.js +234 -57
  37. package/dist/hooks/pre-compact.js +144 -5
  38. package/dist/hooks/pre-delete-check.js +141 -11
  39. package/dist/hooks/quality-event.js +80 -5
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +83 -8
  42. package/dist/hooks/session-start.js +153 -7
  43. package/dist/hooks/user-prompt.js +166 -5
  44. package/package.json +6 -5
  45. package/src/backfill-sessions.ts +5 -4
  46. package/src/cli.ts +6 -1
  47. package/src/commands/doctor.ts +193 -6
  48. package/src/commands/init.ts +235 -6
  49. package/src/commands/install-commands.ts +137 -0
  50. package/src/config.ts +68 -2
  51. package/src/db.ts +115 -2
  52. package/src/docs-tools.ts +8 -6
  53. package/src/hooks/post-edit-context.ts +1 -1
  54. package/src/hooks/post-tool-use.ts +130 -0
  55. package/src/hooks/pre-compact.ts +23 -1
  56. package/src/hooks/pre-delete-check.ts +92 -4
  57. package/src/hooks/security-gate.ts +32 -0
  58. package/src/hooks/session-start.ts +97 -4
  59. package/src/hooks/user-prompt.ts +46 -1
  60. package/src/import-resolver.ts +2 -1
  61. package/src/knowledge-db.ts +169 -0
  62. package/src/knowledge-indexer.ts +704 -0
  63. package/src/knowledge-tools.ts +1413 -0
  64. package/src/license.ts +482 -0
  65. package/src/memory-db.ts +14 -1
  66. package/src/observation-extractor.ts +11 -4
  67. package/src/page-deps.ts +3 -2
  68. package/src/python/coupling-detector.ts +124 -0
  69. package/src/python/domain-enforcer.ts +83 -0
  70. package/src/python/impact-analyzer.ts +95 -0
  71. package/src/python/import-parser.ts +244 -0
  72. package/src/python/import-resolver.ts +135 -0
  73. package/src/python/migration-indexer.ts +115 -0
  74. package/src/python/migration-parser.ts +332 -0
  75. package/src/python/model-indexer.ts +70 -0
  76. package/src/python/model-parser.ts +279 -0
  77. package/src/python/route-indexer.ts +58 -0
  78. package/src/python/route-parser.ts +317 -0
  79. package/src/python-tools.ts +629 -0
  80. package/src/sentinel-db.ts +2 -1
  81. package/src/server.ts +29 -6
  82. package/src/session-archiver.ts +4 -5
  83. package/src/tools.ts +283 -31
  84. package/README.md +0 -40
@@ -9,6 +9,7 @@ import { existsSync as existsSync2, mkdirSync } from "fs";
9
9
  // src/config.ts
10
10
  import { resolve, dirname } from "path";
11
11
  import { existsSync, readFileSync } from "fs";
12
+ import { homedir } from "os";
12
13
  import { parse as parseYaml } from "yaml";
13
14
  import { z } from "zod";
14
15
  var DomainConfigSchema = z.object({
@@ -140,6 +141,49 @@ var CloudConfigSchema = z.object({
140
141
  audit: z.boolean().default(true)
141
142
  }).default({ memory: true, analytics: true, audit: true })
142
143
  }).optional();
144
+ var ConventionsConfigSchema = z.object({
145
+ claudeDirName: z.string().default(".claude").refine(
146
+ (s) => !s.includes("..") && !s.startsWith("/"),
147
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
148
+ ),
149
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
150
+ (s) => !s.includes("..") && !s.startsWith("/"),
151
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
152
+ ),
153
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
154
+ (s) => !s.includes("..") && !s.startsWith("/"),
155
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
156
+ ),
157
+ knowledgeCategories: z.array(z.string()).default([
158
+ "patterns",
159
+ "commands",
160
+ "incidents",
161
+ "reference",
162
+ "protocols",
163
+ "checklists",
164
+ "playbooks",
165
+ "critical",
166
+ "scripts",
167
+ "status",
168
+ "templates",
169
+ "loop-state",
170
+ "session-state",
171
+ "agents"
172
+ ]),
173
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
174
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
175
+ }).optional();
176
+ var PythonDomainConfigSchema = z.object({
177
+ name: z.string(),
178
+ packages: z.array(z.string()),
179
+ allowed_imports_from: z.array(z.string()).default([])
180
+ });
181
+ var PythonConfigSchema = z.object({
182
+ root: z.string(),
183
+ alembic_dir: z.string().optional(),
184
+ domains: z.array(PythonDomainConfigSchema).default([]),
185
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
186
+ }).optional();
143
187
  var PathsConfigSchema = z.object({
144
188
  source: z.string().default("src"),
145
189
  aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
@@ -174,7 +218,9 @@ var RawConfigSchema = z.object({
174
218
  security: SecurityConfigSchema,
175
219
  team: TeamConfigSchema,
176
220
  regression: RegressionConfigSchema,
177
- cloud: CloudConfigSchema
221
+ cloud: CloudConfigSchema,
222
+ conventions: ConventionsConfigSchema,
223
+ python: PythonConfigSchema
178
224
  }).passthrough();
179
225
  var _config = null;
180
226
  var _projectRoot = null;
@@ -238,13 +284,24 @@ function getConfig() {
238
284
  security: parsed.security,
239
285
  team: parsed.team,
240
286
  regression: parsed.regression,
241
- cloud: parsed.cloud
287
+ cloud: parsed.cloud,
288
+ conventions: parsed.conventions,
289
+ python: parsed.python
242
290
  };
291
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
292
+ _config.cloud = {
293
+ enabled: true,
294
+ sync: { memory: true, analytics: true, audit: true },
295
+ ..._config.cloud,
296
+ apiKey: process.env.MASSU_API_KEY
297
+ };
298
+ }
243
299
  return _config;
244
300
  }
245
301
  function getResolvedPaths() {
246
302
  const config = getConfig();
247
303
  const root = getProjectRoot();
304
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
248
305
  return {
249
306
  codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
250
307
  dataDbPath: resolve(root, ".massu/data.db"),
@@ -260,11 +317,20 @@ function getResolvedPaths() {
260
317
  ),
261
318
  extensions: [".ts", ".tsx", ".js", ".jsx"],
262
319
  indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
263
- patternsDir: resolve(root, ".claude/patterns"),
264
- claudeMdPath: resolve(root, ".claude/CLAUDE.md"),
320
+ patternsDir: resolve(root, claudeDirName, "patterns"),
321
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
265
322
  docsMapPath: resolve(root, ".massu/docs-map.json"),
266
323
  helpSitePath: resolve(root, "../" + config.project.name + "-help"),
267
- memoryDbPath: resolve(root, ".massu/memory.db")
324
+ memoryDbPath: resolve(root, ".massu/memory.db"),
325
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
326
+ plansDir: resolve(root, "docs/plans"),
327
+ docsDir: resolve(root, "docs"),
328
+ claudeDir: resolve(root, claudeDirName),
329
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
330
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
331
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
332
+ mcpJsonPath: resolve(root, ".mcp.json"),
333
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
268
334
  };
269
335
  }
270
336
 
@@ -746,6 +812,15 @@ function initMemorySchema(db) {
746
812
  );
747
813
  CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
748
814
  `);
815
+ db.exec(`
816
+ CREATE TABLE IF NOT EXISTS license_cache (
817
+ api_key_hash TEXT PRIMARY KEY,
818
+ tier TEXT NOT NULL,
819
+ valid_until TEXT NOT NULL,
820
+ last_validated TEXT NOT NULL,
821
+ features TEXT DEFAULT '[]'
822
+ );
823
+ `);
749
824
  }
750
825
 
751
826
  // src/hooks/cost-tracker.ts
@@ -7,6 +7,7 @@ import Database from "better-sqlite3";
7
7
  // src/config.ts
8
8
  import { resolve, dirname } from "path";
9
9
  import { existsSync, readFileSync } from "fs";
10
+ import { homedir } from "os";
10
11
  import { parse as parseYaml } from "yaml";
11
12
  import { z } from "zod";
12
13
  var DomainConfigSchema = z.object({
@@ -138,6 +139,49 @@ var CloudConfigSchema = z.object({
138
139
  audit: z.boolean().default(true)
139
140
  }).default({ memory: true, analytics: true, audit: true })
140
141
  }).optional();
142
+ var ConventionsConfigSchema = z.object({
143
+ claudeDirName: z.string().default(".claude").refine(
144
+ (s) => !s.includes("..") && !s.startsWith("/"),
145
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
146
+ ),
147
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
148
+ (s) => !s.includes("..") && !s.startsWith("/"),
149
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
150
+ ),
151
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
152
+ (s) => !s.includes("..") && !s.startsWith("/"),
153
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
154
+ ),
155
+ knowledgeCategories: z.array(z.string()).default([
156
+ "patterns",
157
+ "commands",
158
+ "incidents",
159
+ "reference",
160
+ "protocols",
161
+ "checklists",
162
+ "playbooks",
163
+ "critical",
164
+ "scripts",
165
+ "status",
166
+ "templates",
167
+ "loop-state",
168
+ "session-state",
169
+ "agents"
170
+ ]),
171
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
172
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
173
+ }).optional();
174
+ var PythonDomainConfigSchema = z.object({
175
+ name: z.string(),
176
+ packages: z.array(z.string()),
177
+ allowed_imports_from: z.array(z.string()).default([])
178
+ });
179
+ var PythonConfigSchema = z.object({
180
+ root: z.string(),
181
+ alembic_dir: z.string().optional(),
182
+ domains: z.array(PythonDomainConfigSchema).default([]),
183
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
184
+ }).optional();
141
185
  var PathsConfigSchema = z.object({
142
186
  source: z.string().default("src"),
143
187
  aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
@@ -172,7 +216,9 @@ var RawConfigSchema = z.object({
172
216
  security: SecurityConfigSchema,
173
217
  team: TeamConfigSchema,
174
218
  regression: RegressionConfigSchema,
175
- cloud: CloudConfigSchema
219
+ cloud: CloudConfigSchema,
220
+ conventions: ConventionsConfigSchema,
221
+ python: PythonConfigSchema
176
222
  }).passthrough();
177
223
  var _config = null;
178
224
  var _projectRoot = null;
@@ -236,13 +282,24 @@ function getConfig() {
236
282
  security: parsed.security,
237
283
  team: parsed.team,
238
284
  regression: parsed.regression,
239
- cloud: parsed.cloud
285
+ cloud: parsed.cloud,
286
+ conventions: parsed.conventions,
287
+ python: parsed.python
240
288
  };
289
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
290
+ _config.cloud = {
291
+ enabled: true,
292
+ sync: { memory: true, analytics: true, audit: true },
293
+ ..._config.cloud,
294
+ apiKey: process.env.MASSU_API_KEY
295
+ };
296
+ }
241
297
  return _config;
242
298
  }
243
299
  function getResolvedPaths() {
244
300
  const config = getConfig();
245
301
  const root = getProjectRoot();
302
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
246
303
  return {
247
304
  codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
248
305
  dataDbPath: resolve(root, ".massu/data.db"),
@@ -258,11 +315,20 @@ function getResolvedPaths() {
258
315
  ),
259
316
  extensions: [".ts", ".tsx", ".js", ".jsx"],
260
317
  indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
261
- patternsDir: resolve(root, ".claude/patterns"),
262
- claudeMdPath: resolve(root, ".claude/CLAUDE.md"),
318
+ patternsDir: resolve(root, claudeDirName, "patterns"),
319
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
263
320
  docsMapPath: resolve(root, ".massu/docs-map.json"),
264
321
  helpSitePath: resolve(root, "../" + config.project.name + "-help"),
265
- memoryDbPath: resolve(root, ".massu/memory.db")
322
+ memoryDbPath: resolve(root, ".massu/memory.db"),
323
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
324
+ plansDir: resolve(root, "docs/plans"),
325
+ docsDir: resolve(root, "docs"),
326
+ claudeDir: resolve(root, claudeDirName),
327
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
328
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
329
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
330
+ mcpJsonPath: resolve(root, ".mcp.json"),
331
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
266
332
  };
267
333
  }
268
334
 
@@ -302,7 +368,7 @@ async function main() {
302
368
  }
303
369
  const root = getProjectRoot();
304
370
  const rel = filePath.startsWith(root + "/") ? filePath.slice(root.length + 1) : filePath;
305
- if (!rel.startsWith("src/")) {
371
+ if (!rel.startsWith("src/") && !rel.endsWith(".py")) {
306
372
  process.exit(0);
307
373
  return;
308
374
  }
@@ -9,6 +9,7 @@ import { existsSync as existsSync2, mkdirSync } from "fs";
9
9
  // src/config.ts
10
10
  import { resolve, dirname } from "path";
11
11
  import { existsSync, readFileSync } from "fs";
12
+ import { homedir } from "os";
12
13
  import { parse as parseYaml } from "yaml";
13
14
  import { z } from "zod";
14
15
  var DomainConfigSchema = z.object({
@@ -140,6 +141,49 @@ var CloudConfigSchema = z.object({
140
141
  audit: z.boolean().default(true)
141
142
  }).default({ memory: true, analytics: true, audit: true })
142
143
  }).optional();
144
+ var ConventionsConfigSchema = z.object({
145
+ claudeDirName: z.string().default(".claude").refine(
146
+ (s) => !s.includes("..") && !s.startsWith("/"),
147
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
148
+ ),
149
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
150
+ (s) => !s.includes("..") && !s.startsWith("/"),
151
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
152
+ ),
153
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
154
+ (s) => !s.includes("..") && !s.startsWith("/"),
155
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
156
+ ),
157
+ knowledgeCategories: z.array(z.string()).default([
158
+ "patterns",
159
+ "commands",
160
+ "incidents",
161
+ "reference",
162
+ "protocols",
163
+ "checklists",
164
+ "playbooks",
165
+ "critical",
166
+ "scripts",
167
+ "status",
168
+ "templates",
169
+ "loop-state",
170
+ "session-state",
171
+ "agents"
172
+ ]),
173
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
174
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
175
+ }).optional();
176
+ var PythonDomainConfigSchema = z.object({
177
+ name: z.string(),
178
+ packages: z.array(z.string()),
179
+ allowed_imports_from: z.array(z.string()).default([])
180
+ });
181
+ var PythonConfigSchema = z.object({
182
+ root: z.string(),
183
+ alembic_dir: z.string().optional(),
184
+ domains: z.array(PythonDomainConfigSchema).default([]),
185
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
186
+ }).optional();
143
187
  var PathsConfigSchema = z.object({
144
188
  source: z.string().default("src"),
145
189
  aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
@@ -174,7 +218,9 @@ var RawConfigSchema = z.object({
174
218
  security: SecurityConfigSchema,
175
219
  team: TeamConfigSchema,
176
220
  regression: RegressionConfigSchema,
177
- cloud: CloudConfigSchema
221
+ cloud: CloudConfigSchema,
222
+ conventions: ConventionsConfigSchema,
223
+ python: PythonConfigSchema
178
224
  }).passthrough();
179
225
  var _config = null;
180
226
  var _projectRoot = null;
@@ -238,13 +284,24 @@ function getConfig() {
238
284
  security: parsed.security,
239
285
  team: parsed.team,
240
286
  regression: parsed.regression,
241
- cloud: parsed.cloud
287
+ cloud: parsed.cloud,
288
+ conventions: parsed.conventions,
289
+ python: parsed.python
242
290
  };
291
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
292
+ _config.cloud = {
293
+ enabled: true,
294
+ sync: { memory: true, analytics: true, audit: true },
295
+ ..._config.cloud,
296
+ apiKey: process.env.MASSU_API_KEY
297
+ };
298
+ }
243
299
  return _config;
244
300
  }
245
301
  function getResolvedPaths() {
246
302
  const config = getConfig();
247
303
  const root = getProjectRoot();
304
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
248
305
  return {
249
306
  codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
250
307
  dataDbPath: resolve(root, ".massu/data.db"),
@@ -260,11 +317,20 @@ function getResolvedPaths() {
260
317
  ),
261
318
  extensions: [".ts", ".tsx", ".js", ".jsx"],
262
319
  indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
263
- patternsDir: resolve(root, ".claude/patterns"),
264
- claudeMdPath: resolve(root, ".claude/CLAUDE.md"),
320
+ patternsDir: resolve(root, claudeDirName, "patterns"),
321
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
265
322
  docsMapPath: resolve(root, ".massu/docs-map.json"),
266
323
  helpSitePath: resolve(root, "../" + config.project.name + "-help"),
267
- memoryDbPath: resolve(root, ".massu/memory.db")
324
+ memoryDbPath: resolve(root, ".massu/memory.db"),
325
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
326
+ plansDir: resolve(root, "docs/plans"),
327
+ docsDir: resolve(root, "docs"),
328
+ claudeDir: resolve(root, claudeDirName),
329
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
330
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
331
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
332
+ mcpJsonPath: resolve(root, ".mcp.json"),
333
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
268
334
  };
269
335
  }
270
336
 
@@ -746,6 +812,15 @@ function initMemorySchema(db) {
746
812
  );
747
813
  CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
748
814
  `);
815
+ db.exec(`
816
+ CREATE TABLE IF NOT EXISTS license_cache (
817
+ api_key_hash TEXT PRIMARY KEY,
818
+ tier TEXT NOT NULL,
819
+ valid_until TEXT NOT NULL,
820
+ last_validated TEXT NOT NULL,
821
+ features TEXT DEFAULT '[]'
822
+ );
823
+ `);
749
824
  }
750
825
  function assignImportance(type, vrResult) {
751
826
  switch (type) {
@@ -861,6 +936,7 @@ function detectDecisionPatterns(text) {
861
936
  }
862
937
 
863
938
  // src/observation-extractor.ts
939
+ import { homedir as homedir2 } from "os";
864
940
  var PRIVATE_PATTERNS = [
865
941
  /\/Users\/\w+/,
866
942
  // Absolute macOS paths
@@ -939,7 +1015,9 @@ function classifyToolCall(tc) {
939
1015
  }
940
1016
  case "Read": {
941
1017
  const filePath = tc.input.file_path ?? "unknown";
942
- if (filePath.includes("/plans/") || filePath.includes("CLAUDE.md") || filePath.includes("CURRENT.md")) {
1018
+ const knowledgeSourceFiles = getConfig().conventions?.knowledgeSourceFiles ?? ["CLAUDE.md", "MEMORY.md", "corrections.md"];
1019
+ const plansDir = getResolvedPaths().plansDir;
1020
+ if (filePath.includes(plansDir) || knowledgeSourceFiles.some((f) => filePath.includes(f))) {
943
1021
  const title = `Read: ${shortenPath(filePath)}`;
944
1022
  return {
945
1023
  type: "discovery",
@@ -1049,7 +1127,11 @@ function shortenPath(filePath) {
1049
1127
  if (filePath.startsWith(root + "/")) {
1050
1128
  return filePath.slice(root.length + 1);
1051
1129
  }
1052
- return filePath.replace(/^\/Users\/\w+\//, "~/");
1130
+ const home = homedir2();
1131
+ if (filePath.startsWith(home + "/")) {
1132
+ return "~/" + filePath.slice(home.length + 1);
1133
+ }
1134
+ return filePath;
1053
1135
  }
1054
1136
  function classifyRealTimeToolCall(toolName, toolInput, toolResponse, seenReads2) {
1055
1137
  const tc = {
@@ -1160,59 +1242,13 @@ function trackModification(db, featureKey) {
1160
1242
 
1161
1243
  // src/import-resolver.ts
1162
1244
  import { readFileSync as readFileSync2, existsSync as existsSync3, statSync } from "fs";
1163
- import { resolve as resolve3, dirname as dirname3, join } from "path";
1164
- function resolveImportPath(specifier, fromFile) {
1165
- if (!specifier.startsWith(".") && !specifier.startsWith("@/")) {
1166
- return null;
1167
- }
1168
- let basePath;
1169
- if (specifier.startsWith("@/")) {
1170
- const paths = getResolvedPaths();
1171
- basePath = resolve3(paths.pathAlias["@"] ?? paths.srcDir, specifier.slice(2));
1172
- } else {
1173
- basePath = resolve3(dirname3(fromFile), specifier);
1174
- }
1175
- if (existsSync3(basePath) && !isDirectory(basePath)) {
1176
- return toRelative(basePath);
1177
- }
1178
- const resolvedPaths = getResolvedPaths();
1179
- for (const ext of resolvedPaths.extensions) {
1180
- const withExt = basePath + ext;
1181
- if (existsSync3(withExt)) {
1182
- return toRelative(withExt);
1183
- }
1184
- }
1185
- for (const indexFile of resolvedPaths.indexFiles) {
1186
- const indexPath = join(basePath, indexFile);
1187
- if (existsSync3(indexPath)) {
1188
- return toRelative(indexPath);
1189
- }
1190
- }
1191
- return null;
1192
- }
1193
- function isDirectory(path) {
1194
- try {
1195
- return statSync(path).isDirectory();
1196
- } catch {
1197
- return false;
1198
- }
1199
- }
1200
- function toRelative(absPath) {
1201
- const root = getProjectRoot();
1202
- if (absPath.startsWith(root)) {
1203
- return absPath.slice(root.length + 1);
1204
- }
1205
- return absPath;
1206
- }
1207
-
1208
- // src/validation-engine.ts
1209
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1245
+ import { resolve as resolve4, dirname as dirname3, join } from "path";
1210
1246
 
1211
1247
  // src/security-utils.ts
1212
- import { resolve as resolve4, normalize } from "path";
1248
+ import { resolve as resolve3, normalize } from "path";
1213
1249
  function ensureWithinRoot(filePath, projectRoot) {
1214
- const resolvedRoot = resolve4(projectRoot);
1215
- const resolvedPath = resolve4(resolvedRoot, filePath);
1250
+ const resolvedRoot = resolve3(projectRoot);
1251
+ const resolvedPath = resolve3(resolvedRoot, filePath);
1216
1252
  const normalizedPath = normalize(resolvedPath);
1217
1253
  const normalizedRoot = normalize(resolvedRoot);
1218
1254
  if (!normalizedPath.startsWith(normalizedRoot + "/") && normalizedPath !== normalizedRoot) {
@@ -1254,7 +1290,53 @@ function enforceSeverityFloors(configWeights, defaults) {
1254
1290
  return result;
1255
1291
  }
1256
1292
 
1293
+ // src/import-resolver.ts
1294
+ function resolveImportPath(specifier, fromFile) {
1295
+ if (!specifier.startsWith(".") && !specifier.startsWith("@/")) {
1296
+ return null;
1297
+ }
1298
+ let basePath;
1299
+ if (specifier.startsWith("@/")) {
1300
+ const paths = getResolvedPaths();
1301
+ basePath = resolve4(paths.pathAlias["@"] ?? paths.srcDir, specifier.slice(2));
1302
+ } else {
1303
+ basePath = resolve4(dirname3(fromFile), specifier);
1304
+ }
1305
+ if (existsSync3(basePath) && !isDirectory(basePath)) {
1306
+ return toRelative(basePath);
1307
+ }
1308
+ const resolvedPaths = getResolvedPaths();
1309
+ for (const ext of resolvedPaths.extensions) {
1310
+ const withExt = basePath + ext;
1311
+ if (existsSync3(withExt)) {
1312
+ return toRelative(withExt);
1313
+ }
1314
+ }
1315
+ for (const indexFile of resolvedPaths.indexFiles) {
1316
+ const indexPath = join(basePath, indexFile);
1317
+ if (existsSync3(indexPath)) {
1318
+ return toRelative(indexPath);
1319
+ }
1320
+ }
1321
+ return null;
1322
+ }
1323
+ function isDirectory(path) {
1324
+ try {
1325
+ return statSync(path).isDirectory();
1326
+ } catch {
1327
+ return false;
1328
+ }
1329
+ }
1330
+ function toRelative(absPath) {
1331
+ const root = getProjectRoot();
1332
+ if (absPath.startsWith(root)) {
1333
+ return absPath.slice(root.length + 1);
1334
+ }
1335
+ return absPath;
1336
+ }
1337
+
1257
1338
  // src/validation-engine.ts
1339
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1258
1340
  function getValidationChecks() {
1259
1341
  return getConfig().governance?.validation?.checks ?? {
1260
1342
  rule_compliance: true,
@@ -1524,6 +1606,9 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
1524
1606
  }
1525
1607
 
1526
1608
  // src/hooks/post-tool-use.ts
1609
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
1610
+ import { join as join2 } from "path";
1611
+ import { parse as parseYaml2 } from "yaml";
1527
1612
  var seenReads = /* @__PURE__ */ new Set();
1528
1613
  var currentSessionId = null;
1529
1614
  async function main() {
@@ -1601,6 +1686,41 @@ async function main() {
1601
1686
  }
1602
1687
  } catch (_securityErr) {
1603
1688
  }
1689
+ try {
1690
+ if (tool_name === "Edit" || tool_name === "Write") {
1691
+ const filePath = tool_input.file_path ?? "";
1692
+ if (filePath && filePath.endsWith("MEMORY.md") && filePath.includes("/memory/")) {
1693
+ const issues = checkMemoryFileIntegrity(filePath);
1694
+ if (issues.length > 0) {
1695
+ addObservation(
1696
+ db,
1697
+ session_id,
1698
+ "incident_near_miss",
1699
+ "MEMORY.md integrity issue detected",
1700
+ issues.join("; "),
1701
+ { importance: 4 }
1702
+ );
1703
+ }
1704
+ }
1705
+ }
1706
+ } catch (_memoryErr) {
1707
+ }
1708
+ try {
1709
+ if (tool_name === "Edit" || tool_name === "Write") {
1710
+ const filePath = tool_input.file_path ?? "";
1711
+ if (filePath && isKnowledgeSourceFile(filePath)) {
1712
+ addObservation(
1713
+ db,
1714
+ session_id,
1715
+ "discovery",
1716
+ "Knowledge source file modified - index may be stale",
1717
+ `Edited ${filePath.split("/").pop() ?? filePath}. Run knowledge re-index to update.`,
1718
+ { importance: 3 }
1719
+ );
1720
+ }
1721
+ }
1722
+ } catch (_knowledgeErr) {
1723
+ }
1604
1724
  } finally {
1605
1725
  db.close();
1606
1726
  }
@@ -1640,4 +1760,61 @@ function readStdin() {
1640
1760
  setTimeout(() => resolve5(data), 3e3);
1641
1761
  });
1642
1762
  }
1763
+ function readConventions(cwd) {
1764
+ const defaults = {
1765
+ knowledgeSourceFiles: ["CLAUDE.md", "MEMORY.md", "corrections.md"],
1766
+ claudeDirName: ".claude"
1767
+ };
1768
+ try {
1769
+ const projectRoot = cwd ?? process.cwd();
1770
+ const configPath = join2(projectRoot, "massu.config.yaml");
1771
+ if (!existsSync6(configPath)) return defaults;
1772
+ const content = readFileSync5(configPath, "utf-8");
1773
+ const parsed = parseYaml2(content);
1774
+ if (!parsed || typeof parsed !== "object") return defaults;
1775
+ const conventions = parsed.conventions;
1776
+ if (!conventions || typeof conventions !== "object") return defaults;
1777
+ return {
1778
+ knowledgeSourceFiles: Array.isArray(conventions.knowledgeSourceFiles) ? conventions.knowledgeSourceFiles : defaults.knowledgeSourceFiles,
1779
+ claudeDirName: typeof conventions.claudeDirName === "string" ? conventions.claudeDirName : defaults.claudeDirName
1780
+ };
1781
+ } catch {
1782
+ return defaults;
1783
+ }
1784
+ }
1785
+ function isKnowledgeSourceFile(filePath) {
1786
+ const basename2 = filePath.split("/").pop() ?? "";
1787
+ const conventions = readConventions();
1788
+ const knowledgeSourcePatterns = [
1789
+ ...conventions.knowledgeSourceFiles,
1790
+ "file-index.md",
1791
+ "knowledge-db.ts",
1792
+ "knowledge-indexer.ts",
1793
+ "knowledge-tools.ts"
1794
+ ];
1795
+ return knowledgeSourcePatterns.some((p) => basename2 === p) || filePath.includes("/memory/") || filePath.includes(conventions.claudeDirName + "/");
1796
+ }
1797
+ function checkMemoryFileIntegrity(filePath) {
1798
+ const issues = [];
1799
+ try {
1800
+ if (!existsSync6(filePath)) {
1801
+ issues.push("MEMORY.md file does not exist after write");
1802
+ return issues;
1803
+ }
1804
+ const content = readFileSync5(filePath, "utf-8");
1805
+ const lines = content.split("\n");
1806
+ const MAX_LINES = 200;
1807
+ if (lines.length > MAX_LINES) {
1808
+ issues.push(`MEMORY.md exceeds ${MAX_LINES} lines (currently ${lines.length}). Consider archiving old entries.`);
1809
+ }
1810
+ const requiredSections = ["# Massu Memory", "## Key Learnings", "## Common Gotchas"];
1811
+ for (const section of requiredSections) {
1812
+ if (!content.includes(section)) {
1813
+ issues.push(`Missing required section: "${section}"`);
1814
+ }
1815
+ }
1816
+ } catch (_e) {
1817
+ }
1818
+ return issues;
1819
+ }
1643
1820
  main();