claude-mem-lite 2.5.4 → 2.9.2
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +0 -0
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/README.zh-CN.md +0 -0
- package/commands/mem.md +0 -0
- package/commands/memory.md +0 -0
- package/commands/tools.md +0 -0
- package/commands/update.md +0 -0
- package/dispatch-feedback.mjs +129 -24
- package/dispatch-inject.mjs +73 -34
- package/dispatch-patterns.mjs +173 -0
- package/dispatch-workflow.mjs +0 -0
- package/dispatch.mjs +359 -271
- package/haiku-client.mjs +0 -0
- package/hook-context.mjs +24 -6
- package/hook-episode.mjs +2 -2
- package/hook-handoff.mjs +38 -18
- package/hook-llm.mjs +98 -21
- package/hook-memory.mjs +47 -15
- package/hook-semaphore.mjs +0 -0
- package/hook-shared.mjs +21 -0
- package/hook-update.mjs +262 -0
- package/hook.mjs +165 -28
- package/hooks/hooks.json +0 -0
- package/install.mjs +149 -4
- package/package.json +3 -1
- package/registry/preinstalled.json +13 -0
- package/registry-indexer.mjs +0 -0
- package/registry-retriever.mjs +13 -8
- package/registry-scanner.mjs +0 -0
- package/registry.mjs +15 -7
- package/resource-discovery.mjs +0 -0
- package/schema.mjs +0 -0
- package/scripts/launch.mjs +0 -0
- package/server-internals.mjs +0 -0
- package/server.mjs +58 -13
- package/skill.md +0 -0
- package/tool-schemas.mjs +41 -16
- package/utils.mjs +87 -30
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
|
package/commands/memory.md
CHANGED
|
File without changes
|
package/commands/tools.md
CHANGED
|
File without changes
|
package/commands/update.md
CHANGED
|
File without changes
|
package/dispatch-feedback.mjs
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
236
|
-
const
|
|
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 };
|
package/dispatch-inject.mjs
CHANGED
|
@@ -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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
<skill-content>
|
|
95
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
Use the Agent tool with this agent definition:
|
|
128
|
-
<agent-definition>
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
package/dispatch-workflow.mjs
CHANGED
|
File without changes
|