@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.
@@ -131,6 +131,44 @@ var RegressionConfigSchema = z.object({
131
131
  warning: z.number().default(50)
132
132
  }).optional()
133
133
  }).optional();
134
+ var AutoLearningConfigSchema = z.object({
135
+ enabled: z.boolean().default(true),
136
+ incidentDir: z.string().default("docs/incidents"),
137
+ memoryDir: z.string().default("memory"),
138
+ memoryIndexFile: z.string().default("MEMORY.md"),
139
+ enforcementHooksDir: z.string().default("scripts/hooks"),
140
+ fixDetection: z.object({
141
+ enabled: z.boolean().default(true),
142
+ lookbackDays: z.number().default(7),
143
+ signals: z.array(z.string()).default([
144
+ "removed_broken_code",
145
+ "added_error_handling",
146
+ "method_name_correction",
147
+ "auth_fix",
148
+ "nil_handling_fix",
149
+ "concurrency_fix",
150
+ "async_pattern_fix",
151
+ "added_missing_import"
152
+ ])
153
+ }).default({}),
154
+ failureClassification: z.object({
155
+ enabled: z.boolean().default(true),
156
+ thresholds: z.object({
157
+ known: z.number().default(5),
158
+ similar: z.number().default(3)
159
+ }).default({}),
160
+ scoring: z.object({
161
+ diffPatternWeight: z.number().default(3),
162
+ filePatternWeight: z.number().default(2),
163
+ promptKeywordWeight: z.number().default(2)
164
+ }).default({})
165
+ }).default({}),
166
+ pipeline: z.object({
167
+ requireIncidentReport: z.boolean().default(true),
168
+ requirePreventionRule: z.boolean().default(true),
169
+ requireEnforcement: z.boolean().default(true)
170
+ }).default({})
171
+ }).optional();
134
172
  var CloudConfigSchema = z.object({
135
173
  enabled: z.boolean().default(false),
136
174
  apiKey: z.string().optional(),
@@ -220,7 +258,8 @@ var RawConfigSchema = z.object({
220
258
  regression: RegressionConfigSchema,
221
259
  cloud: CloudConfigSchema,
222
260
  conventions: ConventionsConfigSchema,
223
- python: PythonConfigSchema
261
+ python: PythonConfigSchema,
262
+ autoLearning: AutoLearningConfigSchema
224
263
  }).passthrough();
225
264
  var _config = null;
226
265
  var _projectRoot = null;
@@ -821,6 +860,25 @@ function initMemorySchema(db) {
821
860
  features TEXT DEFAULT '[]'
822
861
  );
823
862
  `);
863
+ db.exec(`
864
+ CREATE TABLE IF NOT EXISTS failure_classes (
865
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
866
+ name TEXT NOT NULL UNIQUE,
867
+ description TEXT NOT NULL,
868
+ diff_patterns TEXT NOT NULL DEFAULT '[]',
869
+ file_patterns TEXT NOT NULL DEFAULT '[]',
870
+ prompt_keywords TEXT NOT NULL DEFAULT '[]',
871
+ incidents TEXT NOT NULL DEFAULT '[]',
872
+ rules TEXT NOT NULL DEFAULT '[]',
873
+ scanner_checks TEXT NOT NULL DEFAULT '[]',
874
+ known_message TEXT NOT NULL DEFAULT '',
875
+ needs_review INTEGER NOT NULL DEFAULT 0,
876
+ created_at TEXT DEFAULT (datetime('now')),
877
+ updated_at TEXT DEFAULT (datetime('now'))
878
+ );
879
+ CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
880
+ CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
881
+ `);
824
882
  }
825
883
  function assignImportance(type, vrResult) {
826
884
  switch (type) {
@@ -894,7 +952,9 @@ function linkSessionToTask(db, sessionId, taskId) {
894
952
  }
895
953
 
896
954
  // src/hooks/user-prompt.ts
897
- import { existsSync as existsSync3 } from "fs";
955
+ import { existsSync as existsSync3, writeFileSync } from "fs";
956
+ import { tmpdir } from "os";
957
+ import { join } from "path";
898
958
  async function main() {
899
959
  try {
900
960
  const input = await readStdin();
@@ -967,6 +1027,35 @@ async function main() {
967
1027
  }
968
1028
  } catch (_memoryNagErr) {
969
1029
  }
1030
+ try {
1031
+ const failureKeywords = [
1032
+ "bug",
1033
+ "broken",
1034
+ "crash",
1035
+ "error",
1036
+ "fail",
1037
+ "fix",
1038
+ "wrong",
1039
+ "missing",
1040
+ "undefined",
1041
+ "null",
1042
+ "exception",
1043
+ "stack trace",
1044
+ "regression",
1045
+ "revert",
1046
+ "doesn't work",
1047
+ "not working",
1048
+ "stopped working",
1049
+ "broke"
1050
+ ];
1051
+ const promptLower = prompt.toLowerCase();
1052
+ const matched = failureKeywords.filter((kw) => promptLower.includes(kw));
1053
+ if (matched.length > 0) {
1054
+ const contextFile = join(tmpdir(), `massu-failure-context-${session_id.slice(0, 8)}-${Date.now()}`);
1055
+ writeFileSync(contextFile, matched.join(" "), "utf-8");
1056
+ }
1057
+ } catch {
1058
+ }
970
1059
  } finally {
971
1060
  db.close();
972
1061
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
- "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
5
+ "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, auto-learning pipeline, tiered tooling (12 free / 72 total), 55+ workflow commands, 15 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
7
7
  "bin": {
8
8
  "massu": "./dist/cli.js"
@@ -52,9 +52,9 @@ Security blocking -> advisory warnings -> matcher-specific -> observability.
52
52
 
53
53
  ---
54
54
 
55
- ## PostToolUse (11 hooks)
55
+ ## PostToolUse (14 hooks)
56
56
 
57
- Security scan -> immediate feedback -> context tracking -> incident capture -> memory sync -> observability.
57
+ Security scan -> immediate feedback -> context tracking -> fix detection -> incident capture -> pipeline triggers -> memory sync -> observability.
58
58
 
59
59
  | position: 1 | CI monitor | standard | Bash(git push) -- immediate push feedback |
60
60
  |---|---|---|---|
@@ -62,17 +62,23 @@ Security scan -> immediate feedback -> context tracking -> incident capture -> m
62
62
  | position: 3 | `pattern-feedback.sh` | standard | Edit\|Write -- immediate pattern violation feedback |
63
63
  | position: 4 | `post-edit-context.js` | strict | Edit\|Write -- detailed semantic analysis |
64
64
  | position: 5 | `post-tool-use.js` | standard | Edit\|Write\|Bash -- structured context tracking |
65
- | position: 6 | `auto-ingest-incident.sh` | strict | Edit\|Write -- auto-capture incident patterns |
66
- | position: 7 | `memory-auto-ingest.sh` | standard | Write -- auto-sync memory files to codegraph SQLite DB |
67
- | position: 8 | `validate-deliverables.sh` | strict | Bash\|Edit\|Write -- deliverable validation |
68
- | position: 9 | `pattern-scanner.sh --single-file` | strict | Edit\|Write -- per-file pattern scan |
69
- | position: 10 | `mcp-usage-tracker.sh` | strict | MCP tools -- append-only MCP audit log |
70
- | position: 11 | `compaction-advisor.sh` | standard | Bash\|Edit\|Write\|Read\|Grep\|Glob -- context tracking, widest matcher |
65
+ | position: 6 | `fix-detector.js` | standard | Edit\|Write -- detect bug fixes via git diff heuristics |
66
+ | position: 7 | `auto-ingest-incident.sh` | strict | Edit\|Write -- auto-capture incident patterns |
67
+ | position: 8 | `incident-pipeline.js` | standard | Write -- trigger rule derivation on incident report writes |
68
+ | position: 9 | `rule-enforcement-pipeline.js` | standard | Write -- trigger enforcement on prevention rule writes |
69
+ | position: 10 | `memory-auto-ingest.sh` | standard | Write -- auto-sync memory files to codegraph SQLite DB |
70
+ | position: 11 | `validate-deliverables.sh` | strict | Bash\|Edit\|Write -- deliverable validation |
71
+ | position: 12 | `pattern-scanner.sh --single-file` | strict | Edit\|Write -- per-file pattern scan |
72
+ | position: 13 | `mcp-usage-tracker.sh` | strict | MCP tools -- append-only MCP audit log |
73
+ | position: 14 | `compaction-advisor.sh` | standard | Bash\|Edit\|Write\|Read\|Grep\|Glob -- context tracking, widest matcher |
71
74
 
72
75
  **Dependencies**:
73
76
  - `output-secret-filter.sh` MUST run before any feedback hooks -- security first
74
77
  - `pattern-feedback.sh` before `post-tool-use.js` -- immediate feedback before tracking
75
- - `memory-auto-ingest.sh` runs after incident capture -- memory sync is data-writing, before validation
78
+ - `fix-detector.js` after `post-tool-use.js` -- needs structured tracking context
79
+ - `incident-pipeline.js` after `auto-ingest-incident.sh` -- incident must be captured first
80
+ - `rule-enforcement-pipeline.js` after `incident-pipeline.js` -- rule derivation before enforcement
81
+ - `memory-auto-ingest.sh` runs after pipeline hooks -- memory sync is data-writing, before validation
76
82
  - `compaction-advisor.sh` MUST be last -- widest matcher, just counts tool calls
77
83
 
78
84
  ---
@@ -105,21 +111,23 @@ Quick state capture -> full DB snapshot.
105
111
 
106
112
  ---
107
113
 
108
- ## Stop (7 hooks)
114
+ ## Stop (8 hooks)
109
115
 
110
- Session summary -> warnings -> memory extraction -> review -> validation.
116
+ Session summary -> auto-learning check -> warnings -> memory extraction -> review -> validation.
111
117
 
112
118
  | position: 1 | `session-end.js` | standard | Write session summary to memory DB |
113
119
  |---|---|---|---|
114
- | position: 2 | Uncommitted changes warning | standard (inline) | Alert user about unstaged work |
115
- | position: 3 | `memory-auto-extract.sh` | standard | Auto-extract memories from DB observations |
116
- | position: 4 | `auto-review-on-stop.sh` | strict | Automated code review of session changes |
117
- | position: 5 | `surface-review-findings.sh` | strict | Display review findings to user |
118
- | position: 6 | `validate-deliverables.sh` | strict | Final deliverable validation |
119
- | position: 7 | `pattern-extractor.sh` | advisory | Extract new patterns from session |
120
+ | position: 2 | `auto-learning-pipeline.js` | standard | Enforce fix→incident→rule→enforcement pipeline completion |
121
+ | position: 3 | Uncommitted changes warning | standard (inline) | Alert user about unstaged work |
122
+ | position: 4 | `memory-auto-extract.sh` | standard | Auto-extract memories from DB observations |
123
+ | position: 5 | `auto-review-on-stop.sh` | strict | Automated code review of session changes |
124
+ | position: 6 | `surface-review-findings.sh` | strict | Display review findings to user |
125
+ | position: 7 | `validate-deliverables.sh` | strict | Final deliverable validation |
126
+ | position: 8 | `pattern-extractor.sh` | advisory | Extract new patterns from session |
120
127
 
121
128
  **Dependencies**:
122
129
  - `session-end.js` MUST be position 1 -- writes DB data that `memory-auto-extract.sh` reads
130
+ - `auto-learning-pipeline.js` MUST run early -- needs to output mandatory instructions before session ends
123
131
  - `memory-auto-extract.sh` MUST come after `session-end.js` -- depends on DB observations
124
132
  - `surface-review-findings.sh` MUST come after `auto-review-on-stop.sh` -- displays its output
125
133
  - `pattern-extractor.sh` runs last -- advisory tier (skipped in minimal/standard profiles)
@@ -8,7 +8,7 @@
8
8
  * 1. massu.config.yaml exists and parses correctly
9
9
  * 2. .mcp.json has massu entry
10
10
  * 3. .claude/settings.local.json has hooks config
11
- * 4. All 11 compiled hook files exist
11
+ * 4. All 15 compiled hook files exist
12
12
  * 5. Knowledge DB exists (.massu/memory.db)
13
13
  * 6. Memory directory exists (~/.claude/projects/.../memory/)
14
14
  * 7. Shell hooks wired in settings.local.json
@@ -7,7 +7,7 @@
7
7
  * 1. Detects project framework (scans package.json)
8
8
  * 2. Generates massu.config.yaml (or preserves existing)
9
9
  * 3. Registers MCP server in .mcp.json (creates or merges)
10
- * 4. Installs all 11 hooks in .claude/settings.local.json
10
+ * 4. Installs all 15 hooks in .claude/settings.local.json
11
11
  * 5. Installs slash commands into .claude/commands/
12
12
  * 6. Initializes memory directory
13
13
  * 7. Prints success summary
@@ -402,6 +402,15 @@ export function buildHooksConfig(hooksDir: string): HooksConfig {
402
402
  matcher: 'Edit|Write',
403
403
  hooks: [
404
404
  { type: 'command', command: hookCmd(hooksDir, 'post-edit-context.js'), timeout: 5 },
405
+ { type: 'command', command: hookCmd(hooksDir, 'fix-detector.js'), timeout: 5 },
406
+ { type: 'command', command: hookCmd(hooksDir, 'classify-failure.js'), timeout: 5 },
407
+ ],
408
+ },
409
+ {
410
+ matcher: 'Write',
411
+ hooks: [
412
+ { type: 'command', command: hookCmd(hooksDir, 'incident-pipeline.js'), timeout: 5 },
413
+ { type: 'command', command: hookCmd(hooksDir, 'rule-enforcement-pipeline.js'), timeout: 5 },
405
414
  ],
406
415
  },
407
416
  ],
@@ -409,6 +418,7 @@ export function buildHooksConfig(hooksDir: string): HooksConfig {
409
418
  {
410
419
  hooks: [
411
420
  { type: 'command', command: hookCmd(hooksDir, 'session-end.js'), timeout: 15 },
421
+ { type: 'command', command: hookCmd(hooksDir, 'auto-learning-pipeline.js'), timeout: 10 },
412
422
  ],
413
423
  },
414
424
  ],
package/src/config.ts CHANGED
@@ -147,6 +147,47 @@ const RegressionConfigSchema = z.object({
147
147
  }).optional();
148
148
  export type RegressionConfig = z.infer<typeof RegressionConfigSchema>;
149
149
 
150
+ // --- Auto-Learning Config ---
151
+ const AutoLearningConfigSchema = z.object({
152
+ enabled: z.boolean().default(true),
153
+ incidentDir: z.string().default('docs/incidents'),
154
+ memoryDir: z.string().default('memory'),
155
+ memoryIndexFile: z.string().default('MEMORY.md'),
156
+ enforcementHooksDir: z.string().default('scripts/hooks'),
157
+ fixDetection: z.object({
158
+ enabled: z.boolean().default(true),
159
+ lookbackDays: z.number().default(7),
160
+ signals: z.array(z.string()).default([
161
+ 'removed_broken_code',
162
+ 'added_error_handling',
163
+ 'method_name_correction',
164
+ 'auth_fix',
165
+ 'nil_handling_fix',
166
+ 'concurrency_fix',
167
+ 'async_pattern_fix',
168
+ 'added_missing_import',
169
+ ]),
170
+ }).default({}),
171
+ failureClassification: z.object({
172
+ enabled: z.boolean().default(true),
173
+ thresholds: z.object({
174
+ known: z.number().default(5),
175
+ similar: z.number().default(3),
176
+ }).default({}),
177
+ scoring: z.object({
178
+ diffPatternWeight: z.number().default(3),
179
+ filePatternWeight: z.number().default(2),
180
+ promptKeywordWeight: z.number().default(2),
181
+ }).default({}),
182
+ }).default({}),
183
+ pipeline: z.object({
184
+ requireIncidentReport: z.boolean().default(true),
185
+ requirePreventionRule: z.boolean().default(true),
186
+ requireEnforcement: z.boolean().default(true),
187
+ }).default({}),
188
+ }).optional();
189
+ export type AutoLearningConfig = z.infer<typeof AutoLearningConfigSchema>;
190
+
150
191
  // --- Cloud Config ---
151
192
  const CloudConfigSchema = z.object({
152
193
  enabled: z.boolean().default(false),
@@ -240,6 +281,7 @@ const RawConfigSchema = z.object({
240
281
  cloud: CloudConfigSchema,
241
282
  conventions: ConventionsConfigSchema,
242
283
  python: PythonConfigSchema,
284
+ autoLearning: AutoLearningConfigSchema,
243
285
  }).passthrough();
244
286
 
245
287
  // --- Final Config interface (derived from Zod) ---
@@ -261,6 +303,7 @@ export interface Config {
261
303
  cloud?: CloudConfig;
262
304
  conventions?: ConventionsConfig;
263
305
  python?: PythonConfig;
306
+ autoLearning?: AutoLearningConfig;
264
307
  }
265
308
 
266
309
  let _config: Config | null = null;
@@ -0,0 +1,195 @@
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 { execSync } from 'child_process';
20
+ import { existsSync, readFileSync, unlinkSync, readdirSync } from 'fs';
21
+ import { tmpdir } from 'os';
22
+ import { join } from 'path';
23
+ import { getProjectRoot, getConfig } from '../config.ts';
24
+
25
+ interface HookInput {
26
+ session_id: string;
27
+ transcript_path: string;
28
+ cwd: string;
29
+ }
30
+
31
+ interface FixSignal {
32
+ file: string;
33
+ signals: string[];
34
+ timestamp: string;
35
+ }
36
+
37
+ function getSessionFlagPath(sessionId: string): string {
38
+ return join(tmpdir(), 'massu-auto-learning', `fixes-${sessionId.slice(0, 12)}.jsonl`);
39
+ }
40
+
41
+ async function main(): Promise<void> {
42
+ try {
43
+ const input = await readStdin();
44
+ const hookInput = JSON.parse(input) as HookInput;
45
+ const config = getConfig();
46
+
47
+ // Check if auto-learning is enabled
48
+ if (config.autoLearning?.enabled === false) {
49
+ process.exit(0);
50
+ return;
51
+ }
52
+
53
+ const root = getProjectRoot();
54
+ const incidentDir = config.autoLearning?.incidentDir ?? 'docs/incidents';
55
+ const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
56
+ const autoLearn = config.autoLearning;
57
+
58
+ // Source 1: Session fix flags from fix-detector
59
+ const flagPath = getSessionFlagPath(hookInput.session_id);
60
+ let sessionFixes: FixSignal[] = [];
61
+ if (existsSync(flagPath)) {
62
+ try {
63
+ sessionFixes = readFileSync(flagPath, 'utf-8')
64
+ .split('\n')
65
+ .filter(Boolean)
66
+ .map(line => JSON.parse(line) as FixSignal);
67
+ } catch { /* ignore parse errors */ }
68
+ }
69
+
70
+ // Source 2: Scan uncommitted git diff for fix patterns (language-agnostic)
71
+ let uncommittedFix = false;
72
+ try {
73
+ const diff = execSync('git diff --name-only', { cwd: root, timeout: 3000, encoding: 'utf-8' });
74
+ if (diff.trim()) {
75
+ const fullDiff = execSync('git diff', { cwd: root, timeout: 5000, encoding: 'utf-8' });
76
+ const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
77
+ const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
78
+ if (fixPatterns > 3 || removedBroken > 1) {
79
+ uncommittedFix = true;
80
+ }
81
+ }
82
+ } catch { /* git not available or no changes */ }
83
+
84
+ if (sessionFixes.length === 0 && !uncommittedFix) {
85
+ // Clean up flag file
86
+ cleanup(flagPath);
87
+ process.exit(0);
88
+ return;
89
+ }
90
+
91
+ // Build pipeline instructions
92
+ const lines: string[] = [];
93
+ lines.push('');
94
+ lines.push('============================================================================');
95
+ lines.push(' MASSU AUTO-LEARNING PIPELINE — ACTION REQUIRED BEFORE SESSION END');
96
+ lines.push('============================================================================');
97
+
98
+ if (sessionFixes.length > 0) {
99
+ lines.push('');
100
+ lines.push(` ${sessionFixes.length} bug fix(es) detected during this session:`);
101
+ lines.push('');
102
+ // Deduplicate by file
103
+ const byFile = new Map<string, string[]>();
104
+ for (const fix of sessionFixes) {
105
+ const existing = byFile.get(fix.file) ?? [];
106
+ existing.push(...fix.signals);
107
+ byFile.set(fix.file, [...new Set(existing)]);
108
+ }
109
+ for (const [file, signals] of byFile) {
110
+ lines.push(` - ${file} (${signals.join(', ')})`);
111
+ }
112
+ }
113
+
114
+ if (uncommittedFix) {
115
+ lines.push('');
116
+ lines.push(' Additional uncommitted fix patterns detected in git diff.');
117
+ }
118
+
119
+ lines.push('');
120
+ lines.push(' Complete these steps before this session ends:');
121
+ lines.push('');
122
+
123
+ if (autoLearn?.pipeline?.requireIncidentReport !== false) {
124
+ lines.push(' STEP 1: INCIDENT REPORT');
125
+ lines.push(` For each distinct bug fixed, create: ${incidentDir}/YYYY-MM-DD-<slug>.md`);
126
+ lines.push(' Include: Date, Severity, Symptoms, Root Cause, Fix, Files Changed, Prevention Rules');
127
+ lines.push('');
128
+ }
129
+
130
+ if (autoLearn?.pipeline?.requirePreventionRule !== false) {
131
+ lines.push(' STEP 2: PREVENTION RULE');
132
+ lines.push(` For each incident, create: ${memoryDir}/feedback_<rule_name>.md`);
133
+ lines.push(' Include frontmatter (name, description, type: feedback) + Why + How to apply');
134
+ lines.push(` Update ${config.autoLearning?.memoryIndexFile ?? 'MEMORY.md'} index`);
135
+ lines.push('');
136
+ }
137
+
138
+ if (autoLearn?.pipeline?.requireEnforcement !== false) {
139
+ lines.push(' STEP 3: ENFORCEMENT PLACEMENT');
140
+ lines.push(' For each new rule, determine enforcement layer(s):');
141
+ lines.push(' a) If statically detectable → add to pattern-feedback hook');
142
+ lines.push(' b) If about editing certain files → add to blast-radius hook');
143
+ lines.push(' c) If about dangerous commands → add to dangerous-command hook');
144
+ lines.push(' d) If critical → add to pre-commit hook');
145
+ lines.push(' e) If needs runtime monitoring → create monitoring producer');
146
+ lines.push('');
147
+ }
148
+
149
+ lines.push(' STEP 4: VERIFY');
150
+ lines.push(' Test any new enforcement hooks to confirm they detect violations.');
151
+ lines.push('');
152
+ lines.push('============================================================================');
153
+ lines.push('');
154
+
155
+ console.log(lines.join('\n'));
156
+
157
+ // Clean up flag file
158
+ cleanup(flagPath);
159
+ } catch {
160
+ // Best-effort: never block Claude Code
161
+ }
162
+ process.exit(0);
163
+ }
164
+
165
+ function cleanup(flagPath: string): void {
166
+ try {
167
+ if (existsSync(flagPath)) unlinkSync(flagPath);
168
+ // Clean up old flag files (>24h)
169
+ const dir = join(tmpdir(), 'massu-auto-learning');
170
+ if (existsSync(dir)) {
171
+ const now = Date.now();
172
+ for (const file of readdirSync(dir)) {
173
+ const fullPath = join(dir, file);
174
+ try {
175
+ const stat = require('fs').statSync(fullPath);
176
+ if (now - stat.mtimeMs > 86400000) {
177
+ unlinkSync(fullPath);
178
+ }
179
+ } catch { /* ignore */ }
180
+ }
181
+ }
182
+ } catch { /* best effort */ }
183
+ }
184
+
185
+ function readStdin(): Promise<string> {
186
+ return new Promise((resolve) => {
187
+ let data = '';
188
+ process.stdin.setEncoding('utf-8');
189
+ process.stdin.on('data', (chunk: string) => { data += chunk; });
190
+ process.stdin.on('end', () => resolve(data));
191
+ setTimeout(() => resolve(data), 5000);
192
+ });
193
+ }
194
+
195
+ main();