@massu/core 0.4.2 → 0.6.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 (125) hide show
  1. package/README.md +40 -0
  2. package/agents/massu-architecture-reviewer.md +104 -0
  3. package/agents/massu-blast-radius-analyzer.md +84 -0
  4. package/agents/massu-competitive-scorer.md +126 -0
  5. package/agents/massu-help-sync.md +73 -0
  6. package/agents/massu-migration-writer.md +94 -0
  7. package/agents/massu-output-scorer.md +87 -0
  8. package/agents/massu-pattern-reviewer.md +84 -0
  9. package/agents/massu-plan-auditor.md +170 -0
  10. package/agents/massu-schema-sync-verifier.md +70 -0
  11. package/agents/massu-security-reviewer.md +98 -0
  12. package/agents/massu-ux-reviewer.md +106 -0
  13. package/commands/_shared-preamble.md +53 -23
  14. package/commands/_shared-references/auto-learning-protocol.md +71 -0
  15. package/commands/_shared-references/blast-radius-protocol.md +76 -0
  16. package/commands/_shared-references/security-pre-screen.md +64 -0
  17. package/commands/_shared-references/test-first-protocol.md +87 -0
  18. package/commands/_shared-references/verification-table.md +52 -0
  19. package/commands/massu-article-review.md +343 -0
  20. package/commands/massu-autoresearch/references/eval-runner.md +84 -0
  21. package/commands/massu-autoresearch/references/safety-rails.md +125 -0
  22. package/commands/massu-autoresearch/references/scoring-protocol.md +151 -0
  23. package/commands/massu-autoresearch.md +258 -0
  24. package/commands/massu-batch.md +44 -12
  25. package/commands/massu-bearings.md +42 -8
  26. package/commands/massu-checkpoint.md +588 -0
  27. package/commands/massu-ci-fix.md +2 -2
  28. package/commands/massu-command-health.md +132 -0
  29. package/commands/massu-command-improve.md +232 -0
  30. package/commands/massu-commit.md +205 -44
  31. package/commands/massu-create-plan.md +239 -57
  32. package/commands/massu-data/references/common-queries.md +79 -0
  33. package/commands/massu-data/references/table-guide.md +50 -0
  34. package/commands/massu-data.md +66 -0
  35. package/commands/massu-dead-code.md +29 -34
  36. package/commands/massu-debug/references/auto-learning.md +61 -0
  37. package/commands/massu-debug/references/codegraph-tracing.md +80 -0
  38. package/commands/massu-debug/references/common-shortcuts.md +98 -0
  39. package/commands/massu-debug/references/investigation-phases.md +294 -0
  40. package/commands/massu-debug/references/report-format.md +107 -0
  41. package/commands/massu-debug.md +105 -386
  42. package/commands/massu-docs.md +1 -1
  43. package/commands/massu-full-audit.md +61 -0
  44. package/commands/massu-gap-enhancement-analyzer.md +276 -16
  45. package/commands/massu-golden-path/references/approval-points.md +216 -0
  46. package/commands/massu-golden-path/references/competitive-mode.md +273 -0
  47. package/commands/massu-golden-path/references/error-handling.md +121 -0
  48. package/commands/massu-golden-path/references/phase-0-requirements.md +53 -0
  49. package/commands/massu-golden-path/references/phase-1-plan-creation.md +168 -0
  50. package/commands/massu-golden-path/references/phase-2-implementation.md +397 -0
  51. package/commands/massu-golden-path/references/phase-2.5-gap-analyzer.md +156 -0
  52. package/commands/massu-golden-path/references/phase-3-simplify.md +40 -0
  53. package/commands/massu-golden-path/references/phase-4-commit.md +94 -0
  54. package/commands/massu-golden-path/references/phase-5-push.md +116 -0
  55. package/commands/massu-golden-path/references/phase-5.5-production-verify.md +170 -0
  56. package/commands/massu-golden-path/references/phase-6-completion.md +113 -0
  57. package/commands/massu-golden-path/references/qa-evaluator-spec.md +137 -0
  58. package/commands/massu-golden-path/references/sprint-contract-protocol.md +117 -0
  59. package/commands/massu-golden-path/references/vr-visual-calibration.md +73 -0
  60. package/commands/massu-golden-path.md +114 -848
  61. package/commands/massu-guide.md +72 -69
  62. package/commands/massu-hooks.md +27 -12
  63. package/commands/massu-hotfix.md +221 -144
  64. package/commands/massu-incident.md +49 -20
  65. package/commands/massu-infra-audit.md +187 -0
  66. package/commands/massu-learning-audit.md +211 -0
  67. package/commands/massu-loop/references/auto-learning.md +49 -0
  68. package/commands/massu-loop/references/checkpoint-audit.md +40 -0
  69. package/commands/massu-loop/references/guardrails.md +17 -0
  70. package/commands/massu-loop/references/iteration-structure.md +115 -0
  71. package/commands/massu-loop/references/loop-controller.md +188 -0
  72. package/commands/massu-loop/references/plan-extraction.md +78 -0
  73. package/commands/massu-loop/references/vr-plan-spec.md +140 -0
  74. package/commands/massu-loop-playwright.md +9 -9
  75. package/commands/massu-loop.md +115 -670
  76. package/commands/massu-new-pattern.md +423 -0
  77. package/commands/massu-perf.md +422 -0
  78. package/commands/massu-plan-audit.md +1 -1
  79. package/commands/massu-plan.md +389 -122
  80. package/commands/massu-production-verify.md +433 -0
  81. package/commands/massu-push.md +62 -378
  82. package/commands/massu-recap.md +29 -3
  83. package/commands/massu-rollback.md +613 -0
  84. package/commands/massu-scaffold-hook.md +2 -4
  85. package/commands/massu-scaffold-page.md +2 -3
  86. package/commands/massu-scaffold-router.md +1 -2
  87. package/commands/massu-security.md +619 -0
  88. package/commands/massu-simplify.md +115 -85
  89. package/commands/massu-squirrels.md +2 -2
  90. package/commands/massu-tdd.md +38 -22
  91. package/commands/massu-test.md +3 -3
  92. package/commands/massu-type-mismatch-audit.md +469 -0
  93. package/commands/massu-ui-audit.md +587 -0
  94. package/commands/massu-verify-playwright.md +287 -32
  95. package/commands/massu-verify.md +150 -46
  96. package/dist/cli.js +1451 -1047
  97. package/dist/hooks/post-tool-use.js +75 -6
  98. package/dist/hooks/user-prompt.js +16 -0
  99. package/package.json +6 -2
  100. package/patterns/build-patterns.md +302 -0
  101. package/patterns/component-patterns.md +246 -0
  102. package/patterns/display-patterns.md +185 -0
  103. package/patterns/form-patterns.md +890 -0
  104. package/patterns/integration-testing-checklist.md +445 -0
  105. package/patterns/security-patterns.md +219 -0
  106. package/patterns/testing-patterns.md +569 -0
  107. package/patterns/tool-routing.md +81 -0
  108. package/patterns/ui-patterns.md +371 -0
  109. package/protocols/plan-implementation.md +267 -0
  110. package/protocols/recovery.md +225 -0
  111. package/protocols/verification.md +404 -0
  112. package/reference/command-taxonomy.md +178 -0
  113. package/reference/cr-rules-reference.md +76 -0
  114. package/reference/hook-execution-order.md +148 -0
  115. package/reference/lessons-learned.md +175 -0
  116. package/reference/patterns-quickref.md +208 -0
  117. package/reference/standards.md +135 -0
  118. package/reference/subagents-reference.md +17 -0
  119. package/reference/vr-verification-reference.md +867 -0
  120. package/src/commands/init.ts +27 -0
  121. package/src/commands/install-commands.ts +149 -53
  122. package/src/hooks/post-tool-use.ts +17 -0
  123. package/src/hooks/user-prompt.ts +21 -0
  124. package/src/memory-file-ingest.ts +127 -0
  125. package/src/memory-tools.ts +34 -1
