@massu/core 0.6.3 → 0.8.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.
@@ -0,0 +1,259 @@
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 } 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
+ require('fs').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();
@@ -0,0 +1,186 @@
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: Fix Detector
7
+ // Detects when a bug fix is being applied during a session by
8
+ // analyzing git diffs of edited files. Sets session-level state
9
+ // so the auto-learning pipeline can trigger at session end.
10
+ //
11
+ // Part of the Auto-Learning Pipeline:
12
+ // [Fix Detected] → Incident Report → Rule → Enforcement
13
+ //
14
+ // Must complete in <1000ms.
15
+ // ============================================================
16
+
17
+ import { execSync } from 'child_process';
18
+ import { existsSync, appendFileSync, mkdirSync } from 'fs';
19
+ import { tmpdir } from 'os';
20
+ import { join } from 'path';
21
+ import { getProjectRoot, getConfig } from '../config.ts';
22
+
23
+ interface HookInput {
24
+ session_id: string;
25
+ tool_name: string;
26
+ tool_input: { file_path?: string };
27
+ }
28
+
29
+ interface FixSignal {
30
+ file: string;
31
+ signals: string[];
32
+ timestamp: string;
33
+ }
34
+
35
+ // Fix detection heuristics — each returns true if the pattern matches
36
+ const FIX_HEURISTICS: Array<{ name: string; test: (diff: string) => boolean }> = [
37
+ {
38
+ name: 'removed_broken_code',
39
+ test: (diff) => /^-.*\b(bug|broken|wrong|incorrect|typo|crash|error|fail|miss|stale)\b/m.test(diff),
40
+ },
41
+ {
42
+ name: 'added_error_handling',
43
+ test: (diff) => {
44
+ const added = (diff.match(/^\+.*(try|except|catch|guard|if.*nil|if.*None|validate|assert|raise|throw)/gm) || []).length;
45
+ return added > 2;
46
+ },
47
+ },
48
+ {
49
+ name: 'method_name_correction',
50
+ test: (diff) => {
51
+ const removed = diff.match(/^-.*\.([a-z_]+)\(/m);
52
+ const added = diff.match(/^\+.*\.([a-z_]+)\(/m);
53
+ return !!(removed && added && removed[1] !== added[1]);
54
+ },
55
+ },
56
+ {
57
+ name: 'auth_fix',
58
+ test: (diff) => /^\+.*(token|auth|header|X-Service|Bearer|credential)/im.test(diff),
59
+ },
60
+ {
61
+ name: 'nil_handling_fix',
62
+ test: (diff) => /^\+.*(= nil|= None|\.isNil|is None|!= nil|is not None|guard let|if let|optional)/m.test(diff) && /^-/m.test(diff),
63
+ },
64
+ {
65
+ name: 'concurrency_fix',
66
+ test: (diff) => /^\+.*(timeout|semaphore|lock|mutex|throttle|rate.limit|max_conn)/im.test(diff),
67
+ },
68
+ {
69
+ name: 'async_pattern_fix',
70
+ test: (diff) => /^\+.*(@MainActor|async with|asyncio\.timeout|\.await)/.test(diff) && /^-/m.test(diff),
71
+ },
72
+ {
73
+ name: 'added_missing_import',
74
+ test: (diff) => /^\+.*(import|from.*import|require)/.test(diff) && !/^-.*(import|from.*import|require)/m.test(diff),
75
+ },
76
+ ];
77
+
78
+ function getSessionFlagPath(sessionId: string): string {
79
+ const dir = join(tmpdir(), 'massu-auto-learning');
80
+ if (!existsSync(dir)) {
81
+ mkdirSync(dir, { recursive: true });
82
+ }
83
+ return join(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
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 || !existsSync(filePath)) {
93
+ process.exit(0);
94
+ return;
95
+ }
96
+
97
+ // Only check code files
98
+ if (!/\.(py|swift|ts|tsx|js|jsx|rs|go|rb|sh)$/.test(filePath)) {
99
+ process.exit(0);
100
+ return;
101
+ }
102
+
103
+ // Skip incident/memory/doc files (those ARE pipeline output)
104
+ const config = getConfig();
105
+ const incidentDir = config.autoLearning?.incidentDir ?? 'docs/incidents';
106
+ const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
107
+ if (filePath.includes(incidentDir) || filePath.includes(memoryDir) || filePath.includes('MEMORY.md')) {
108
+ process.exit(0);
109
+ return;
110
+ }
111
+
112
+ // Check if auto-learning is enabled
113
+ if (config.autoLearning?.enabled === false || config.autoLearning?.fixDetection?.enabled === false) {
114
+ process.exit(0);
115
+ return;
116
+ }
117
+
118
+ // Get git diff for this file
119
+ const root = getProjectRoot();
120
+ let diff = '';
121
+ try {
122
+ diff = execSync(`git diff -- "${filePath}"`, { cwd: root, timeout: 3000, encoding: 'utf-8' });
123
+ if (!diff) {
124
+ diff = execSync(`git diff HEAD -- "${filePath}"`, { cwd: root, timeout: 3000, encoding: 'utf-8' });
125
+ }
126
+ } catch {
127
+ process.exit(0);
128
+ return;
129
+ }
130
+
131
+ if (!diff) {
132
+ process.exit(0);
133
+ return;
134
+ }
135
+
136
+ // Run fix detection heuristics
137
+ const enabledSignals = new Set(config.autoLearning?.fixDetection?.signals ?? FIX_HEURISTICS.map(h => h.name));
138
+ const detected: string[] = [];
139
+ for (const heuristic of FIX_HEURISTICS) {
140
+ if (enabledSignals.has(heuristic.name) && heuristic.test(diff)) {
141
+ detected.push(heuristic.name);
142
+ }
143
+ }
144
+
145
+ if (detected.length === 0) {
146
+ process.exit(0);
147
+ return;
148
+ }
149
+
150
+ // Record the fix detection
151
+ const signal: FixSignal = {
152
+ file: filePath,
153
+ signals: detected,
154
+ timestamp: new Date().toISOString(),
155
+ };
156
+
157
+ const flagPath = getSessionFlagPath(hookInput.session_id);
158
+ appendFileSync(flagPath, JSON.stringify(signal) + '\n');
159
+
160
+ // Count total fixes this session
161
+ const lines = require('fs').readFileSync(flagPath, 'utf-8').split('\n').filter(Boolean);
162
+ if (lines.length === 1) {
163
+ // First fix detected — output advisory
164
+ console.log(
165
+ `[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(', ')}). ` +
166
+ `The auto-learning pipeline will prompt you at session end to create an incident report, ` +
167
+ `derive a prevention rule, and add enforcement.`
168
+ );
169
+ }
170
+ } catch {
171
+ // Best-effort: never block Claude Code
172
+ }
173
+ process.exit(0);
174
+ }
175
+
176
+ function readStdin(): Promise<string> {
177
+ return new Promise((resolve) => {
178
+ let data = '';
179
+ process.stdin.setEncoding('utf-8');
180
+ process.stdin.on('data', (chunk: string) => { data += chunk; });
181
+ process.stdin.on('end', () => resolve(data));
182
+ setTimeout(() => resolve(data), 3000);
183
+ });
184
+ }
185
+
186
+ main();
@@ -0,0 +1,190 @@
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: Incident-to-Rule Pipeline
7
+ // When a new incident report is written, automatically triggers
8
+ // the next step: deriving a prevention rule.
9
+ //
10
+ // Part of the Auto-Learning Pipeline:
11
+ // Fix Detected → [Incident Report] → RULE DERIVATION → Enforcement
12
+ //
13
+ // Triggers on: Write to docs/incidents/*.md (configurable)
14
+ // Must complete in <500ms.
15
+ // ============================================================
16
+
17
+ import { existsSync, readFileSync, readdirSync } from 'fs';
18
+ import { basename, dirname, resolve } from 'path';
19
+ import { getProjectRoot, getConfig } from '../config.ts';
20
+ import { getMemoryDb, scoreFailureClasses, appendIncidentToFailureClass, addFailureClass } from '../memory-db.ts';
21
+
22
+ interface HookInput {
23
+ session_id: string;
24
+ tool_name: string;
25
+ tool_input: { file_path?: string; content?: string };
26
+ }
27
+
28
+ async function main(): Promise<void> {
29
+ try {
30
+ const input = await readStdin();
31
+ const hookInput = JSON.parse(input) as HookInput;
32
+ const filePath = hookInput.tool_input?.file_path;
33
+
34
+ if (!filePath) {
35
+ process.exit(0);
36
+ return;
37
+ }
38
+
39
+ const config = getConfig();
40
+ if (config.autoLearning?.enabled === false) {
41
+ process.exit(0);
42
+ return;
43
+ }
44
+
45
+ const root = getProjectRoot();
46
+ const incidentDir = config.autoLearning?.incidentDir ?? 'docs/incidents';
47
+ const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
48
+ // Only trigger on incident report files
49
+ const relPath = filePath.startsWith(root + '/') ? filePath.slice(root.length + 1) : filePath;
50
+ if (!relPath.startsWith(incidentDir) || !relPath.endsWith('.md')) {
51
+ process.exit(0);
52
+ return;
53
+ }
54
+
55
+ if (!existsSync(filePath)) {
56
+ process.exit(0);
57
+ return;
58
+ }
59
+
60
+ // Extract title from the incident report
61
+ const content = readFileSync(filePath, 'utf-8');
62
+ const titleMatch = content.match(/^#\s+(.+)/m);
63
+ const title = titleMatch?.[1] ?? basename(filePath, '.md');
64
+
65
+ // Check if a corresponding rule already exists
66
+ const slug = basename(filePath, '.md')
67
+ .replace(/^\d{4}-\d{2}-\d{2}-?/, '')
68
+ .toLowerCase();
69
+
70
+ const memoryDirAbs = resolve(root, memoryDir);
71
+ let hasExistingRule = false;
72
+ if (existsSync(memoryDirAbs)) {
73
+ const ruleFiles = readdirSync(memoryDirAbs).filter(f => f.startsWith('feedback_'));
74
+ for (const ruleFile of ruleFiles) {
75
+ if (ruleFile.toLowerCase().includes(slug.slice(0, 20))) {
76
+ hasExistingRule = true;
77
+ break;
78
+ }
79
+ }
80
+ }
81
+
82
+ if (hasExistingRule) {
83
+ // Rule already exists — no action needed
84
+ process.exit(0);
85
+ return;
86
+ }
87
+
88
+ // Check if incident has prevention rules section
89
+ const hasPrevention = /## Prevention Rules|## Prevention|## Rules/i.test(content);
90
+
91
+ if (config.autoLearning?.pipeline?.requirePreventionRule === false) {
92
+ process.exit(0);
93
+ return;
94
+ }
95
+
96
+ // Output rule derivation instructions
97
+ const lines: string[] = [];
98
+ lines.push('');
99
+ lines.push('============================================================================');
100
+ lines.push(' AUTO-LEARNING: Incident Report Created — Rule Derivation Required');
101
+ lines.push('============================================================================');
102
+ lines.push('');
103
+ lines.push(` Incident: ${title}`);
104
+ lines.push(` File: ${filePath}`);
105
+ lines.push('');
106
+
107
+ if (!hasPrevention) {
108
+ lines.push(' No "## Prevention Rules" section found in the incident report.');
109
+ lines.push(' Add one first, then proceed with rule derivation.');
110
+ lines.push('');
111
+ }
112
+
113
+ lines.push(' DERIVE A PREVENTION RULE:');
114
+ lines.push(` a) Read the incident root cause and prevention rules`);
115
+ lines.push(` b) Create: ${memoryDir}/feedback_<rule_name>.md`);
116
+ lines.push(' Template:');
117
+ lines.push(' ---');
118
+ lines.push(' name: <Rule Name>');
119
+ lines.push(' description: <one-line description>');
120
+ lines.push(' type: feedback');
121
+ lines.push(' ---');
122
+ lines.push(' <Rule statement>');
123
+ lines.push(' **Why:** <Root cause from incident>');
124
+ lines.push(' **How to apply:** <Concrete steps>');
125
+ lines.push('');
126
+ lines.push(` c) Add one-line entry to ${config.autoLearning?.memoryIndexFile ?? 'MEMORY.md'}`);
127
+ lines.push('');
128
+ lines.push(' This step is MANDATORY per the auto-learning pipeline.');
129
+ lines.push('============================================================================');
130
+ lines.push('');
131
+
132
+ console.log(lines.join('\n'));
133
+
134
+ // ============================================================
135
+ // Taxonomy Update: Score incident against failure classes
136
+ // If KNOWN match → append incident to existing class
137
+ // If NEW → create stub entry with needs_review=true
138
+ // ============================================================
139
+ try {
140
+ const taxonomyConfig = config.autoLearning?.failureClassification;
141
+ if (taxonomyConfig?.enabled !== false) {
142
+ const db = getMemoryDb();
143
+ try {
144
+ const thresholds = taxonomyConfig?.thresholds ?? { known: 5, similar: 3 };
145
+ const scoringWeights = taxonomyConfig?.scoring;
146
+
147
+ // Extract incident number from filename (e.g., "incident-042.md" → "42")
148
+ const incidentNumMatch = basename(filePath, '.md').match(/(\d+)/);
149
+ const incidentId = incidentNumMatch ? incidentNumMatch[1] : basename(filePath, '.md');
150
+
151
+ // Score incident content against taxonomy
152
+ const bestMatch = scoreFailureClasses(db, content, filePath, title, scoringWeights);
153
+
154
+ if (bestMatch && bestMatch.score >= thresholds.similar) {
155
+ // Known or similar — append incident to existing class
156
+ appendIncidentToFailureClass(db, bestMatch.name, incidentId);
157
+ } else {
158
+ // New pattern — create stub entry for review
159
+ const stubName = `auto_${slug.replace(/[^a-z0-9_]/g, '_').slice(0, 50)}`;
160
+ addFailureClass(db, {
161
+ name: stubName,
162
+ description: `Auto-created from incident ${incidentId}: ${title.slice(0, 100)}`,
163
+ incidents: [incidentId],
164
+ needsReview: true,
165
+ });
166
+ }
167
+ } finally {
168
+ db.close();
169
+ }
170
+ }
171
+ } catch {
172
+ // Best-effort: taxonomy update failure must not block the pipeline
173
+ }
174
+ } catch {
175
+ // Best-effort: never block Claude Code
176
+ }
177
+ process.exit(0);
178
+ }
179
+
180
+ function readStdin(): Promise<string> {
181
+ return new Promise((resolve) => {
182
+ let data = '';
183
+ process.stdin.setEncoding('utf-8');
184
+ process.stdin.on('data', (chunk: string) => { data += chunk; });
185
+ process.stdin.on('end', () => resolve(data));
186
+ setTimeout(() => resolve(data), 3000);
187
+ });
188
+ }
189
+
190
+ main();