@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.
- package/README.md +40 -0
- package/agents/massu-architecture-reviewer.md +104 -0
- package/agents/massu-blast-radius-analyzer.md +84 -0
- package/agents/massu-competitive-scorer.md +126 -0
- package/agents/massu-help-sync.md +73 -0
- package/agents/massu-migration-writer.md +94 -0
- package/agents/massu-output-scorer.md +87 -0
- package/agents/massu-pattern-reviewer.md +84 -0
- package/agents/massu-plan-auditor.md +170 -0
- package/agents/massu-schema-sync-verifier.md +70 -0
- package/agents/massu-security-reviewer.md +98 -0
- package/agents/massu-ux-reviewer.md +106 -0
- package/commands/_shared-preamble.md +53 -23
- package/commands/_shared-references/auto-learning-protocol.md +71 -0
- package/commands/_shared-references/blast-radius-protocol.md +76 -0
- package/commands/_shared-references/security-pre-screen.md +64 -0
- package/commands/_shared-references/test-first-protocol.md +87 -0
- package/commands/_shared-references/verification-table.md +52 -0
- package/commands/massu-article-review.md +343 -0
- package/commands/massu-autoresearch/references/eval-runner.md +84 -0
- package/commands/massu-autoresearch/references/safety-rails.md +125 -0
- package/commands/massu-autoresearch/references/scoring-protocol.md +151 -0
- package/commands/massu-autoresearch.md +258 -0
- package/commands/massu-batch.md +44 -12
- package/commands/massu-bearings.md +42 -8
- package/commands/massu-checkpoint.md +588 -0
- package/commands/massu-ci-fix.md +2 -2
- package/commands/massu-command-health.md +132 -0
- package/commands/massu-command-improve.md +232 -0
- package/commands/massu-commit.md +205 -44
- package/commands/massu-create-plan.md +239 -57
- package/commands/massu-data/references/common-queries.md +79 -0
- package/commands/massu-data/references/table-guide.md +50 -0
- package/commands/massu-data.md +66 -0
- package/commands/massu-dead-code.md +29 -34
- package/commands/massu-debug/references/auto-learning.md +61 -0
- package/commands/massu-debug/references/codegraph-tracing.md +80 -0
- package/commands/massu-debug/references/common-shortcuts.md +98 -0
- package/commands/massu-debug/references/investigation-phases.md +294 -0
- package/commands/massu-debug/references/report-format.md +107 -0
- package/commands/massu-debug.md +105 -386
- package/commands/massu-docs.md +1 -1
- package/commands/massu-full-audit.md +61 -0
- package/commands/massu-gap-enhancement-analyzer.md +276 -16
- package/commands/massu-golden-path/references/approval-points.md +216 -0
- package/commands/massu-golden-path/references/competitive-mode.md +273 -0
- package/commands/massu-golden-path/references/error-handling.md +121 -0
- package/commands/massu-golden-path/references/phase-0-requirements.md +53 -0
- package/commands/massu-golden-path/references/phase-1-plan-creation.md +168 -0
- package/commands/massu-golden-path/references/phase-2-implementation.md +397 -0
- package/commands/massu-golden-path/references/phase-2.5-gap-analyzer.md +156 -0
- package/commands/massu-golden-path/references/phase-3-simplify.md +40 -0
- package/commands/massu-golden-path/references/phase-4-commit.md +94 -0
- package/commands/massu-golden-path/references/phase-5-push.md +116 -0
- package/commands/massu-golden-path/references/phase-5.5-production-verify.md +170 -0
- package/commands/massu-golden-path/references/phase-6-completion.md +113 -0
- package/commands/massu-golden-path/references/qa-evaluator-spec.md +137 -0
- package/commands/massu-golden-path/references/sprint-contract-protocol.md +117 -0
- package/commands/massu-golden-path/references/vr-visual-calibration.md +73 -0
- package/commands/massu-golden-path.md +114 -848
- package/commands/massu-guide.md +72 -69
- package/commands/massu-hooks.md +27 -12
- package/commands/massu-hotfix.md +221 -144
- package/commands/massu-incident.md +49 -20
- package/commands/massu-infra-audit.md +187 -0
- package/commands/massu-learning-audit.md +211 -0
- package/commands/massu-loop/references/auto-learning.md +49 -0
- package/commands/massu-loop/references/checkpoint-audit.md +40 -0
- package/commands/massu-loop/references/guardrails.md +17 -0
- package/commands/massu-loop/references/iteration-structure.md +115 -0
- package/commands/massu-loop/references/loop-controller.md +188 -0
- package/commands/massu-loop/references/plan-extraction.md +78 -0
- package/commands/massu-loop/references/vr-plan-spec.md +140 -0
- package/commands/massu-loop-playwright.md +9 -9
- package/commands/massu-loop.md +115 -670
- package/commands/massu-new-pattern.md +423 -0
- package/commands/massu-perf.md +422 -0
- package/commands/massu-plan-audit.md +1 -1
- package/commands/massu-plan.md +389 -122
- package/commands/massu-production-verify.md +433 -0
- package/commands/massu-push.md +62 -378
- package/commands/massu-recap.md +29 -3
- package/commands/massu-rollback.md +613 -0
- package/commands/massu-scaffold-hook.md +2 -4
- package/commands/massu-scaffold-page.md +2 -3
- package/commands/massu-scaffold-router.md +1 -2
- package/commands/massu-security.md +619 -0
- package/commands/massu-simplify.md +115 -85
- package/commands/massu-squirrels.md +2 -2
- package/commands/massu-tdd.md +38 -22
- package/commands/massu-test.md +3 -3
- package/commands/massu-type-mismatch-audit.md +469 -0
- package/commands/massu-ui-audit.md +587 -0
- package/commands/massu-verify-playwright.md +287 -32
- package/commands/massu-verify.md +150 -46
- package/dist/cli.js +1451 -1047
- package/dist/hooks/post-tool-use.js +75 -6
- package/dist/hooks/user-prompt.js +16 -0
- package/package.json +6 -2
- package/patterns/build-patterns.md +302 -0
- package/patterns/component-patterns.md +246 -0
- package/patterns/display-patterns.md +185 -0
- package/patterns/form-patterns.md +890 -0
- package/patterns/integration-testing-checklist.md +445 -0
- package/patterns/security-patterns.md +219 -0
- package/patterns/testing-patterns.md +569 -0
- package/patterns/tool-routing.md +81 -0
- package/patterns/ui-patterns.md +371 -0
- package/protocols/plan-implementation.md +267 -0
- package/protocols/recovery.md +225 -0
- package/protocols/verification.md +404 -0
- package/reference/command-taxonomy.md +178 -0
- package/reference/cr-rules-reference.md +76 -0
- package/reference/hook-execution-order.md +148 -0
- package/reference/lessons-learned.md +175 -0
- package/reference/patterns-quickref.md +208 -0
- package/reference/standards.md +135 -0
- package/reference/subagents-reference.md +17 -0
- package/reference/vr-verification-reference.md +867 -0
- package/src/commands/init.ts +27 -0
- package/src/commands/install-commands.ts +149 -53
- package/src/hooks/post-tool-use.ts +17 -0
- package/src/hooks/user-prompt.ts +21 -0
- package/src/memory-file-ingest.ts +127 -0
- package/src/memory-tools.ts +34 -1
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
5
|
+
* `massu install-commands` — Install massu slash commands, agents, patterns,
|
|
6
|
+
* protocols, and reference files into a project.
|
|
6
7
|
*
|
|
7
|
-
* Copies all massu
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
//
|
|
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
|
|
39
|
+
* Resolve the path to a bundled asset directory.
|
|
26
40
|
* Handles both npm-installed and local development scenarios.
|
|
27
41
|
*/
|
|
28
|
-
export function
|
|
42
|
+
export function resolveAssetDir(assetName: string): string | null {
|
|
29
43
|
const cwd = process.cwd();
|
|
30
44
|
|
|
31
|
-
// 1. npm-installed: node_modules/@massu/core/
|
|
32
|
-
const nodeModulesPath = resolve(cwd, 'node_modules/@massu/core
|
|
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 → ../
|
|
38
|
-
const distRelPath = resolve(__dirname, '
|
|
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/ → ../../
|
|
44
|
-
const srcRelPath = resolve(__dirname, '
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
152
|
+
const stats = syncDirectory(sourceDir, targetDir);
|
|
153
|
+
return { ...stats, commandsDir: targetDir };
|
|
154
|
+
}
|
|
78
155
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
156
|
+
// ============================================================
|
|
157
|
+
// Install All Assets
|
|
158
|
+
// ============================================================
|
|
82
159
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 {
|
|
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
|
|
203
|
+
console.log('Massu AI - Install Project Assets');
|
|
116
204
|
console.log('==================================');
|
|
117
205
|
console.log('');
|
|
118
206
|
|
|
119
|
-
const result =
|
|
207
|
+
const result = installAll(projectRoot);
|
|
120
208
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
227
|
+
const grandTotal = result.totalInstalled + result.totalUpdated + result.totalSkipped;
|
|
132
228
|
console.log('');
|
|
133
|
-
console.log(` ${
|
|
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') {
|
package/src/hooks/user-prompt.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/memory-tools.ts
CHANGED
|
@@ -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
|
// ============================================================
|