@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,228 +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
|
-
// Stop Hook: Auto-Learning Pipeline Enforcer
|
|
7
|
-
// At session end, checks if bug fixes were applied without
|
|
8
|
-
// completing the full incident → rule → enforcement pipeline.
|
|
9
|
-
// Outputs mandatory instructions for Claude to follow.
|
|
10
|
-
//
|
|
11
|
-
// Part of the Auto-Learning Pipeline:
|
|
12
|
-
// Fix Detected → [SESSION END CHECK] → Pipeline Instructions
|
|
13
|
-
//
|
|
14
|
-
// This is the FORCING FUNCTION that ensures no fix goes
|
|
15
|
-
// undocumented. Claude cannot end the session without completing
|
|
16
|
-
// the pipeline steps.
|
|
17
|
-
// ============================================================
|
|
18
|
-
|
|
19
|
-
import { execFileSync } from 'child_process';
|
|
20
|
-
import { existsSync, readFileSync, unlinkSync, readdirSync, statSync } from 'fs';
|
|
21
|
-
import { tmpdir } from 'os';
|
|
22
|
-
import { join } from 'path';
|
|
23
|
-
import { getProjectRoot, getConfig } from '../config.ts';
|
|
24
|
-
|
|
25
|
-
// P-H002 (plan-stage-c-high-batch): bound git-diff reads so monorepos with
|
|
26
|
-
// 10MB+ working trees don't trigger Stop-hook timeout. Short-stat first,
|
|
27
|
-
// only read full diff body when estimated bytes <= cap. execFileSync argv
|
|
28
|
-
// form is defense-in-depth (P-001 pattern).
|
|
29
|
-
const MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024; // 2MB; ~25k lines at 80 bytes/line
|
|
30
|
-
|
|
31
|
-
interface HookInput {
|
|
32
|
-
session_id: string;
|
|
33
|
-
transcript_path: string;
|
|
34
|
-
cwd: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface FixSignal {
|
|
38
|
-
file: string;
|
|
39
|
-
signals: string[];
|
|
40
|
-
timestamp: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getSessionFlagPath(sessionId: string): string {
|
|
44
|
-
return join(tmpdir(), 'massu-auto-learning', `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function main(): Promise<void> {
|
|
48
|
-
try {
|
|
49
|
-
const input = await readStdin();
|
|
50
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
51
|
-
const config = getConfig();
|
|
52
|
-
|
|
53
|
-
// Check if auto-learning is enabled
|
|
54
|
-
if (config.autoLearning?.enabled === false) {
|
|
55
|
-
process.exit(0);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const root = getProjectRoot();
|
|
60
|
-
const incidentDir = config.autoLearning?.incidentDir ?? 'docs/incidents';
|
|
61
|
-
const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
|
|
62
|
-
const autoLearn = config.autoLearning;
|
|
63
|
-
|
|
64
|
-
// Source 1: Session fix flags from fix-detector
|
|
65
|
-
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
66
|
-
let sessionFixes: FixSignal[] = [];
|
|
67
|
-
if (existsSync(flagPath)) {
|
|
68
|
-
try {
|
|
69
|
-
sessionFixes = readFileSync(flagPath, 'utf-8')
|
|
70
|
-
.split('\n')
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
.map(line => JSON.parse(line) as FixSignal);
|
|
73
|
-
} catch { /* ignore parse errors */ }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Source 2: Scan uncommitted git diff for fix patterns (language-agnostic).
|
|
77
|
-
// Two-stage: (1) name-only to confirm any changes, (2) shortstat to estimate
|
|
78
|
-
// bytes, (3) full diff body ONLY if estimate <= MAX_FULL_DIFF_BYTES.
|
|
79
|
-
let uncommittedFix = false;
|
|
80
|
-
try {
|
|
81
|
-
const nameOnly = execFileSync('git', ['diff', '--name-only'], {
|
|
82
|
-
cwd: root,
|
|
83
|
-
timeout: 3000,
|
|
84
|
-
encoding: 'utf-8',
|
|
85
|
-
maxBuffer: 1024 * 1024,
|
|
86
|
-
});
|
|
87
|
-
if (nameOnly.trim()) {
|
|
88
|
-
const shortstat = execFileSync('git', ['diff', '--shortstat'], {
|
|
89
|
-
cwd: root,
|
|
90
|
-
timeout: 2000,
|
|
91
|
-
encoding: 'utf-8',
|
|
92
|
-
maxBuffer: 64 * 1024,
|
|
93
|
-
});
|
|
94
|
-
const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? '0', 10);
|
|
95
|
-
const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? '0', 10);
|
|
96
|
-
const estimatedBytes = (insertions + deletions) * 80; // ~80 bytes/line avg
|
|
97
|
-
if (estimatedBytes <= MAX_FULL_DIFF_BYTES) {
|
|
98
|
-
const fullDiff = execFileSync('git', ['diff'], {
|
|
99
|
-
cwd: root,
|
|
100
|
-
timeout: 5000,
|
|
101
|
-
encoding: 'utf-8',
|
|
102
|
-
maxBuffer: MAX_FULL_DIFF_BYTES,
|
|
103
|
-
});
|
|
104
|
-
const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
|
|
105
|
-
const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
|
|
106
|
-
if (fixPatterns > 3 || removedBroken > 1) {
|
|
107
|
-
uncommittedFix = true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// else: diff exceeds cap — skip pattern scan rather than risk timeout.
|
|
111
|
-
// The fix-detector hook fires on per-Edit/Write and already populates
|
|
112
|
-
// sessionFixes for the realistic case; full-diff fallback is a safety
|
|
113
|
-
// net that we can correctly skip for huge trees.
|
|
114
|
-
}
|
|
115
|
-
} catch { /* git not available or no changes or buffer overflow */ }
|
|
116
|
-
|
|
117
|
-
if (sessionFixes.length === 0 && !uncommittedFix) {
|
|
118
|
-
// Clean up flag file
|
|
119
|
-
cleanup(flagPath);
|
|
120
|
-
process.exit(0);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Build pipeline instructions
|
|
125
|
-
const lines: string[] = [];
|
|
126
|
-
lines.push('');
|
|
127
|
-
lines.push('============================================================================');
|
|
128
|
-
lines.push(' MASSU AUTO-LEARNING PIPELINE — ACTION REQUIRED BEFORE SESSION END');
|
|
129
|
-
lines.push('============================================================================');
|
|
130
|
-
|
|
131
|
-
if (sessionFixes.length > 0) {
|
|
132
|
-
lines.push('');
|
|
133
|
-
lines.push(` ${sessionFixes.length} bug fix(es) detected during this session:`);
|
|
134
|
-
lines.push('');
|
|
135
|
-
// Deduplicate by file
|
|
136
|
-
const byFile = new Map<string, string[]>();
|
|
137
|
-
for (const fix of sessionFixes) {
|
|
138
|
-
const existing = byFile.get(fix.file) ?? [];
|
|
139
|
-
existing.push(...fix.signals);
|
|
140
|
-
byFile.set(fix.file, [...new Set(existing)]);
|
|
141
|
-
}
|
|
142
|
-
for (const [file, signals] of byFile) {
|
|
143
|
-
lines.push(` - ${file} (${signals.join(', ')})`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (uncommittedFix) {
|
|
148
|
-
lines.push('');
|
|
149
|
-
lines.push(' Additional uncommitted fix patterns detected in git diff.');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
lines.push('');
|
|
153
|
-
lines.push(' Complete these steps before this session ends:');
|
|
154
|
-
lines.push('');
|
|
155
|
-
|
|
156
|
-
if (autoLearn?.pipeline?.requireIncidentReport !== false) {
|
|
157
|
-
lines.push(' STEP 1: INCIDENT REPORT');
|
|
158
|
-
lines.push(` For each distinct bug fixed, create: ${incidentDir}/YYYY-MM-DD-<slug>.md`);
|
|
159
|
-
lines.push(' Include: Date, Severity, Symptoms, Root Cause, Fix, Files Changed, Prevention Rules');
|
|
160
|
-
lines.push('');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (autoLearn?.pipeline?.requirePreventionRule !== false) {
|
|
164
|
-
lines.push(' STEP 2: PREVENTION RULE');
|
|
165
|
-
lines.push(` For each incident, create: ${memoryDir}/feedback_<rule_name>.md`);
|
|
166
|
-
lines.push(' Include frontmatter (name, description, type: feedback) + Why + How to apply');
|
|
167
|
-
lines.push(` Update ${config.autoLearning?.memoryIndexFile ?? 'MEMORY.md'} index`);
|
|
168
|
-
lines.push('');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (autoLearn?.pipeline?.requireEnforcement !== false) {
|
|
172
|
-
lines.push(' STEP 3: ENFORCEMENT PLACEMENT');
|
|
173
|
-
lines.push(' For each new rule, determine enforcement layer(s):');
|
|
174
|
-
lines.push(' a) If statically detectable → add to pattern-feedback hook');
|
|
175
|
-
lines.push(' b) If about editing certain files → add to blast-radius hook');
|
|
176
|
-
lines.push(' c) If about dangerous commands → add to dangerous-command hook');
|
|
177
|
-
lines.push(' d) If critical → add to pre-commit hook');
|
|
178
|
-
lines.push(' e) If needs runtime monitoring → create monitoring producer');
|
|
179
|
-
lines.push('');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
lines.push(' STEP 4: VERIFY');
|
|
183
|
-
lines.push(' Test any new enforcement hooks to confirm they detect violations.');
|
|
184
|
-
lines.push('');
|
|
185
|
-
lines.push('============================================================================');
|
|
186
|
-
lines.push('');
|
|
187
|
-
|
|
188
|
-
console.log(lines.join('\n'));
|
|
189
|
-
|
|
190
|
-
// Clean up flag file
|
|
191
|
-
cleanup(flagPath);
|
|
192
|
-
} catch {
|
|
193
|
-
// Best-effort: never block Claude Code
|
|
194
|
-
}
|
|
195
|
-
process.exit(0);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function cleanup(flagPath: string): void {
|
|
199
|
-
try {
|
|
200
|
-
if (existsSync(flagPath)) unlinkSync(flagPath);
|
|
201
|
-
// Clean up old flag files (>24h)
|
|
202
|
-
const dir = join(tmpdir(), 'massu-auto-learning');
|
|
203
|
-
if (existsSync(dir)) {
|
|
204
|
-
const now = Date.now();
|
|
205
|
-
for (const file of readdirSync(dir)) {
|
|
206
|
-
const fullPath = join(dir, file);
|
|
207
|
-
try {
|
|
208
|
-
const stat = statSync(fullPath);
|
|
209
|
-
if (now - stat.mtimeMs > 86400000) {
|
|
210
|
-
unlinkSync(fullPath);
|
|
211
|
-
}
|
|
212
|
-
} catch { /* ignore */ }
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
} catch { /* best effort */ }
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function readStdin(): Promise<string> {
|
|
219
|
-
return new Promise((resolve) => {
|
|
220
|
-
let data = '';
|
|
221
|
-
process.stdin.setEncoding('utf-8');
|
|
222
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
223
|
-
process.stdin.on('end', () => resolve(data));
|
|
224
|
-
setTimeout(() => resolve(data), 5000);
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
main();
|
|
@@ -1,259 +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
|
-
// PostToolUse Hook: Failure Classification
|
|
7
|
-
// Classifies bug fixes against known failure patterns before
|
|
8
|
-
// demanding the full incident loop. Prevents rule bloat by
|
|
9
|
-
// recognizing known patterns vs genuinely novel failures.
|
|
10
|
-
//
|
|
11
|
-
// Part of the Auto-Learning Pipeline:
|
|
12
|
-
// [Fix Detected] → [CLASSIFY] → Incident Report → Rule → Enforcement
|
|
13
|
-
//
|
|
14
|
-
// Classification:
|
|
15
|
-
// SCORE >= known threshold → KNOWN — Reference existing rules, no new deliverables
|
|
16
|
-
// SCORE >= similar threshold → SIMILAR — Check existing rules first
|
|
17
|
-
// SCORE < similar threshold → NEW — Full incident loop mandatory
|
|
18
|
-
//
|
|
19
|
-
// Triggers on: Edit|Write to code files
|
|
20
|
-
// Must complete in <1000ms.
|
|
21
|
-
// ============================================================
|
|
22
|
-
|
|
23
|
-
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
|
|
24
|
-
import { tmpdir } from 'os';
|
|
25
|
-
import { join, basename } from 'path';
|
|
26
|
-
import { getProjectRoot, getConfig } from '../config.ts';
|
|
27
|
-
import { getMemoryDb, scoreFailureClasses } from '../memory-db.ts';
|
|
28
|
-
|
|
29
|
-
interface HookInput {
|
|
30
|
-
session_id: string;
|
|
31
|
-
tool_name: string;
|
|
32
|
-
tool_input: {
|
|
33
|
-
file_path?: string;
|
|
34
|
-
old_string?: string;
|
|
35
|
-
new_string?: string;
|
|
36
|
-
content?: string;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Bug fix detection heuristics
|
|
41
|
-
const BUG_FIX_INDICATORS = [
|
|
42
|
-
/\b(catch|error|throw|fail|fix|bug|broken|missing|crash|wrong|typo|incorrect)\b/i,
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|swift|rs|go|rb|sh)$/;
|
|
46
|
-
|
|
47
|
-
function getDedupeMarkerPath(sessionId: string, filePath: string): string {
|
|
48
|
-
// One reminder per file per calendar day
|
|
49
|
-
const day = new Date().toISOString().slice(0, 10);
|
|
50
|
-
const hash = simpleHash(filePath);
|
|
51
|
-
return join(tmpdir(), `massu-classify-${day}-${sessionId.slice(0, 8)}-${hash}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function simpleHash(str: string): string {
|
|
55
|
-
let h = 0;
|
|
56
|
-
for (let i = 0; i < str.length; i++) {
|
|
57
|
-
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
|
58
|
-
}
|
|
59
|
-
return Math.abs(h).toString(36);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function readFailureContextFiles(): string {
|
|
63
|
-
const dir = tmpdir();
|
|
64
|
-
let context = '';
|
|
65
|
-
try {
|
|
66
|
-
const files = readdirSync(dir).filter(f => f.startsWith('massu-failure-context-'));
|
|
67
|
-
for (const file of files) {
|
|
68
|
-
try {
|
|
69
|
-
context += ' ' + readFileSync(join(dir, file), 'utf-8');
|
|
70
|
-
} catch { /* ignore */ }
|
|
71
|
-
}
|
|
72
|
-
} catch { /* ignore */ }
|
|
73
|
-
return context.trim();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function cleanupFailureContextFiles(): void {
|
|
77
|
-
const dir = tmpdir();
|
|
78
|
-
try {
|
|
79
|
-
const files = readdirSync(dir).filter(f => f.startsWith('massu-failure-context-'));
|
|
80
|
-
for (const file of files) {
|
|
81
|
-
try { unlinkSync(join(dir, file)); } catch { /* ignore */ }
|
|
82
|
-
}
|
|
83
|
-
} catch { /* ignore */ }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function main(): Promise<void> {
|
|
87
|
-
try {
|
|
88
|
-
const input = await readStdin();
|
|
89
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
90
|
-
const filePath = hookInput.tool_input?.file_path;
|
|
91
|
-
|
|
92
|
-
if (!filePath) {
|
|
93
|
-
process.exit(0);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const config = getConfig();
|
|
98
|
-
|
|
99
|
-
// Check if feature is enabled
|
|
100
|
-
if (config.autoLearning?.enabled === false ||
|
|
101
|
-
config.autoLearning?.failureClassification?.enabled === false) {
|
|
102
|
-
process.exit(0);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Only check code files
|
|
107
|
-
if (!CODE_EXTENSIONS.test(filePath)) {
|
|
108
|
-
process.exit(0);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Skip incident/memory/doc files (those are pipeline output)
|
|
113
|
-
const root = getProjectRoot();
|
|
114
|
-
const incidentDir = config.autoLearning?.incidentDir ?? 'docs/incidents';
|
|
115
|
-
const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
|
|
116
|
-
const relPath = filePath.startsWith(root + '/') ? filePath.slice(root.length + 1) : filePath;
|
|
117
|
-
if (relPath.startsWith(incidentDir) || relPath.includes(memoryDir) || relPath.includes('MEMORY.md')) {
|
|
118
|
-
process.exit(0);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Detect if this edit looks like a bug fix
|
|
123
|
-
const oldString = hookInput.tool_input?.old_string ?? '';
|
|
124
|
-
const newString = hookInput.tool_input?.new_string ?? '';
|
|
125
|
-
const content = hookInput.tool_input?.content ?? '';
|
|
126
|
-
const matchText = `${oldString} ${newString} ${content}`;
|
|
127
|
-
|
|
128
|
-
let isBugFix = false;
|
|
129
|
-
|
|
130
|
-
// Check for failure context markers (from UserPromptSubmit)
|
|
131
|
-
const promptContext = readFailureContextFiles();
|
|
132
|
-
if (promptContext) {
|
|
133
|
-
isBugFix = true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Check for bug fix indicators in the edit text
|
|
137
|
-
if (!isBugFix) {
|
|
138
|
-
for (const pattern of BUG_FIX_INDICATORS) {
|
|
139
|
-
if (pattern.test(matchText)) {
|
|
140
|
-
isBugFix = true;
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Check for column/key name changes (common fix pattern)
|
|
147
|
-
if (!isBugFix && oldString && newString) {
|
|
148
|
-
const oldKeys = oldString.match(/[a-z_]+:/g)?.sort().join(',');
|
|
149
|
-
const newKeys = newString.match(/[a-z_]+:/g)?.sort().join(',');
|
|
150
|
-
if (oldKeys && newKeys && oldKeys !== newKeys) {
|
|
151
|
-
isBugFix = true;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (!isBugFix) {
|
|
156
|
-
process.exit(0);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// De-duplicate: skip if already reminded for this file today
|
|
161
|
-
const dedupeMarker = getDedupeMarkerPath(hookInput.session_id, filePath);
|
|
162
|
-
if (existsSync(dedupeMarker)) {
|
|
163
|
-
process.exit(0);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Mark that we've classified this file
|
|
168
|
-
try {
|
|
169
|
-
writeFileSync(dedupeMarker, '1');
|
|
170
|
-
} catch { /* ignore */ }
|
|
171
|
-
|
|
172
|
-
// Score against failure taxonomy in database
|
|
173
|
-
const db = getMemoryDb();
|
|
174
|
-
try {
|
|
175
|
-
const scoringConfig = config.autoLearning?.failureClassification?.scoring;
|
|
176
|
-
const thresholds = config.autoLearning?.failureClassification?.thresholds ?? { known: 5, similar: 3 };
|
|
177
|
-
const bestMatch = scoreFailureClasses(db, matchText, filePath, promptContext, scoringConfig);
|
|
178
|
-
|
|
179
|
-
if (!bestMatch || bestMatch.score === 0) {
|
|
180
|
-
// No taxonomy entries yet, or no match — full enforcement
|
|
181
|
-
outputNewPattern(basename(filePath), bestMatch);
|
|
182
|
-
} else if (bestMatch.score >= thresholds.known) {
|
|
183
|
-
outputKnownPattern(bestMatch);
|
|
184
|
-
} else if (bestMatch.score >= thresholds.similar) {
|
|
185
|
-
outputSimilarPattern(bestMatch);
|
|
186
|
-
} else {
|
|
187
|
-
outputNewPattern(basename(filePath), bestMatch);
|
|
188
|
-
}
|
|
189
|
-
} finally {
|
|
190
|
-
db.close();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Clean up consumed context files to prevent cross-bug contamination
|
|
194
|
-
cleanupFailureContextFiles();
|
|
195
|
-
} catch {
|
|
196
|
-
// Best-effort: never block Claude Code
|
|
197
|
-
}
|
|
198
|
-
process.exit(0);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function outputKnownPattern(match: FailureClassMatch): void {
|
|
202
|
-
const lines: string[] = [];
|
|
203
|
-
lines.push('');
|
|
204
|
-
lines.push(`[KNOWN PATTERN] ${match.name} (score: ${match.score}, ${match.incidentCount} prior incident(s))`);
|
|
205
|
-
if (match.knownMessage) {
|
|
206
|
-
lines.push(` ${match.knownMessage}`);
|
|
207
|
-
}
|
|
208
|
-
if (match.rules.length > 0) {
|
|
209
|
-
lines.push(` Covered by: ${match.rules.join(', ')}`);
|
|
210
|
-
}
|
|
211
|
-
lines.push(' No new rules needed. Reference existing incident if logging.');
|
|
212
|
-
console.log(lines.join('\n'));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function outputSimilarPattern(match: FailureClassMatch): void {
|
|
216
|
-
const lines: string[] = [];
|
|
217
|
-
lines.push('');
|
|
218
|
-
lines.push(`[POSSIBLE MATCH] Resembles ${match.name} (score: ${match.score})`);
|
|
219
|
-
if (match.rules.length > 0) {
|
|
220
|
-
lines.push(` Check if existing rules cover this case: ${match.rules.join(', ')}`);
|
|
221
|
-
}
|
|
222
|
-
lines.push(' If genuinely new: create incident + prevention rule + enforcement.');
|
|
223
|
-
console.log(lines.join('\n'));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function outputNewPattern(fileName: string, match: FailureClassMatch | null): void {
|
|
227
|
-
const lines: string[] = [];
|
|
228
|
-
lines.push('');
|
|
229
|
-
if (match && match.score > 0) {
|
|
230
|
-
lines.push(`[NEW PATTERN] No known failure class matches this fix in ${fileName} (best: ${match.name}, score: ${match.score}).`);
|
|
231
|
-
} else {
|
|
232
|
-
lines.push(`[NEW PATTERN] Bug fix detected in ${fileName} — no failure classes in taxonomy yet.`);
|
|
233
|
-
}
|
|
234
|
-
lines.push(' Full incident loop required:');
|
|
235
|
-
lines.push(' 1. INCIDENT REPORT');
|
|
236
|
-
lines.push(' 2. PREVENTION RULE (if new failure pattern)');
|
|
237
|
-
lines.push(' 3. ENFORCEMENT (hook or static check)');
|
|
238
|
-
console.log(lines.join('\n'));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
interface FailureClassMatch {
|
|
242
|
-
name: string;
|
|
243
|
-
score: number;
|
|
244
|
-
incidentCount: number;
|
|
245
|
-
rules: string[];
|
|
246
|
-
knownMessage: string;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function readStdin(): Promise<string> {
|
|
250
|
-
return new Promise((resolve) => {
|
|
251
|
-
let data = '';
|
|
252
|
-
process.stdin.setEncoding('utf-8');
|
|
253
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
254
|
-
process.stdin.on('end', () => resolve(data));
|
|
255
|
-
setTimeout(() => resolve(data), 3000);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
main();
|
|
@@ -1,66 +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
|
-
// PostToolUse Hook: Cost Tracker
|
|
7
|
-
// Estimates token usage from tool input/output sizes and
|
|
8
|
-
// records cost events for per-session cost intelligence.
|
|
9
|
-
// Must complete in <500ms.
|
|
10
|
-
// ============================================================
|
|
11
|
-
|
|
12
|
-
import { getMemoryDb } 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
|
-
tool_name: string;
|
|
20
|
-
tool_input: Record<string, unknown>;
|
|
21
|
-
tool_response: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Approximate: 4 characters per token (industry rule of thumb)
|
|
25
|
-
const CHARS_PER_TOKEN = 4;
|
|
26
|
-
|
|
27
|
-
function estimateTokens(text: string): number {
|
|
28
|
-
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function main(): Promise<void> {
|
|
32
|
-
try {
|
|
33
|
-
const input = await readStdin();
|
|
34
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
35
|
-
const { session_id, tool_name, tool_input, tool_response } = hookInput;
|
|
36
|
-
|
|
37
|
-
const inputStr = JSON.stringify(tool_input);
|
|
38
|
-
const estimatedInputTokens = estimateTokens(inputStr);
|
|
39
|
-
const estimatedOutputTokens = estimateTokens(tool_response ?? '');
|
|
40
|
-
|
|
41
|
-
const db = getMemoryDb();
|
|
42
|
-
try {
|
|
43
|
-
db.prepare(`
|
|
44
|
-
INSERT INTO tool_cost_events (session_id, tool_name, estimated_input_tokens, estimated_output_tokens, model)
|
|
45
|
-
VALUES (?, ?, ?, ?, ?)
|
|
46
|
-
`).run(session_id, tool_name, estimatedInputTokens, estimatedOutputTokens, '');
|
|
47
|
-
} finally {
|
|
48
|
-
db.close();
|
|
49
|
-
}
|
|
50
|
-
} catch (_e) {
|
|
51
|
-
// Best-effort: never block Claude Code
|
|
52
|
-
}
|
|
53
|
-
process.exit(0);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function readStdin(): Promise<string> {
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
let data = '';
|
|
59
|
-
process.stdin.setEncoding('utf-8');
|
|
60
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
61
|
-
process.stdin.on('end', () => resolve(data));
|
|
62
|
-
setTimeout(() => resolve(data), 3000);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
main();
|