@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.
- package/README.md +2 -2
- package/dist/cli.js +180 -5
- package/dist/hooks/auto-learning-pipeline.js +481 -0
- package/dist/hooks/classify-failure.js +1146 -0
- package/dist/hooks/cost-tracker.js +59 -1
- package/dist/hooks/fix-detector.js +474 -0
- package/dist/hooks/incident-pipeline.js +1114 -0
- package/dist/hooks/post-edit-context.js +40 -1
- package/dist/hooks/post-tool-use.js +59 -1
- package/dist/hooks/pre-compact.js +59 -1
- package/dist/hooks/pre-delete-check.js +40 -1
- package/dist/hooks/quality-event.js +59 -1
- package/dist/hooks/rule-enforcement-pipeline.js +453 -0
- package/dist/hooks/session-end.js +59 -1
- package/dist/hooks/session-start.js +60 -2
- package/dist/hooks/user-prompt.js +91 -2
- package/package.json +2 -2
- package/reference/hook-execution-order.md +25 -17
- package/src/commands/doctor.ts +1 -1
- package/src/commands/init.ts +11 -1
- package/src/config.ts +43 -0
- package/src/hooks/auto-learning-pipeline.ts +195 -0
- package/src/hooks/classify-failure.ts +259 -0
- package/src/hooks/fix-detector.ts +186 -0
- package/src/hooks/incident-pipeline.ts +190 -0
- package/src/hooks/rule-enforcement-pipeline.ts +159 -0
- package/src/hooks/session-start.ts +1 -1
- package/src/hooks/user-prompt.ts +21 -1
- package/src/license.ts +2 -1
- package/src/mcp-bridge-tools.ts +1 -1
- package/src/memory-db.ts +201 -0
|
@@ -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.
|
|
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,
|
|
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 (
|
|
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 | `
|
|
66
|
-
| position: 7 | `
|
|
67
|
-
| position: 8 | `
|
|
68
|
-
| position: 9 | `
|
|
69
|
-
| position: 10 | `
|
|
70
|
-
| position: 11 | `
|
|
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
|
-
- `
|
|
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 (
|
|
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 |
|
|
115
|
-
| position: 3 |
|
|
116
|
-
| position: 4 | `auto-
|
|
117
|
-
| position: 5 | `
|
|
118
|
-
| position: 6 | `
|
|
119
|
-
| position: 7 | `
|
|
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)
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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
|
+
* 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
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
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();
|