@massu/core 0.7.0 → 0.8.1

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.
@@ -149,6 +149,18 @@ var AutoLearningConfigSchema = z.object({
149
149
  "added_missing_import"
150
150
  ])
151
151
  }).default({}),
152
+ failureClassification: z.object({
153
+ enabled: z.boolean().default(true),
154
+ thresholds: z.object({
155
+ known: z.number().default(5),
156
+ similar: z.number().default(3)
157
+ }).default({}),
158
+ scoring: z.object({
159
+ diffPatternWeight: z.number().default(3),
160
+ filePatternWeight: z.number().default(2),
161
+ promptKeywordWeight: z.number().default(2)
162
+ }).default({})
163
+ }).default({}),
152
164
  pipeline: z.object({
153
165
  requireIncidentReport: z.boolean().default(true),
154
166
  requirePreventionRule: z.boolean().default(true),
@@ -151,6 +151,18 @@ var AutoLearningConfigSchema = z.object({
151
151
  "added_missing_import"
152
152
  ])
153
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({}),
154
166
  pipeline: z.object({
155
167
  requireIncidentReport: z.boolean().default(true),
156
168
  requirePreventionRule: z.boolean().default(true),
@@ -848,6 +860,25 @@ function initMemorySchema(db) {
848
860
  features TEXT DEFAULT '[]'
849
861
  );
850
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
+ `);
851
882
  }
852
883
  function assignImportance(type, vrResult) {
853
884
  switch (type) {
@@ -1635,11 +1666,10 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
1635
1666
  // src/hooks/post-tool-use.ts
1636
1667
  import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
1637
1668
  import { join as join2 } from "path";
1638
- import { parse as parseYaml3 } from "yaml";
1669
+ import { parse as parseYaml2 } from "yaml";
1639
1670
 
1640
1671
  // src/memory-file-ingest.ts
1641
1672
  import { readFileSync as readFileSync5, existsSync as existsSync6, readdirSync } from "fs";
1642
- import { parse as parseYaml2 } from "yaml";
1643
1673
  function ingestMemoryFile(db, sessionId, filePath) {
1644
1674
  if (!existsSync6(filePath)) return "skipped";
1645
1675
  const content = readFileSync5(filePath, "utf-8");
@@ -1651,7 +1681,13 @@ function ingestMemoryFile(db, sessionId, filePath) {
1651
1681
  let confidence;
1652
1682
  if (frontmatterMatch) {
1653
1683
  try {
1654
- const fm = parseYaml2(frontmatterMatch[1]);
1684
+ const fm = {};
1685
+ for (const line of frontmatterMatch[1].split("\n")) {
1686
+ const sep = line.indexOf(":");
1687
+ if (sep > 0) {
1688
+ fm[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
1689
+ }
1690
+ }
1655
1691
  name = fm.name ?? basename2;
1656
1692
  description = fm.description ?? "";
1657
1693
  type = fm.type ?? "discovery";
@@ -1866,7 +1902,7 @@ function readConventions(cwd) {
1866
1902
  const configPath = join2(projectRoot, "massu.config.yaml");
1867
1903
  if (!existsSync7(configPath)) return defaults;
1868
1904
  const content = readFileSync6(configPath, "utf-8");
1869
- const parsed = parseYaml3(content);
1905
+ const parsed = parseYaml2(content);
1870
1906
  if (!parsed || typeof parsed !== "object") return defaults;
1871
1907
  const conventions = parsed.conventions;
1872
1908
  if (!conventions || typeof conventions !== "object") return defaults;
@@ -151,6 +151,18 @@ var AutoLearningConfigSchema = z.object({
151
151
  "added_missing_import"
152
152
  ])
153
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({}),
154
166
  pipeline: z.object({
155
167
  requireIncidentReport: z.boolean().default(true),
156
168
  requirePreventionRule: z.boolean().default(true),
@@ -848,6 +860,25 @@ function initMemorySchema(db) {
848
860
  features TEXT DEFAULT '[]'
849
861
  );
850
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
+ `);
851
882
  }
852
883
  function assignImportance(type, vrResult) {
853
884
  switch (type) {
@@ -150,6 +150,18 @@ var AutoLearningConfigSchema = z.object({
150
150
  "added_missing_import"
151
151
  ])
152
152
  }).default({}),
153
+ failureClassification: z.object({
154
+ enabled: z.boolean().default(true),
155
+ thresholds: z.object({
156
+ known: z.number().default(5),
157
+ similar: z.number().default(3)
158
+ }).default({}),
159
+ scoring: z.object({
160
+ diffPatternWeight: z.number().default(3),
161
+ filePatternWeight: z.number().default(2),
162
+ promptKeywordWeight: z.number().default(2)
163
+ }).default({})
164
+ }).default({}),
153
165
  pipeline: z.object({
154
166
  requireIncidentReport: z.boolean().default(true),
155
167
  requirePreventionRule: z.boolean().default(true),
@@ -151,6 +151,18 @@ var AutoLearningConfigSchema = z.object({
151
151
  "added_missing_import"
152
152
  ])
153
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({}),
154
166
  pipeline: z.object({
155
167
  requireIncidentReport: z.boolean().default(true),
156
168
  requirePreventionRule: z.boolean().default(true),
@@ -848,6 +860,25 @@ function initMemorySchema(db) {
848
860
  features TEXT DEFAULT '[]'
849
861
  );
850
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
+ `);
851
882
  }
852
883
 
853
884
  // src/hooks/quality-event.ts
@@ -149,6 +149,18 @@ var AutoLearningConfigSchema = z.object({
149
149
  "added_missing_import"
150
150
  ])
151
151
  }).default({}),
152
+ failureClassification: z.object({
153
+ enabled: z.boolean().default(true),
154
+ thresholds: z.object({
155
+ known: z.number().default(5),
156
+ similar: z.number().default(3)
157
+ }).default({}),
158
+ scoring: z.object({
159
+ diffPatternWeight: z.number().default(3),
160
+ filePatternWeight: z.number().default(2),
161
+ promptKeywordWeight: z.number().default(2)
162
+ }).default({})
163
+ }).default({}),
152
164
  pipeline: z.object({
153
165
  requireIncidentReport: z.boolean().default(true),
154
166
  requirePreventionRule: z.boolean().default(true),
@@ -348,7 +360,8 @@ async function main() {
348
360
  process.exit(0);
349
361
  return;
350
362
  }
351
- if (!relPath.includes(memoryDir) && !relPath.includes("memory/") && !relPath.includes(".claude/")) {
363
+ const claudeDir = config.conventions?.claudeDirName ?? ".claude";
364
+ if (!relPath.includes(memoryDir) && !relPath.includes("memory/") && !relPath.includes(claudeDir + "/")) {
352
365
  process.exit(0);
353
366
  return;
354
367
  }
@@ -151,6 +151,18 @@ var AutoLearningConfigSchema = z.object({
151
151
  "added_missing_import"
152
152
  ])
153
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({}),
154
166
  pipeline: z.object({
155
167
  requireIncidentReport: z.boolean().default(true),
156
168
  requirePreventionRule: z.boolean().default(true),
@@ -848,6 +860,25 @@ function initMemorySchema(db) {
848
860
  features TEXT DEFAULT '[]'
849
861
  );
850
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
+ `);
851
882
  }
852
883
  function enqueueSyncPayload(db, payload) {
853
884
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
@@ -151,6 +151,18 @@ var AutoLearningConfigSchema = z.object({
151
151
  "added_missing_import"
152
152
  ])
153
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({}),
154
166
  pipeline: z.object({
155
167
  requireIncidentReport: z.boolean().default(true),
156
168
  requirePreventionRule: z.boolean().default(true),
@@ -854,6 +866,25 @@ function initMemorySchema(db) {
854
866
  features TEXT DEFAULT '[]'
855
867
  );
856
868
  `);
869
+ db.exec(`
870
+ CREATE TABLE IF NOT EXISTS failure_classes (
871
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
872
+ name TEXT NOT NULL UNIQUE,
873
+ description TEXT NOT NULL,
874
+ diff_patterns TEXT NOT NULL DEFAULT '[]',
875
+ file_patterns TEXT NOT NULL DEFAULT '[]',
876
+ prompt_keywords TEXT NOT NULL DEFAULT '[]',
877
+ incidents TEXT NOT NULL DEFAULT '[]',
878
+ rules TEXT NOT NULL DEFAULT '[]',
879
+ scanner_checks TEXT NOT NULL DEFAULT '[]',
880
+ known_message TEXT NOT NULL DEFAULT '',
881
+ needs_review INTEGER NOT NULL DEFAULT 0,
882
+ created_at TEXT DEFAULT (datetime('now')),
883
+ updated_at TEXT DEFAULT (datetime('now'))
884
+ );
885
+ CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
886
+ CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
887
+ `);
857
888
  }
858
889
  function autoDetectTaskId(planFile) {
859
890
  if (!planFile) return null;
@@ -955,7 +986,7 @@ async function main() {
955
986
  process.stdout.write(
956
987
  `=== MASSU AI: Active ===
957
988
  Session memory, code intelligence, and governance are now active.
958
- 11 hooks monitoring this session. Type "${getConfig().toolPrefix ?? "massu"}_sync" to index your codebase.
989
+ 15 hooks monitoring this session. Type "${getConfig().toolPrefix ?? "massu"}_sync" to index your codebase.
959
990
  === END MASSU ===
960
991
 
961
992
  `
@@ -151,6 +151,18 @@ var AutoLearningConfigSchema = z.object({
151
151
  "added_missing_import"
152
152
  ])
153
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({}),
154
166
  pipeline: z.object({
155
167
  requireIncidentReport: z.boolean().default(true),
156
168
  requirePreventionRule: z.boolean().default(true),
@@ -848,6 +860,25 @@ function initMemorySchema(db) {
848
860
  features TEXT DEFAULT '[]'
849
861
  );
850
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
+ `);
851
882
  }
852
883
  function assignImportance(type, vrResult) {
853
884
  switch (type) {
@@ -921,7 +952,9 @@ function linkSessionToTask(db, sessionId, taskId) {
921
952
  }
922
953
 
923
954
  // src/hooks/user-prompt.ts
924
- import { existsSync as existsSync3 } from "fs";
955
+ import { existsSync as existsSync3, writeFileSync } from "fs";
956
+ import { tmpdir } from "os";
957
+ import { join } from "path";
925
958
  async function main() {
926
959
  try {
927
960
  const input = await readStdin();
@@ -994,6 +1027,35 @@ async function main() {
994
1027
  }
995
1028
  } catch (_memoryNagErr) {
996
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
+ }
997
1059
  } finally {
998
1060
  db.close();
999
1061
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
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",
@@ -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
@@ -346,25 +346,18 @@ type HooksConfig = Record<string, HookGroup[]>;
346
346
  * Handles both local development and npm-installed scenarios.
347
347
  */
348
348
  export function resolveHooksDir(): string {
349
- // Try to find the hooks in node_modules first (installed via npm)
350
- const cwd = process.cwd();
351
- const nodeModulesPath = resolve(cwd, 'node_modules/@massu/core/dist/hooks');
352
- if (existsSync(nodeModulesPath)) {
353
- return 'node_modules/@massu/core/dist/hooks';
354
- }
355
-
356
- // Fall back to finding relative to this source file
357
- const localPath = resolve(__dirname, '../dist/hooks');
358
- if (existsSync(localPath)) {
359
- return localPath;
360
- }
361
-
362
- // Default to node_modules path (will be created on npm install)
349
+ // Always use node_modules/@massu/core/dist/hooks relative to project root.
350
+ // hookCmd() wraps each command with a parent-directory walk to find the
351
+ // project root, so hooks resolve correctly even from subdirectories.
363
352
  return 'node_modules/@massu/core/dist/hooks';
364
353
  }
365
354
 
366
355
  function hookCmd(hooksDir: string, hookFile: string): string {
367
- return `node ${hooksDir}/${hookFile}`;
356
+ // Walk up from cwd to find the directory containing node_modules/@massu/core,
357
+ // then cd there before running the hook. This handles subdirectories like
358
+ // website/, packages/foo/, etc. where node_modules doesn't exist.
359
+ const hookPath = `${hooksDir}/${hookFile}`;
360
+ return `d="$PWD"; while [ "$d" != "/" ] && [ ! -f "$d/${hookPath}" ]; do d="$(dirname "$d")"; done; cd "$d" && node ${hookPath}`;
368
361
  }
369
362
 
370
363
  export function buildHooksConfig(hooksDir: string): HooksConfig {
@@ -402,6 +395,15 @@ export function buildHooksConfig(hooksDir: string): HooksConfig {
402
395
  matcher: 'Edit|Write',
403
396
  hooks: [
404
397
  { type: 'command', command: hookCmd(hooksDir, 'post-edit-context.js'), timeout: 5 },
398
+ { type: 'command', command: hookCmd(hooksDir, 'fix-detector.js'), timeout: 5 },
399
+ { type: 'command', command: hookCmd(hooksDir, 'classify-failure.js'), timeout: 5 },
400
+ ],
401
+ },
402
+ {
403
+ matcher: 'Write',
404
+ hooks: [
405
+ { type: 'command', command: hookCmd(hooksDir, 'incident-pipeline.js'), timeout: 5 },
406
+ { type: 'command', command: hookCmd(hooksDir, 'rule-enforcement-pipeline.js'), timeout: 5 },
405
407
  ],
406
408
  },
407
409
  ],
@@ -409,6 +411,7 @@ export function buildHooksConfig(hooksDir: string): HooksConfig {
409
411
  {
410
412
  hooks: [
411
413
  { type: 'command', command: hookCmd(hooksDir, 'session-end.js'), timeout: 15 },
414
+ { type: 'command', command: hookCmd(hooksDir, 'auto-learning-pipeline.js'), timeout: 10 },
412
415
  ],
413
416
  },
414
417
  ],
package/src/config.ts CHANGED
@@ -168,6 +168,18 @@ const AutoLearningConfigSchema = z.object({
168
168
  'added_missing_import',
169
169
  ]),
170
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({}),
171
183
  pipeline: z.object({
172
184
  requireIncidentReport: z.boolean().default(true),
173
185
  requirePreventionRule: z.boolean().default(true),
@@ -67,14 +67,14 @@ async function main(): Promise<void> {
67
67
  } catch { /* ignore parse errors */ }
68
68
  }
69
69
 
70
- // Source 2: Scan uncommitted git diff for fix patterns
70
+ // Source 2: Scan uncommitted git diff for fix patterns (language-agnostic)
71
71
  let uncommittedFix = false;
72
72
  try {
73
73
  const diff = execSync('git diff --name-only', { cwd: root, timeout: 3000, encoding: 'utf-8' });
74
74
  if (diff.trim()) {
75
75
  const fullDiff = execSync('git diff', { cwd: root, timeout: 5000, encoding: 'utf-8' });
76
- const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|@MainActor|asyncio\.timeout|X-Service-Token|\.save\(|return False)/gm) || []).length;
77
- const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|\.store\(|= nil|error)/gm) || []).length;
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
78
  if (fixPatterns > 3 || removedBroken > 1) {
79
79
  uncommittedFix = true;
80
80
  }