@cachly-dev/mcp-server 0.5.5 → 0.5.7

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/dist/index.js CHANGED
@@ -62,6 +62,39 @@ const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
62
62
  * └──────────────────────────────────────────────────────────────────────┘
63
63
  */
64
64
  const EMBED_PROVIDER = (process.env.CACHLY_EMBED_PROVIDER ?? detectEmbedProvider()).toLowerCase();
65
+ // ── Confidence Decay Config (all values configurable via env) ─────────────────
66
+ const CONFIDENCE_WARN_DAYS = Number(process.env.CACHLY_CONFIDENCE_WARN_DAYS ?? 5); // → 0.7
67
+ const CONFIDENCE_STALE_DAYS = Number(process.env.CACHLY_CONFIDENCE_STALE_DAYS ?? 10); // → 0.5
68
+ const CONFIDENCE_WARN_VALUE = 0.7;
69
+ const CONFIDENCE_STALE_VALUE = 0.5;
70
+ /** Calculate current confidence for a lesson based on how long since last verified. */
71
+ function calculateConfidence(lesson) {
72
+ const ref = lesson.verified_at ?? lesson.ts;
73
+ const ageMs = Date.now() - new Date(ref).getTime();
74
+ const ageDays = ageMs / 86400000;
75
+ if (ageDays >= CONFIDENCE_STALE_DAYS)
76
+ return CONFIDENCE_STALE_VALUE;
77
+ if (ageDays >= CONFIDENCE_WARN_DAYS)
78
+ return CONFIDENCE_WARN_VALUE;
79
+ // Linear interpolation between fresh (1.0) and warn threshold
80
+ return 1.0 - (ageDays / CONFIDENCE_WARN_DAYS) * (1.0 - CONFIDENCE_WARN_VALUE);
81
+ }
82
+ /** Render a confidence badge string. */
83
+ function confidenceBadge(confidence, ageDays) {
84
+ if (confidence >= 0.9)
85
+ return '✅';
86
+ if (confidence >= 0.7)
87
+ return `⚠️ (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — verify before applying)`;
88
+ return `🔴 STALE (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — likely outdated!)`;
89
+ }
90
+ /** Category-specific required fields for structured lessons. */
91
+ const STRUCTURED_TEMPLATES = {
92
+ 'deploy': { required: ['commands'], hint: 'deploy:* needs commands[]' },
93
+ 'bash': { required: ['commands'], hint: 'bash:* needs commands[]' },
94
+ 'infra': { required: ['commands'], hint: 'infra:* needs commands[] + verified on real system' },
95
+ 'pricing': { required: [], hint: 'pricing:* — add context with source (e.g. "Stripe Dashboard Apr 2026")' },
96
+ 'stripe': { required: [], hint: 'stripe:* — add context with Stripe API version' },
97
+ };
65
98
  /** Fast content hash for index invalidation (not cryptographic, just change detection) */
