@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.
- package/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +12521 -0
- package/dist/hooks/cost-tracker.js +80 -5
- package/dist/hooks/post-edit-context.js +72 -6
- package/dist/hooks/post-tool-use.js +234 -57
- package/dist/hooks/pre-compact.js +144 -5
- package/dist/hooks/pre-delete-check.js +141 -11
- package/dist/hooks/quality-event.js +80 -5
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +83 -8
- package/dist/hooks/session-start.js +153 -7
- package/dist/hooks/user-prompt.js +166 -5
- package/package.json +6 -5
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -1
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +235 -6
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/db.ts +115 -2
- package/src/docs-tools.ts +8 -6
- package/src/hooks/post-edit-context.ts +1 -1
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-start.ts +97 -4
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +14 -1
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/sentinel-db.ts +2 -1
- package/src/server.ts +29 -6
- package/src/session-archiver.ts +4 -5
- package/src/tools.ts +283 -31
- package/README.md +0 -40
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `massu install-commands` — Install massu slash commands into a project.
|
|
6
|
+
*
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
13
|
+
import { resolve, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { getConfig } from '../config.ts';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Command Installation
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the path to the bundled commands directory.
|
|
26
|
+
* Handles both npm-installed and local development scenarios.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveCommandsDir(): string | null {
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
|
|
31
|
+
// 1. npm-installed: node_modules/@massu/core/commands
|
|
32
|
+
const nodeModulesPath = resolve(cwd, 'node_modules/@massu/core/commands');
|
|
33
|
+
if (existsSync(nodeModulesPath)) {
|
|
34
|
+
return nodeModulesPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Relative to compiled dist/cli.js → ../commands
|
|
38
|
+
const distRelPath = resolve(__dirname, '../commands');
|
|
39
|
+
if (existsSync(distRelPath)) {
|
|
40
|
+
return distRelPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3. Relative to source src/commands/ → ../../commands
|
|
44
|
+
const srcRelPath = resolve(__dirname, '../../commands');
|
|
45
|
+
if (existsSync(srcRelPath)) {
|
|
46
|
+
return srcRelPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface InstallCommandsResult {
|
|
53
|
+
installed: number;
|
|
54
|
+
updated: number;
|
|
55
|
+
skipped: number;
|
|
56
|
+
commandsDir: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function installCommands(projectRoot: string): InstallCommandsResult {
|
|
60
|
+
const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
|
|
61
|
+
const targetDir = resolve(projectRoot, claudeDirName, 'commands');
|
|
62
|
+
|
|
63
|
+
// Ensure .claude/commands directory exists
|
|
64
|
+
if (!existsSync(targetDir)) {
|
|
65
|
+
mkdirSync(targetDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Find source commands
|
|
69
|
+
const sourceDir = resolveCommandsDir();
|
|
70
|
+
if (!sourceDir) {
|
|
71
|
+
console.error(' ERROR: Could not find massu commands directory.');
|
|
72
|
+
console.error(' Try reinstalling: npm install @massu/core');
|
|
73
|
+
return { installed: 0, updated: 0, skipped: 0, commandsDir: targetDir };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Read all command files from source
|
|
77
|
+
const sourceFiles = readdirSync(sourceDir).filter(f => f.endsWith('.md'));
|
|
78
|
+
|
|
79
|
+
let installed = 0;
|
|
80
|
+
let updated = 0;
|
|
81
|
+
let skipped = 0;
|
|
82
|
+
|
|
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');
|
|
87
|
+
|
|
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++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { installed, updated, skipped, commandsDir: targetDir };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================
|
|
108
|
+
// Standalone CLI Runner
|
|
109
|
+
// ============================================================
|
|
110
|
+
|
|
111
|
+
export async function runInstallCommands(): Promise<void> {
|
|
112
|
+
const projectRoot = process.cwd();
|
|
113
|
+
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log('Massu AI - Install Slash Commands');
|
|
116
|
+
console.log('==================================');
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
const result = installCommands(projectRoot);
|
|
120
|
+
|
|
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`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const total = result.installed + result.updated + result.skipped;
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(` ${total} slash commands available in ${result.commandsDir}`);
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(' Restart your Claude Code session to use them.');
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
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, '
|
|
344
|
-
claudeMdPath: resolve(root, '
|
|
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
|
|
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
|
+
}
|
package/src/docs-tools.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { readFileSync, existsSync } from 'fs';
|
|
5
5
|
import { resolve, basename } from 'path';
|
|
6
|
-
import { getConfig, getResolvedPaths } from './config.ts';
|
|
6
|
+
import { getConfig, getResolvedPaths, getProjectRoot } from './config.ts';
|
|
7
|
+
import { ensureWithinRoot } from './security-utils.ts';
|
|
7
8
|
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
8
9
|
|
|
9
10
|
/** Prefix a base tool name with the configured tool prefix. */
|
|
@@ -252,10 +253,11 @@ function extractFrontmatter(content: string): Record<string, string> | null {
|
|
|
252
253
|
* Extract procedure names from a router file.
|
|
253
254
|
*/
|
|
254
255
|
function extractProcedureNames(routerPath: string): string[] {
|
|
255
|
-
const
|
|
256
|
+
const root = getProjectRoot();
|
|
257
|
+
const absPath = ensureWithinRoot(resolve(getResolvedPaths().srcDir, '..', routerPath), root);
|
|
256
258
|
if (!existsSync(absPath)) {
|
|
257
259
|
// Try from project root
|
|
258
|
-
const altPath = resolve(getResolvedPaths().srcDir, '../server/api/routers', basename(routerPath));
|
|
260
|
+
const altPath = ensureWithinRoot(resolve(getResolvedPaths().srcDir, '../server/api/routers', basename(routerPath)), root);
|
|
259
261
|
if (!existsSync(altPath)) return [];
|
|
260
262
|
return extractProcedureNamesFromContent(readFileSync(altPath, 'utf-8'));
|
|
261
263
|
}
|
|
@@ -325,7 +327,7 @@ function handleDocsAudit(args: Record<string, unknown>): ToolResult {
|
|
|
325
327
|
const mapping = docsMap.mappings.find(m => m.id === mappingId);
|
|
326
328
|
if (!mapping) continue;
|
|
327
329
|
|
|
328
|
-
const helpPagePath = resolve(getResolvedPaths().helpSitePath, mapping.helpPage);
|
|
330
|
+
const helpPagePath = ensureWithinRoot(resolve(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
|
|
329
331
|
|
|
330
332
|
if (!existsSync(helpPagePath)) {
|
|
331
333
|
results.push({
|
|
@@ -394,7 +396,7 @@ function handleDocsAudit(args: Record<string, unknown>): ToolResult {
|
|
|
394
396
|
// Also flag inherited user guides
|
|
395
397
|
for (const [guideName, parentId] of Object.entries(docsMap.userGuideInheritance.examples)) {
|
|
396
398
|
if (parentId === mappingId) {
|
|
397
|
-
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());
|
|
398
400
|
if (existsSync(guidePath)) {
|
|
399
401
|
const guideContent = readFileSync(guidePath, 'utf-8');
|
|
400
402
|
const guideFrontmatter = extractFrontmatter(guideContent);
|
|
@@ -439,7 +441,7 @@ function handleDocsCoverage(args: Record<string, unknown>): ToolResult {
|
|
|
439
441
|
: docsMap.mappings;
|
|
440
442
|
|
|
441
443
|
for (const mapping of mappings) {
|
|
442
|
-
const helpPagePath = resolve(getResolvedPaths().helpSitePath, mapping.helpPage);
|
|
444
|
+
const helpPagePath = ensureWithinRoot(resolve(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
|
|
443
445
|
const exists = existsSync(helpPagePath);
|
|
444
446
|
let hasContent = false;
|
|
445
447
|
let lineCount = 0;
|
|
@@ -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
|
}
|
|
@@ -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();
|
package/src/hooks/pre-compact.ts
CHANGED
|
@@ -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, {
|