@cachly-dev/mcp-server 0.5.4 → 0.5.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "MCP server – manage cachly.dev instances from any AI assistant (Copilot, Claude, Cursor, Windsurf)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -71,6 +71,39 @@ const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
71
71
  */
72
72
  const EMBED_PROVIDER = (process.env.CACHLY_EMBED_PROVIDER ?? detectEmbedProvider()).toLowerCase();
73
73
 
74
+ // ── Confidence Decay Config (all values configurable via env) ─────────────────
75
+ const CONFIDENCE_WARN_DAYS = Number(process.env.CACHLY_CONFIDENCE_WARN_DAYS ?? 5); // → 0.7
76
+ const CONFIDENCE_STALE_DAYS = Number(process.env.CACHLY_CONFIDENCE_STALE_DAYS ?? 10); // → 0.5
77
+ const CONFIDENCE_WARN_VALUE = 0.7;
78
+ const CONFIDENCE_STALE_VALUE = 0.5;
79
+
80
+ /** Calculate current confidence for a lesson based on how long since last verified. */
81
+ function calculateConfidence(lesson: { verified_at?: string; ts: string; recall_count?: number }): number {
82
+ const ref = lesson.verified_at ?? lesson.ts;
83
+ const ageMs = Date.now() - new Date(ref).getTime();
84
+ const ageDays = ageMs / 86400000;
85
+ if (ageDays >= CONFIDENCE_STALE_DAYS) return CONFIDENCE_STALE_VALUE;
86
+ if (ageDays >= CONFIDENCE_WARN_DAYS) return CONFIDENCE_WARN_VALUE;
87
+ // Linear interpolation between fresh (1.0) and warn threshold
88
+ return 1.0 - (ageDays / CONFIDENCE_WARN_DAYS) * (1.0 - CONFIDENCE_WARN_VALUE);
89
+ }
90
+
91
+ /** Render a confidence badge string. */
92
+ function confidenceBadge(confidence: number, ageDays: number): string {
93
+ if (confidence >= 0.9) return '✅';
94
+ if (confidence >= 0.7) return `⚠️ (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — verify before applying)`;
95
+ return `🔴 STALE (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — likely outdated!)`;
96
+ }
97
+
98
+ /** Category-specific required fields for structured lessons. */
99
+ const STRUCTURED_TEMPLATES: Record<string, { required: string[]; hint: string }> = {
100
+ 'deploy': { required: ['commands'], hint: 'deploy:* needs commands[]' },
101
+ 'bash': { required: ['commands'], hint: 'bash:* needs commands[]' },
102
+ 'infra': { required: ['commands'], hint: 'infra:* needs commands[] + verified on real system' },
103
+ 'pricing': { required: [], hint: 'pricing:* — add context with source (e.g. "Stripe Dashboard Apr 2026")' },
104
+ 'stripe': { required: [], hint: 'stripe:* — add context with Stripe API version' },
105
+ };
106
+
74
107
  /** Fast content hash for index invalidation (not cryptographic, just change detection) */