66
99
  function simpleHash(text) {
67
100
  return createHash('md5').update(text).digest('hex').slice(0, 12);
@@ -1142,7 +1175,9 @@ const TOOLS = [
1142
1175
  'Fields: topic (short slug like "deploy:web"), outcome ("success"|"failure"), ' +
1143
1176
  'what_worked (what solved it), what_failed (what did NOT work), context (extra details). ' +
1144
1177
  'Supports structured metadata: severity, file_paths (files involved), commands (working commands), tags. ' +
1145
- 'Deduplication: if a lesson for this topic already exists, it is updated instead of duplicated. ' +
1178
+ 'Deduplication: if a lesson for this topic already exists, it is updated with full audit trail. ' +
1179
+ 'Contradiction detection: warns if new outcome conflicts with existing lesson outcome. ' +
1180
+ 'Confidence: lesson starts at 1.0, decays after 5d (→0.7) and 10d (→0.5) without recall. ' +
1146
1181
  'Example: learn_from_attempts(topic="deploy:api", outcome="success", what_worked="nohup docker compose up -d --build", what_failed="docker compose up hangs on SSH timeout", severity="critical", commands=["nohup docker compose up -d --build"])',
1147
1182
  inputSchema: {
1148
1183
  type: 'object',
@@ -1181,7 +1216,9 @@ const TOOLS = [
1181
1216
  name: 'recall_best_solution',
1182
1217
  description: 'Recall the best known solution for a topic from past lessons. ' +
1183
1218
  'Call this BEFORE attempting any task that might have been done before. ' +
1184
- 'Returns the most recent successful lesson for the topic, or a summary of attempts. ' +
1219
+ 'Returns the most recent successful lesson for the topic, with confidence indicator. ' +
1220
+ '⚠️ badge = lesson is >5d old (verify before applying). 🔴 = >10d old (likely stale!). ' +
1221
+ 'Recalling a lesson resets its confidence clock to 1.0 (marks as recently verified). ' +
1185
1222
  'Example: recall_best_solution(topic="deploy:web") → returns the working deploy command.',
1186
1223
  inputSchema: {
1187
1224
  type: 'object',
@@ -2219,21 +2256,50 @@ async function handleTool(name, args) {
2219
2256
  const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], } = args;
2220
2257
  const redis = await getConnection(instance_id);
2221
2258
  const ts = new Date().toISOString();
2222
- // ── Deduplication: check for existing lesson with same topic ─────────────
2259
+ // ── Structured template hints ──────────────────────────────────────────
2260
+ const category = topic.split(':')[0];
2261
+ const template = STRUCTURED_TEMPLATES[category];
2262
+ const templateWarnings = [];
2263
+ if (template) {
2264
+ for (const req of template.required) {
2265
+ if (req === 'commands' && commands.length === 0) {
2266
+ templateWarnings.push(`📋 ${template.hint}`);
2267
+ }
2268
+ }
2269
+ }
2270
+ // ── Deduplication + audit trail ────────────────────────────────────────
2223
2271
  let isUpdate = false;
2224
2272
  let recallCount = 0;
2225
- if (outcome === 'success') {
2226
- const existing = await redis.get(`cachly:lesson:best:${topic}`);
2227
- if (existing) {
2228
- try {
2229
- const prev = JSON.parse(existing);
2230
- recallCount = (prev.recall_count ?? 0); // preserve existing recall count
2231
- isUpdate = true;
2273
+ let auditTrail = [];
2274
+ const existingRaw = await redis.get(`cachly:lesson:best:${topic}`);
2275
+ if (existingRaw) {
2276
+ try {
2277
+ const prev = JSON.parse(existingRaw);
2278
+ recallCount = prev.recall_count ?? 0;
2279
+ auditTrail = prev.audit_trail ?? [];
2280
+ auditTrail.push({ ts, action: 'updated', prev_outcome: prev.outcome });
2281
+ if (auditTrail.length > 20)
2282
+ auditTrail = auditTrail.slice(-20);
2283
+ isUpdate = true;
2284
+ // ── Contradiction detection ─────────────────────────────────────────
2285
+ const contradictionWarning = [];
2286
+ if (prev.outcome === 'success' && outcome === 'failure') {
2287
+ contradictionWarning.push(`⚠️ **Contradiction detected!** Existing lesson has outcome: \`success\`, but you're storing \`failure\`.`, `The existing "success" lesson will be preserved. Only the audit trail is updated.`, `If you meant to mark this as failed permanently, store a new lesson with a distinct topic slug.`);
2288
+ }
2289
+ else if (prev.outcome === 'failure' && outcome === 'success') {
2290
+ contradictionWarning.push(`✅ **Conflict resolved!** Previous lesson was \`failure\` — now overwriting with \`success\`.`);
2291
+ }
2292
+ if (contradictionWarning.length > 0) {
2293
+ // Store contradiction audit but don't block
2294
+ auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
2232
2295
  }
2233
- catch { /* ignore parse error */ }
2234
2296
  }
2297
+ catch { /* ignore parse error */ }
2298
+ }
2299
+ else {
2300
+ auditTrail = [{ ts, action: 'created' }];
2235
2301
  }
2236
- const lesson = JSON.stringify({
2302
+ const lessonObj = {
2237
2303
  topic,
2238
2304
  outcome,
2239
2305
  what_worked,
@@ -2245,15 +2311,30 @@ async function handleTool(name, args) {
2245
2311
  tags,
2246
2312
  recall_count: recallCount,
2247
2313
  ts,
2248
- version: 2,
2249
- });
2250
- // Always append to the history list
2314
+ verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
2315
+ confidence: 1.0,
2316
+ audit_trail: auditTrail,
2317
+ version: 3,
2318
+ };
2319
+ const lesson = JSON.stringify(lessonObj);
2320
+ // Always append to the history list (audit log)
2251
2321
  const listKey = `cachly:lessons:${topic}`;
2252
2322
  await redis.rpush(listKey, lesson);
2253
- // Update best key only for success/partial (not for failures)
2323
+ // Update best key for success/partial; for failure only update if no success exists
2254
2324
  if (outcome === 'success' || outcome === 'partial') {
2255
2325
  await redis.set(`cachly:lesson:best:${topic}`, lesson);
2256
2326
  }
2327
+ else if (!existingRaw) {
2328
+ await redis.set(`cachly:lesson:best:${topic}`, lesson);
2329
+ }
2330
+ // Track in decision log for session replay
2331
+ try {
2332
+ const dlKey = 'cachly:session:decision-log';
2333
+ const dlEntry = JSON.stringify({ ts, topic, outcome, what_worked: what_worked.slice(0, 120) });
2334
+ await redis.rpush(dlKey, dlEntry);
2335
+ await redis.ltrim(dlKey, -50, -1);
2336
+ }
2337
+ catch { /* non-critical */ }
2257
2338
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
2258
2339
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
2259
2340
  const action = isUpdate ? 'updated' : 'stored';
@@ -2268,8 +2349,9 @@ async function handleTool(name, args) {
2268
2349
  tags.length > 0 ? `**Tags:** ${tags.map(t => `#${t}`).join(' ')}` : '',
2269
2350
  ``,
2270
2351
  isUpdate
2271
- ? `♻️ Updated existing lesson (preserved recall count: ${recallCount})`
2352
+ ? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
2272
2353
  : `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
2354
+ ...templateWarnings,
2273
2355
  ].filter(l => l !== '').join('\n');
2274
2356
  }
2275
2357
  case 'recall_best_solution': {
@@ -2279,12 +2361,35 @@ async function handleTool(name, args) {
2279
2361
  const best = await redis.get(`cachly:lesson:best:${topic}`);
2280
2362
  if (best) {
2281
2363
  const lesson = JSON.parse(best);
2282
- // Increment recall_count
2283
- const updatedLesson = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1 };
2364
+ // ── Confidence decay check ───────────────────────────────────────────
2365
+ const confidence = calculateConfidence(lesson);
2366
+ const ref = lesson.verified_at ?? lesson.ts;
2367
+ const ageDays = (Date.now() - new Date(ref).getTime()) / 86400000;
2368
+ const badge = confidenceBadge(confidence, ageDays);
2369
+ // Recall resets verified_at (confidence clock restart)
2370
+ const updatedLesson = {
2371
+ ...lesson,
2372
+ recall_count: (lesson.recall_count ?? 0) + 1,
2373
+ verified_at: new Date().toISOString(),
2374
+ confidence: 1.0,
2375
+ };
2284
2376
  await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
2285
2377
  const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
2378
+ const auditSummary = (lesson.audit_trail ?? []).length > 1
2379
+ ? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
2380
+ : '';
2381
+ // "Remember when..." — emotional header for lessons > 60 days old
2382
+ const ageFromStoreDays = (Date.now() - new Date(lesson.ts).getTime()) / 86400000;
2383
+ const rememberWhen = ageFromStoreDays > 60
2384
+ ? `💭 _Remember when you solved this ${Math.round(ageFromStoreDays / 30)} months ago? Still works._`
2385
+ : '';
2386
+ // "Never Google This Again" — suggest pinning after 3rd recall
2387
+ const suggestPin = updatedLesson.recall_count === 3 && !lesson.pinned
2388
+ ? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
2389
+ : '';
2286
2390
  return [
2287
- `✅ **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}× · ${new Date(lesson.ts).toLocaleDateString('de-DE')}`,
2391
+ rememberWhen,
2392
+ `${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
2288
2393
  ``,
2289
2394
  `**What worked:** ${lesson.what_worked}`,
2290
2395
  lesson.what_failed ? `**What failed (avoid this):** ${lesson.what_failed}` : '',
@@ -2292,6 +2397,8 @@ async function handleTool(name, args) {
2292
2397
  (lesson.file_paths ?? []).length > 0 ? `**Files:** ${(lesson.file_paths ?? []).map((f) => `\`${f}\``).join(', ')}` : '',
2293
2398
  (lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c) => `\`${c}\``).join(', ')}` : '',
2294
2399
  (lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t) => `#${t}`).join(' ')}` : '',
2400
+ auditSummary,
2401
+ suggestPin,
2295
2402
  ].filter(l => l !== '').join('\n');
2296
2403
  }
2297
2404
  // Partial match: scan all lesson keys for topic substring
@@ -2527,13 +2634,56 @@ async function handleTool(name, args) {
2527
2634
  ? lessons.filter(l => focusTerms.some(term => l.topic.toLowerCase().includes(term) ||
2528
2635
  (l.tags ?? []).some((t) => t.toLowerCase().includes(term))))
2529
2636
  : [];
2530
- // 6. Save session start marker
2637
+ // 6. Streak tracking
2638
+ let streakDays = 0;
2639
+ let streakRecord = 0;
2640
+ let streakMessage = '';
2641
+ try {
2642
+ const streakRaw = await redis.get('cachly:streak:current');
2643
+ const streak = streakRaw ? JSON.parse(streakRaw) : null;
2644
+ const today = new Date().toISOString().slice(0, 10);
2645
+ if (streak) {
2646
+ const lastDate = streak.last_date;
2647
+ const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
2648
+ if (lastDate === today) {
2649
+ // Already counted today
2650
+ streakDays = streak.days;
2651
+ streakRecord = streak.record;
2652
+ }
2653
+ else if (lastDate === yesterday) {
2654
+ // Continuing streak
2655
+ streakDays = streak.days + 1;
2656
+ streakRecord = Math.max(streakDays, streak.record);
2657
+ await redis.set('cachly:streak:current', JSON.stringify({ days: streakDays, last_date: today, record: streakRecord }));
2658
+ }
2659
+ else {
2660
+ // Streak broken
2661
+ streakDays = 1;
2662
+ streakRecord = streak.record;
2663
+ await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: streakRecord }));
2664
+ }
2665
+ }
2666
+ else {
2667
+ // First session ever
2668
+ streakDays = 1;
2669
+ streakRecord = 1;
2670
+ await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: 1 }));
2671
+ }
2672
+ if (streakDays >= 7)
2673
+ streakMessage = `🔥 **${streakDays}-day streak!** ${streakDays === streakRecord ? ' New record!' : `Best: ${streakRecord}d`}`;
2674
+ else if (streakDays > 1)
2675
+ streakMessage = `🔥 ${streakDays}-day streak`;
2676
+ }
2677
+ catch { /* non-critical */ }
2678
+ // 7. Save session start marker
2531
2679
  await redis.set('cachly:session:current', JSON.stringify({
2532
2680
  started: new Date().toISOString(),
2533
2681
  focus,
2534
2682
  }), 'EX', 86400); // auto-expire after 24h if session_end never called
