@massu/core 0.1.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +7772 -3140
- package/dist/hooks/cost-tracker.js +103 -40
- package/dist/hooks/post-edit-context.js +74 -8
- package/dist/hooks/post-tool-use.js +268 -106
- package/dist/hooks/pre-compact.js +167 -43
- package/dist/hooks/pre-delete-check.js +159 -42
- package/dist/hooks/quality-event.js +103 -40
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +143 -84
- package/dist/hooks/session-start.js +186 -49
- package/dist/hooks/user-prompt.js +189 -43
- package/package.json +10 -15
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -0
- package/src/cloud-sync.ts +14 -18
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +230 -5
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/cost-tracker.ts +11 -6
- package/src/db.ts +115 -2
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +21 -16
- package/src/hooks/post-edit-context.ts +4 -4
- 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-end.ts +3 -3
- package/src/hooks/session-start.ts +99 -6
- 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 +1364 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/prompt-analyzer.ts +9 -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/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +45 -89
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +29 -7
- package/src/session-archiver.ts +4 -5
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +1032 -44
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/README.md +0 -40
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// PreToolUse Hook: Pre-Deletion Feature Impact Check
|
|
7
7
|
// Detects file deletion patterns (rm, git rm, Write with empty content)
|
|
8
8
|
// and runs sentinel impact analysis. Blocks if critical features orphaned.
|
|
9
|
+
// Also protects knowledge system files from accidental deletion (P7-005).
|
|
9
10
|
// Must complete in <500ms.
|
|
10
11
|
// ============================================================
|
|
11
12
|
|
|
@@ -27,6 +28,14 @@ interface HookInput {
|
|
|
27
28
|
|
|
28
29
|
const PROJECT_ROOT = getProjectRoot();
|
|
29
30
|
|
|
31
|
+
// Knowledge system files that must never be silently deleted.
|
|
32
|
+
// These files underpin knowledge indexing and memory retrieval.
|
|
33
|
+
const KNOWLEDGE_PROTECTED_FILES = [
|
|
34
|
+
'knowledge-db.ts',
|
|
35
|
+
'knowledge-indexer.ts',
|
|
36
|
+
'knowledge-tools.ts',
|
|
37
|
+
];
|
|
38
|
+
|
|
30
39
|
function getDataDb(): Database.Database | null {
|
|
31
40
|
const dbPath = getResolvedPaths().dataDbPath;
|
|
32
41
|
if (!existsSync(dbPath)) return null;
|
|
@@ -39,6 +48,43 @@ function getDataDb(): Database.Database | null {
|
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
|
|
51
|
+
/**
|
|
52
|
+
* P7-005: Check if the tool call targets a protected knowledge system file.
|
|
53
|
+
* Returns a warning message if a protected file would be deleted/emptied.
|
|
54
|
+
*/
|
|
55
|
+
function checkKnowledgeFileProtection(input: HookInput): string | null {
|
|
56
|
+
const candidateFiles: string[] = [];
|
|
57
|
+
|
|
58
|
+
if (input.tool_name === 'Bash' && input.tool_input.command) {
|
|
59
|
+
const cmd = input.tool_input.command;
|
|
60
|
+
const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
|
|
61
|
+
if (rmMatch) {
|
|
62
|
+
const parts = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
|
|
63
|
+
candidateFiles.push(...parts);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (input.tool_name === 'Write' && input.tool_input.file_path) {
|
|
68
|
+
const content = input.tool_input.content || '';
|
|
69
|
+
if (content.trim().length === 0) {
|
|
70
|
+
candidateFiles.push(input.tool_input.file_path);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const f of candidateFiles) {
|
|
75
|
+
const basename = f.split('/').pop() ?? f;
|
|
76
|
+
if (KNOWLEDGE_PROTECTED_FILES.includes(basename)) {
|
|
77
|
+
return (
|
|
78
|
+
`KNOWLEDGE SYSTEM PROTECTION: "${basename}" is a core knowledge system file. ` +
|
|
79
|
+
`Deleting it will break knowledge indexing and memory retrieval. ` +
|
|
80
|
+
`Create a replacement before removing.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
42
88
|
function extractDeletedFiles(input: HookInput): string[] {
|
|
43
89
|
const files: string[] = [];
|
|
44
90
|
|
|
@@ -51,7 +97,7 @@ function extractDeletedFiles(input: HookInput): string[] {
|
|
|
51
97
|
const paths = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
|
|
52
98
|
for (const p of paths) {
|
|
53
99
|
const relPath = p.startsWith('src/') ? p : p.replace(PROJECT_ROOT + '/', '');
|
|
54
|
-
if (relPath.startsWith('src/')) {
|
|
100
|
+
if (relPath.startsWith('src/') || relPath.endsWith('.py')) {
|
|
55
101
|
files.push(relPath);
|
|
56
102
|
}
|
|
57
103
|
}
|
|
@@ -63,7 +109,7 @@ function extractDeletedFiles(input: HookInput): string[] {
|
|
|
63
109
|
const content = input.tool_input.content || '';
|
|
64
110
|
if (content.trim().length === 0) {
|
|
65
111
|
const relPath = input.tool_input.file_path.replace(PROJECT_ROOT + '/', '');
|
|
66
|
-
if (relPath.startsWith('src/')) {
|
|
112
|
+
if (relPath.startsWith('src/') || relPath.endsWith('.py')) {
|
|
67
113
|
files.push(relPath);
|
|
68
114
|
}
|
|
69
115
|
}
|
|
@@ -77,6 +123,14 @@ async function main(): Promise<void> {
|
|
|
77
123
|
const input = await readStdin();
|
|
78
124
|
const hookInput = JSON.parse(input) as HookInput;
|
|
79
125
|
|
|
126
|
+
// P7-005: Check knowledge file protection before anything else
|
|
127
|
+
const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
|
|
128
|
+
if (knowledgeWarning) {
|
|
129
|
+
process.stdout.write(JSON.stringify({ message: knowledgeWarning }));
|
|
130
|
+
process.exit(0);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
80
134
|
const deletedFiles = extractDeletedFiles(hookInput);
|
|
81
135
|
if (deletedFiles.length === 0) {
|
|
82
136
|
process.exit(0);
|
|
@@ -90,11 +144,14 @@ async function main(): Promise<void> {
|
|
|
90
144
|
return;
|
|
91
145
|
}
|
|
92
146
|
|
|
147
|
+
// The sentinel registry table name (defined by sentinel-db schema)
|
|
148
|
+
const SENTINEL_TABLE = 'massu_sentinel';
|
|
149
|
+
|
|
93
150
|
try {
|
|
94
151
|
// Check if any sentinel tables exist
|
|
95
152
|
const tableExists = db.prepare(
|
|
96
|
-
|
|
97
|
-
).get();
|
|
153
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
|
|
154
|
+
).get(SENTINEL_TABLE);
|
|
98
155
|
|
|
99
156
|
if (!tableExists) {
|
|
100
157
|
process.exit(0);
|
|
@@ -129,6 +186,37 @@ async function main(): Promise<void> {
|
|
|
129
186
|
// Output warning but don't block (user can proceed)
|
|
130
187
|
process.stdout.write(JSON.stringify({ message: msg.join('\n') }));
|
|
131
188
|
}
|
|
189
|
+
// Check Python import graph for deleted .py files
|
|
190
|
+
const pyFiles = deletedFiles.filter(f => f.endsWith('.py'));
|
|
191
|
+
if (pyFiles.length > 0) {
|
|
192
|
+
try {
|
|
193
|
+
for (const pyFile of pyFiles) {
|
|
194
|
+
const importers = db.prepare(
|
|
195
|
+
'SELECT source_file FROM massu_py_imports WHERE target_file = ?'
|
|
196
|
+
).all(pyFile) as { source_file: string }[];
|
|
197
|
+
|
|
198
|
+
const routes = db.prepare(
|
|
199
|
+
'SELECT method, path FROM massu_py_routes WHERE file = ?'
|
|
200
|
+
).all(pyFile) as { method: string; path: string }[];
|
|
201
|
+
|
|
202
|
+
const models = db.prepare(
|
|
203
|
+
'SELECT class_name FROM massu_py_models WHERE file = ?'
|
|
204
|
+
).all(pyFile) as { class_name: string }[];
|
|
205
|
+
|
|
206
|
+
if (importers.length > 0 || routes.length > 0 || models.length > 0) {
|
|
207
|
+
const parts: string[] = [];
|
|
208
|
+
if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
|
|
209
|
+
if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
|
|
210
|
+
if (models.length > 0) parts.push(`defines ${models.length} models`);
|
|
211
|
+
|
|
212
|
+
const msg = `PYTHON IMPACT: "${pyFile}" ${parts.join(', ')}. Check dependents before deleting.`;
|
|
213
|
+
process.stdout.write(JSON.stringify({ message: msg }));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// Python tables may not exist yet
|
|
218
|
+
}
|
|
219
|
+
}
|
|
132
220
|
} finally {
|
|
133
221
|
db.close();
|
|
134
222
|
}
|
|
@@ -20,6 +20,7 @@ interface HookInput {
|
|
|
20
20
|
command?: string;
|
|
21
21
|
file_path?: string;
|
|
22
22
|
content?: string;
|
|
23
|
+
new_string?: string;
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -77,6 +78,26 @@ function checkFilePath(filePath: string): string | null {
|
|
|
77
78
|
return null;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
const DANGEROUS_PYTHON_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
82
|
+
{ pattern: /\beval\s*\(/, label: 'Python eval() — arbitrary code execution' },
|
|
83
|
+
{ pattern: /\bexec\s*\(/, label: 'Python exec() — arbitrary code execution' },
|
|
84
|
+
{ pattern: /\b__import__\s*\(/, label: 'Python __import__() — dynamic import (potential code injection)' },
|
|
85
|
+
{ pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: 'subprocess.call(shell=True) — shell injection risk' },
|
|
86
|
+
{ pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: 'subprocess.Popen(shell=True) — shell injection risk' },
|
|
87
|
+
{ pattern: /os\.system\s*\(/, label: 'os.system() — shell injection risk' },
|
|
88
|
+
{ pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: 'f-string in SQL — SQL injection risk' },
|
|
89
|
+
{ pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: 'String formatting in SQL — SQL injection risk' },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function checkPythonContent(content: string): string | null {
|
|
93
|
+
for (const { pattern, label } of DANGEROUS_PYTHON_PATTERNS) {
|
|
94
|
+
if (pattern.test(content)) {
|
|
95
|
+
return label;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
async function main(): Promise<void> {
|
|
81
102
|
try {
|
|
82
103
|
const input = await readStdin();
|
|
@@ -100,6 +121,17 @@ async function main(): Promise<void> {
|
|
|
100
121
|
}));
|
|
101
122
|
}
|
|
102
123
|
}
|
|
124
|
+
|
|
125
|
+
// Check Python file content for dangerous patterns (Write uses content, Edit uses new_string)
|
|
126
|
+
const pyContent = tool_input.content || tool_input.new_string;
|
|
127
|
+
if ((tool_name === 'Write' || tool_name === 'Edit') && tool_input.file_path?.endsWith('.py') && pyContent) {
|
|
128
|
+
const pyViolation = checkPythonContent(pyContent);
|
|
129
|
+
if (pyViolation) {
|
|
130
|
+
process.stdout.write(JSON.stringify({
|
|
131
|
+
message: `SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}\nFile: ${tool_input.file_path}\nReview carefully before proceeding.`,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
103
135
|
} catch {
|
|
104
136
|
// Hooks must never crash
|
|
105
137
|
}
|
package/src/hooks/session-end.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// Dependencies: P1-002, P5-001, P5-002
|
|
9
9
|
// ============================================================
|
|
10
10
|
|
|
11
|
-
import { getMemoryDb, endSession, addSummary,
|
|
11
|
+
import { getMemoryDb, endSession, addSummary, createSession, addConversationTurn, addToolCallDetail, getLastProcessedLine, setLastProcessedLine } from '../memory-db.ts';
|
|
12
12
|
import { generateCurrentMd } from '../session-state-generator.ts';
|
|
13
13
|
import { archiveAndRegenerate } from '../session-archiver.ts';
|
|
14
|
-
import { parseTranscriptFrom,
|
|
14
|
+
import { parseTranscriptFrom, estimateTokens } from '../transcript-parser.ts';
|
|
15
15
|
import { syncToCloud, drainSyncQueue } from '../cloud-sync.ts';
|
|
16
16
|
import { calculateQualityScore, storeQualityScore, backfillQualityScores } from '../analytics.ts';
|
|
17
17
|
import { extractTokenUsage, calculateCost, storeSessionCost } from '../cost-tracker.ts';
|
|
@@ -64,7 +64,7 @@ async function main(): Promise<void> {
|
|
|
64
64
|
// 4.6. Calculate and store quality score
|
|
65
65
|
try {
|
|
66
66
|
const { score, breakdown } = calculateQualityScore(db, session_id);
|
|
67
|
-
if (score
|
|
67
|
+
if (score !== 50) {
|
|
68
68
|
storeQualityScore(db, session_id, score, breakdown);
|
|
69
69
|
}
|
|
70
70
|
backfillQualityScores(db);
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
// ============================================================
|
|
10
10
|
|
|
11
11
|
import { getMemoryDb, getSessionSummaries, getRecentObservations, getFailedAttempts, getCrossTaskProgress, autoDetectTaskId, linkSessionToTask, createSession } from '../memory-db.ts';
|
|
12
|
-
import { getConfig } from '../config.ts';
|
|
12
|
+
import { getConfig, getResolvedPaths } from '../config.ts';
|
|
13
|
+
import { readFileSync, existsSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
13
15
|
import type Database from 'better-sqlite3';
|
|
14
16
|
|
|
15
17
|
interface HookInput {
|
|
@@ -56,7 +58,7 @@ async function main(): Promise<void> {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
// Build context
|
|
59
|
-
const context = buildContext(db, session_id, source ?? 'startup', tokenBudget, session?.task_id ?? null);
|
|
61
|
+
const context = await buildContext(db, session_id, source ?? 'startup', tokenBudget, session?.task_id ?? null);
|
|
60
62
|
|
|
61
63
|
if (context.trim()) {
|
|
62
64
|
process.stdout.write(context);
|
|
@@ -80,7 +82,7 @@ function getTokenBudget(source: string): number {
|
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
function buildContext(db: Database.Database, sessionId: string, source: string, tokenBudget: number, taskId: string | null): string {
|
|
85
|
+
async function buildContext(db: Database.Database, sessionId: string, source: string, tokenBudget: number, taskId: string | null): Promise<string> {
|
|
84
86
|
const sections: Array<{ text: string; importance: number }> = [];
|
|
85
87
|
|
|
86
88
|
// 1. Failed attempts (highest priority - DON'T RETRY warnings)
|
|
@@ -138,7 +140,50 @@ function buildContext(db: Database.Database, sessionId: string, source: string,
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
// 5.
|
|
143
|
+
// 5. Prevention rules from corrections.md
|
|
144
|
+
const preventionRules = loadCorrectionsPreventionRules();
|
|
145
|
+
if (preventionRules.length > 0) {
|
|
146
|
+
let rulesText = '### Active Prevention Rules (from corrections.md)\n';
|
|
147
|
+
for (const rule of preventionRules) {
|
|
148
|
+
rulesText += `- ${rule}\n`;
|
|
149
|
+
}
|
|
150
|
+
sections.push({ text: rulesText, importance: 9 });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 6. Knowledge index status (warm-up check)
|
|
154
|
+
try {
|
|
155
|
+
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
156
|
+
if (existsSync(knowledgeDbPath)) {
|
|
157
|
+
const Database = (await import('better-sqlite3')).default;
|
|
158
|
+
const kdb = new Database(knowledgeDbPath, { readonly: true });
|
|
159
|
+
try {
|
|
160
|
+
const stats = kdb.prepare(
|
|
161
|
+
'SELECT COUNT(*) as doc_count, MAX(indexed_at) as last_indexed FROM knowledge_documents'
|
|
162
|
+
).get() as { doc_count: number; last_indexed: string | null };
|
|
163
|
+
if (stats.doc_count > 0 && stats.last_indexed) {
|
|
164
|
+
const ageMs = Date.now() - new Date(stats.last_indexed).getTime();
|
|
165
|
+
const ageHours = Math.round(ageMs / 3600000);
|
|
166
|
+
if (ageHours > 24) {
|
|
167
|
+
sections.push({
|
|
168
|
+
text: `### Knowledge Index Status\nIndex has ${stats.doc_count} documents, last indexed ${ageHours}h ago. Consider re-indexing.\n`,
|
|
169
|
+
importance: 3,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
} else if (stats.doc_count === 0) {
|
|
173
|
+
sections.push({
|
|
174
|
+
text: '### Knowledge Index Status\nKnowledge index is empty. Run knowledge indexing to populate it.\n',
|
|
175
|
+
importance: 2,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
kdb.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (_knowledgeErr) {
|
|
183
|
+
// Best-effort: never block session start
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 7. Recent observations sorted by importance
|
|
142
187
|
const recentObs = getRecentObservations(db, 20);
|
|
143
188
|
if (recentObs.length > 0) {
|
|
144
189
|
let obsText = '### Recent Observations\n';
|
|
@@ -153,7 +198,7 @@ function buildContext(db: Database.Database, sessionId: string, source: string,
|
|
|
153
198
|
sections.sort((a, b) => b.importance - a.importance);
|
|
154
199
|
|
|
155
200
|
let usedTokens = 0;
|
|
156
|
-
const headerTokens = estimateTokens('===
|
|
201
|
+
const headerTokens = estimateTokens('=== Massu Memory: Previous Session Context ===\n\n=== END Massu Memory ===\n');
|
|
157
202
|
usedTokens += headerTokens;
|
|
158
203
|
|
|
159
204
|
const includedSections: string[] = [];
|
|
@@ -167,7 +212,7 @@ function buildContext(db: Database.Database, sessionId: string, source: string,
|
|
|
167
212
|
|
|
168
213
|
if (includedSections.length === 0) return '';
|
|
169
214
|
|
|
170
|
-
return `===
|
|
215
|
+
return `=== Massu Memory: Previous Session Context ===\n\n${includedSections.join('\n')}\n=== END Massu Memory ===\n`;
|
|
171
216
|
}
|
|
172
217
|
|
|
173
218
|
function estimateTokens(text: string): number {
|
|
@@ -207,4 +252,52 @@ function safeParseJson(json: string): Record<string, string> | null {
|
|
|
207
252
|
}
|
|
208
253
|
}
|
|
209
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Load prevention rules from corrections.md in the memory directory.
|
|
257
|
+
* Parses the markdown table format: | Date | Wrong Behavior | Correction | Prevention Rule |
|
|
258
|
+
* Returns only the prevention rule column values.
|
|
259
|
+
* Graceful degradation: returns empty array if file doesn't exist or can't be parsed.
|
|
260
|
+
*/
|
|
261
|
+
function loadCorrectionsPreventionRules(): string[] {
|
|
262
|
+
try {
|
|
263
|
+
// Memory path follows Claude's project directory convention
|
|
264
|
+
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
265
|
+
const cwd = process.cwd();
|
|
266
|
+
const config = getConfig();
|
|
267
|
+
const claudeDirName = config.conventions?.claudeDirName ?? '.claude';
|
|
268
|
+
// Convert cwd to Claude's directory format: /Users/x/project -> -Users-x-project
|
|
269
|
+
const projectDirName = cwd.replace(/\//g, '-').replace(/^-/, '');
|
|
270
|
+
const correctionsPath = join(homeDir, claudeDirName, 'projects', projectDirName, 'memory', 'corrections.md');
|
|
271
|
+
|
|
272
|
+
if (!existsSync(correctionsPath)) return [];
|
|
273
|
+
|
|
274
|
+
const content = readFileSync(correctionsPath, 'utf-8');
|
|
275
|
+
const lines = content.split('\n');
|
|
276
|
+
const rules: string[] = [];
|
|
277
|
+
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
// Match table rows: | date | wrong | correction | prevention |
|
|
280
|
+
// Skip header row and separator row
|
|
281
|
+
const trimmed = line.trim();
|
|
282
|
+
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) continue;
|
|
283
|
+
|
|
284
|
+
const cells = trimmed.split('|').map(c => c.trim()).filter(c => c.length > 0);
|
|
285
|
+
if (cells.length < 4) continue;
|
|
286
|
+
|
|
287
|
+
// Skip header and separator rows
|
|
288
|
+
if (cells[0] === 'Date' || cells[0].startsWith('-')) continue;
|
|
289
|
+
|
|
290
|
+
const preventionRule = cells[3];
|
|
291
|
+
if (preventionRule && !preventionRule.startsWith('-') && !preventionRule.startsWith('<!--')) {
|
|
292
|
+
rules.push(preventionRule);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return rules;
|
|
297
|
+
} catch (_e) {
|
|
298
|
+
// Graceful degradation: never block session start
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
210
303
|
main();
|
package/src/hooks/user-prompt.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
// Captures user prompts for search and context.
|
|
8
8
|
// ============================================================
|
|
9
9
|
|
|
10
|
-
import { getMemoryDb, createSession, addUserPrompt, linkSessionToTask, autoDetectTaskId } from '../memory-db.ts';
|
|
10
|
+
import { getMemoryDb, createSession, addUserPrompt, linkSessionToTask, autoDetectTaskId, addObservation } from '../memory-db.ts';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { getResolvedPaths } from '../config.ts';
|
|
11
13
|
|
|
12
14
|
interface HookInput {
|
|
13
15
|
session_id: string;
|
|
@@ -55,6 +57,35 @@ async function main(): Promise<void> {
|
|
|
55
57
|
|
|
56
58
|
// 4. Insert prompt
|
|
57
59
|
addUserPrompt(db, session_id, prompt.trim(), promptNumber);
|
|
60
|
+
|
|
61
|
+
// 5. Knowledge-aware prompt enrichment: detect file references and check knowledge index
|
|
62
|
+
try {
|
|
63
|
+
const fileRefs = extractFileReferences(prompt);
|
|
64
|
+
if (fileRefs.length > 0) {
|
|
65
|
+
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
66
|
+
if (knowledgeDbPath && existsSync(knowledgeDbPath)) {
|
|
67
|
+
const Database = (await import('better-sqlite3')).default;
|
|
68
|
+
const kdb = new Database(knowledgeDbPath, { readonly: true });
|
|
69
|
+
try {
|
|
70
|
+
const placeholders = fileRefs.map(() => '?').join(',');
|
|
71
|
+
const matches = kdb.prepare(
|
|
72
|
+
`SELECT DISTINCT file_path FROM knowledge_documents WHERE file_path IN (${placeholders})`
|
|
73
|
+
).all(...fileRefs) as Array<{ file_path: string }>;
|
|
74
|
+
if (matches.length > 0) {
|
|
75
|
+
addObservation(db, session_id, 'discovery',
|
|
76
|
+
`Knowledge entries exist for referenced files`,
|
|
77
|
+
`Files with knowledge context: ${matches.map(m => m.file_path).join(', ')}`,
|
|
78
|
+
{ importance: 2 }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
kdb.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (_knowledgeErr) {
|
|
87
|
+
// Best-effort: never block prompt capture
|
|
88
|
+
}
|
|
58
89
|
} finally {
|
|
59
90
|
db.close();
|
|
60
91
|
}
|
|
@@ -64,6 +95,20 @@ async function main(): Promise<void> {
|
|
|
64
95
|
process.exit(0);
|
|
65
96
|
}
|
|
66
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Extract file path references from user prompt text.
|
|
100
|
+
* Matches patterns like src/foo/bar.ts, packages/core/src/x.ts, etc.
|
|
101
|
+
*/
|
|
102
|
+
function extractFileReferences(prompt: string): string[] {
|
|
103
|
+
const filePattern = /(?:^|\s)((?:src|packages|lib)\/[\w./-]+\.(?:ts|tsx|js|jsx|md))/g;
|
|
104
|
+
const matches: string[] = [];
|
|
105
|
+
let match: RegExpExecArray | null;
|
|
106
|
+
while ((match = filePattern.exec(prompt)) !== null) {
|
|
107
|
+
matches.push(match[1]);
|
|
108
|
+
}
|
|
109
|
+
return [...new Set(matches)];
|
|
110
|
+
}
|
|
111
|
+
|
|
67
112
|
async function getGitBranch(): Promise<string | undefined> {
|
|
68
113
|
try {
|
|
69
114
|
const { spawnSync } = await import('child_process');
|
package/src/import-resolver.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { readFileSync, existsSync, statSync } from 'fs';
|
|
|
5
5
|
import { resolve, dirname, join } from 'path';
|
|
6
6
|
import type Database from 'better-sqlite3';
|
|
7
7
|
import { getResolvedPaths, getProjectRoot } from './config.ts';
|
|
8
|
+
import { ensureWithinRoot } from './security-utils.ts';
|
|
8
9
|
|
|
9
10
|
interface ImportEdge {
|
|
10
11
|
source_file: string;
|
|
@@ -182,7 +183,7 @@ export function buildImportIndex(dataDb: Database.Database, codegraphDb: Databas
|
|
|
182
183
|
let batch: ImportEdge[] = [];
|
|
183
184
|
|
|
184
185
|
for (const file of files) {
|
|
185
|
-
const absPath = resolve(projectRoot, file.path);
|
|
186
|
+
const absPath = ensureWithinRoot(resolve(projectRoot, file.path), projectRoot);
|
|
186
187
|
if (!existsSync(absPath)) continue;
|
|
187
188
|
|
|
188
189
|
let source: string;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
7
|
+
import { getConfig, getResolvedPaths } from './config.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Connection to Massu Knowledge's SQLite database.
|
|
11
|
+
* Stores indexed .claude/ knowledge: rules, patterns, incidents, verifications, cross-references.
|
|
12
|
+
* Separate from codegraph.db (CodeGraph data) and memory.db (session memory).
|
|
13
|
+
*/
|
|
14
|
+
export function getKnowledgeDb(): Database.Database {
|
|
15
|
+
const dbPath = getResolvedPaths().knowledgeDbPath;
|
|
16
|
+
const dir = dirname(dbPath);
|
|
17
|
+
if (!existsSync(dir)) {
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const db = new Database(dbPath);
|
|
21
|
+
db.pragma('journal_mode = WAL');
|
|
22
|
+
db.pragma('foreign_keys = ON');
|
|
23
|
+
initKnowledgeSchema(db);
|
|
24
|
+
return db;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function initKnowledgeSchema(db: Database.Database): void {
|
|
28
|
+
db.exec(`
|
|
29
|
+
-- Core document chunks (parsed from .claude/**/*.md)
|
|
30
|
+
CREATE TABLE IF NOT EXISTS knowledge_documents (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
file_path TEXT NOT NULL,
|
|
33
|
+
category TEXT NOT NULL,
|
|
34
|
+
title TEXT NOT NULL,
|
|
35
|
+
description TEXT,
|
|
36
|
+
content_hash TEXT NOT NULL,
|
|
37
|
+
indexed_at TEXT NOT NULL,
|
|
38
|
+
indexed_at_epoch INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_kd_filepath ON knowledge_documents(file_path);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_kd_category ON knowledge_documents(category);
|
|
43
|
+
|
|
44
|
+
-- Structured chunks within documents
|
|
45
|
+
CREATE TABLE IF NOT EXISTS knowledge_chunks (
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
document_id INTEGER NOT NULL REFERENCES knowledge_documents(id) ON DELETE CASCADE,
|
|
48
|
+
chunk_type TEXT NOT NULL CHECK(chunk_type IN (
|
|
49
|
+
'section', 'table_row', 'code_block', 'rule', 'incident', 'pattern', 'command', 'mismatch'
|
|
50
|
+
)),
|
|
51
|
+
heading TEXT,
|
|
52
|
+
content TEXT NOT NULL,
|
|
53
|
+
line_start INTEGER,
|
|
54
|
+
line_end INTEGER,
|
|
55
|
+
metadata TEXT DEFAULT '{}'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_kc_doc ON knowledge_chunks(document_id);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_kc_type ON knowledge_chunks(chunk_type);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_kc_heading ON knowledge_chunks(heading);
|
|
61
|
+
|
|
62
|
+
-- Canonical Rules index
|
|
63
|
+
CREATE TABLE IF NOT EXISTS knowledge_rules (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
rule_id TEXT UNIQUE NOT NULL,
|
|
66
|
+
rule_text TEXT NOT NULL,
|
|
67
|
+
vr_type TEXT,
|
|
68
|
+
reference_path TEXT,
|
|
69
|
+
severity TEXT DEFAULT 'HIGH',
|
|
70
|
+
prevention_summary TEXT
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_kr_id ON knowledge_rules(rule_id);
|
|
74
|
+
|
|
75
|
+
-- Verification Requirements index
|
|
76
|
+
CREATE TABLE IF NOT EXISTS knowledge_verifications (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
vr_type TEXT UNIQUE NOT NULL,
|
|
79
|
+
command TEXT NOT NULL,
|
|
80
|
+
expected TEXT NOT NULL,
|
|
81
|
+
use_when TEXT NOT NULL,
|
|
82
|
+
catches TEXT,
|
|
83
|
+
category TEXT
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_kv_type ON knowledge_verifications(vr_type);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_kv_category ON knowledge_verifications(category);
|
|
88
|
+
|
|
89
|
+
-- Incident index
|
|
90
|
+
CREATE TABLE IF NOT EXISTS knowledge_incidents (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
incident_num INTEGER UNIQUE NOT NULL,
|
|
93
|
+
date TEXT NOT NULL,
|
|
94
|
+
type TEXT NOT NULL,
|
|
95
|
+
gap_found TEXT NOT NULL,
|
|
96
|
+
prevention TEXT NOT NULL,
|
|
97
|
+
cr_added TEXT,
|
|
98
|
+
root_cause TEXT,
|
|
99
|
+
user_quote TEXT
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_ki_num ON knowledge_incidents(incident_num);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_ki_type ON knowledge_incidents(type);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_ki_cr ON knowledge_incidents(cr_added);
|
|
105
|
+
|
|
106
|
+
-- Schema mismatch quick lookup
|
|
107
|
+
CREATE TABLE IF NOT EXISTS knowledge_schema_mismatches (
|
|
108
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
109
|
+
table_name TEXT NOT NULL,
|
|
110
|
+
wrong_column TEXT NOT NULL,
|
|
111
|
+
correct_column TEXT NOT NULL,
|
|
112
|
+
source TEXT DEFAULT '${getConfig().conventions?.knowledgeSourceFiles?.[0] ?? 'CLAUDE.md'}'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_ksm_table ON knowledge_schema_mismatches(table_name);
|
|
116
|
+
|
|
117
|
+
-- Cross-reference graph
|
|
118
|
+
CREATE TABLE IF NOT EXISTS knowledge_edges (
|
|
119
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
120
|
+
source_type TEXT NOT NULL,
|
|
121
|
+
source_id TEXT NOT NULL,
|
|
122
|
+
target_type TEXT NOT NULL,
|
|
123
|
+
target_id TEXT NOT NULL,
|
|
124
|
+
edge_type TEXT NOT NULL
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_ke_source ON knowledge_edges(source_type, source_id);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_ke_target ON knowledge_edges(target_type, target_id);
|
|
129
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_ke_unique ON knowledge_edges(source_type, source_id, target_type, target_id, edge_type);
|
|
130
|
+
|
|
131
|
+
-- Staleness tracking
|
|
132
|
+
CREATE TABLE IF NOT EXISTS knowledge_meta (
|
|
133
|
+
key TEXT PRIMARY KEY,
|
|
134
|
+
value TEXT NOT NULL
|
|
135
|
+
);
|
|
136
|
+
`);
|
|
137
|
+
|
|
138
|
+
// FTS5 in separate exec (can fail if schema mismatch on existing table)
|
|
139
|
+
try {
|
|
140
|
+
db.exec(`
|
|
141
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
|
|
142
|
+
heading, content, chunk_type, file_path
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE TRIGGER IF NOT EXISTS kc_fts_insert AFTER INSERT ON knowledge_chunks BEGIN
|
|
146
|
+
INSERT INTO knowledge_fts(rowid, heading, content, chunk_type, file_path)
|
|
147
|
+
SELECT new.id, new.heading, new.content, new.chunk_type, kd.file_path
|
|
148
|
+
FROM knowledge_documents kd WHERE kd.id = new.document_id;
|
|
149
|
+
END;
|
|
150
|
+
|
|
151
|
+
CREATE TRIGGER IF NOT EXISTS kc_fts_delete AFTER DELETE ON knowledge_chunks BEGIN
|
|
152
|
+
INSERT INTO knowledge_fts(knowledge_fts, rowid, heading, content, chunk_type, file_path)
|
|
153
|
+
SELECT 'delete', old.id, old.heading, old.content, old.chunk_type, kd.file_path
|
|
154
|
+
FROM knowledge_documents kd WHERE kd.id = old.document_id;
|
|
155
|
+
END;
|
|
156
|
+
|
|
157
|
+
CREATE TRIGGER IF NOT EXISTS kc_fts_update AFTER UPDATE ON knowledge_chunks BEGIN
|
|
158
|
+
INSERT INTO knowledge_fts(knowledge_fts, rowid, heading, content, chunk_type, file_path)
|
|
159
|
+
SELECT 'delete', old.id, old.heading, old.content, old.chunk_type, kd.file_path
|
|
160
|
+
FROM knowledge_documents kd WHERE kd.id = old.document_id;
|
|
161
|
+
INSERT INTO knowledge_fts(rowid, heading, content, chunk_type, file_path)
|
|
162
|
+
SELECT new.id, new.heading, new.content, new.chunk_type, kd.file_path
|
|
163
|
+
FROM knowledge_documents kd WHERE kd.id = new.document_id;
|
|
164
|
+
END;
|
|
165
|
+
`);
|
|
166
|
+
} catch {
|
|
167
|
+
// FTS5 table may already exist with different schema — not fatal
|
|
168
|
+
}
|
|
169
|
+
}
|