@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,413 +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-002: PostToolUse Observation Hook
|
|
7
|
-
// Captures tool usage as observations (lightweight, no AI needed).
|
|
8
|
-
// Must complete in <500ms.
|
|
9
|
-
// ============================================================
|
|
10
|
-
|
|
11
|
-
import { getMemoryDb, addObservation, createSession, deduplicateFailedAttempt, addSummary } from '../memory-db.ts';
|
|
12
|
-
import { classifyRealTimeToolCall, detectPlanProgress } from '../observation-extractor.ts';
|
|
13
|
-
import { logAuditEntry } from '../audit-trail.ts';
|
|
14
|
-
import { trackModification, recordTestResult } from '../regression-detector.ts';
|
|
15
|
-
import { validateFile, storeValidationResult } from '../validation-engine.ts';
|
|
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';
|
|
20
|
-
import { ingestMemoryFile } from '../memory-file-ingest.ts';
|
|
21
|
-
|
|
22
|
-
interface HookInput {
|
|
23
|
-
session_id: string;
|
|
24
|
-
transcript_path: string;
|
|
25
|
-
cwd: string;
|
|
26
|
-
hook_event_name: string;
|
|
27
|
-
tool_name: string;
|
|
28
|
-
tool_input: Record<string, unknown>;
|
|
29
|
-
tool_response: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// In-memory dedup for Read calls within this session
|
|
33
|
-
const seenReads = new Set<string>();
|
|
34
|
-
let currentSessionId: string | null = null;
|
|
35
|
-
|
|
36
|
-
async function main(): Promise<void> {
|
|
37
|
-
try {
|
|
38
|
-
const input = await readStdin();
|
|
39
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
40
|
-
const { session_id, tool_name, tool_input, tool_response } = hookInput;
|
|
41
|
-
|
|
42
|
-
// Reset seen reads if session changed
|
|
43
|
-
if (currentSessionId !== session_id) {
|
|
44
|
-
seenReads.clear();
|
|
45
|
-
currentSessionId = session_id;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const db = getMemoryDb();
|
|
49
|
-
try {
|
|
50
|
-
// Ensure session exists
|
|
51
|
-
createSession(db, session_id);
|
|
52
|
-
|
|
53
|
-
// Classify and filter
|
|
54
|
-
const observation = classifyRealTimeToolCall(tool_name, tool_input, tool_response, seenReads);
|
|
55
|
-
if (!observation) {
|
|
56
|
-
process.exit(0);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Deduplicate failed attempts
|
|
61
|
-
if (observation.type === 'failed_attempt') {
|
|
62
|
-
deduplicateFailedAttempt(db, session_id, observation.title, observation.detail, observation.opts);
|
|
63
|
-
} else {
|
|
64
|
-
addObservation(db, session_id, observation.type, observation.title, observation.detail, observation.opts);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Auto-detect plan progress
|
|
68
|
-
if (tool_response) {
|
|
69
|
-
const progress = detectPlanProgress(tool_response);
|
|
70
|
-
if (progress.length > 0) {
|
|
71
|
-
// Update plan_progress in session summary
|
|
72
|
-
updatePlanProgress(db, session_id, progress);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Audit trail logging for file changes
|
|
77
|
-
try {
|
|
78
|
-
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
79
|
-
const filePath = (tool_input.file_path as string) ?? '';
|
|
80
|
-
logAuditEntry(db, {
|
|
81
|
-
sessionId: session_id,
|
|
82
|
-
eventType: 'code_change',
|
|
83
|
-
actor: 'ai',
|
|
84
|
-
filePath,
|
|
85
|
-
changeType: tool_name === 'Write' ? 'create' : 'edit',
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Track modification for regression detection
|
|
89
|
-
if (filePath) {
|
|
90
|
-
const featureMatch = filePath.match(/(?:routers|components|app\/\(([^)]+)\))\/([^/.]+)/);
|
|
91
|
-
if (featureMatch) {
|
|
92
|
-
const featureKey = featureMatch[1] ?? featureMatch[2];
|
|
93
|
-
trackModification(db, featureKey);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
} catch (_auditErr) {
|
|
98
|
-
// Best-effort: never block post-tool-use
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Real-time validation for Edit/Write
|
|
102
|
-
try {
|
|
103
|
-
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
104
|
-
const filePath = (tool_input.file_path as string) ?? '';
|
|
105
|
-
if (filePath && (filePath.endsWith('.ts') || filePath.endsWith('.tsx'))) {
|
|
106
|
-
const projectRoot = hookInput.cwd;
|
|
107
|
-
const checks = validateFile(filePath, projectRoot);
|
|
108
|
-
const violations = checks.filter(c => c.severity === 'error' || c.severity === 'critical');
|
|
109
|
-
if (violations.length > 0) {
|
|
110
|
-
storeValidationResult(db, filePath, checks, session_id);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
} catch (_validationErr) {
|
|
115
|
-
// Best-effort: never block post-tool-use
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Auto-security scoring for router/API files
|
|
119
|
-
try {
|
|
120
|
-
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
121
|
-
const filePath = (tool_input.file_path as string) ?? '';
|
|
122
|
-
if (filePath && (filePath.includes('routers/') || filePath.includes('api/'))) {
|
|
123
|
-
const projectRoot = hookInput.cwd;
|
|
124
|
-
const { riskScore, findings } = scoreFileSecurity(filePath, projectRoot);
|
|
125
|
-
if (findings.length > 0) {
|
|
126
|
-
storeSecurityScore(db, session_id, filePath, riskScore, findings);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
} catch (_securityErr) {
|
|
131
|
-
// Best-effort: never block post-tool-use
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// P-H029 (plan-stage-c-high-batch): wire recordTestResult() into the
|
|
135
|
-
// post-tool-use hook so `feature_health` dashboard reflects real test
|
|
136
|
-
// deltas. Pre-fix: `trackModification` fired but `recordTestResult` was
|
|
137
|
-
// unit-tested yet never called; dashboard showed tests_passing=0 for
|
|
138
|
-
// every feature.
|
|
139
|
-
//
|
|
140
|
-
// Strategy: when a Bash tool call runs a test runner AND the output
|
|
141
|
-
// includes parseable pass/fail counts, call recordTestResult for every
|
|
142
|
-
// feature with `modifications_since_test > 0`. This resets their counter
|
|
143
|
-
// and updates pass/fail tallies based on the run.
|
|
144
|
-
try {
|
|
145
|
-
if (tool_name === 'Bash') {
|
|
146
|
-
const command = (tool_input.command as string) ?? '';
|
|
147
|
-
if (isTestRunnerCommand(command)) {
|
|
148
|
-
const counts = parseTestRunOutput(tool_response ?? '');
|
|
149
|
-
if (counts) {
|
|
150
|
-
const modifiedFeatures = db
|
|
151
|
-
.prepare(
|
|
152
|
-
'SELECT feature_key FROM feature_health WHERE modifications_since_test > 0',
|
|
153
|
-
)
|
|
154
|
-
.all() as Array<{ feature_key: string }>;
|
|
155
|
-
for (const row of modifiedFeatures) {
|
|
156
|
-
recordTestResult(db, row.feature_key, counts.passing, counts.failing);
|
|
157
|
-
}
|
|
158
|
-
// Also record a session-level aggregate so the dashboard has at
|
|
159
|
-
// least one row even when no features were modified.
|
|
160
|
-
if (modifiedFeatures.length === 0) {
|
|
161
|
-
recordTestResult(db, '_session_test_run', counts.passing, counts.failing);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} catch (_testResultErr) {
|
|
167
|
-
// Best-effort: never block post-tool-use
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// MEMORY.md integrity check on write
|
|
171
|
-
try {
|
|
172
|
-
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
173
|
-
const filePath = (tool_input.file_path as string) ?? '';
|
|
174
|
-
if (filePath && filePath.endsWith('MEMORY.md') && filePath.includes('/memory/')) {
|
|
175
|
-
const issues = checkMemoryFileIntegrity(filePath);
|
|
176
|
-
if (issues.length > 0) {
|
|
177
|
-
addObservation(db, session_id, 'incident_near_miss',
|
|
178
|
-
'MEMORY.md integrity issue detected',
|
|
179
|
-
issues.join('; '),
|
|
180
|
-
{ importance: 4 }
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
} catch (_memoryErr) {
|
|
186
|
-
// Best-effort: never block post-tool-use
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Memory file auto-ingest: when Claude writes a memory/*.md file,
|
|
190
|
-
// parse frontmatter and ingest into observations table
|
|
191
|
-
try {
|
|
192
|
-
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
193
|
-
const filePath = (tool_input.file_path as string) ?? '';
|
|
194
|
-
if (filePath && filePath.includes('/memory/') && filePath.endsWith('.md')) {
|
|
195
|
-
const basename = filePath.split('/').pop() ?? '';
|
|
196
|
-
if (basename !== 'MEMORY.md') {
|
|
197
|
-
ingestMemoryFile(db, session_id, filePath);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
} catch (_memoryIngestErr) {
|
|
202
|
-
// Best-effort: never block post-tool-use
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Knowledge index staleness check on knowledge file edits
|
|
206
|
-
try {
|
|
207
|
-
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
208
|
-
const filePath = (tool_input.file_path as string) ?? '';
|
|
209
|
-
if (filePath && isKnowledgeSourceFile(filePath)) {
|
|
210
|
-
addObservation(db, session_id, 'discovery',
|
|
211
|
-
'Knowledge source file modified - index may be stale',
|
|
212
|
-
`Edited ${filePath.split('/').pop() ?? filePath}. Run knowledge re-index to update.`,
|
|
213
|
-
{ importance: 3 }
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} catch (_knowledgeErr) {
|
|
218
|
-
// Best-effort: never block post-tool-use
|
|
219
|
-
}
|
|
220
|
-
} finally {
|
|
221
|
-
db.close();
|
|
222
|
-
}
|
|
223
|
-
} catch (_e) {
|
|
224
|
-
// Best-effort: never block Claude Code
|
|
225
|
-
}
|
|
226
|
-
process.exit(0);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function updatePlanProgress(db: import('better-sqlite3').Database, sessionId: string, progress: Array<{ planItem: string; status: string }>): void {
|
|
230
|
-
// Get or create latest summary's plan_progress
|
|
231
|
-
const existing = db.prepare(
|
|
232
|
-
'SELECT id, plan_progress FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1'
|
|
233
|
-
).get(sessionId) as { id: number; plan_progress: string } | undefined;
|
|
234
|
-
|
|
235
|
-
if (existing) {
|
|
236
|
-
try {
|
|
237
|
-
const currentProgress = JSON.parse(existing.plan_progress) as Record<string, string>;
|
|
238
|
-
for (const p of progress) {
|
|
239
|
-
currentProgress[p.planItem] = p.status;
|
|
240
|
-
}
|
|
241
|
-
db.prepare('UPDATE session_summaries SET plan_progress = ? WHERE id = ?')
|
|
242
|
-
.run(JSON.stringify(currentProgress), existing.id);
|
|
243
|
-
} catch (_e) {
|
|
244
|
-
// Skip if JSON parse fails
|
|
245
|
-
}
|
|
246
|
-
} else {
|
|
247
|
-
// Create a minimal summary with plan progress
|
|
248
|
-
const progressMap: Record<string, string> = {};
|
|
249
|
-
for (const p of progress) {
|
|
250
|
-
progressMap[p.planItem] = p.status;
|
|
251
|
-
}
|
|
252
|
-
addSummary(db, sessionId, { planProgress: progressMap });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* P-H029: Detect test-runner commands. Conservative match — only commands that
|
|
258
|
-
* START with a recognized test runner so a build script invoking `npm run test`
|
|
259
|
-
* is included but a script that merely mentions "test" in a filename is not.
|
|
260
|
-
*/
|
|
261
|
-
function isTestRunnerCommand(command: string): boolean {
|
|
262
|
-
const trimmed = command.trim().toLowerCase();
|
|
263
|
-
// Strip leading `cd <dir> && ` or `(cd <dir> && ...)` prefix so we match
|
|
264
|
-
// the actual test command.
|
|
265
|
-
const stripped = trimmed
|
|
266
|
-
.replace(/^cd\s+\S+\s*(&&|;)\s*/, '')
|
|
267
|
-
.replace(/^\(\s*cd\s+\S+\s*(&&|;)\s*/, '');
|
|
268
|
-
const testRunnerPrefixes = [
|
|
269
|
-
'npm test', 'npm run test', 'npx vitest', 'npx jest', 'vitest', 'jest',
|
|
270
|
-
'pnpm test', 'pnpm run test', 'yarn test', 'pytest', 'go test', 'cargo test',
|
|
271
|
-
];
|
|
272
|
-
return testRunnerPrefixes.some((prefix) => stripped.startsWith(prefix));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* P-H029: Parse test-run output for pass/fail counts. Supports vitest
|
|
277
|
-
* (`Tests N passed (N)`, `Tests X failed | Y passed (Z)`), jest
|
|
278
|
-
* (`Tests: X failed, Y passed, Z total`), and pytest (`X passed, Y failed`).
|
|
279
|
-
* Returns null if no parseable summary line found.
|
|
280
|
-
*/
|
|
281
|
-
function parseTestRunOutput(output: string): { passing: number; failing: number } | null {
|
|
282
|
-
// vitest: " Tests 439 passed (439)" or " Tests 3 failed | 436 passed (439)"
|
|
283
|
-
const vitestSplit = output.match(/Tests?\s+(?:(\d+)\s+failed\s+\|\s+)?(\d+)\s+passed/);
|
|
284
|
-
if (vitestSplit) {
|
|
285
|
-
return {
|
|
286
|
-
passing: parseInt(vitestSplit[2], 10),
|
|
287
|
-
failing: vitestSplit[1] ? parseInt(vitestSplit[1], 10) : 0,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
// jest: "Tests: 1 failed, 5 passed, 6 total"
|
|
291
|
-
const jest = output.match(/Tests?:\s+(?:(\d+)\s+failed,\s+)?(\d+)\s+passed/);
|
|
292
|
-
if (jest) {
|
|
293
|
-
return {
|
|
294
|
-
passing: parseInt(jest[2], 10),
|
|
295
|
-
failing: jest[1] ? parseInt(jest[1], 10) : 0,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
// pytest: "5 passed, 2 failed in 1.23s" or "5 passed in 1.23s"
|
|
299
|
-
const pytestPassed = output.match(/(\d+)\s+passed/);
|
|
300
|
-
const pytestFailed = output.match(/(\d+)\s+failed/);
|
|
301
|
-
if (pytestPassed) {
|
|
302
|
-
return {
|
|
303
|
-
passing: parseInt(pytestPassed[1], 10),
|
|
304
|
-
failing: pytestFailed ? parseInt(pytestFailed[1], 10) : 0,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function readStdin(): Promise<string> {
|
|
311
|
-
return new Promise((resolve) => {
|
|
312
|
-
let data = '';
|
|
313
|
-
process.stdin.setEncoding('utf-8');
|
|
314
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
315
|
-
process.stdin.on('end', () => resolve(data));
|
|
316
|
-
setTimeout(() => resolve(data), 3000);
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Read the conventions section from massu.config.yaml directly.
|
|
322
|
-
* Hooks are compiled with esbuild and cannot use getConfig() from config.ts.
|
|
323
|
-
* Falls back to sensible defaults if the config file is not found.
|
|
324
|
-
*/
|
|
325
|
-
function readConventions(cwd?: string): {
|
|
326
|
-
knowledgeSourceFiles: string[];
|
|
327
|
-
claudeDirName: string;
|
|
328
|
-
} {
|
|
329
|
-
const defaults = {
|
|
330
|
-
knowledgeSourceFiles: ['CLAUDE.md', 'MEMORY.md', 'corrections.md'],
|
|
331
|
-
claudeDirName: '.claude',
|
|
332
|
-
};
|
|
333
|
-
try {
|
|
334
|
-
const projectRoot = cwd ?? process.cwd();
|
|
335
|
-
const configPath = join(projectRoot, 'massu.config.yaml');
|
|
336
|
-
if (!existsSync(configPath)) return defaults;
|
|
337
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
338
|
-
// 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.
|
|
339
|
-
const parsed = parseYaml(content) as Record<string, unknown> | null;
|
|
340
|
-
if (!parsed || typeof parsed !== 'object') return defaults;
|
|
341
|
-
const conventions = parsed.conventions as Record<string, unknown> | undefined;
|
|
342
|
-
if (!conventions || typeof conventions !== 'object') return defaults;
|
|
343
|
-
return {
|
|
344
|
-
knowledgeSourceFiles: Array.isArray(conventions.knowledgeSourceFiles)
|
|
345
|
-
? conventions.knowledgeSourceFiles as string[]
|
|
346
|
-
: defaults.knowledgeSourceFiles,
|
|
347
|
-
claudeDirName: typeof conventions.claudeDirName === 'string'
|
|
348
|
-
? conventions.claudeDirName
|
|
349
|
-
: defaults.claudeDirName,
|
|
350
|
-
};
|
|
351
|
-
} catch {
|
|
352
|
-
return defaults;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Check if a file path is a knowledge source file (CLAUDE.md, corrections.md,
|
|
358
|
-
* memory files, or knowledge system source files).
|
|
359
|
-
* When these are edited, the knowledge index may become stale.
|
|
360
|
-
*/
|
|
361
|
-
function isKnowledgeSourceFile(filePath: string): boolean {
|
|
362
|
-
const basename = filePath.split('/').pop() ?? '';
|
|
363
|
-
const conventions = readConventions();
|
|
364
|
-
const knowledgeSourcePatterns = [
|
|
365
|
-
...conventions.knowledgeSourceFiles,
|
|
366
|
-
'file-index.md',
|
|
367
|
-
'knowledge-db.ts',
|
|
368
|
-
'knowledge-indexer.ts',
|
|
369
|
-
'knowledge-tools.ts',
|
|
370
|
-
];
|
|
371
|
-
return knowledgeSourcePatterns.some(p => basename === p) ||
|
|
372
|
-
filePath.includes('/memory/') ||
|
|
373
|
-
filePath.includes(conventions.claudeDirName + '/');
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Check MEMORY.md file integrity after a write.
|
|
378
|
-
* Verifies: file exists, has expected structure, and is under line limit.
|
|
379
|
-
* Returns array of issue descriptions (empty = all good).
|
|
380
|
-
*/
|
|
381
|
-
function checkMemoryFileIntegrity(filePath: string): string[] {
|
|
382
|
-
const issues: string[] = [];
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
if (!existsSync(filePath)) {
|
|
386
|
-
issues.push('MEMORY.md file does not exist after write');
|
|
387
|
-
return issues;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
391
|
-
const lines = content.split('\n');
|
|
392
|
-
|
|
393
|
-
// Check line count (CLAUDE.md truncates after ~200 lines)
|
|
394
|
-
const MAX_LINES = 200;
|
|
395
|
-
if (lines.length > MAX_LINES) {
|
|
396
|
-
issues.push(`MEMORY.md exceeds ${MAX_LINES} lines (currently ${lines.length}). Consider archiving old entries.`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Check required structure sections
|
|
400
|
-
const requiredSections = ['# Massu Memory', '## Key Learnings', '## Common Gotchas'];
|
|
401
|
-
for (const section of requiredSections) {
|
|
402
|
-
if (!content.includes(section)) {
|
|
403
|
-
issues.push(`Missing required section: "${section}"`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
} catch (_e) {
|
|
407
|
-
// Graceful degradation: don't report issues if we can't check
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return issues;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
main();
|
package/src/hooks/pre-compact.ts
DELETED
|
@@ -1,168 +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-006: PreCompact State Snapshot Hook
|
|
7
|
-
// Captures current session state into DB before compaction destroys context.
|
|
8
|
-
// ============================================================
|
|
9
|
-
|
|
10
|
-
import { getMemoryDb, addSummary, createSession, addObservation } from '../memory-db.ts';
|
|
11
|
-
import { logAuditEntry } from '../audit-trail.ts';
|
|
12
|
-
import type { SessionSummary } from '../memory-db.ts';
|
|
13
|
-
|
|
14
|
-
interface HookInput {
|
|
15
|
-
session_id: string;
|
|
16
|
-
transcript_path: string;
|
|
17
|
-
cwd: string;
|
|
18
|
-
hook_event_name: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function main(): Promise<void> {
|
|
22
|
-
try {
|
|
23
|
-
const input = await readStdin();
|
|
24
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
25
|
-
const { session_id } = hookInput;
|
|
26
|
-
|
|
27
|
-
const db = getMemoryDb();
|
|
28
|
-
try {
|
|
29
|
-
// Ensure session exists
|
|
30
|
-
createSession(db, session_id);
|
|
31
|
-
|
|
32
|
-
// 1. Get all observations for this session
|
|
33
|
-
const observations = db.prepare(
|
|
34
|
-
'SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC'
|
|
35
|
-
).all(session_id) as Array<Record<string, unknown>>;
|
|
36
|
-
|
|
37
|
-
// 2. Get user prompts
|
|
38
|
-
const prompts = db.prepare(
|
|
39
|
-
'SELECT prompt_text FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC'
|
|
40
|
-
).all(session_id) as Array<{ prompt_text: string }>;
|
|
41
|
-
|
|
42
|
-
// 3. Generate mid-session summary snapshot
|
|
43
|
-
const summary = buildSnapshotSummary(observations, prompts);
|
|
44
|
-
|
|
45
|
-
// 4. Store with pre_compact marker in plan_progress
|
|
46
|
-
addSummary(db, session_id, summary);
|
|
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
|
-
|
|
70
|
-
// Log compaction event for audit trail continuity
|
|
71
|
-
try {
|
|
72
|
-
logAuditEntry(db, {
|
|
73
|
-
sessionId: session_id,
|
|
74
|
-
eventType: 'compaction',
|
|
75
|
-
actor: 'hook',
|
|
76
|
-
metadata: { observations_count: observations.length, prompts_count: prompts.length },
|
|
77
|
-
});
|
|
78
|
-
} catch (_auditErr) {
|
|
79
|
-
// Best-effort: never block compaction
|
|
80
|
-
}
|
|
81
|
-
} finally {
|
|
82
|
-
db.close();
|
|
83
|
-
}
|
|
84
|
-
} catch (_e) {
|
|
85
|
-
// Best-effort: never block Claude Code
|
|
86
|
-
}
|
|
87
|
-
process.exit(0);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function buildSnapshotSummary(
|
|
91
|
-
observations: Array<Record<string, unknown>>,
|
|
92
|
-
prompts: Array<{ prompt_text: string }>
|
|
93
|
-
): SessionSummary {
|
|
94
|
-
const request = prompts[0]?.prompt_text?.slice(0, 500) ?? undefined;
|
|
95
|
-
|
|
96
|
-
const completed = observations
|
|
97
|
-
.filter(o => ['feature', 'bugfix', 'refactor'].includes(o.type as string))
|
|
98
|
-
.map(o => `- ${o.title}`)
|
|
99
|
-
.join('\n');
|
|
100
|
-
|
|
101
|
-
const failedAttempts = observations
|
|
102
|
-
.filter(o => o.type === 'failed_attempt')
|
|
103
|
-
.map(o => `- ${o.title}`)
|
|
104
|
-
.join('\n');
|
|
105
|
-
|
|
106
|
-
const decisions = observations
|
|
107
|
-
.filter(o => o.type === 'decision')
|
|
108
|
-
.map(o => `- ${o.title}`)
|
|
109
|
-
.join('\n');
|
|
110
|
-
|
|
111
|
-
// Collect file changes
|
|
112
|
-
const filesCreated: string[] = [];
|
|
113
|
-
const filesModified: string[] = [];
|
|
114
|
-
for (const o of observations) {
|
|
115
|
-
if (o.type !== 'file_change') continue;
|
|
116
|
-
const files = safeParseJson(o.files_involved as string, []) as string[];
|
|
117
|
-
const title = o.title as string;
|
|
118
|
-
if (title.startsWith('Created')) filesCreated.push(...files);
|
|
119
|
-
else if (title.startsWith('Edited')) filesModified.push(...files);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Collect plan progress
|
|
123
|
-
const planProgress: Record<string, string> = { snapshot_type: 'pre_compact' };
|
|
124
|
-
for (const o of observations) {
|
|
125
|
-
if (!o.plan_item) continue;
|
|
126
|
-
planProgress[o.plan_item as string] = 'in_progress';
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Verification results
|
|
130
|
-
const verificationResults: Record<string, string> = {};
|
|
131
|
-
for (const o of observations) {
|
|
132
|
-
if (o.type !== 'vr_check') continue;
|
|
133
|
-
const vrType = o.vr_type as string;
|
|
134
|
-
const passed = (o.title as string).includes('PASS');
|
|
135
|
-
if (vrType) verificationResults[vrType] = passed ? 'PASS' : 'FAIL';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
request,
|
|
140
|
-
completed: completed || undefined,
|
|
141
|
-
failedAttempts: failedAttempts || undefined,
|
|
142
|
-
decisions: decisions || undefined,
|
|
143
|
-
filesCreated: [...new Set(filesCreated)],
|
|
144
|
-
filesModified: [...new Set(filesModified)],
|
|
145
|
-
verificationResults,
|
|
146
|
-
planProgress,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function safeParseJson(json: string, fallback: unknown): unknown {
|
|
151
|
-
try {
|
|
152
|
-
return JSON.parse(json);
|
|
153
|
-
} catch (_e) {
|
|
154
|
-
return fallback;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function readStdin(): Promise<string> {
|
|
159
|
-
return new Promise((resolve) => {
|
|
160
|
-
let data = '';
|
|
161
|
-
process.stdin.setEncoding('utf-8');
|
|
162
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
163
|
-
process.stdin.on('end', () => resolve(data));
|
|
164
|
-
setTimeout(() => resolve(data), 3000);
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
main();
|