@massu/core 1.10.7 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // Stop Hook: Auto-Learning Pipeline Enforcer
7
- // At session end, checks if bug fixes were applied without
8
- // completing the full incident → rule → enforcement pipeline.
9
- // Outputs mandatory instructions for Claude to follow.
10
- //
11
- // Part of the Auto-Learning Pipeline:
12
- // Fix Detected → [SESSION END CHECK] → Pipeline Instructions
13
- //
14
- // This is the FORCING FUNCTION that ensures no fix goes
15
- // undocumented. Claude cannot end the session without completing
16
- // the pipeline steps.
17
- // ============================================================
18
-
19
- import { execFileSync } from 'child_process';
20
- import { existsSync, readFileSync, unlinkSync, readdirSync, statSync } from 'fs';
21
- import { tmpdir } from 'os';
22
- import { join } from 'path';
23
- import { getProjectRoot, getConfig } from '../config.ts';
24
-
25
- // P-H002 (plan-stage-c-high-batch): bound git-diff reads so monorepos with
26
- // 10MB+ working trees don't trigger Stop-hook timeout. Short-stat first,
27
- // only read full diff body when estimated bytes <= cap. execFileSync argv
28
- // form is defense-in-depth (P-001 pattern).
29
- const MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024; // 2MB; ~25k lines at 80 bytes/line
30
-
31
- interface HookInput {
32
- session_id: string;
33
- transcript_path: string;
34
- cwd: string;
35
- }
36
-
37
- interface FixSignal {
38
- file: string;
39
- signals: string[];
40
- timestamp: string;
41
- }
42
-
43
- function getSessionFlagPath(sessionId: string): string {
44
- return join(tmpdir(), 'massu-auto-learning', `fixes-${sessionId.slice(0, 12)}.jsonl`);
45
- }
46
-
47
- async function main(): Promise<void> {
48
- try {
49
- const input = await readStdin();
50
- const hookInput = JSON.parse(input) as HookInput;
51
- const config = getConfig();
52
-
53
- // Check if auto-learning is enabled
54
- if (config.autoLearning?.enabled === false) {
55
- process.exit(0);
56
- return;
57
- }
58
-
59
- const root = getProjectRoot();
60
- const incidentDir = config.autoLearning?.incidentDir ?? 'docs/incidents';
61
- const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
62
- const autoLearn = config.autoLearning;
63
-
64
- // Source 1: Session fix flags from fix-detector
65
- const flagPath = getSessionFlagPath(hookInput.session_id);
66
- let sessionFixes: FixSignal[] = [];
67
- if (existsSync(flagPath)) {
68
- try {
69
- sessionFixes = readFileSync(flagPath, 'utf-8')
70
- .split('\n')
71
- .filter(Boolean)
72
- .map(line => JSON.parse(line) as FixSignal);
73
- } catch { /* ignore parse errors */ }
74
- }
75
-
76
- // Source 2: Scan uncommitted git diff for fix patterns (language-agnostic).
77
- // Two-stage: (1) name-only to confirm any changes, (2) shortstat to estimate
78
- // bytes, (3) full diff body ONLY if estimate <= MAX_FULL_DIFF_BYTES.
79
- let uncommittedFix = false;
80
- try {
81
- const nameOnly = execFileSync('git', ['diff', '--name-only'], {
82
- cwd: root,
83
- timeout: 3000,
84
- encoding: 'utf-8',
85
- maxBuffer: 1024 * 1024,
86
- });
87
- if (nameOnly.trim()) {
88
- const shortstat = execFileSync('git', ['diff', '--shortstat'], {
89
- cwd: root,
90
- timeout: 2000,
91
- encoding: 'utf-8',
92
- maxBuffer: 64 * 1024,
93
- });
94
- const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? '0', 10);
95
- const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? '0', 10);
96
- const estimatedBytes = (insertions + deletions) * 80; // ~80 bytes/line avg
97
- if (estimatedBytes <= MAX_FULL_DIFF_BYTES) {
98
- const fullDiff = execFileSync('git', ['diff'], {
99
- cwd: root,
100
- timeout: 5000,
101
- encoding: 'utf-8',
102
- maxBuffer: MAX_FULL_DIFF_BYTES,
103
- });
104
- const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
105
- const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
106
- if (fixPatterns > 3 || removedBroken > 1) {
107
- uncommittedFix = true;
108
- }
109
- }
110
- // else: diff exceeds cap — skip pattern scan rather than risk timeout.
111
- // The fix-detector hook fires on per-Edit/Write and already populates
112
- // sessionFixes for the realistic case; full-diff fallback is a safety
113
- // net that we can correctly skip for huge trees.
114
- }
115
- } catch { /* git not available or no changes or buffer overflow */ }
116
-
117
- if (sessionFixes.length === 0 && !uncommittedFix) {
118
- // Clean up flag file
119
- cleanup(flagPath);
120
- process.exit(0);
121
- return;
122
- }
123
-
124
- // Build pipeline instructions
125
- const lines: string[] = [];
126
- lines.push('');
127
- lines.push('============================================================================');
128
- lines.push(' MASSU AUTO-LEARNING PIPELINE — ACTION REQUIRED BEFORE SESSION END');
129
- lines.push('============================================================================');
130
-
131
- if (sessionFixes.length > 0) {
132
- lines.push('');
133
- lines.push(` ${sessionFixes.length} bug fix(es) detected during this session:`);
134
- lines.push('');
135
- // Deduplicate by file
136
- const byFile = new Map<string, string[]>();
137
- for (const fix of sessionFixes) {
138
- const existing = byFile.get(fix.file) ?? [];
139
- existing.push(...fix.signals);
140
- byFile.set(fix.file, [...new Set(existing)]);
141
- }
142
- for (const [file, signals] of byFile) {
143
- lines.push(` - ${file} (${signals.join(', ')})`);
144
- }
145
- }
146
-
147
- if (uncommittedFix) {
148
- lines.push('');
149
- lines.push(' Additional uncommitted fix patterns detected in git diff.');
150
- }
151
-
152
- lines.push('');
153
- lines.push(' Complete these steps before this session ends:');
154
- lines.push('');
155
-
156
- if (autoLearn?.pipeline?.requireIncidentReport !== false) {
157
- lines.push(' STEP 1: INCIDENT REPORT');
158
- lines.push(` For each distinct bug fixed, create: ${incidentDir}/YYYY-MM-DD-<slug>.md`);
159
- lines.push(' Include: Date, Severity, Symptoms, Root Cause, Fix, Files Changed, Prevention Rules');
160
- lines.push('');
161
- }
162
-
163
- if (autoLearn?.pipeline?.requirePreventionRule !== false) {
164
- lines.push(' STEP 2: PREVENTION RULE');
165
- lines.push(` For each incident, create: ${memoryDir}/feedback_<rule_name>.md`);
166
- lines.push(' Include frontmatter (name, description, type: feedback) + Why + How to apply');
167
- lines.push(` Update ${config.autoLearning?.memoryIndexFile ?? 'MEMORY.md'} index`);
168
- lines.push('');
169
- }
170
-
171
- if (autoLearn?.pipeline?.requireEnforcement !== false) {
172
- lines.push(' STEP 3: ENFORCEMENT PLACEMENT');
173
- lines.push(' For each new rule, determine enforcement layer(s):');
174
- lines.push(' a) If statically detectable → add to pattern-feedback hook');
175
- lines.push(' b) If about editing certain files → add to blast-radius hook');
176
- lines.push(' c) If about dangerous commands → add to dangerous-command hook');
177
- lines.push(' d) If critical → add to pre-commit hook');
178
- lines.push(' e) If needs runtime monitoring → create monitoring producer');
179
- lines.push('');
180
- }
181
-
182
- lines.push(' STEP 4: VERIFY');
183
- lines.push(' Test any new enforcement hooks to confirm they detect violations.');
184
- lines.push('');
185
- lines.push('============================================================================');
186
- lines.push('');
187
-
188
- console.log(lines.join('\n'));
189
-
190
- // Clean up flag file
191
- cleanup(flagPath);
192
- } catch {
193
- // Best-effort: never block Claude Code
194
- }
195
- process.exit(0);
196
- }
197
-
198
- function cleanup(flagPath: string): void {
199
- try {
200
- if (existsSync(flagPath)) unlinkSync(flagPath);
201
- // Clean up old flag files (>24h)
202
- const dir = join(tmpdir(), 'massu-auto-learning');
203
- if (existsSync(dir)) {
204
- const now = Date.now();
205
- for (const file of readdirSync(dir)) {
206
- const fullPath = join(dir, file);
207
- try {
208
- const stat = statSync(fullPath);
209
- if (now - stat.mtimeMs > 86400000) {
210
- unlinkSync(fullPath);
211
- }
212
- } catch { /* ignore */ }
213
- }
214
- }
215
- } catch { /* best effort */ }
216
- }
217
-
218
- function readStdin(): Promise<string> {
219
- return new Promise((resolve) => {
220
- let data = '';
221
- process.stdin.setEncoding('utf-8');
222
- process.stdin.on('data', (chunk: string) => { data += chunk; });
223
- process.stdin.on('end', () => resolve(data));
224
- setTimeout(() => resolve(data), 5000);
225
- });
226
- }
227
-
228
- main();
@@ -1,259 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // PostToolUse Hook: Failure Classification
7
- // Classifies bug fixes against known failure patterns before
8
- // demanding the full incident loop. Prevents rule bloat by
9
- // recognizing known patterns vs genuinely novel failures.
10
- //
11
- // Part of the Auto-Learning Pipeline:
12
- // [Fix Detected] → [CLASSIFY] → Incident Report → Rule → Enforcement
13
- //
14
- // Classification:
15
- // SCORE >= known threshold → KNOWN — Reference existing rules, no new deliverables
16
- // SCORE >= similar threshold → SIMILAR — Check existing rules first
17
- // SCORE < similar threshold → NEW — Full incident loop mandatory
18
- //
19
- // Triggers on: Edit|Write to code files
20
- // Must complete in <1000ms.
21
- // ============================================================
22
-
23
- import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
24
- import { tmpdir } from 'os';
25
- import { join, basename } from 'path';
26
- import { getProjectRoot, getConfig } from '../config.ts';
27
- import { getMemoryDb, scoreFailureClasses } from '../memory-db.ts';
28
-
29
- interface HookInput {
30
- session_id: string;
31
- tool_name: string;
32
- tool_input: {
33
- file_path?: string;
34
- old_string?: string;
35
- new_string?: string;
36
- content?: string;
37
- };
38
- }
39
-
40
- // Bug fix detection heuristics
41
- const BUG_FIX_INDICATORS = [
42
- /\b(catch|error|throw|fail|fix|bug|broken|missing|crash|wrong|typo|incorrect)\b/i,
43
- ];
44
-
45
- const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|swift|rs|go|rb|sh)$/;
46
-
47
- function getDedupeMarkerPath(sessionId: string, filePath: string): string {
48
- // One reminder per file per calendar day
49
- const day = new Date().toISOString().slice(0, 10);
50
- const hash = simpleHash(filePath);
51
- return join(tmpdir(), `massu-classify-${day}-${sessionId.slice(0, 8)}-${hash}`);
52
- }
53
-
54
- function simpleHash(str: string): string {
55
- let h = 0;
56
- for (let i = 0; i < str.length; i++) {
57
- h = ((h << 5) - h + str.charCodeAt(i)) | 0;
58
- }
59
- return Math.abs(h).toString(36);
60
- }
61
-
62
- function readFailureContextFiles(): string {
63
- const dir = tmpdir();
64
- let context = '';
65
- try {
66
- const files = readdirSync(dir).filter(f => f.startsWith('massu-failure-context-'));
67
- for (const file of files) {
68
- try {
69
- context += ' ' + readFileSync(join(dir, file), 'utf-8');
70
- } catch { /* ignore */ }
71
- }
72
- } catch { /* ignore */ }
73
- return context.trim();
74
- }
75
-
76
- function cleanupFailureContextFiles(): void {
77
- const dir = tmpdir();
78
- try {
79
- const files = readdirSync(dir).filter(f => f.startsWith('massu-failure-context-'));
80
- for (const file of files) {
81
- try { unlinkSync(join(dir, file)); } catch { /* ignore */ }
82
- }
83
- } catch { /* ignore */ }
84
- }
85
-
86
- async function main(): Promise<void> {
87
- try {
88
- const input = await readStdin();
89
- const hookInput = JSON.parse(input) as HookInput;
90
- const filePath = hookInput.tool_input?.file_path;
91
-
92
- if (!filePath) {
93
- process.exit(0);
94
- return;
95
- }
96
-
97
- const config = getConfig();
98
-
99
- // Check if feature is enabled
100
- if (config.autoLearning?.enabled === false ||
101
- config.autoLearning?.failureClassification?.enabled === false) {
102
- process.exit(0);
103
- return;
104
- }
105
-
106
- // Only check code files
107
- if (!CODE_EXTENSIONS.test(filePath)) {
108
- process.exit(0);
109
- return;
110
- }
111
-
112
- // Skip incident/memory/doc files (those are pipeline output)
113
- const root = getProjectRoot();
114
- const incidentDir = config.autoLearning?.incidentDir ?? 'docs/incidents';
115
- const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
116
- const relPath = filePath.startsWith(root + '/') ? filePath.slice(root.length + 1) : filePath;
117
- if (relPath.startsWith(incidentDir) || relPath.includes(memoryDir) || relPath.includes('MEMORY.md')) {
118
- process.exit(0);
119
- return;
120
- }
121
-
122
- // Detect if this edit looks like a bug fix
123
- const oldString = hookInput.tool_input?.old_string ?? '';
124
- const newString = hookInput.tool_input?.new_string ?? '';
125
- const content = hookInput.tool_input?.content ?? '';
126
- const matchText = `${oldString} ${newString} ${content}`;
127
-
128
- let isBugFix = false;
129
-
130
- // Check for failure context markers (from UserPromptSubmit)
131
- const promptContext = readFailureContextFiles();
132
- if (promptContext) {
133
- isBugFix = true;
134
- }
135
-
136
- // Check for bug fix indicators in the edit text
137
- if (!isBugFix) {
138
- for (const pattern of BUG_FIX_INDICATORS) {
139
- if (pattern.test(matchText)) {
140
- isBugFix = true;
141
- break;
142
- }
143
- }
144
- }
145
-
146
- // Check for column/key name changes (common fix pattern)
147
- if (!isBugFix && oldString && newString) {
148
- const oldKeys = oldString.match(/[a-z_]+:/g)?.sort().join(',');
149
- const newKeys = newString.match(/[a-z_]+:/g)?.sort().join(',');
150
- if (oldKeys && newKeys && oldKeys !== newKeys) {
151
- isBugFix = true;
152
- }
153
- }
154
-
155
- if (!isBugFix) {
156
- process.exit(0);
157
- return;
158
- }
159
-
160
- // De-duplicate: skip if already reminded for this file today
161
- const dedupeMarker = getDedupeMarkerPath(hookInput.session_id, filePath);
162
- if (existsSync(dedupeMarker)) {
163
- process.exit(0);
164
- return;
165
- }
166
-
167
- // Mark that we've classified this file
168
- try {
169
- writeFileSync(dedupeMarker, '1');
170
- } catch { /* ignore */ }
171
-
172
- // Score against failure taxonomy in database
173
- const db = getMemoryDb();
174
- try {
175
- const scoringConfig = config.autoLearning?.failureClassification?.scoring;
176
- const thresholds = config.autoLearning?.failureClassification?.thresholds ?? { known: 5, similar: 3 };
177
- const bestMatch = scoreFailureClasses(db, matchText, filePath, promptContext, scoringConfig);
178
-
179
- if (!bestMatch || bestMatch.score === 0) {
180
- // No taxonomy entries yet, or no match — full enforcement
181
- outputNewPattern(basename(filePath), bestMatch);
182
- } else if (bestMatch.score >= thresholds.known) {
183
- outputKnownPattern(bestMatch);
184
- } else if (bestMatch.score >= thresholds.similar) {
185
- outputSimilarPattern(bestMatch);
186
- } else {
187
- outputNewPattern(basename(filePath), bestMatch);
188
- }
189
- } finally {
190
- db.close();
191
- }
192
-
193
- // Clean up consumed context files to prevent cross-bug contamination
194
- cleanupFailureContextFiles();
195
- } catch {
196
- // Best-effort: never block Claude Code
197
- }
198
- process.exit(0);
199
- }
200
-
201
- function outputKnownPattern(match: FailureClassMatch): void {
202
- const lines: string[] = [];
203
- lines.push('');
204
- lines.push(`[KNOWN PATTERN] ${match.name} (score: ${match.score}, ${match.incidentCount} prior incident(s))`);
205
- if (match.knownMessage) {
206
- lines.push(` ${match.knownMessage}`);
207
- }
208
- if (match.rules.length > 0) {
209
- lines.push(` Covered by: ${match.rules.join(', ')}`);
210
- }
211
- lines.push(' No new rules needed. Reference existing incident if logging.');
212
- console.log(lines.join('\n'));
213
- }
214
-
215
- function outputSimilarPattern(match: FailureClassMatch): void {
216
- const lines: string[] = [];
217
- lines.push('');
218
- lines.push(`[POSSIBLE MATCH] Resembles ${match.name} (score: ${match.score})`);
219
- if (match.rules.length > 0) {
220
- lines.push(` Check if existing rules cover this case: ${match.rules.join(', ')}`);
221
- }
222
- lines.push(' If genuinely new: create incident + prevention rule + enforcement.');
223
- console.log(lines.join('\n'));
224
- }
225
-
226
- function outputNewPattern(fileName: string, match: FailureClassMatch | null): void {
227
- const lines: string[] = [];
228
- lines.push('');
229
- if (match && match.score > 0) {
230
- lines.push(`[NEW PATTERN] No known failure class matches this fix in ${fileName} (best: ${match.name}, score: ${match.score}).`);
231
- } else {
232
- lines.push(`[NEW PATTERN] Bug fix detected in ${fileName} — no failure classes in taxonomy yet.`);
233
- }
234
- lines.push(' Full incident loop required:');
235
- lines.push(' 1. INCIDENT REPORT');
236
- lines.push(' 2. PREVENTION RULE (if new failure pattern)');
237
- lines.push(' 3. ENFORCEMENT (hook or static check)');
238
- console.log(lines.join('\n'));
239
- }
240
-
241
- interface FailureClassMatch {
242
- name: string;
243
- score: number;
244
- incidentCount: number;
245
- rules: string[];
246
- knownMessage: string;
247
- }
248
-
249
- function readStdin(): Promise<string> {
250
- return new Promise((resolve) => {
251
- let data = '';
252
- process.stdin.setEncoding('utf-8');
253
- process.stdin.on('data', (chunk: string) => { data += chunk; });
254
- process.stdin.on('end', () => resolve(data));
255
- setTimeout(() => resolve(data), 3000);
256
- });
257
- }
258
-
259
- main();
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // PostToolUse Hook: Cost Tracker
7
- // Estimates token usage from tool input/output sizes and
8
- // records cost events for per-session cost intelligence.
9
- // Must complete in <500ms.
10
- // ============================================================
11
-
12
- import { getMemoryDb } from '../memory-db.ts';
13
-
14
- interface HookInput {
15
- session_id: string;
16
- transcript_path: string;
17
- cwd: string;
18
- hook_event_name: string;
19
- tool_name: string;
20
- tool_input: Record<string, unknown>;
21
- tool_response: string;
22
- }
23
-
24
- // Approximate: 4 characters per token (industry rule of thumb)
25
- const CHARS_PER_TOKEN = 4;
26
-
27
- function estimateTokens(text: string): number {
28
- return Math.ceil(text.length / CHARS_PER_TOKEN);
29
- }
30
-
31
- async function main(): Promise<void> {
32
- try {
33
- const input = await readStdin();
34
- const hookInput = JSON.parse(input) as HookInput;
35
- const { session_id, tool_name, tool_input, tool_response } = hookInput;
36
-
37
- const inputStr = JSON.stringify(tool_input);
38
- const estimatedInputTokens = estimateTokens(inputStr);
39
- const estimatedOutputTokens = estimateTokens(tool_response ?? '');
40
-
41
- const db = getMemoryDb();
42
- try {
43
- db.prepare(`
44
- INSERT INTO tool_cost_events (session_id, tool_name, estimated_input_tokens, estimated_output_tokens, model)
45
- VALUES (?, ?, ?, ?, ?)
46
- `).run(session_id, tool_name, estimatedInputTokens, estimatedOutputTokens, '');
47
- } finally {
48
- db.close();
49
- }
50
- } catch (_e) {
51
- // Best-effort: never block Claude Code
52
- }
53
- process.exit(0);
54
- }
55
-
56
- function readStdin(): Promise<string> {
57
- return new Promise((resolve) => {
58
- let data = '';
59
- process.stdin.setEncoding('utf-8');
60
- process.stdin.on('data', (chunk: string) => { data += chunk; });
61
- process.stdin.on('end', () => resolve(data));
62
- setTimeout(() => resolve(data), 3000);
63
- });
64
- }
65
-
66
- main();