@@ -17,6 +17,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
17
17
  import { resolve, basename, dirname } from 'path';
18
18
  import { fileURLToPath } from 'url';
19
19
  import { homedir } from 'os';
20
+ import { backfillMemoryFiles } from '../memory-file-ingest.ts';
20
21
 
21
22
  const __filename = fileURLToPath(import.meta.url);
22
23
  const __dirname = dirname(__filename);
@@ -588,6 +589,32 @@ export async function runInit(): Promise<void> {
588
589
  console.log(' Created initial MEMORY.md');
589
590
  }
590
591
 
592
+ // Step 6b: Auto-backfill existing memory files into database
593
+ try {
594
+ const claudeDirName = '.claude';
595
+ const encodedRoot = projectRoot.replace(/\//g, '-');
596
+ const computedMemoryDir = resolve(homedir(), claudeDirName, 'projects', encodedRoot, 'memory');
597
+
598
+ const memFiles = existsSync(computedMemoryDir)
599
+ ? readdirSync(computedMemoryDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md')
600
+ : [];
601
+
602
+ if (memFiles.length > 0) {
603
+ const { getMemoryDb } = await import('../memory-db.ts');
604
+ const db = getMemoryDb();
605
+ try {
606
+ const stats = backfillMemoryFiles(db, computedMemoryDir, `init-${Date.now()}`);
607
+ if (stats.inserted > 0 || stats.updated > 0) {
608
+ console.log(` Backfilled ${stats.inserted + stats.updated} memory files into database (${stats.inserted} new, ${stats.updated} updated)`);
609
+ }
610
+ } finally {
611
+ db.close();
612
+ }
613
+ }
614
+ } catch (_backfillErr) {
615
+ // Best-effort: don't fail init if backfill fails
616
+ }
617
+
591
618
  // Step 7: Databases info
592
619
  console.log(' Databases will auto-create on first session');
593
620
 
@@ -2,15 +2,16 @@
2
2
  // Licensed under BSL 1.1 - see LICENSE file for details.
3
3
 
4
4
  /**
5
- * `massu install-commands` — Install massu slash commands into a project.
5
+ * `massu install-commands` — Install massu slash commands, agents, patterns,
6
+ * protocols, and reference files into a project.
6
7
  *
7
- * Copies all massu command .md files from the package's commands/ directory
8
- * into the project's .claude/commands/ directory. Existing massu commands
9
- * are updated; non-massu commands are preserved.
8
+ * Copies all massu assets from the npm package into the project's .claude/
9
+ * directory. Existing massu files are updated; non-massu files are preserved.
10
+ * Handles subdirectories recursively (e.g., golden-path/references/).
10
11
  */
11
12
 
12
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
13
- import { resolve, dirname } from 'path';
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
14
+ import { resolve, dirname, relative, join } from 'path';
14
15
  import { fileURLToPath } from 'url';
15
16
  import { getConfig } from '../config.ts';
16
17
 
@@ -18,30 +19,43 @@ const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = dirname(__filename);
19
20
 
20
21
  // ============================================================
21
- // Command Installation
22
+ // Asset Types
23
+ // ============================================================
24
+
25
+ /** Asset categories distributed by massu */
26
+ const ASSET_TYPES = [
27
+ { name: 'commands', targetSubdir: 'commands', description: 'slash commands' },
28
+ { name: 'agents', targetSubdir: 'agents', description: 'agent definitions' },
29
+ { name: 'patterns', targetSubdir: 'patterns', description: 'pattern files' },
30
+ { name: 'protocols', targetSubdir: 'protocols', description: 'protocol files' },
31
+ { name: 'reference', targetSubdir: 'reference', description: 'reference files' },
32
+ ] as const;
33
+
34
+ // ============================================================
35
+ // Directory Resolution
22
36
  // ============================================================
23
37
 
24
38
  /**
25
- * Resolve the path to the bundled commands directory.
39
+ * Resolve the path to a bundled asset directory.
26
40
  * Handles both npm-installed and local development scenarios.
27
41
  */
28
- export function resolveCommandsDir(): string | null {
42
+ export function resolveAssetDir(assetName: string): string | null {
29
43
  const cwd = process.cwd();
30
44
 
31
- // 1. npm-installed: node_modules/@massu/core/commands
32
- const nodeModulesPath = resolve(cwd, 'node_modules/@massu/core/commands');
45
+ // 1. npm-installed: node_modules/@massu/core/{assetName}
46
+ const nodeModulesPath = resolve(cwd, 'node_modules/@massu/core', assetName);
33
47
  if (existsSync(nodeModulesPath)) {
34
48
  return nodeModulesPath;
35
49
  }
36
50
 
37
- // 2. Relative to compiled dist/cli.js → ../commands
38
- const distRelPath = resolve(__dirname, '../commands');
51
+ // 2. Relative to compiled dist/cli.js → ../{assetName}
52
+ const distRelPath = resolve(__dirname, '..', assetName);
39
53
  if (existsSync(distRelPath)) {
40
54
  return distRelPath;
41
55
  }
42
56
 
43
- // 3. Relative to source src/commands/ → ../../commands
44
- const srcRelPath = resolve(__dirname, '../../commands');
57
+ // 3. Relative to source src/commands/ → ../../{assetName}
58
+ const srcRelPath = resolve(__dirname, '../..', assetName);
45
59
  if (existsSync(srcRelPath)) {
46
60
  return srcRelPath;
47
61
  }
@@ -49,6 +63,70 @@ export function resolveCommandsDir(): string | null {
49
63
  return null;
50
64
  }
51
65
 
66
+ /** Legacy alias for backwards compatibility */
67
+ export function resolveCommandsDir(): string | null {
68
+ return resolveAssetDir('commands');
69
+ }
70
+
71
+ // ============================================================
72
+ // Recursive File Sync
73
+ // ============================================================
74
+
75
+ interface SyncStats {
76
+ installed: number;
77
+ updated: number;
78
+ skipped: number;
79
+ }
80
+
81
+ /**
82
+ * Recursively sync all .md files from sourceDir to targetDir.
83
+ * Creates subdirectories as needed. Preserves non-massu files.
84
+ */
85
+ function syncDirectory(sourceDir: string, targetDir: string): SyncStats {
86
+ const stats: SyncStats = { installed: 0, updated: 0, skipped: 0 };
87
+
88
+ if (!existsSync(targetDir)) {
89
+ mkdirSync(targetDir, { recursive: true });
90
+ }
91
+
92
+ const entries = readdirSync(sourceDir);
93
+
94
+ for (const entry of entries) {
95
+ const sourcePath = resolve(sourceDir, entry);
96
+ const targetPath = resolve(targetDir, entry);
97
+ const entryStat = statSync(sourcePath);
98
+
99
+ if (entryStat.isDirectory()) {
100
+ // Recurse into subdirectories
101
+ const subStats = syncDirectory(sourcePath, targetPath);
102
+ stats.installed += subStats.installed;
103
+ stats.updated += subStats.updated;
104
+ stats.skipped += subStats.skipped;
105
+ } else if (entry.endsWith('.md')) {
106
+ const sourceContent = readFileSync(sourcePath, 'utf-8');
107
+
108
+ if (existsSync(targetPath)) {
109
+ const existingContent = readFileSync(targetPath, 'utf-8');
110
+ if (existingContent === sourceContent) {
111
+ stats.skipped++;
112
+ continue;
113
+ }
114
+ writeFileSync(targetPath, sourceContent, 'utf-8');
115
+ stats.updated++;
116
+ } else {
117
+ writeFileSync(targetPath, sourceContent, 'utf-8');
118
+ stats.installed++;
119
+ }
120
+ }
121
+ }
122
+
123
+ return stats;
124
+ }
125
+
126
+ // ============================================================
127
+ // Install Commands (legacy API — preserved for backwards compat)
128
+ // ============================================================
129
+
52
130
  export interface InstallCommandsResult {
53
131
  installed: number;
54
132
  updated: number;
@@ -60,48 +138,58 @@ export function installCommands(projectRoot: string): InstallCommandsResult {
60
138
  const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
61
139
  const targetDir = resolve(projectRoot, claudeDirName, 'commands');
62
140
 
63
- // Ensure .claude/commands directory exists
64
141
  if (!existsSync(targetDir)) {
65
142
  mkdirSync(targetDir, { recursive: true });
66
143
  }
67
144
 
68
- // Find source commands
69
- const sourceDir = resolveCommandsDir();
145
+ const sourceDir = resolveAssetDir('commands');
70
146
  if (!sourceDir) {
71
147
  console.error(' ERROR: Could not find massu commands directory.');
72
148
  console.error(' Try reinstalling: npm install @massu/core');
73
149
  return { installed: 0, updated: 0, skipped: 0, commandsDir: targetDir };
74
150
  }
75
151
 
76
- // Read all command files from source
77
- const sourceFiles = readdirSync(sourceDir).filter(f => f.endsWith('.md'));
152
+ const stats = syncDirectory(sourceDir, targetDir);
153
+ return { ...stats, commandsDir: targetDir };
154
+ }
78
155
 
79
- let installed = 0;
80
- let updated = 0;
81
- let skipped = 0;
156
+ // ============================================================
157
+ // Install All Assets
158
+ // ============================================================
82
159
 
83
- for (const file of sourceFiles) {
84
- const sourcePath = resolve(sourceDir, file);
85
- const targetPath = resolve(targetDir, file);
86
- const sourceContent = readFileSync(sourcePath, 'utf-8');
160
+ export interface InstallAllResult {
161
+ assets: Record<string, SyncStats>;
162
+ totalInstalled: number;
163
+ totalUpdated: number;
164
+ totalSkipped: number;
165
+ claudeDir: string;
166
+ }
87
167
 
88
- if (existsSync(targetPath)) {
89
- const existingContent = readFileSync(targetPath, 'utf-8');
90
- if (existingContent === sourceContent) {
91
- skipped++;
92
- continue;
93
- }
94
- // Update existing command
95
- writeFileSync(targetPath, sourceContent, 'utf-8');
96
- updated++;
97
- } else {
98
- // Install new command
99
- writeFileSync(targetPath, sourceContent, 'utf-8');
100
- installed++;
168
+ export function installAll(projectRoot: string): InstallAllResult {
169
+ const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
170
+ const claudeDir = resolve(projectRoot, claudeDirName);
171
+
172
+ const assets: Record<string, SyncStats> = {};
173
+ let totalInstalled = 0;
174
+ let totalUpdated = 0;
175
+ let totalSkipped = 0;
176
+
177
+ for (const assetType of ASSET_TYPES) {
178
+ const sourceDir = resolveAssetDir(assetType.name);
179
+ if (!sourceDir) {
180
+ continue;
101
181
  }
182
+
183
+ const targetDir = resolve(claudeDir, assetType.targetSubdir);
184
+ const stats = syncDirectory(sourceDir, targetDir);
185
+
186
+ assets[assetType.name] = stats;
187
+ totalInstalled += stats.installed;
188
+ totalUpdated += stats.updated;
189
+ totalSkipped += stats.skipped;
102
190
  }
103
191
 
104
- return { installed, updated, skipped, commandsDir: targetDir };
192
+ return { assets, totalInstalled, totalUpdated, totalSkipped, claudeDir };
105
193
  }
106
194
 
107
195
  // ============================================================
@@ -112,25 +200,33 @@ export async function runInstallCommands(): Promise<void> {
112
200
  const projectRoot = process.cwd();
113
201
 
114
202
  console.log('');
115
- console.log('Massu AI - Install Slash Commands');
203
+ console.log('Massu AI - Install Project Assets');
116
204
  console.log('==================================');
117
205
  console.log('');
118
206
 
119
- const result = installCommands(projectRoot);
207
+ const result = installAll(projectRoot);
120
208
 
121
- if (result.installed > 0) {
122
- console.log(` Installed ${result.installed} new commands`);
123
- }
124
- if (result.updated > 0) {
125
- console.log(` Updated ${result.updated} existing commands`);
126
- }
127
- if (result.skipped > 0) {
128
- console.log(` ${result.skipped} commands already up to date`);
209
+ // Report per-asset-type
210
+ for (const assetType of ASSET_TYPES) {
211
+ const stats = result.assets[assetType.name];
212
+ if (!stats) {
213
+ continue;
214
+ }
215
+ const total = stats.installed + stats.updated + stats.skipped;
216
+ if (total === 0) continue;
217
+
218
+ const parts: string[] = [];
219
+ if (stats.installed > 0) parts.push(`${stats.installed} new`);
220
+ if (stats.updated > 0) parts.push(`${stats.updated} updated`);
221
+ if (stats.skipped > 0) parts.push(`${stats.skipped} current`);
222
+
223
+ const description = assetType.description;
224
+ console.log(` ${description}: ${parts.join(', ')} (${total} total)`);
129
225
  }
130
226
 
131
- const total = result.installed + result.updated + result.skipped;
227
+ const grandTotal = result.totalInstalled + result.totalUpdated + result.totalSkipped;
132
228
  console.log('');
133
- console.log(` ${total} slash commands available in ${result.commandsDir}`);
229
+ console.log(` ${grandTotal} total files synced to ${result.claudeDir}`);
134
230
  console.log('');
135
231
  console.log(' Restart your Claude Code session to use them.');
136
232
  console.log('');
@@ -17,6 +17,7 @@ import { scoreFileSecurity, storeSecurityScore } from '../security-scorer.ts';
17
17
  import { readFileSync, existsSync } from 'fs';
18
18
  import { join } from 'path';
19
19
  import { parse as parseYaml } from 'yaml';
20
+ import { ingestMemoryFile } from '../memory-file-ingest.ts';
20
21
 
21
22
  interface HookInput {
22
23
  session_id: string;
@@ -149,6 +150,22 @@ async function main(): Promise<void> {
149
150
  // Best-effort: never block post-tool-use
150
151
  }
151
152
 
153
+ // Memory file auto-ingest: when Claude writes a memory/*.md file,
154
+ // parse frontmatter and ingest into observations table
155
+ try {
156
+ if (tool_name === 'Edit' || tool_name === 'Write') {
157
+ const filePath = (tool_input.file_path as string) ?? '';
158
+ if (filePath && filePath.includes('/memory/') && filePath.endsWith('.md')) {
159
+ const basename = filePath.split('/').pop() ?? '';
160
+ if (basename !== 'MEMORY.md') {
161
+ ingestMemoryFile(db, session_id, filePath);
162
+ }
163
+ }
164
+ }
165
+ } catch (_memoryIngestErr) {
166
+ // Best-effort: never block post-tool-use
167
+ }
168
+
152
169
  // Knowledge index staleness check on knowledge file edits
153
170
  try {
154
171
  if (tool_name === 'Edit' || tool_name === 'Write') {
@@ -86,6 +86,27 @@ async function main(): Promise<void> {
86
86
  } catch (_knowledgeErr) {
87
87
  // Best-effort: never block prompt capture
88
88
  }
89
+ // 6. Memory enforcement: nag when significant work detected but no memory ingestion
90
+ try {
91
+ const significantSignals = ['fix', 'implement', 'migrate', 'refactor', 'debug', 'decision', 'chose', 'architecture', 'redesign', 'rewrite'];
92
+ const promptLower = prompt.toLowerCase();
93
+ const signalCount = significantSignals.filter(s => promptLower.includes(s)).length;
94
+
95
+ if (signalCount >= 2) {
96
+ const memoryFileCount = db.prepare(
97
+ "SELECT COUNT(*) as count FROM observations WHERE session_id = ? AND title LIKE '[memory-file] %'"
98
+ ).get(session_id) as { count: number };
99
+
100
+ if (memoryFileCount.count === 0) {
101
+ process.stderr.write(
102
+ '\n[MEMORY REMINDER] Significant work detected but no memory files have been written.\n' +
103
+ 'Consider saving learnings to memory/*.md files for future sessions.\n\n'
104
+ );
105
+ }
106
+ }
107
+ } catch (_memoryNagErr) {
108
+ // Best-effort: never block prompt capture
109
+ }
89
110
  } finally {
90
111
  db.close();
91
112
  }
@@ -0,0 +1,127 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ // ============================================================
5
+ // Memory File Auto-Ingest
6
+ // Shared module for parsing memory/*.md files and ingesting
7
+ // their YAML frontmatter + content into the observations table.
8
+ // Used by: post-tool-use.ts, memory-tools.ts, init.ts
9
+ // ============================================================
10
+
11
+ import type Database from 'better-sqlite3';
12
+ import { readFileSync, existsSync, readdirSync } from 'fs';
13
+ import { join } from 'path';
14
+ import { parse as parseYaml } from 'yaml';
15
+ import { addObservation } from './memory-db.ts';
16
+
17
+ export type IngestResult = 'inserted' | 'updated' | 'skipped';
18
+
19
+ /**
20
+ * Parse a memory/*.md file's YAML frontmatter and ingest it into the
21
+ * observations table. Deduplicates by title prefix `[memory-file] {name}`.
22
+ *
23
+ * @returns 'inserted' | 'updated' | 'skipped'
24
+ */
25
+ export function ingestMemoryFile(
26
+ db: Database.Database,
27
+ sessionId: string,
28
+ filePath: string,
29
+ ): IngestResult {
30
+ if (!existsSync(filePath)) return 'skipped';
31
+
32
+ const content = readFileSync(filePath, 'utf-8');
33
+ const basename = (filePath.split('/').pop() ?? '').replace('.md', '');
34
+
35
+ // Parse YAML frontmatter (between first --- and second ---)
36
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
37
+
38
+ let name = basename;
39
+ let description = '';
40
+ let type = 'discovery';
41
+ let confidence: number | undefined;
42
+
43
+ if (frontmatterMatch) {
44
+ try {
45
+ const fm = parseYaml(frontmatterMatch[1]) as Record<string, unknown>;
46
+ name = (fm.name as string) ?? basename;
47
+ description = (fm.description as string) ?? '';
48
+ type = (fm.type as string) ?? 'discovery';
49
+ confidence = fm.confidence != null ? Number(fm.confidence) : undefined;
50
+ } catch {
51
+ // Use defaults if YAML parsing fails
52
+ }
53
+ }
54
+
55
+ // Map memory types to observation types
56
+ const obsType = mapMemoryTypeToObservationType(type);
57
+
58
+ // Calculate importance from confidence (0.0-1.0 -> 1-5)
59
+ const importance = confidence != null
60
+ ? Math.max(1, Math.min(5, Math.round(confidence * 4 + 1)))
61
+ : 4;
62
+
63
+ // Extract body (after second ---)
64
+ const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
65
+ const body = bodyMatch ? bodyMatch[1].trim().slice(0, 500) : '';
66
+
67
+ const title = `[memory-file] ${name}`;
68
+ const detail = description ? `${description}\n\n${body}` : body;
69
+
70
+ // Deduplicate: check if this exact title exists
71
+ const existing = db.prepare(
72
+ 'SELECT id FROM observations WHERE title = ? LIMIT 1'
73
+ ).get(title) as { id: number } | undefined;
74
+
75
+ if (existing) {
76
+ db.prepare('UPDATE observations SET detail = ?, importance = ? WHERE id = ?')
77
+ .run(detail, importance, existing.id);
78
+ return 'updated';
79
+ } else {
80
+ addObservation(db, sessionId, obsType, title, detail, { importance });
81
+ return 'inserted';
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Bulk-ingest all memory/*.md files from a directory.
87
+ * Skips MEMORY.md (the index file).
88
+ *
89
+ * @returns { inserted, updated, skipped, total }
90
+ */
91
+ export function backfillMemoryFiles(
92
+ db: Database.Database,
93
+ memoryDir: string,
94
+ sessionId?: string,
95
+ ): { inserted: number; updated: number; skipped: number; total: number } {
96
+ const stats = { inserted: 0, updated: 0, skipped: 0, total: 0 };
97
+
98
+ if (!existsSync(memoryDir)) return stats;
99
+
100
+ const files = readdirSync(memoryDir).filter(
101
+ f => f.endsWith('.md') && f !== 'MEMORY.md'
102
+ );
103
+ stats.total = files.length;
104
+
105
+ const sid = sessionId ?? `backfill-${Date.now()}`;
106
+
107
+ for (const file of files) {
108
+ const result = ingestMemoryFile(db, sid, join(memoryDir, file));
109
+ stats[result]++;
110
+ }
111
+
112
+ return stats;
113
+ }
114
+
115
+ function mapMemoryTypeToObservationType(memoryType: string): string {
116
+ switch (memoryType) {
117
+ case 'user':
118
+ case 'feedback':
119
+ return 'decision';
120
+ case 'project':
121
+ return 'feature';
122
+ case 'reference':
123
+ return 'discovery';
124
+ default:
125
+ return 'discovery';
126
+ }
127
+ }
@@ -13,7 +13,8 @@ import {
13
13
  assignImportance,
14
14
  createSession,
15
15
  } from './memory-db.ts';
16
- import { getConfig } from './config.ts';
16
+ import { getConfig, getResolvedPaths } from './config.ts';
17
+ import { backfillMemoryFiles } from './memory-file-ingest.ts';
17
18
 
18
19
  /** Prefix a base tool name with the configured tool prefix. */
19
20
  function p(baseName: string): string {
@@ -101,6 +102,16 @@ export function getMemoryToolDefinitions(): ToolDefinition[] {
101
102
  required: [],
102
103
  },
103
104
  },
105
+ // P4-007: memory_backfill
106
+ {
107
+ name: p('memory_backfill'),
108
+ description: 'Scan all memory/*.md files and ingest into database. Run after massu init or to recover from DB loss. Parses YAML frontmatter and deduplicates by title.',
109
+ inputSchema: {
110
+ type: 'object',
111
+ properties: {},
112
+ required: [],
113
+ },
114
+ },
104
115
  // P4-006: memory_ingest
105
116
  {
106
117
  name: p('memory_ingest'),
@@ -154,6 +165,8 @@ export function handleMemoryToolCall(
154
165
  return handleFailures(args, memoryDb);
155
166
  case 'memory_ingest':
156
167
  return handleIngest(args, memoryDb);
168
+ case 'memory_backfill':
169
+ return handleBackfill(memoryDb);
157
170
  default:
158
171
  return text(`Unknown memory tool: ${name}`);
159
172
  }
@@ -374,6 +387,26 @@ function handleIngest(args: Record<string, unknown>, db: Database.Database): Too
374
387
  return text(`Observation #${id} recorded successfully.\nType: ${type}\nTitle: ${title}\nImportance: ${importance}\nSession: ${activeSession.session_id.slice(0, 8)}...`);
375
388
  }
376
389
 
390
+ function handleBackfill(db: Database.Database): ToolResult {
391
+ const memoryDir = getResolvedPaths().memoryDir;
392
+ const stats = backfillMemoryFiles(db, memoryDir);
393
+
394
+ const lines = [
395
+ '## Memory Backfill Results',
396
+ '',
397
+ `- **Total files scanned**: ${stats.total}`,
398
+ `- **Inserted (new)**: ${stats.inserted}`,
399
+ `- **Updated (existing)**: ${stats.updated}`,
400
+ `- **Skipped (not found)**: ${stats.skipped}`,
401
+ '',
402
+ stats.total === 0
403
+ ? 'No memory files found in memory directory.'
404
+ : `Successfully processed ${stats.inserted + stats.updated} of ${stats.total} memory files.`,
405
+ ];
406
+
407
+ return text(lines.join('\n'));
408
+ }
409
+
377
410
  // ============================================================
378
411
  // Helpers
379
412
  // ============================================================