claude-mem-lite 2.5.3 → 2.9.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.3.2",
13
+ "version": "2.9.1",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.5.3",
3
+ "version": "2.9.1",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/.mcp.json CHANGED
File without changes
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
File without changes
package/README.zh-CN.md CHANGED
File without changes
package/commands/mem.md CHANGED
File without changes
File without changes
package/commands/tools.md CHANGED
File without changes
File without changes
@@ -2,7 +2,7 @@
2
2
  // Runs at Stop hook to track adoption and outcomes of recommendations
3
3
 
4
4
  import { getSessionInvocations, updateInvocation, updateResourceStats } from './registry.mjs';
5
- import { debugCatch, EDIT_TOOLS } from './utils.mjs';
5
+ import { debugCatch, debugLog, EDIT_TOOLS } from './utils.mjs';
6
6
 
7
7
  // ─── Adoption Detection ──────────────────────────────────────────────────────
8
8
 
@@ -60,26 +60,80 @@ function detectSkillAdoption(resourceName, invocationName, skillInput) {
60
60
  }
61
61
 
62
62
  /**
63
- * Check if a recommended resource was adopted in the session events.
63
+ * Check if event timestamp is within the behavioral detection window.
64
+ * @param {number} eventTs Event timestamp (ms)
65
+ * @param {number} recTime Recommendation time (ms)
66
+ * @param {number} windowMs Window size (default 10 minutes)
67
+ * @returns {boolean}
68
+ */
69
+ const BEHAVIORAL_WINDOW_MS = 600000; // 10 minutes (widened from 2 min for methodology skills)
70
+
71
+ function isWithinWindow(eventTs, recTime, windowMs = BEHAVIORAL_WINDOW_MS) {
72
+ if (recTime === null || recTime === undefined || eventTs === null || eventTs === undefined) return true;
73
+ const delta = eventTs - recTime;
74
+ return delta >= 0 && delta <= windowMs;
75
+ }
76
+
77
+ /**
78
+ * Detect TDD pattern: Bash(test fail) → Edit → Bash(test pass)
79
+ */
80
+ function detectTDDPattern(events, recTime) {
81
+ let testFailed = false, edited = false;
82
+ for (const e of events) {
83
+ if (!isWithinWindow(e.timestamp, recTime)) continue;
84
+ const cmd = (e.tool_input?.command || '').toLowerCase();
85
+ const resp = e.tool_response || '';
86
+ if (e.tool_name === 'Bash' && /test|jest|vitest|pytest|mocha/i.test(cmd) && /fail|error|FAIL/i.test(resp)) {
87
+ testFailed = true;
88
+ }
89
+ if (testFailed && EDIT_TOOLS.has(e.tool_name)) edited = true;
90
+ if (edited && e.tool_name === 'Bash' && /test|jest|vitest|pytest|mocha/i.test(cmd) && /pass|passed|✓|\bok\b/i.test(resp) && !/fail|error/i.test(resp)) {
91
+ return true;
92
+ }
93
+ }
94
+ return false;
95
+ }
96
+
97
+ /**
98
+ * Detect verification pattern: successful test/lint/build near session end
99
+ */
100
+ function detectVerificationPattern(events, recTime) {
101
+ const lastEvents = events.slice(-5);
102
+ return lastEvents.some(e => {
103
+ if (!isWithinWindow(e.timestamp, recTime)) return false;
104
+ if (e.tool_name !== 'Bash') return false;
105
+ const cmd = (e.tool_input?.command || '').toLowerCase();
106
+ const resp = e.tool_response || '';
107
+ const isVerifyCmd = /\b(test|lint|eslint|build|tsc|typecheck|vitest|jest)\b/.test(cmd);
108
+ const isSuccess = resp.length > 0 && !/error|fail|exception/i.test(resp);
109
+ return isVerifyCmd && isSuccess;
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Multi-tier adoption detection for recommended resources.
115
+ * Returns { adopted: boolean, score: number } where score indicates confidence:
116
+ * 1.0 = explicit (Skill/Agent tool invocation)
117
+ * 0.5 = behavioral (detected methodology pattern: TDD, debug, review)
118
+ * 0.2 = inferred (verification pattern near session end)
119
+ *
64
120
  * @param {object} invocation Invocation record with resource info
65
121
  * @param {object[]} sessionEvents Array of tool events from the session
66
- * @returns {boolean} true if the resource was used
122
+ * @returns {{ adopted: boolean, score: number }}
67
123
  */
68
124
  function detectAdoption(invocation, sessionEvents) {
69
- if (!sessionEvents || sessionEvents.length === 0) return false;
125
+ if (!sessionEvents || sessionEvents.length === 0) return { adopted: false, score: 0 };
70
126
 
71
127
  const { resource_name, resource_type, invocation_name } = invocation;
72
128
 
129
+ // Tier 1: Explicit adoption — Skill/Agent tool invocation (strongest signal)
73
130
  for (const event of sessionEvents) {
74
- // Skill adoption: Claude used the Skill tool with matching name
75
131
  if (resource_type === 'skill' && event.tool_name === 'Skill') {
76
132
  if (detectSkillAdoption(resource_name, invocation_name || '', event.tool_input?.skill)) {
77
- return true;
133
+ return { adopted: true, score: 1.0 };
78
134
  }
79
135
  }
80
136
 
81
- // Agent adoption: Claude used Agent tool with matching agent type/description
82
- // Normalizes hyphens/colons to spaces for comparison (e.g. "code-review-ai" ↔ "code review ai")
83
137
  if (resource_type === 'agent' && event.tool_name === 'Agent') {
84
138
  const desc = (event.tool_input?.description || '').toLowerCase();
85
139
  const prompt = (event.tool_input?.prompt || '').toLowerCase();
@@ -95,36 +149,59 @@ function detectAdoption(invocation, sessionEvents) {
95
149
  subTypeCompact === nameCompact ||
96
150
  subType.includes(nameNorm) || subType.includes(nameCompact) ||
97
151
  subTypeNorm.includes(nameNorm)) {
98
- return true;
152
+ return { adopted: true, score: 1.0 };
99
153
  }
100
154
  }
101
155
  }
102
156
 
103
- // Behavioral adoption: detect usage patterns matching the recommended resource
157
+ // Tier 2: Behavioral adoption methodology patterns (10 min window)
104
158
  const resourceLower = resource_name.toLowerCase();
159
+ const recTime = invocation.created_at ? new Date(invocation.created_at).getTime() : 0;
105
160
 
106
- // Debugging pattern: Read→Bash(error)→Read→Edit cycle
161
+ // TDD pattern: Bash(test fail) Edit → Bash(test pass)
162
+ if (resourceLower.includes('tdd') || resourceLower.includes('test-driven')) {
163
+ if (detectTDDPattern(sessionEvents, recTime)) {
164
+ return { adopted: true, score: 0.5 };
165
+ }
166
+ }
167
+
168
+ // Debugging pattern: Read → Bash(error) → Edit cycle
107
169
  if (resourceLower.includes('debug') || resourceLower.includes('troubleshoot')) {
108
- let hasRead = false, hasBashError = false, hasEditAfterError = false;
109
- for (const e of sessionEvents) {
110
- if (e.tool_name === 'Read') hasRead = true;
111
- if (e.tool_name === 'Bash' && /error|fail|exception/i.test(e.tool_response || '')) hasBashError = true;
112
- if (hasBashError && EDIT_TOOLS.has(e.tool_name)) hasEditAfterError = true;
170
+ const firstRelevant = sessionEvents.find(e =>
171
+ e.tool_name === 'Read' ||
172
+ (e.tool_name === 'Bash' && /error|fail|exception/i.test(e.tool_response || ''))
173
+ );
174
+ if (isWithinWindow(firstRelevant?.timestamp, recTime)) {
175
+ let hasRead = false, hasBashError = false, hasEditAfterError = false;
176
+ for (const e of sessionEvents) {
177
+ if (e.tool_name === 'Read') hasRead = true;
178
+ if (e.tool_name === 'Bash' && /error|fail|exception/i.test(e.tool_response || '')) hasBashError = true;
179
+ if (hasBashError && EDIT_TOOLS.has(e.tool_name)) hasEditAfterError = true;
180
+ }
181
+ if (hasRead && hasBashError && hasEditAfterError) {
182
+ return { adopted: true, score: 0.5 };
183
+ }
113
184
  }
114
- if (hasRead && hasBashError && hasEditAfterError) return true;
115
185
  }
116
186
 
117
187
  // Code review pattern: Agent with 'review' in prompt/description
118
188
  if (resourceLower.includes('review')) {
119
189
  for (const e of sessionEvents) {
120
- if (e.tool_name === 'Agent') {
190
+ if (e.tool_name === 'Agent' && isWithinWindow(e.timestamp, recTime)) {
121
191
  const text = ((e.tool_input?.prompt || '') + (e.tool_input?.description || '')).toLowerCase();
122
- if (text.includes('review')) return true;
192
+ if (text.includes('review')) return { adopted: true, score: 0.5 };
123
193
  }
124
194
  }
125
195
  }
126
196
 
127
- return false;
197
+ // Tier 3: Inferred adoption — verification near session end
198
+ if (resourceLower.includes('verif') || resourceLower.includes('quality') || resourceLower.includes('check')) {
199
+ if (detectVerificationPattern(sessionEvents, recTime)) {
200
+ return { adopted: true, score: 0.2 };
201
+ }
202
+ }
203
+
204
+ return { adopted: false, score: 0 };
128
205
  }
129
206
 
130
207
  // ─── Outcome Detection ───────────────────────────────────────────────────────
@@ -175,7 +252,7 @@ function detectOutcome(sessionEvents) {
175
252
  * @returns {string} Rejection reason
176
253
  */
177
254
  function classifyRejection(invocation, sessionEvents) {
178
- if (!sessionEvents || sessionEvents.length === 0) return 'session_end';
255
+ if (!sessionEvents || sessionEvents.length === 0) return 'no_events';
179
256
 
180
257
  const recTime = new Date(invocation.created_at).getTime();
181
258
  const afterEvents = sessionEvents.filter(e =>
@@ -230,10 +307,12 @@ export async function collectFeedback(db, sessionId, sessionEvents = []) {
230
307
  // Skip if already collected (prevents double-collection from stop + session-start)
231
308
  if (inv.outcome) continue;
232
309
 
233
- const adopted = detectAdoption(inv, sessionEvents);
310
+ const { adopted, score: adoptScore } = detectAdoption(inv, sessionEvents);
234
311
  const outcome = adopted ? detectOutcome(sessionEvents) : 'ignored';
235
- const score = adopted ? (outcome === 'success' ? 1.0 : outcome === 'partial' ? 0.5 : 0.2) : 0;
236
- const rejection_reason = adopted ? null : classifyRejection(inv, sessionEvents);
312
+ // Combine adoption confidence with outcome quality
313
+ const outcomeMultiplier = outcome === 'success' ? 1.0 : outcome === 'partial' ? 0.7 : 0.3;
314
+ const score = adopted ? adoptScore * outcomeMultiplier : 0;
315
+ const rejection_reason = adopted ? null : (classifyRejection(inv, sessionEvents) || 'unclassified');
237
316
 
238
317
  // Update invocation record
239
318
  updateInvocation(db, inv.id, {
@@ -251,10 +330,36 @@ export async function collectFeedback(db, sessionId, sessionEvents = []) {
251
330
  }
252
331
  }
253
332
  }
333
+
334
+ // Auto-demote zombie resources: >10 recommendations with 0 adoptions → on_request mode
335
+ // This prevents chronic false positives from continuing to waste recommendation slots
336
+ autodemoteZombies(db);
254
337
  } catch (e) {
255
338
  debugCatch(e, 'collectFeedback');
256
339
  }
257
340
  }
258
341
 
342
+ /**
343
+ * Auto-demote resources with high recommendation count and zero adoption to on_request mode.
344
+ * Zombie threshold: recommend_count > 10 AND adopt_count = 0.
345
+ * Only demotes resources currently in 'proactive' mode.
346
+ */
347
+ function autodemoteZombies(db) {
348
+ try {
349
+ const demoted = db.prepare(`
350
+ UPDATE resources SET recommendation_mode = 'on_request', updated_at = datetime('now')
351
+ WHERE COALESCE(recommend_count, 0) > 10
352
+ AND COALESCE(adopt_count, 0) = 0
353
+ AND COALESCE(recommendation_mode, 'proactive') = 'proactive'
354
+ AND status = 'active'
355
+ `).run();
356
+ if (demoted.changes > 0) {
357
+ debugLog('INFO', 'feedback', `auto-demoted ${demoted.changes} zombie resources to on_request`);
358
+ }
359
+ } catch (e) {
360
+ debugCatch(e, 'autodemoteZombies');
361
+ }
362
+ }
363
+
259
364
  // Test exports
260
365
  export { detectAdoption as _detectAdoption };
@@ -41,38 +41,54 @@ function isNativeSkill(name) {
41
41
 
42
42
  // ─── Injection Templates ─────────────────────────────────────────────────────
43
43
 
44
+ /**
45
+ * Build the lead line: reason if available, otherwise capability summary.
46
+ */
47
+ function leadLine(resource, reason) {
48
+ return reason || truncate(resource.capability_summary, 120);
49
+ }
50
+
44
51
  /**
45
52
  * Invocable skill template -- tells Claude to invoke via Skill tool.
46
53
  * Used when the resource has an invocation_name (registered as a Claude Code skill/plugin).
47
54
  * @param {object} resource Resource object from DB
55
+ * @param {string} [reason] Why this was recommended
48
56
  * @returns {string} Injection text instructing Skill tool invocation
49
57
  */
50
- function injectSkillInvocable(resource) {
51
- return `[Auto-suggestion] A relevant skill is available for this task. ` +
52
- `Invoke it now: use the Skill tool with skill="${resource.invocation_name}". ` +
53
- `Capability: ${truncate(resource.capability_summary, 100)}`;
58
+ function injectSkillInvocable(resource, reason) {
59
+ const lines = [`[Recommended] ${leadLine(resource, reason)}`];
60
+ lines.push(`→ Invoke: Skill tool with skill="${resource.invocation_name}"`);
61
+ if (reason && resource.capability_summary) {
62
+ lines.push(`Capability: ${truncate(resource.capability_summary, 100)}`);
63
+ }
64
+ return lines.join('\n');
54
65
  }
55
66
 
56
67
  /**
57
68
  * Native skill template -- tells Claude to use the skill command.
58
69
  * Used when skill exists in ~/.claude/skills/ but has no invocation_name.
59
70
  * @param {object} resource Resource object from DB
71
+ * @param {string} [reason] Why this was recommended
60
72
  * @returns {string} Injection text referencing the native skill command
61
73
  */
62
- function injectSkillNative(resource) {
63
- return `[Auto-suggestion] A relevant skill "${resource.name}" is available for this task. ` +
64
- `Use: /skill ${resource.name}. ` +
65
- `Capability: ${truncate(resource.capability_summary, 100)}`;
74
+ function injectSkillNative(resource, reason) {
75
+ const lines = [`[Recommended] ${leadLine(resource, reason)}`];
76
+ lines.push(`→ Use: /skill ${resource.name}`);
77
+ if (reason && resource.capability_summary) {
78
+ lines.push(`Capability: ${truncate(resource.capability_summary, 100)}`);
79
+ }
80
+ return lines.join('\n');
66
81
  }
67
82
 
68
83
  /**
69
84
  * Managed skill template -- includes content for Claude to use directly.
70
85
  * Used when skill is in managed/ directory (not installed natively).
71
86
  * @param {object} resource Resource object from DB
87
+ * @param {string} [reason] Why this was recommended
72
88
  * @returns {string} Injection text with embedded skill content
73
89
  */
74
- function injectSkillManaged(resource) {
75
- if (!isAllowedPath(resource.local_path)) return injectSkillNative(resource);
90
+ function injectSkillManaged(resource, reason) {
91
+ if (!isAllowedPath(resource.local_path)) return injectSkillNative(resource, reason);
76
92
  let content = '';
77
93
  try {
78
94
  content = readFileSync(resource.local_path, 'utf8');
@@ -89,23 +105,28 @@ function injectSkillManaged(resource) {
89
105
 
90
106
  const truncatedContent = truncateContent(content, MAX_INJECTION_CHARS - 300);
91
107
 
92
- return `[Auto-suggestion] Recommended skill for this task: "${resource.name}"
93
- Capability: ${truncate(resource.capability_summary, 100)}
94
- <skill-content>
95
- ${truncatedContent}
96
- </skill-content>`;
108
+ const lines = [`[Recommended] "${resource.name}" — ${truncate(resource.capability_summary, 100)}`];
109
+ if (reason) lines.push(`Why: ${reason}`);
110
+ lines.push('<skill-content>');
111
+ lines.push(truncatedContent);
112
+ lines.push('</skill-content>');
113
+ return lines.join('\n');
97
114
  }
98
115
 
99
116
  /**
100
117
  * Agent template -- guides Claude to use Agent tool with the agent definition.
101
118
  * @param {object} resource Resource object from DB
119
+ * @param {string} [reason] Why this was recommended
102
120
  * @returns {string} Injection text with agent definition for Agent tool delegation
103
121
  */
104
- function injectAgent(resource) {
122
+ function injectAgent(resource, reason) {
105
123
  if (!isAllowedPath(resource.local_path)) {
106
- return `[Auto-suggestion] A specialized agent "${resource.name}" is recommended for this task. ` +
107
- `Capability: ${truncate(resource.capability_summary, 100)}. ` +
108
- `Use the Agent tool to delegate this work.`;
124
+ const lines = [`[Recommended] ${leadLine(resource, reason)}`];
125
+ lines.push(`→ Use Agent tool to delegate: "${resource.name}"`);
126
+ if (reason && resource.capability_summary) {
127
+ lines.push(`Capability: ${truncate(resource.capability_summary, 100)}`);
128
+ }
129
+ return lines.join('\n');
109
130
  }
110
131
  let agentDef = '';
111
132
  try {
@@ -122,17 +143,21 @@ function injectAgent(resource) {
122
143
 
123
144
  if (agentDef) {
124
145
  const truncatedDef = truncateContent(agentDef, MAX_INJECTION_CHARS - 300);
125
- return `[Auto-suggestion] A specialized agent "${resource.name}" is recommended for this task.
126
- Capability: ${truncate(resource.capability_summary, 100)}
127
- Use the Agent tool with this agent definition:
128
- <agent-definition>
129
- ${truncatedDef}
130
- </agent-definition>`;
146
+ const lines = [`[Recommended] "${resource.name}" ${truncate(resource.capability_summary, 100)}`];
147
+ if (reason) lines.push(`Why: ${reason}`);
148
+ lines.push('Use the Agent tool with this agent definition:');
149
+ lines.push('<agent-definition>');
150
+ lines.push(truncatedDef);
151
+ lines.push('</agent-definition>');
152
+ return lines.join('\n');
131
153
  }
132
154
 
133
- return `[Auto-suggestion] A specialized agent "${resource.name}" is recommended for this task. ` +
134
- `Capability: ${truncate(resource.capability_summary, 100)}. ` +
135
- `Use the Agent tool to delegate this work.`;
155
+ const lines = [`[Recommended] ${leadLine(resource, reason)}`];
156
+ lines.push(`→ Use Agent tool to delegate: "${resource.name}"`);
157
+ if (reason && resource.capability_summary) {
158
+ lines.push(`Capability: ${truncate(resource.capability_summary, 100)}`);
159
+ }
160
+ return lines.join('\n');
136
161
  }
137
162
 
138
163
  // ─── Main Render ─────────────────────────────────────────────────────────────
@@ -153,18 +178,16 @@ export function renderInjection(resource, reason) {
153
178
  // Priority: if invocation_name is set, the skill is a registered Claude Code skill/plugin
154
179
  // → instruct Claude to invoke via Skill tool (enables adoption tracking)
155
180
  if (resource.invocation_name) {
156
- injection = injectSkillInvocable(resource);
181
+ injection = injectSkillInvocable(resource, reason);
157
182
  } else if (isNativeSkill(resource.name)) {
158
- injection = injectSkillNative(resource);
183
+ injection = injectSkillNative(resource, reason);
159
184
  } else {
160
- injection = injectSkillManaged(resource);
185
+ injection = injectSkillManaged(resource, reason);
161
186
  }
162
187
  } else {
163
- injection = injectAgent(resource);
188
+ injection = injectAgent(resource, reason);
164
189
  }
165
190
 
166
- if (reason) injection += `\nReason: ${reason}`;
167
-
168
191
  // Hard limit enforcement
169
192
  if (injection.length > MAX_INJECTION_CHARS) {
170
193
  injection = injection.slice(0, MAX_INJECTION_CHARS - 3) + '...';
@@ -172,3 +195,19 @@ export function renderInjection(resource, reason) {
172
195
 
173
196
  return injection;
174
197
  }
198
+
199
+ /**
200
+ * Render a lightweight one-line hint for medium-confidence recommendations.
201
+ * Costs ~30 tokens instead of ~500 for full injection.
202
+ * @param {object} resource Resource object from DB
203
+ * @returns {string} Single-line hint text
204
+ */
205
+ export function renderHint(resource) {
206
+ const cap = truncate(resource.capability_summary || '', 80);
207
+ const invoke = resource.invocation_name
208
+ ? ` (Skill: "${resource.invocation_name}")`
209
+ : resource.type === 'agent'
210
+ ? ` (Agent: "${resource.name}")`
211
+ : '';
212
+ return `[Hint] Consider: "${resource.name}" — ${cap}${invoke}`;
213
+ }
@@ -0,0 +1,173 @@
1
+ // claude-mem-lite: Failure pattern detection for pain-point dispatch
2
+ // Detects when Claude is struggling (repeated test failures, same errors, blind editing)
3
+ // and signals the dispatch system to recommend resources at the right moment.
4
+
5
+ import { EDIT_TOOLS } from './utils.mjs';
6
+
7
+ // ─── Constants ───────────────────────────────────────────────────────────────
8
+
9
+ const WINDOW_SIZE = 20;
10
+
11
+ const TEST_CMD_RE = /\b(test|jest|vitest|pytest|mocha|cypress|playwright|cargo\s+test|go\s+test)\b/i;
12
+ const TEST_FAIL_RE = /\bfail|error|FAIL|Error|panic|exception/i;
13
+ const ERROR_CLASS_RE = /\b(type\s*error|syntax\s*error|reference\s*error|module\s*not\s*found|TS\d{4}|E\d{4}|error\s+\w+:)/i;
14
+ const VERIFY_CMD_RE = /\b(test|jest|vitest|pytest|lint|eslint|tsc|typecheck|build)\b/i;
15
+
16
+ // Read-only tools that don't break a blind-editing streak
17
+ const PASSIVE_TOOLS = new Set(['Read', 'Glob', 'Grep']);
18
+
19
+ // ─── Pattern Detection ───────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Detect failure patterns in recent session events.
23
+ * Analyzes a sliding window of the last 20 events to identify when Claude
24
+ * is struggling with repeated failures or blind editing.
25
+ *
26
+ * @param {Array<{tool_name: string, tool_input?: object, tool_response?: string}>} events
27
+ * Array of session events (tool invocations with name, input, and response)
28
+ * @returns {{ pattern: string, resource_intent: string, confidence: number } | null}
29
+ * Detected pattern with recommended resource intent and confidence, or null
30
+ */
31
+ export function detectFailurePattern(events) {
32
+ if (!events || events.length === 0) return null;
33
+
34
+ // Only consider the last WINDOW_SIZE events
35
+ const window = events.slice(-WINDOW_SIZE);
36
+
37
+ // Check patterns in priority order
38
+ return detectRepeatedTestFail(window)
39
+ || detectRepeatedBashError(window)
40
+ || detectBlindEditing(window);
41
+ }
42
+
43
+ // ─── Pattern: repeated-test-fail ─────────────────────────────────────────────
44
+
45
+ /**
46
+ * Detect (Edit → Bash[test fail]) cycles appearing 2+ times.
47
+ * An edit tool followed by a Bash command that runs tests and fails.
48
+ */
49
+ function detectRepeatedTestFail(window) {
50
+ let cycles = 0;
51
+ let sawEdit = false;
52
+
53
+ for (const event of window) {
54
+ const tool = event.tool_name;
55
+
56
+ if (EDIT_TOOLS.has(tool)) {
57
+ sawEdit = true;
58
+ continue;
59
+ }
60
+
61
+ if (sawEdit && tool === 'Bash') {
62
+ const cmd = event.tool_input?.command || '';
63
+ const resp = event.tool_response || '';
64
+ if (TEST_CMD_RE.test(cmd) && TEST_FAIL_RE.test(resp)) {
65
+ cycles++;
66
+ sawEdit = false;
67
+ continue;
68
+ }
69
+ }
70
+
71
+ // Non-edit, non-matching-bash resets the edit flag
72
+ // (but keep counting if we see more edits later)
73
+ if (!EDIT_TOOLS.has(tool)) {
74
+ sawEdit = false;
75
+ }
76
+ }
77
+
78
+ if (cycles < 2) return null;
79
+
80
+ return {
81
+ pattern: 'repeated-test-fail',
82
+ resource_intent: 'fix',
83
+ confidence: 0.7 + Math.min(cycles - 2, 3) * 0.1,
84
+ };
85
+ }
86
+
87
+ // ─── Pattern: repeated-bash-error ────────────────────────────────────────────
88
+
89
+ /**
90
+ * Detect 3+ Bash errors (test failures or compilation errors) in the window.
91
+ */
92
+ function detectRepeatedBashError(window) {
93
+ let errorCount = 0;
94
+
95
+ for (const event of window) {
96
+ if (event.tool_name !== 'Bash') continue;
97
+ const resp = event.tool_response || '';
98
+ if (TEST_FAIL_RE.test(resp) || ERROR_CLASS_RE.test(resp)) {
99
+ errorCount++;
100
+ }
101
+ }
102
+
103
+ if (errorCount < 3) return null;
104
+
105
+ return {
106
+ pattern: 'repeated-bash-error',
107
+ resource_intent: 'fix',
108
+ confidence: 0.6 + Math.min(errorCount - 3, 4) * 0.1,
109
+ };
110
+ }
111
+
112
+ // ─── Pattern: blind-editing ──────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Detect 5+ consecutive edits to the same file without any test/lint/build verification.
116
+ * Read/Grep/Glob are passive and don't break the streak; other tools do.
117
+ * Bash with a verify command (test/lint/tsc/build) breaks the streak.
118
+ */
119
+ function detectBlindEditing(window) {
120
+ let streak = 0;
121
+ let targetFile = null;
122
+
123
+ for (const event of window) {
124
+ const tool = event.tool_name;
125
+
126
+ // Edit tools extend the streak
127
+ if (EDIT_TOOLS.has(tool)) {
128
+ const file = event.tool_input?.file_path || '';
129
+ if (!targetFile) {
130
+ targetFile = file;
131
+ streak = 1;
132
+ } else if (file === targetFile) {
133
+ streak++;
134
+ } else {
135
+ // Different file — restart streak
136
+ targetFile = file;
137
+ streak = 1;
138
+ }
139
+ continue;
140
+ }
141
+
142
+ // Passive tools don't break the streak
143
+ if (PASSIVE_TOOLS.has(tool)) {
144
+ continue;
145
+ }
146
+
147
+ // Bash with verify command breaks the streak
148
+ if (tool === 'Bash') {
149
+ const cmd = event.tool_input?.command || '';
150
+ if (VERIFY_CMD_RE.test(cmd)) {
151
+ streak = 0;
152
+ targetFile = null;
153
+ continue;
154
+ }
155
+ // Non-verify Bash breaks the streak too
156
+ streak = 0;
157
+ targetFile = null;
158
+ continue;
159
+ }
160
+
161
+ // Any other tool breaks the streak
162
+ streak = 0;
163
+ targetFile = null;
164
+ }
165
+
166
+ if (streak < 5) return null;
167
+
168
+ return {
169
+ pattern: 'blind-editing',
170
+ resource_intent: 'test',
171
+ confidence: 0.5 + Math.min(streak - 5, 5) * 0.1,
172
+ };
173
+ }
File without changes