@massu/core 1.10.8 → 1.12.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/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +517 -397
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
-
|
|
5
|
-
// ============================================================
|
|
6
|
-
// P3-001: Enhanced SessionStart Hook
|
|
7
|
-
// Injects context from previous sessions into new sessions.
|
|
8
|
-
// Output: plain text to stdout (auto-injected by Claude Code)
|
|
9
|
-
// ============================================================
|
|
10
|
-
|
|
11
|
-
import { getMemoryDb, getSessionSummaries, getRecentObservations, getFailedAttempts, getCrossTaskProgress, autoDetectTaskId, linkSessionToTask, createSession } from '../memory-db.ts';
|
|
12
|
-
import { getConfig, getResolvedPaths } from '../config.ts';
|
|
13
|
-
import { readFileSync, existsSync } from 'fs';
|
|
14
|
-
import { join, resolve } from 'path';
|
|
15
|
-
import { parse as parseYaml } from 'yaml';
|
|
16
|
-
import type Database from 'better-sqlite3';
|
|
17
|
-
import { runDetection } from '../detect/index.ts';
|
|
18
|
-
import { computeFingerprint } from '../detect/drift.ts';
|
|
19
|
-
import { isPidAlive } from '../lib/pidLiveness.ts';
|
|
20
|
-
|
|
21
|
-
interface HookInput {
|
|
22
|
-
session_id: string;
|
|
23
|
-
transcript_path: string;
|
|
24
|
-
cwd: string;
|
|
25
|
-
hook_event_name: string;
|
|
26
|
-
source?: 'startup' | 'resume' | 'clear' | 'compact';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function main(): Promise<void> {
|
|
30
|
-
try {
|
|
31
|
-
// Read stdin
|
|
32
|
-
const input = await readStdin();
|
|
33
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
34
|
-
const { session_id, source } = hookInput;
|
|
35
|
-
|
|
36
|
-
const db = getMemoryDb();
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// Create session if not exists
|
|
40
|
-
const gitBranch = await getGitBranch();
|
|
41
|
-
createSession(db, session_id, { branch: gitBranch });
|
|
42
|
-
|
|
43
|
-
// Check if session has a plan_file and link task
|
|
44
|
-
const session = db.prepare('SELECT plan_file, task_id FROM sessions WHERE session_id = ?').get(session_id) as { plan_file: string | null; task_id: string | null } | undefined;
|
|
45
|
-
if (session?.plan_file && !session.task_id) {
|
|
46
|
-
const taskId = autoDetectTaskId(session.plan_file);
|
|
47
|
-
if (taskId) linkSessionToTask(db, session_id, taskId);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Token budget based on source
|
|
51
|
-
const tokenBudget = getTokenBudget(source ?? 'startup');
|
|
52
|
-
|
|
53
|
-
// Check if this is the very first session (no prior sessions)
|
|
54
|
-
const sessionCount = db.prepare('SELECT COUNT(*) as count FROM sessions').get() as { count: number };
|
|
55
|
-
if (sessionCount.count <= 1 && (source === 'startup' || !source)) {
|
|
56
|
-
process.stdout.write(
|
|
57
|
-
'=== MASSU AI: Active ===\n' +
|
|
58
|
-
'Session memory, code intelligence, and governance are now active.\n' +
|
|
59
|
-
`11 hooks monitoring this session. Type "${getConfig().toolPrefix ?? 'massu'}_sync" to index your codebase.\n` +
|
|
60
|
-
'=== END MASSU ===\n\n'
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Build context
|
|
65
|
-
const context = await buildContext(db, session_id, source ?? 'startup', tokenBudget, session?.task_id ?? null);
|
|
66
|
-
|
|
67
|
-
if (context.trim()) {
|
|
68
|
-
process.stdout.write(context);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// P5-001: drift banner (runs after memory context, independent of it).
|
|
72
|
-
// Plan 3a Phase 6: when a live watcher daemon exists, the drift banner
|
|
73
|
-
// is suppressed in favor of a compact watcher banner.
|
|
74
|
-
const driftBanner = await buildDriftBanner();
|
|
75
|
-
if (driftBanner) {
|
|
76
|
-
process.stdout.write(driftBanner);
|
|
77
|
-
} else {
|
|
78
|
-
const watcherBanner = buildWatcherBanner();
|
|
79
|
-
if (watcherBanner) process.stdout.write(watcherBanner);
|
|
80
|
-
}
|
|
81
|
-
} finally {
|
|
82
|
-
db.close();
|
|
83
|
-
}
|
|
84
|
-
} catch (_e) {
|
|
85
|
-
// Best-effort: never block Claude Code
|
|
86
|
-
process.exit(0);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getTokenBudget(source: string): number {
|
|
91
|
-
switch (source) {
|
|
92
|
-
case 'compact': return 4000;
|
|
93
|
-
case 'startup': return 2000;
|
|
94
|
-
case 'resume': return 1000;
|
|
95
|
-
case 'clear': return 2000;
|
|
96
|
-
default: return 2000;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function buildContext(db: Database.Database, sessionId: string, source: string, tokenBudget: number, taskId: string | null): Promise<string> {
|
|
101
|
-
const sections: Array<{ text: string; importance: number }> = [];
|
|
102
|
-
|
|
103
|
-
// 1. Failed attempts (highest priority - DON'T RETRY warnings)
|
|
104
|
-
const failures = getFailedAttempts(db, undefined, 10);
|
|
105
|
-
if (failures.length > 0) {
|
|
106
|
-
let failText = '### Failed Attempts (DO NOT RETRY)\n';
|
|
107
|
-
for (const f of failures) {
|
|
108
|
-
const recurrence = f.recurrence_count > 1 ? ` (${f.recurrence_count}x)` : '';
|
|
109
|
-
failText += `- ${f.title}${recurrence}\n`;
|
|
110
|
-
}
|
|
111
|
-
sections.push({ text: failText, importance: 10 });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 2. For compact: include current session's own observations
|
|
115
|
-
if (source === 'compact') {
|
|
116
|
-
const currentObs = getRecentObservations(db, 30, sessionId);
|
|
117
|
-
if (currentObs.length > 0) {
|
|
118
|
-
let currentText = '### Current Session Observations (restored after compaction)\n';
|
|
119
|
-
for (const obs of currentObs) {
|
|
120
|
-
currentText += `- [${obs.type}] ${obs.title}\n`;
|
|
121
|
-
}
|
|
122
|
-
sections.push({ text: currentText, importance: 9 });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 3. Recent session summaries
|
|
127
|
-
const summaryCount = source === 'compact' ? 5 : 3;
|
|
128
|
-
const summaries = getSessionSummaries(db, summaryCount);
|
|
129
|
-
if (summaries.length > 0) {
|
|
130
|
-
for (const s of summaries) {
|
|
131
|
-
let sumText = `### Session (${s.created_at.split('T')[0]})\n`;
|
|
132
|
-
if (s.request) sumText += `**Task**: ${s.request.slice(0, 200)}\n`;
|
|
133
|
-
if (s.completed) sumText += `**Completed**: ${s.completed.slice(0, 300)}\n`;
|
|
134
|
-
if (s.failed_attempts) sumText += `**Failed**: ${s.failed_attempts.slice(0, 200)}\n`;
|
|
135
|
-
|
|
136
|
-
const progress = safeParseJson(s.plan_progress);
|
|
137
|
-
if (progress && Object.keys(progress).length > 0) {
|
|
138
|
-
const total = Object.keys(progress).length;
|
|
139
|
-
const complete = Object.values(progress).filter(v => v === 'complete').length;
|
|
140
|
-
sumText += `**Plan**: ${complete}/${total} complete\n`;
|
|
141
|
-
}
|
|
142
|
-
sections.push({ text: sumText, importance: 7 });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 4. Cross-task progress if task_id exists
|
|
147
|
-
if (taskId) {
|
|
148
|
-
const progress = getCrossTaskProgress(db, taskId);
|
|
149
|
-
if (Object.keys(progress).length > 0) {
|
|
150
|
-
const total = Object.keys(progress).length;
|
|
151
|
-
const complete = Object.values(progress).filter(v => v === 'complete').length;
|
|
152
|
-
let progressText = `### Cross-Session Task Progress (${taskId})\n`;
|
|
153
|
-
progressText += `${complete}/${total} items complete\n`;
|
|
154
|
-
sections.push({ text: progressText, importance: 8 });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 5. Prevention rules from corrections.md
|
|
159
|
-
const preventionRules = loadCorrectionsPreventionRules();
|
|
160
|
-
if (preventionRules.length > 0) {
|
|
161
|
-
let rulesText = '### Active Prevention Rules (from corrections.md)\n';
|
|
162
|
-
for (const rule of preventionRules) {
|
|
163
|
-
rulesText += `- ${rule}\n`;
|
|
164
|
-
}
|
|
165
|
-
sections.push({ text: rulesText, importance: 9 });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 6. Knowledge index status (warm-up check)
|
|
169
|
-
try {
|
|
170
|
-
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
171
|
-
if (existsSync(knowledgeDbPath)) {
|
|
172
|
-
const Database = (await import('better-sqlite3')).default;
|
|
173
|
-
const kdb = new Database(knowledgeDbPath, { readonly: true });
|
|
174
|
-
try {
|
|
175
|
-
const stats = kdb.prepare(
|
|
176
|
-
'SELECT COUNT(*) as doc_count, MAX(indexed_at) as last_indexed FROM knowledge_documents'
|
|
177
|
-
).get() as { doc_count: number; last_indexed: string | null };
|
|
178
|
-
if (stats.doc_count > 0 && stats.last_indexed) {
|
|
179
|
-
const ageMs = Date.now() - new Date(stats.last_indexed).getTime();
|
|
180
|
-
const ageHours = Math.round(ageMs / 3600000);
|
|
181
|
-
if (ageHours > 24) {
|
|
182
|
-
sections.push({
|
|
183
|
-
text: `### Knowledge Index Status\nIndex has ${stats.doc_count} documents, last indexed ${ageHours}h ago. Consider re-indexing.\n`,
|
|
184
|
-
importance: 3,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
} else if (stats.doc_count === 0) {
|
|
188
|
-
sections.push({
|
|
189
|
-
text: '### Knowledge Index Status\nKnowledge index is empty. Run knowledge indexing to populate it.\n',
|
|
190
|
-
importance: 2,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
} finally {
|
|
194
|
-
kdb.close();
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
} catch (_knowledgeErr) {
|
|
198
|
-
// Best-effort: never block session start
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 7. Recent observations sorted by importance
|
|
202
|
-
const recentObs = getRecentObservations(db, 20);
|
|
203
|
-
if (recentObs.length > 0) {
|
|
204
|
-
let obsText = '### Recent Observations\n';
|
|
205
|
-
const sorted = [...recentObs].sort((a, b) => b.importance - a.importance);
|
|
206
|
-
for (const obs of sorted) {
|
|
207
|
-
obsText += `- [${obs.type}|imp:${obs.importance}] ${obs.title} (${obs.created_at.split('T')[0]})\n`;
|
|
208
|
-
}
|
|
209
|
-
sections.push({ text: obsText, importance: 5 });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Fill token budget from high-importance to low-importance
|
|
213
|
-
sections.sort((a, b) => b.importance - a.importance);
|
|
214
|
-
|
|
215
|
-
let usedTokens = 0;
|
|
216
|
-
const headerTokens = estimateTokens('=== Massu Memory: Previous Session Context ===\n\n=== END Massu Memory ===\n');
|
|
217
|
-
usedTokens += headerTokens;
|
|
218
|
-
|
|
219
|
-
const includedSections: string[] = [];
|
|
220
|
-
for (const section of sections) {
|
|
221
|
-
const sectionTokens = estimateTokens(section.text);
|
|
222
|
-
if (usedTokens + sectionTokens <= tokenBudget) {
|
|
223
|
-
includedSections.push(section.text);
|
|
224
|
-
usedTokens += sectionTokens;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (includedSections.length === 0) return '';
|
|
229
|
-
|
|
230
|
-
return `=== Massu Memory: Previous Session Context ===\n\n${includedSections.join('\n')}\n=== END Massu Memory ===\n`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function estimateTokens(text: string): number {
|
|
234
|
-
return Math.ceil(text.length / 4);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function getGitBranch(): Promise<string | undefined> {
|
|
238
|
-
try {
|
|
239
|
-
const { spawnSync } = await import('child_process');
|
|
240
|
-
const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
241
|
-
encoding: 'utf-8',
|
|
242
|
-
timeout: 5000,
|
|
243
|
-
});
|
|
244
|
-
if (result.status !== 0 || result.error) return undefined;
|
|
245
|
-
return result.stdout.trim();
|
|
246
|
-
} catch (_e) {
|
|
247
|
-
return undefined;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function readStdin(): Promise<string> {
|
|
252
|
-
return new Promise((resolve) => {
|
|
253
|
-
let data = '';
|
|
254
|
-
process.stdin.setEncoding('utf-8');
|
|
255
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
256
|
-
process.stdin.on('end', () => resolve(data));
|
|
257
|
-
// Timeout after 3s
|
|
258
|
-
setTimeout(() => resolve(data), 3000);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* P5-001: compare the fingerprint stored in massu.config.yaml (detection.fingerprint,
|
|
264
|
-
* stamped by init/refresh/upgrade) against a freshly-computed fingerprint. If they
|
|
265
|
-
* disagree, return a plain-text banner. Returns '' on any error or when the
|
|
266
|
-
* config has no fingerprint (back-compat with v1 configs).
|
|
267
|
-
*/
|
|
268
|
-
async function buildDriftBanner(): Promise<string> {
|
|
269
|
-
try {
|
|
270
|
-
// Plan #2 P4-004: explicit opt-out for users in deliberate mid-migration windows.
|
|
271
|
-
// Stays at the top so MASSU_DRIFT_QUIET=1 remains the strongest signal
|
|
272
|
-
// (iter-1 G8: env-var override beats watcher-state suppression).
|
|
273
|
-
if (process.env.MASSU_DRIFT_QUIET === '1') return '';
|
|
274
|
-
|
|
275
|
-
// Plan 3a Phase 6: if a live watcher daemon refreshed within the last 24h,
|
|
276
|
-
// suppress this banner — the watcher already keeps the config current.
|
|
277
|
-
if (watcherIsLiveAndFresh()) return '';
|
|
278
|
-
|
|
279
|
-
const configPath = resolve(process.cwd(), 'massu.config.yaml');
|
|
280
|
-
if (!existsSync(configPath)) return '';
|
|
281
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
282
|
-
// pattern-scanner-allow: yaml-parse — reason: compiled standalone hook (esbuild bundle). Per P2-023a, hooks cannot import getConfig() — they run in the Claude Code subprocess context with no module resolution path back to packages/core. Direct YAML parse is the only available access pattern.
|
|
283
|
-
const parsed = parseYaml(content) as Record<string, unknown> | null;
|
|
284
|
-
if (!parsed || typeof parsed !== 'object') return '';
|
|
285
|
-
const det = parsed.detection as Record<string, unknown> | undefined;
|
|
286
|
-
const storedFp = typeof det?.fingerprint === 'string' ? (det.fingerprint as string) : null;
|
|
287
|
-
if (!storedFp) return '';
|
|
288
|
-
// Plan #2 P4-006: skip the codebase introspector pass — the drift banner
|
|
289
|
-
// only needs the fingerprint, not the introspected detail. Saves up to 2s
|
|
290
|
-
// wall-clock from the hook's 5-second budget.
|
|
291
|
-
const detection = await runDetection(process.cwd(), undefined, { skipIntrospect: true });
|
|
292
|
-
const currentFp = computeFingerprint(detection);
|
|
293
|
-
if (currentFp === storedFp) return '';
|
|
294
|
-
return (
|
|
295
|
-
'=== Massu Config Drift ===\n' +
|
|
296
|
-
'Detected stack has changed since last config refresh.\n' +
|
|
297
|
-
`Fingerprint: ${storedFp.slice(0, 16)} -> ${currentFp.slice(0, 16)}\n` +
|
|
298
|
-
'Run: npx massu config refresh\n' +
|
|
299
|
-
'(this will update massu.config.yaml AND any commands that need\n' +
|
|
300
|
-
' re-templating for your new stack)\n' +
|
|
301
|
-
'Tip: set MASSU_DRIFT_QUIET=1 to suppress this banner during mid-migration.\n' +
|
|
302
|
-
'=== END ===\n'
|
|
303
|
-
);
|
|
304
|
-
} catch (_e) {
|
|
305
|
-
// Never block session start on drift-check failure.
|
|
306
|
-
return '';
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function safeParseJson(json: string): Record<string, string> | null {
|
|
311
|
-
try {
|
|
312
|
-
return JSON.parse(json);
|
|
313
|
-
} catch (_e) {
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Load prevention rules from corrections.md in the memory directory.
|
|
320
|
-
* Parses the markdown table format: | Date | Wrong Behavior | Correction | Prevention Rule |
|
|
321
|
-
* Returns only the prevention rule column values.
|
|
322
|
-
* Graceful degradation: returns empty array if file doesn't exist or can't be parsed.
|
|
323
|
-
*/
|
|
324
|
-
function loadCorrectionsPreventionRules(): string[] {
|
|
325
|
-
try {
|
|
326
|
-
// Memory path follows Claude's project directory convention
|
|
327
|
-
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
328
|
-
const cwd = process.cwd();
|
|
329
|
-
const config = getConfig();
|
|
330
|
-
const claudeDirName = config.conventions?.claudeDirName ?? '.claude';
|
|
331
|
-
// Convert cwd to Claude's directory format: /Users/x/project -> -Users-x-project.
|
|
332
|
-
// Match both forward slashes (POSIX) and backslashes (Windows) so the lookup
|
|
333
|
-
// works cross-platform — without this, Windows users silently miss prevention
|
|
334
|
-
// rules because their projectDirName never resolves.
|
|
335
|
-
const projectDirName = cwd.replace(/[/\\]/g, '-').replace(/^-/, '');
|
|
336
|
-
const correctionsPath = join(homeDir, claudeDirName, 'projects', projectDirName, 'memory', 'corrections.md');
|
|
337
|
-
|
|
338
|
-
if (!existsSync(correctionsPath)) return [];
|
|
339
|
-
|
|
340
|
-
const content = readFileSync(correctionsPath, 'utf-8');
|
|
341
|
-
const lines = content.split('\n');
|
|
342
|
-
const rules: string[] = [];
|
|
343
|
-
|
|
344
|
-
for (const line of lines) {
|
|
345
|
-
// Match table rows: | date | wrong | correction | prevention |
|
|
346
|
-
// Skip header row and separator row
|
|
347
|
-
const trimmed = line.trim();
|
|
348
|
-
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) continue;
|
|
349
|
-
|
|
350
|
-
const cells = trimmed.split('|').map(c => c.trim()).filter(c => c.length > 0);
|
|
351
|
-
if (cells.length < 4) continue;
|
|
352
|
-
|
|
353
|
-
// Skip header and separator rows
|
|
354
|
-
if (cells[0] === 'Date' || cells[0].startsWith('-')) continue;
|
|
355
|
-
|
|
356
|
-
const preventionRule = cells[3];
|
|
357
|
-
if (preventionRule && !preventionRule.startsWith('-') && !preventionRule.startsWith('<!--')) {
|
|
358
|
-
rules.push(preventionRule);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return rules;
|
|
363
|
-
} catch (_e) {
|
|
364
|
-
// Graceful degradation: never block session start
|
|
365
|
-
return [];
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ============================================================
|
|
370
|
-
// Plan 3a Phase 6: watcher-aware banner support
|
|
371
|
-
// ============================================================
|
|
372
|
-
|
|
373
|
-
interface WatchStateShape {
|
|
374
|
-
schema_version?: number;
|
|
375
|
-
daemonPid?: number | null;
|
|
376
|
-
lastRefreshAt?: string | null;
|
|
377
|
-
startedAt?: string | null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function readWatchStateRaw(cwd: string): WatchStateShape | null {
|
|
381
|
-
try {
|
|
382
|
-
const path = resolve(cwd, '.massu', 'watch-state.json');
|
|
383
|
-
if (!existsSync(path)) return null;
|
|
384
|
-
const obj = JSON.parse(readFileSync(path, 'utf-8'));
|
|
385
|
-
if (!obj || typeof obj !== 'object') return null;
|
|
386
|
-
return obj as WatchStateShape;
|
|
387
|
-
} catch {
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function watcherIsLiveAndFresh(): boolean {
|
|
393
|
-
// MASSU_DRIFT_QUIET takes precedence (caller already short-circuited).
|
|
394
|
-
// Fresh = last refresh within 24h AND daemonPid is alive.
|
|
395
|
-
const state = readWatchStateRaw(process.cwd());
|
|
396
|
-
if (!state) return false;
|
|
397
|
-
if (typeof state.daemonPid !== 'number' || state.daemonPid <= 0) return false;
|
|
398
|
-
if (!isPidAlive(state.daemonPid)) return false;
|
|
399
|
-
if (typeof state.lastRefreshAt !== 'string') return false;
|
|
400
|
-
const last = Date.parse(state.lastRefreshAt);
|
|
401
|
-
if (!Number.isFinite(last)) return false;
|
|
402
|
-
const ageMs = Date.now() - last;
|
|
403
|
-
return ageMs >= 0 && ageMs < 24 * 60 * 60 * 1000;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function buildWatcherBanner(): string {
|
|
407
|
-
// P4-004 ordering: MASSU_DRIFT_QUIET wins everywhere.
|
|
408
|
-
if (process.env.MASSU_DRIFT_QUIET === '1') return '';
|
|
409
|
-
const state = readWatchStateRaw(process.cwd());
|
|
410
|
-
if (!state) return '';
|
|
411
|
-
if (typeof state.daemonPid !== 'number' || state.daemonPid <= 0) return '';
|
|
412
|
-
if (!isPidAlive(state.daemonPid)) return '';
|
|
413
|
-
if (typeof state.lastRefreshAt !== 'string') return '';
|
|
414
|
-
const last = Date.parse(state.lastRefreshAt);
|
|
415
|
-
if (!Number.isFinite(last)) return '';
|
|
416
|
-
const ageMs = Date.now() - last;
|
|
417
|
-
if (ageMs < 0 || ageMs >= 24 * 60 * 60 * 1000) return '';
|
|
418
|
-
|
|
419
|
-
const ageStr = formatAge(ageMs);
|
|
420
|
-
return (
|
|
421
|
-
'=== Massu Watcher ===\n' +
|
|
422
|
-
`[massu] watcher running, last refresh: ${ageStr} ago (pid ${state.daemonPid})\n` +
|
|
423
|
-
'=== END ===\n'
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function formatAge(ms: number): string {
|
|
428
|
-
const sec = Math.round(ms / 1000);
|
|
429
|
-
if (sec < 60) return `${sec}s`;
|
|
430
|
-
const min = Math.round(sec / 60);
|
|
431
|
-
if (min < 60) return `${min}m`;
|
|
432
|
-
const hr = Math.round(min / 60);
|
|
433
|
-
return `${hr}h`;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
main();
|
package/src/hooks/user-prompt.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
-
|
|
5
|
-
// ============================================================
|
|
6
|
-
// P3-004: UserPromptSubmit Hook
|
|
7
|
-
// Captures user prompts for search and context.
|
|
8
|
-
// ============================================================
|
|
9
|
-
|
|
10
|
-
import { getMemoryDb, createSession, addUserPrompt, linkSessionToTask, autoDetectTaskId, addObservation } from '../memory-db.ts';
|
|
11
|
-
import { existsSync } from 'fs';
|
|
12
|
-
import { getResolvedPaths } from '../config.ts';
|
|
13
|
-
|
|
14
|
-
interface HookInput {
|
|
15
|
-
session_id: string;
|
|
16
|
-
transcript_path: string;
|
|
17
|
-
cwd: string;
|
|
18
|
-
hook_event_name: string;
|
|
19
|
-
prompt: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function main(): Promise<void> {
|
|
23
|
-
try {
|
|
24
|
-
const input = await readStdin();
|
|
25
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
26
|
-
const { session_id, prompt } = hookInput;
|
|
27
|
-
|
|
28
|
-
if (!prompt || !prompt.trim()) {
|
|
29
|
-
process.exit(0);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const db = getMemoryDb();
|
|
34
|
-
try {
|
|
35
|
-
// 1. Create session if not exists
|
|
36
|
-
const gitBranch = await getGitBranch();
|
|
37
|
-
createSession(db, session_id, { branch: gitBranch });
|
|
38
|
-
|
|
39
|
-
// 2. Scan prompt for plan file references
|
|
40
|
-
const planFileMatch = prompt.match(/([^\s]+docs\/plans\/[^\s]+\.md)/);
|
|
41
|
-
if (planFileMatch) {
|
|
42
|
-
const planFile = planFileMatch[1];
|
|
43
|
-
db.prepare('UPDATE sessions SET plan_file = ? WHERE session_id = ?').run(planFile, session_id);
|
|
44
|
-
|
|
45
|
-
// Auto-detect and link task_id
|
|
46
|
-
const taskId = autoDetectTaskId(planFile);
|
|
47
|
-
if (taskId) {
|
|
48
|
-
linkSessionToTask(db, session_id, taskId);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 3. Get current prompt count for this session
|
|
53
|
-
const countResult = db.prepare(
|
|
54
|
-
'SELECT COUNT(*) as count FROM user_prompts WHERE session_id = ?'
|
|
55
|
-
).get(session_id) as { count: number };
|
|
56
|
-
const promptNumber = countResult.count + 1;
|
|
57
|
-
|
|
58
|
-
// 4. Insert prompt
|
|
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
|
-
}
|
|
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
|
-
}
|
|
110
|
-
} finally {
|
|
111
|
-
db.close();
|
|
112
|
-
}
|
|
113
|
-
} catch (_e) {
|
|
114
|
-
// Best-effort: never block Claude Code
|
|
115
|
-
}
|
|
116
|
-
process.exit(0);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Extract file path references from user prompt text.
|
|
121
|
-
* Matches patterns like src/foo/bar.ts, packages/core/src/x.ts, etc.
|
|
122
|
-
*/
|
|
123
|
-
function extractFileReferences(prompt: string): string[] {
|
|
124
|
-
const filePattern = /(?:^|\s)((?:src|packages|lib)\/[\w./-]+\.(?:ts|tsx|js|jsx|md))/g;
|
|
125
|
-
const matches: string[] = [];
|
|
126
|
-
let match: RegExpExecArray | null;
|
|
127
|
-
while ((match = filePattern.exec(prompt)) !== null) {
|
|
128
|
-
matches.push(match[1]);
|
|
129
|
-
}
|
|
130
|
-
return [...new Set(matches)];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function getGitBranch(): Promise<string | undefined> {
|
|
134
|
-
try {
|
|
135
|
-
const { spawnSync } = await import('child_process');
|
|
136
|
-
const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
137
|
-
encoding: 'utf-8',
|
|
138
|
-
timeout: 5000,
|
|
139
|
-
});
|
|
140
|
-
if (result.status !== 0 || result.error) return undefined;
|
|
141
|
-
return result.stdout.trim();
|
|
142
|
-
} catch (_e) {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function readStdin(): Promise<string> {
|
|
148
|
-
return new Promise((resolve) => {
|
|
149
|
-
let data = '';
|
|
150
|
-
process.stdin.setEncoding('utf-8');
|
|
151
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
152
|
-
process.stdin.on('end', () => resolve(data));
|
|
153
|
-
setTimeout(() => resolve(data), 3000);
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
main();
|