75
108
  function simpleHash(text: string): string {
76
109
  return createHash('md5').update(text).digest('hex').slice(0, 12);
@@ -807,8 +840,8 @@ const TOOLS = [
807
840
  description:
808
841
  'Create a new managed Valkey/Redis cache instance on cachly.dev. ' +
809
842
  'Free tier provisions in ~30 seconds. Paid tiers return a Stripe checkout URL. ' +
810
- 'Available tiers: free (30 MB), dev (256 MB, €8/mo), pro (1 GB, €25/mo), ' +
811
- 'speed (1 GB Dragonfly + Semantic Cache, €39/mo), business (8 GB, €99/mo).',
843
+ 'Available tiers: free (25 MB), dev (200 MB, €19/mo), pro (900 MB, €49/mo), ' +
844
+ 'speed (900 MB Dragonfly + Semantic Cache, €79/mo), business (7 GB, €199/mo).',
812
845
  inputSchema: {
813
846
  type: 'object',
814
847
  properties: {
@@ -1277,7 +1310,9 @@ const TOOLS = [
1277
1310
  'Fields: topic (short slug like "deploy:web"), outcome ("success"|"failure"), ' +
1278
1311
  'what_worked (what solved it), what_failed (what did NOT work), context (extra details). ' +
1279
1312
  'Supports structured metadata: severity, file_paths (files involved), commands (working commands), tags. ' +
1280
- 'Deduplication: if a lesson for this topic already exists, it is updated instead of duplicated. ' +
1313
+ 'Deduplication: if a lesson for this topic already exists, it is updated with full audit trail. ' +
1314
+ 'Contradiction detection: warns if new outcome conflicts with existing lesson outcome. ' +
1315
+ 'Confidence: lesson starts at 1.0, decays after 5d (→0.7) and 10d (→0.5) without recall. ' +
1281
1316
  '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"])',
1282
1317
  inputSchema: {
1283
1318
  type: 'object',
@@ -1317,7 +1352,9 @@ const TOOLS = [
1317
1352
  description:
1318
1353
  'Recall the best known solution for a topic from past lessons. ' +
1319
1354
  'Call this BEFORE attempting any task that might have been done before. ' +
1320
- 'Returns the most recent successful lesson for the topic, or a summary of attempts. ' +
1355
+ 'Returns the most recent successful lesson for the topic, with confidence indicator. ' +
1356
+ '⚠️ badge = lesson is >5d old (verify before applying). 🔴 = >10d old (likely stale!). ' +
1357
+ 'Recalling a lesson resets its confidence clock to 1.0 (marks as recently verified). ' +
1321
1358
  'Example: recall_best_solution(topic="deploy:web") → returns the working deploy command.',
1322
1359
  inputSchema: {
1323
1360
  type: 'object',
@@ -2535,21 +2572,59 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2535
2572
  const redis = await getConnection(instance_id);
2536
2573
  const ts = new Date().toISOString();
2537
2574
 
2538
- // ── Deduplication: check for existing lesson with same topic ─────────────
2575
+ // ── Structured template hints ──────────────────────────────────────────
2576
+ const category = topic.split(':')[0];
2577
+ const template = STRUCTURED_TEMPLATES[category];
2578
+ const templateWarnings: string[] = [];
2579
+ if (template) {
2580
+ for (const req of template.required) {
2581
+ if (req === 'commands' && commands.length === 0) {
2582
+ templateWarnings.push(`📋 ${template.hint}`);
2583
+ }
2584
+ }
2585
+ }
2586
+
2587
+ // ── Deduplication + audit trail ────────────────────────────────────────
2539
2588
  let isUpdate = false;
2540
2589
  let recallCount = 0;
2541
- if (outcome === 'success') {
2542
- const existing = await redis.get(`cachly:lesson:best:${topic}`);
2543
- if (existing) {
2544
- try {
2545
- const prev = JSON.parse(existing) as { recall_count?: number };
2546
- recallCount = (prev.recall_count ?? 0); // preserve existing recall count
2547
- isUpdate = true;
2548
- } catch { /* ignore parse error */ }
2549
- }
2590
+ let auditTrail: Array<{ ts: string; action: string; prev_outcome?: string }> = [];
2591
+ const existingRaw = await redis.get(`cachly:lesson:best:${topic}`);
2592
+ if (existingRaw) {
2593
+ try {
2594
+ const prev = JSON.parse(existingRaw) as {
2595
+ recall_count?: number;
2596
+ outcome?: string;
2597
+ audit_trail?: Array<{ ts: string; action: string; prev_outcome?: string }>;
2598
+ };
2599
+ recallCount = prev.recall_count ?? 0;
2600
+ auditTrail = prev.audit_trail ?? [];
2601
+ auditTrail.push({ ts, action: 'updated', prev_outcome: prev.outcome });
2602
+ if (auditTrail.length > 20) auditTrail = auditTrail.slice(-20);
2603
+ isUpdate = true;
2604
+
2605
+ // ── Contradiction detection ─────────────────────────────────────────
2606
+ const contradictionWarning: string[] = [];
2607
+ if (prev.outcome === 'success' && outcome === 'failure') {
2608
+ contradictionWarning.push(
2609
+ `⚠️ **Contradiction detected!** Existing lesson has outcome: \`success\`, but you're storing \`failure\`.`,
2610
+ `The existing "success" lesson will be preserved. Only the audit trail is updated.`,
2611
+ `If you meant to mark this as failed permanently, store a new lesson with a distinct topic slug.`,
2612
+ );
2613
+ } else if (prev.outcome === 'failure' && outcome === 'success') {
2614
+ contradictionWarning.push(
2615
+ `✅ **Conflict resolved!** Previous lesson was \`failure\` — now overwriting with \`success\`.`,
2616
+ );
2617
+ }
2618
+ if (contradictionWarning.length > 0) {
2619
+ // Store contradiction audit but don't block
2620
+ auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
2621
+ }
2622
+ } catch { /* ignore parse error */ }
2623
+ } else {
2624
+ auditTrail = [{ ts, action: 'created' }];
2550
2625
  }
2551
2626
 
2552
- const lesson = JSON.stringify({
2627
+ const lessonObj = {
2553
2628
  topic,
2554
2629
  outcome,
2555
2630
  what_worked,
@@ -2561,18 +2636,32 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2561
2636
  tags,
2562
2637
  recall_count: recallCount,
2563
2638
  ts,
2564
- version: 2,
2565
- });
2639
+ verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
2640
+ confidence: 1.0,
2641
+ audit_trail: auditTrail,
2642
+ version: 3,
2643
+ };
2644
+ const lesson = JSON.stringify(lessonObj);
2566
2645
 
2567
- // Always append to the history list
2646
+ // Always append to the history list (audit log)
2568
2647
  const listKey = `cachly:lessons:${topic}`;
2569
2648
  await redis.rpush(listKey, lesson);
2570
2649
 
2571
- // Update best key only for success/partial (not for failures)
2650
+ // Update best key for success/partial; for failure only update if no success exists
2572
2651
  if (outcome === 'success' || outcome === 'partial') {
2573
2652
  await redis.set(`cachly:lesson:best:${topic}`, lesson);
2653
+ } else if (!existingRaw) {
2654
+ await redis.set(`cachly:lesson:best:${topic}`, lesson);
2574
2655
  }
2575
2656
 
2657
+ // Track in decision log for session replay
2658
+ try {
2659
+ const dlKey = 'cachly:session:decision-log';
2660
+ const dlEntry = JSON.stringify({ ts, topic, outcome, what_worked: what_worked.slice(0, 120) });
2661
+ await redis.rpush(dlKey, dlEntry);
2662
+ await redis.ltrim(dlKey, -50, -1);
2663
+ } catch { /* non-critical */ }
2664
+
2576
2665
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
2577
2666
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
2578
2667
  const action = isUpdate ? 'updated' : 'stored';
@@ -2587,8 +2676,9 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2587
2676
  tags.length > 0 ? `**Tags:** ${tags.map(t => `#${t}`).join(' ')}` : '',
2588
2677
  ``,
2589
2678
  isUpdate
2590
- ? `♻️ Updated existing lesson (preserved recall count: ${recallCount})`
2679
+ ? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
2591
2680
  : `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
2681
+ ...templateWarnings,
2592
2682
  ].filter(l => l !== '').join('\n');
2593
2683
  }
2594
2684
 
@@ -2601,16 +2691,32 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2601
2691
  if (best) {
2602
2692
  const lesson = JSON.parse(best) as {
2603
2693
  topic: string; outcome: string; what_worked: string; what_failed?: string;
2604
- context?: string; ts: string; severity?: string; file_paths?: string[];
2605
- commands?: string[]; tags?: string[]; recall_count?: number;
2694
+ context?: string; ts: string; verified_at?: string; severity?: string;
2695
+ file_paths?: string[]; commands?: string[]; tags?: string[];
2696
+ recall_count?: number; audit_trail?: unknown[];
2697
+ };
2698
+
2699
+ // ── Confidence decay check ───────────────────────────────────────────
2700
+ const confidence = calculateConfidence(lesson);
2701
+ const ref = lesson.verified_at ?? lesson.ts;
2702
+ const ageDays = (Date.now() - new Date(ref).getTime()) / 86400000;
2703
+ const badge = confidenceBadge(confidence, ageDays);
2704
+
2705
+ // Recall resets verified_at (confidence clock restart)
2706
+ const updatedLesson = {
2707
+ ...lesson,
2708
+ recall_count: (lesson.recall_count ?? 0) + 1,
2709
+ verified_at: new Date().toISOString(),
2710
+ confidence: 1.0,
2606
2711
  };
2607
- // Increment recall_count
2608
- const updatedLesson = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1 };
2609
2712
  await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
2610
2713
 
2611
2714
  const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
2715
+ const auditSummary = (lesson.audit_trail ?? []).length > 1
2716
+ ? `\n_Audit: ${(lesson.audit_trail ?? []).length} changes · ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
2717
+ : '';
2612
2718
  return [
2613
- `✅ **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}× · ${new Date(lesson.ts).toLocaleDateString('de-DE')}`,
2719
+ `${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
2614
2720
  ``,
2615
2721
  `**What worked:** ${lesson.what_worked}`,
2616
2722
  lesson.what_failed ? `**What failed (avoid this):** ${lesson.what_failed}` : '',
@@ -2618,6 +2724,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2618
2724
  (lesson.file_paths ?? []).length > 0 ? `**Files:** ${(lesson.file_paths ?? []).map((f: string) => `\`${f}\``).join(', ')}` : '',
2619
2725
  (lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c: string) => `\`${c}\``).join(', ')}` : '',
2620
2726
  (lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t: string) => `#${t}`).join(' ')}` : '',
2727
+ auditSummary,
2621
2728
  ].filter(l => l !== '').join('\n');
2622
2729
  }
2623
2730
 
@@ -2848,7 +2955,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2848
2955
  // 2. Fetch all lesson values for recency sorting + focus matching
2849
2956
  type Lesson = {
2850
2957
  topic: string; outcome: string; what_worked: string; what_failed?: string;
2851
- ts: string; severity?: string; recall_count?: number; tags?: string[];
2958
+ ts: string; verified_at?: string; severity?: string; recall_count?: number;
2959
+ tags?: string[]; confidence?: number; audit_trail?: unknown[];
2852
2960
  };
2853
2961
  const lessons: Lesson[] = [];
2854
2962
  for (const k of lessonKeys) {
@@ -2977,6 +3085,35 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2977
3085
  lines.push('');
2978
3086
  }
2979
3087
 
3088
+ // ── Stale / low-confidence lessons (confidence decay) ─────────────────
3089
+ const staleSuccessLessons = lessons.filter(l => {
3090
+ if (l.outcome !== 'success' && l.outcome !== 'partial') return false;
3091
+ return calculateConfidence(l) < CONFIDENCE_WARN_VALUE;
3092
+ });
3093
+ if (staleSuccessLessons.length > 0) {
3094
+ lines.push(`🔴 **Stale lessons** (not recalled in >${CONFIDENCE_WARN_DAYS}d — verify before applying):`);
3095
+ for (const l of staleSuccessLessons.slice(0, 4)) {
3096
+ const conf = calculateConfidence(l);
3097
+ const ageDays = Math.round((Date.now() - new Date(l.verified_at ?? l.ts).getTime()) / 86400000);
3098
+ const flag = conf < CONFIDENCE_STALE_VALUE ? '🔴' : '⚠️';
3099
+ lines.push(` ${flag} \`${l.topic}\` — ${ageDays}d stale, ${(conf * 100).toFixed(0)}% confidence`);
3100
+ }
3101
+ lines.push(` _Run \`recall_best_solution\` on these to reset their confidence clock._`);
3102
+ lines.push('');
3103
+ }
3104
+
3105
+ // ── Session Replay: show last session's decision log ──────────────────
3106
+ const lastSessionAny = lastSession as unknown as { decision_log?: Array<{ topic: string; outcome: string; what_worked: string }> } | null;
3107
+ if (lastSessionAny?.decision_log?.length) {
3108
+ const dl = lastSessionAny.decision_log;
3109
+ const successes = dl.filter(d => d.outcome === 'success');
3110
+ const failures = dl.filter(d => d.outcome === 'failure');
3111
+ lines.push(`🎬 **Last session decisions** (${dl.length} lessons stored):`);
3112
+ if (successes.length > 0) lines.push(` ✅ Worked: ${successes.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
3113
+ if (failures.length > 0) lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
3114
+ lines.push('');
3115
+ }
3116
+
2980
3117
  return lines.join('\n');
2981
3118
  }
2982
3119
 
@@ -3007,12 +3144,21 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3007
3144
  } catch { /* ignore */ }
3008
3145
  }
3009
3146
 
3147
+ // ── Session Replay: capture decision log ─────────────────────────────
3148
+ let decisionLog: Array<{ ts: string; topic: string; outcome: string; what_worked: string }> = [];
3149
+ try {
3150
+ const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
3151
+ decisionLog = dlEntries.map(e => JSON.parse(e) as { ts: string; topic: string; outcome: string; what_worked: string });
3152
+ await redis.del('cachly:session:decision-log');
3153
+ } catch { /* non-critical */ }
3154
+
3010
3155
  const sessionRecord = {
3011
3156
  ts: now.toISOString(),
3012
3157
  summary,
3013
3158
  files_changed,
3014
3159
  ...(lessons_learned !== undefined ? { lessons_learned } : {}),
3015
3160
  ...(durationMin !== undefined ? { duration_min: durationMin } : {}),
3161
+ ...(decisionLog.length > 0 ? { decision_log: decisionLog } : {}),
3016
3162
  };
3017
3163
 
3018
3164
  // Save as "last session"
@@ -3425,12 +3571,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3425
3571
  });
3426
3572
 
3427
3573
  // Load lessons for analysis
3428
- type Lesson = { topic: string; outcome: string; recall_count?: number; ts: string; severity?: string };
3429
- const lessons: Lesson[] = [];
3574
+ type DrLesson = {
3575
+ topic: string; outcome: string; recall_count?: number; ts: string;
3576
+ verified_at?: string; severity?: string; audit_trail?: unknown[];
3577
+ };
3578
+ const lessons: DrLesson[] = [];
3430
3579
  for (const k of lessonKeys) {
3431
3580
  const raw = await redis.get(k);
3432
3581
  if (!raw) continue;
3433
- try { lessons.push(JSON.parse(raw) as Lesson); } catch { /* skip */ }
3582
+ try { lessons.push(JSON.parse(raw) as DrLesson); } catch { /* skip */ }
3434
3583
  }
3435
3584
 
3436
3585
  // Last session
@@ -3446,6 +3595,10 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3446
3595
  const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
3447
3596
  // Critical lessons
3448
3597
  const criticalLessons = lessons.filter(l => l.severity === 'critical');
3598
+ // Confidence decay analysis
3599
+ const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
3600
+ const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
3601
+ const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
3449
3602
 
3450
3603
  // Quality score (0-100)
3451
3604
  let score = 50;
@@ -3457,12 +3610,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3457
3610
  if (openFailures.length === 0) score += 5;
3458
3611
  const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
3459
3612
  if (unusedRatio < 0.5) score += 10;
3613
+ if (staleLessons.length === 0) score += 5;
3460
3614
 
3461
3615
  const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
3462
3616
 
3463
3617
  checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
3464
- checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical)`);
3618
+ checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail)`);
3465
3619
  checks.push(`💾 **Context entries:** ${ctxCount}`);
3620
+ checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
3621
+ checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
3466
3622
 
3467
3623
  if (lastSession) {
3468
3624
  const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
@@ -3486,6 +3642,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3486
3642
  issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
3487
3643
  }
3488
3644
 
3645
+ if (staleLessons.length > 0) {
3646
+ 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(', ')}`);
3647
+ issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
3648
+ }
3649
+
3650
+ if (warnLessons.length > 0) {
3651
+ issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
3652
+ }
3653
+
3489
3654
  if (unusedRatio > 0.7 && lessons.length > 5) {
3490
3655
  issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
3491
3656
  }