2535
2683
  // ── Build briefing ──────────────────────────────────────────────────────
2536
2684
  const lines = ['🧠 **Session Briefing**', ''];
2685
+ if (streakMessage)
2686
+ lines.push(streakMessage, '');
2537
2687
  // Handoff from previous window (if any)
2538
2688
  const handoffRaw = await redis.get('cachly:session:handoff');
2539
2689
  if (handoffRaw) {
@@ -2611,6 +2761,36 @@ async function handleTool(name, args) {
2611
2761
  }
2612
2762
  lines.push('');
2613
2763
  }
2764
+ // ── Stale / low-confidence lessons (confidence decay) ─────────────────
2765
+ const staleSuccessLessons = lessons.filter(l => {
2766
+ if (l.outcome !== 'success' && l.outcome !== 'partial')
2767
+ return false;
2768
+ return calculateConfidence(l) < CONFIDENCE_WARN_VALUE;
2769
+ });
2770
+ if (staleSuccessLessons.length > 0) {
2771
+ lines.push(`🔴 **Stale lessons** (not recalled in >${CONFIDENCE_WARN_DAYS}d — verify before applying):`);
2772
+ for (const l of staleSuccessLessons.slice(0, 4)) {
2773
+ const conf = calculateConfidence(l);
2774
+ const ageDays = Math.round((Date.now() - new Date(l.verified_at ?? l.ts).getTime()) / 86400000);
2775
+ const flag = conf < CONFIDENCE_STALE_VALUE ? '🔴' : '⚠️';
2776
+ lines.push(` ${flag} \`${l.topic}\` — ${ageDays}d stale, ${(conf * 100).toFixed(0)}% confidence`);
2777
+ }
2778
+ lines.push(` _Run \`recall_best_solution\` on these to reset their confidence clock._`);
2779
+ lines.push('');
2780
+ }
2781
+ // ── Session Replay: show last session's decision log ──────────────────
2782
+ const lastSessionAny = lastSession;
2783
+ if (lastSessionAny?.decision_log?.length) {
2784
+ const dl = lastSessionAny.decision_log;
2785
+ const successes = dl.filter(d => d.outcome === 'success');
2786
+ const failures = dl.filter(d => d.outcome === 'failure');
2787
+ lines.push(`🎬 **Last session decisions** (${dl.length} lessons stored):`);
2788
+ if (successes.length > 0)
2789
+ lines.push(` ✅ Worked: ${successes.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
2790
+ if (failures.length > 0)
2791
+ lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
2792
+ lines.push('');
2793
+ }
2614
2794
  return lines.join('\n');
2615
2795
  }
2616
2796
  // ── session_end ───────────────────────────────────────────────────────────
@@ -2628,12 +2808,21 @@ async function handleTool(name, args) {
2628
2808
  }
2629
2809
  catch { /* ignore */ }
2630
2810
  }
2811
+ // ── Session Replay: capture decision log ─────────────────────────────
2812
+ let decisionLog = [];
2813
+ try {
2814
+ const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
2815
+ decisionLog = dlEntries.map(e => JSON.parse(e));
2816
+ await redis.del('cachly:session:decision-log');
2817
+ }
2818
+ catch { /* non-critical */ }
2631
2819
  const sessionRecord = {
2632
2820
  ts: now.toISOString(),
2633
2821
  summary,
2634
2822
  files_changed,
2635
2823
  ...(lessons_learned !== undefined ? { lessons_learned } : {}),
2636
2824
  ...(durationMin !== undefined ? { duration_min: durationMin } : {}),
2825
+ ...(decisionLog.length > 0 ? { decision_log: decisionLog } : {}),
2637
2826
  };
2638
2827
  // Save as "last session"
2639
2828
  await redis.set('cachly:session:last', JSON.stringify(sessionRecord));
@@ -3000,6 +3189,10 @@ async function handleTool(name, args) {
3000
3189
  const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
3001
3190
  // Critical lessons
3002
3191
  const criticalLessons = lessons.filter(l => l.severity === 'critical');
3192
+ // Confidence decay analysis
3193
+ const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
3194
+ const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
3195
+ const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
3003
3196
  // Quality score (0-100)
3004
3197
  let score = 50;
3005
3198
  if (lessonKeys.length >= 5)
@@ -3017,10 +3210,14 @@ async function handleTool(name, args) {
3017
3210
  const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
3018
3211
  if (unusedRatio < 0.5)
3019
3212
  score += 10;
3213
+ if (staleLessons.length === 0)
3214
+ score += 5;
3020
3215
  const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
3021
3216
  checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
3022
- checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical)`);
3217
+ checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail)`);
3023
3218
  checks.push(`💾 **Context entries:** ${ctxCount}`);
3219
+ checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
3220
+ checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
3024
3221
  if (lastSession) {
3025
3222
  const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
3026
3223
  const ageStr = ageMin < 60 ? `${ageMin}m` : ageMin < 1440 ? `${Math.round(ageMin / 60)}h` : `${Math.round(ageMin / 1440)}d`;
@@ -3041,6 +3238,13 @@ async function handleTool(name, args) {
3041
3238
  if (openFailures.length > 0) {
3042
3239
  issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
3043
3240
  }
3241
+ if (staleLessons.length > 0) {
3242
+ issues.push(`🔴 ${staleLessons.length} STALE lesson${staleLessons.length > 1 ? 's' : ''} (>${CONFIDENCE_STALE_DAYS}d, confidence <${CONFIDENCE_STALE_VALUE * 100}%): ${staleLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
3243
+ issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
3244
+ }
3245
+ if (warnLessons.length > 0) {
3246
+ issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
3247
+ }
3044
3248
  if (unusedRatio > 0.7 && lessons.length > 5) {
3045
3249
  issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
3046
3250
  }