@cachly-dev/mcp-server 0.9.0 → 0.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/src/index.ts CHANGED
@@ -55,7 +55,7 @@ import { Redis } from 'ioredis';
55
55
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
56
56
  const JWT = process.env.CACHLY_JWT ?? '';
57
57
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
58
- const CURRENT_VERSION = '0.8.3';
58
+ const CURRENT_VERSION = '0.9.2';
59
59
  /**
60
60
  * ┌──────────────────────────────────────────────────────────────────────┐
61
61
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -2194,6 +2194,58 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
2194
2194
 
2195
2195
  import { detectNamespace } from './namespace.js';
2196
2196
 
2197
+ // ── Layer 1: Causal Knowledge Graph (CKG) helpers ────────────────────────────
2198
+ // Implements the CKG from the 10x Vision Document.
2199
+ // Nodes: cachly:ckg:node:{id} → { id, domain, type, count, ts }
2200
+ // Edges: cachly:ckg:edge:{from}:{edgeType}:{to} → { from, to, edgeType, successes, trials, confidence, last_updated }
2201
+
2202
+ type CKGEdge = {
2203
+ from: string; to: string; edgeType: string;
2204
+ successes: number; trials: number; confidence: number; last_updated: string;
2205
+ };
2206
+ type CKGNode = { id: string; domain: string; type: string; count: number; ts: string };
2207
+
2208
+ const STOPWORDS_CKG = new Set(['that','this','with','from','when','then','also','have','been','will','were','they','them','than','more','some','into','over','only','just','where','while','which','there','their','would','could','should','after','before','about']);
2209
+
2210
+ function ckgSlug(text: string): string {
2211
+ return text.toLowerCase().replace(/[^a-z0-9\-:]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
2212
+ }
2213
+
2214
+ /** Extract 1-3 significant keywords from free text for a problem concept */
2215
+ function extractProblemConcept(text: string): string | null {
2216
+ const words = text.toLowerCase()
2217
+ .replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/)
2218
+ .filter(w => w.length > 3 && !STOPWORDS_CKG.has(w))
2219
+ .slice(0, 5);
2220
+ if (words.length === 0) return null;
2221
+ return words.slice(0, 2).join('-');
2222
+ }
2223
+
2224
+ async function ckgUpsertNode(redis: Redis, id: string, domain: string, type: string): Promise<void> {
2225
+ const key = `cachly:ckg:node:${id}`;
2226
+ const raw = await redis.get(key);
2227
+ const node: CKGNode = raw ? JSON.parse(raw) : { id, domain, type, count: 0, ts: new Date().toISOString() };
2228
+ node.count = (node.count || 0) + 1;
2229
+ node.ts = new Date().toISOString();
2230
+ await redis.set(key, JSON.stringify(node));
2231
+ }
2232
+
2233
+ async function ckgUpdateEdge(redis: Redis, from: string, edgeType: string, to: string, success: boolean, partial = false): Promise<void> {
2234
+ const key = `cachly:ckg:edge:${from}:${edgeType}:${to}`;
2235
+ const raw = await redis.get(key);
2236
+ const edge: CKGEdge = raw ? JSON.parse(raw) : { from, to, edgeType, successes: 0, trials: 0, confidence: 0, last_updated: '' };
2237
+ edge.trials = (edge.trials || 0) + 1;
2238
+ if (success) edge.successes = (edge.successes || 0) + 1;
2239
+ else if (partial) edge.successes = (edge.successes || 0) + 0.5;
2240
+ // Beta distribution smoothed confidence: (successes+1) / (trials+2)
2241
+ edge.confidence = (edge.successes + 1) / (edge.trials + 2);
2242
+ edge.last_updated = new Date().toISOString();
2243
+ await redis.set(key, JSON.stringify(edge));
2244
+ // Index: set of edge keys per source node (for fast traversal)
2245
+ await redis.sadd(`cachly:ckg:idx:from:${from}`, key);
2246
+ await redis.sadd(`cachly:ckg:idx:to:${to}`, key);
2247
+ }
2248
+
2197
2249
  // ── Tools ─────────────────────────────────────────────────────────────────────
2198
2250
 
2199
2251
  const TOOLS = [
@@ -3556,60 +3608,53 @@ const TOOLS = [
3556
3608
  required: [],
3557
3609
  },
3558
3610
  },
3559
- // ── v0.9 — compact_recover ─────────────────────────────────────────────────
3611
+ // ── Layer 1: Causal Knowledge Graph ────────────────────────────────────────
3560
3612
  {
3561
- name: 'compact_recover',
3613
+ name: 'brain_search',
3562
3614
  description:
3563
- 'FAST context recovery after Claude/Cursor/Copilot context window compaction. ' +
3564
- 'Call this as the FIRST tool call after any context limit hit or window restart — before reading any files. ' +
3565
- 'Returns: last checkpoint (task + files + next_step), session handoff if any, last 5 recalled lessons, open TODOs. ' +
3566
- 'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
3567
- 'Typical use in CLAUDE.md: "If context is compacted, call compact_recover immediately."',
3615
+ 'BM25+ full-text search over ALL brain data: lessons, context entries, session history, CKG nodes, roadmap items. ' +
3616
+ 'Unlike smart_recall (which focuses on lessons + context), brain_search casts a wider net. ' +
3617
+ 'Use when smart_recall returns nothing or when you want to find anything the brain knows about a topic.',
3568
3618
  inputSchema: {
3569
3619
  type: 'object',
3570
3620
  properties: {
3571
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
3572
- focus: {
3573
- type: 'string',
3574
- description: 'What you are trying to recover context for — used to surface relevant lessons (optional)',
3575
- },
3621
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3622
+ query: { type: 'string', description: 'What to search for' },
3623
+ limit: { type: 'number', description: 'Max results (default: 15)' },
3576
3624
  },
3577
- required: ['instance_id'],
3625
+ required: ['instance_id', 'query'],
3578
3626
  },
3579
3627
  },
3580
- // ── v0.9 — brain_from_git ─────────────────────────────────────────────────
3581
3628
  {
3582
- name: 'brain_from_git',
3629
+ name: 'ckg_inspect',
3583
3630
  description:
3584
- 'Bootstrap the AI Brain from git history — reads commit messages and automatically stores lessons. ' +
3585
- 'Scans the last N days of commits, extracts actionable knowledge (fixes, reverts, deploys, migrations), ' +
3586
- 'and bulk-loads them into the Brain as lessons. ' +
3587
- 'Run once to pre-load months of team knowledge before the first session_start. ' +
3588
- 'Skips commits already in the Brain (safe to re-run). ' +
3589
- 'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
3590
- '→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
3631
+ 'Inspect the Causal Knowledge Graph (CKG) for a concept. Shows all typed edges (fixes, requires, co-occurs, causes) ' +
3632
+ 'with Bayesian confidence scores. Use to understand what the brain knows about a topic and which fixes have the ' +
3633
+ 'highest confidence. Also shows related concepts via graph traversal.',
3591
3634
  inputSchema: {
3592
3635
  type: 'object',
3593
3636
  properties: {
3594
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
3595
- workspace_path: {
3596
- type: 'string',
3597
- description: 'Absolute path to the git repository root (e.g. "/home/you/myproject")',
3598
- },
3599
- days: {
3600
- type: 'number',
3601
- description: 'How many days of history to scan (default: 180, max: 365)',
3602
- },
3603
- max_commits: {
3604
- type: 'number',
3605
- description: 'Maximum number of commits to process (default: 200, max: 500)',
3606
- },
3607
- author: {
3608
- type: 'string',
3609
- description: 'Filter to commits by this git author email or name (optional — omit to scan all authors)',
3610
- },
3637
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3638
+ concept: { type: 'string', description: 'Concept to inspect, e.g. "fix:clickhouse-ipv6" or "docker"' },
3639
+ max_hops: { type: 'number', description: 'Traversal depth (default: 2)' },
3640
+ },
3641
+ required: ['instance_id', 'concept'],
3642
+ },
3643
+ },
3644
+ {
3645
+ name: 'brain_predict',
3646
+ description:
3647
+ 'Predictive Pre-fetch Engine (PPE): given your current context (what you\'re working on), traverses the CKG ' +
3648
+ 'to predict likely failures and pre-load relevant fixes. Returns top predicted pitfalls + highest-confidence fixes. ' +
3649
+ 'Call at session_start when working on a specific feature or debugging area.',
3650
+ inputSchema: {
3651
+ type: 'object',
3652
+ properties: {
3653
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3654
+ context: { type: 'string', description: 'What you\'re working on, e.g. "upgrading Keycloak from 21 to 24"' },
3655
+ top_k: { type: 'number', description: 'Max predictions to return (default: 5)' },
3611
3656
  },
3612
- required: ['instance_id', 'workspace_path'],
3657
+ required: ['instance_id', 'context'],
3613
3658
  },
3614
3659
  },
3615
3660
  ] as const;
@@ -4704,6 +4749,51 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4704
4749
  await redis.ltrim(dlKey, -50, -1);
4705
4750
  } catch { /* non-critical */ }
4706
4751
 
4752
+ // ── Layer 1+2: CKG update (Causal Knowledge Graph + Belief Update Engine) ──
4753
+ try {
4754
+ const conceptId = ckgSlug(topic);
4755
+ const domain = topic.split(':')[0] ?? 'unknown';
4756
+ const conceptType = domain; // fix, debug, deploy, infra, api, etc.
4757
+
4758
+ // Upsert concept node
4759
+ await ckgUpsertNode(redis, conceptId, domain, conceptType);
4760
+
4761
+ // Tag co-occurrence edges
4762
+ for (const tag of tags) {
4763
+ const tagId = ckgSlug(`tag:${tag}`);
4764
+ await ckgUpsertNode(redis, tagId, 'tag', 'tag');
4765
+ await ckgUpdateEdge(redis, conceptId, 'co-occurs', tagId, outcome === 'success', outcome === 'partial');
4766
+ }
4767
+
4768
+ // depends_on → requires edges (structural, always confidence 1.0 direction)
4769
+ for (const dep of depends_on) {
4770
+ const depId = ckgSlug(dep);
4771
+ await ckgUpdateEdge(redis, conceptId, 'requires', depId, true);
4772
+ }
4773
+
4774
+ // fixes edge: if category=fix and outcome=success, link to problem concept
4775
+ if ((domain === 'fix' || domain === 'debug') && (outcome === 'success' || outcome === 'partial')) {
4776
+ const problemText = what_failed || ctx || '';
4777
+ const problemConcept = problemText ? extractProblemConcept(problemText) : null;
4778
+ if (problemConcept) {
4779
+ const problemId = ckgSlug(`problem:${problemConcept}`);
4780
+ await ckgUpsertNode(redis, problemId, 'problem', 'problem');
4781
+ await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, outcome === 'success', outcome === 'partial');
4782
+ }
4783
+ }
4784
+
4785
+ // causes edge: if outcome=failure, link topic concept to the problem context
4786
+ if (outcome === 'failure' && (what_failed || what_worked)) {
4787
+ const causeText = what_failed || what_worked;
4788
+ const causeConcept = extractProblemConcept(causeText);
4789
+ if (causeConcept) {
4790
+ const causeId = ckgSlug(`cause:${causeConcept}`);
4791
+ await ckgUpsertNode(redis, causeId, 'cause', 'cause');
4792
+ await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
4793
+ }
4794
+ }
4795
+ } catch { /* CKG updates are non-critical */ }
4796
+
4707
4797
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
4708
4798
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
4709
4799
  const action = isUpdate ? 'updated' : 'stored';
@@ -5224,7 +5314,36 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
5224
5314
  // Brain health
5225
5315
  lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
5226
5316
 
5227
- // Focus-relevant lessons
5317
+ // ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
5318
+ if (lessons.length >= 3) {
5319
+ const domainMap = new Map<string, { total: number; success: number; critical: number }>();
5320
+ for (const l of lessons) {
5321
+ const dom = l.topic.split(':')[0] ?? 'other';
5322
+ if (!domainMap.has(dom)) domainMap.set(dom, { total: 0, success: 0, critical: 0 });
5323
+ const d = domainMap.get(dom)!;
5324
+ d.total++;
5325
+ if (l.outcome === 'success') d.success++;
5326
+ if (l.severity === 'critical') d.critical++;
5327
+ }
5328
+ const sorted = [...domainMap.entries()].sort((a, b) => b[1].total - a[1].total).slice(0, 6);
5329
+ const hasContestedDomains = sorted.some(([, d]) => d.success < d.total * 0.4 && d.total >= 2);
5330
+ if (sorted.length > 0) {
5331
+ lines.push(`🗺️ **Knowledge Coverage:**`);
5332
+ for (const [dom, d] of sorted) {
5333
+ const pct = Math.round((d.success / d.total) * 100);
5334
+ const filled = Math.round(pct / 10);
5335
+ const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
5336
+ const flag = d.critical > 0 ? ' 🔴' : pct < 40 && d.total >= 2 ? ' ⚠️' : '';
5337
+ lines.push(` ${bar} ${dom.padEnd(18)} ${String(pct).padStart(3)}% (${d.success}/${d.total} confirmed)${flag}`);
5338
+ }
5339
+ if (hasContestedDomains) {
5340
+ lines.push(` ⚠️ _Some domains have contested beliefs — use \`ckg_inspect\` to review_`);
5341
+ }
5342
+ lines.push('');
5343
+ }
5344
+ }
5345
+
5346
+
5228
5347
  if (focusLessons.length > 0) {
5229
5348
  lines.push(`🎯 **Relevant for "${focus}":**`);
5230
5349
  for (const l of focusLessons.slice(0, 4)) {
@@ -7396,7 +7515,58 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7396
7515
  .split(/\s+/)
7397
7516
  .filter(t => t.length > 2);
7398
7517
 
7399
- // Scan all lessons + history entries
7518
+ const SEV_ICON: Record<string, string> = { critical: '🔴', major: '🟠', minor: '🟡' };
7519
+
7520
+ // ── Layer 1: CKG graph traversal (if graph data exists) ─────────────────
7521
+ type CKGResult = { conceptId: string; edge: CKGEdge; lesson?: { topic: string; what_worked?: string; ts: string; outcome: string; recall_count?: number; severity?: string } };
7522
+ const ckgResults: CKGResult[] = [];
7523
+ try {
7524
+ for (const token of tokens.slice(0, 4)) {
7525
+ // Search for CKG nodes matching this token
7526
+ const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(token)}`);
7527
+ const toKeys = await redis.smembers(`cachly:ckg:idx:to:${ckgSlug(token)}`);
7528
+ // Also try pattern: scan nodes containing the token
7529
+ const nodeKeys: string[] = [];
7530
+ const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
7531
+ await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
7532
+ for (const nodeKey of nodeKeys.slice(0, 10)) {
7533
+ const nodeRaw = await redis.get(nodeKey);
7534
+ if (!nodeRaw) continue;
7535
+ const node: CKGNode = JSON.parse(nodeRaw);
7536
+ // Get edges from this node
7537
+ const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
7538
+ for (const ek of edgeKeys.slice(0, 20)) {
7539
+ const edgeRaw = await redis.get(ek);
7540
+ if (!edgeRaw) continue;
7541
+ const edge: CKGEdge = JSON.parse(edgeRaw);
7542
+ if (edge.edgeType !== 'fixes' && edge.edgeType !== 'requires') continue;
7543
+ // Find lesson for this concept
7544
+ const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from.replace(/-/g, ':').replace(/^fix:/, 'fix:')}`);
7545
+ const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
7546
+ ckgResults.push({ conceptId: node.id, edge, lesson });
7547
+ }
7548
+ }
7549
+ for (const ek of [...fromKeys, ...toKeys].slice(0, 20)) {
7550
+ const edgeRaw = await redis.get(ek);
7551
+ if (!edgeRaw) continue;
7552
+ const edge: CKGEdge = JSON.parse(edgeRaw);
7553
+ const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
7554
+ const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
7555
+ ckgResults.push({ conceptId: edge.from, edge, lesson });
7556
+ }
7557
+ }
7558
+ } catch { /* CKG traversal non-critical */ }
7559
+
7560
+ // Deduplicate CKG results and sort by confidence
7561
+ const ckgSeen = new Set<string>();
7562
+ const ckgDeduped = ckgResults.filter(r => {
7563
+ const key = `${r.edge.from}:${r.edge.edgeType}:${r.edge.to}`;
7564
+ if (ckgSeen.has(key)) return false;
7565
+ ckgSeen.add(key);
7566
+ return true;
7567
+ }).sort((a, b) => b.edge.confidence - a.edge.confidence).slice(0, max_depth);
7568
+
7569
+ // ── Layer 2 (fallback): text similarity over all lessons ─────────────────
7400
7570
  let cursor = 0;
7401
7571
  const lessonKeys: string[] = [];
7402
7572
  do {
@@ -7418,7 +7588,6 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7418
7588
  if (!raw) continue;
7419
7589
  try {
7420
7590
  const l = JSON.parse(raw) as Lesson;
7421
- // Skip if filter tags given and lesson has none of them
7422
7591
  if (filterTags.length > 0 && !(l.tags ?? []).some((t: string) => filterTags.includes(t))) continue;
7423
7592
  const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? '', l.context ?? '']
7424
7593
  .join(' ').toLowerCase();
@@ -7426,11 +7595,10 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7426
7595
  if (score > 0) scored.push({ score, lesson: l, key: k });
7427
7596
  } catch { /* skip */ }
7428
7597
  }
7429
-
7430
7598
  scored.sort((a, b) => b.score - a.score);
7431
7599
  const chain = scored.slice(0, max_depth);
7432
7600
 
7433
- if (chain.length === 0) {
7601
+ if (chain.length === 0 && ckgDeduped.length === 0) {
7434
7602
  return [
7435
7603
  `🔍 **Causal Trace: "${problem}"**`,
7436
7604
  ``,
@@ -7449,55 +7617,68 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7449
7617
  ].join('\n');
7450
7618
  }
7451
7619
 
7452
- // Build causal chain narrative: failures first → then successes
7453
- const failures = chain.filter(c => c.lesson.outcome !== 'success');
7454
- const solutions = chain.filter(c => c.lesson.outcome === 'success');
7455
-
7456
- const SEV_ICON: Record<string, string> = { critical: '🔴', major: '🟠', minor: '🟡' };
7457
-
7458
7620
  const lines: string[] = [
7459
7621
  `🔍 **Causal Trace: "${problem}"**`,
7460
7622
  ``,
7461
- `Found **${chain.length}** related lessons. Reconstructed causal chain:`,
7462
- ``,
7463
7623
  ];
7464
7624
 
7465
- if (failures.length > 0) {
7466
- lines.push(`**Root causes & failure chain:**`);
7467
- failures.forEach((c, i) => {
7468
- const l = c.lesson;
7469
- const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
7470
- lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
7471
- if (l.what_failed) lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
7472
- });
7473
- lines.push('');
7625
+ // Show CKG graph results first if available
7626
+ if (ckgDeduped.length > 0) {
7627
+ lines.push(`### 🕸️ CKG Graph (confidence-ranked)`);
7628
+ for (const r of ckgDeduped) {
7629
+ const confPct = Math.round(r.edge.confidence * 100);
7630
+ const confBar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
7631
+ lines.push(` ${r.edge.from} **→[${r.edge.edgeType}]→** ${r.edge.to}`);
7632
+ lines.push(` ${confBar} ${confPct}% confidence (${r.edge.successes}/${r.edge.trials} confirmed)`);
7633
+ if (r.lesson?.what_worked) lines.push(` ✅ Fix: ${r.lesson.what_worked.slice(0, 150)}`);
7634
+ lines.push('');
7635
+ }
7474
7636
  }
7475
7637
 
7476
- if (solutions.length > 0) {
7477
- lines.push(`**Solutions that worked before:**`);
7478
- solutions.forEach((c, i) => {
7479
- const l = c.lesson;
7480
- const date = new Date(l.ts).toLocaleDateString('de-DE');
7481
- lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}× `);
7482
- if (l.what_worked) lines.push(` ${l.what_worked.slice(0, 200)}`);
7483
- });
7484
- lines.push('');
7485
- }
7638
+ // Build text-based causal chain narrative
7639
+ if (chain.length > 0) {
7640
+ lines.push(ckgDeduped.length > 0 ? `### 📚 Text Search (${chain.length} related lessons)` : `Found **${chain.length}** related lessons. Reconstructed causal chain:`, '');
7641
+ const failures = chain.filter(c => c.lesson.outcome !== 'success');
7642
+ const solutions = chain.filter(c => c.lesson.outcome === 'success');
7486
7643
 
7487
- const topSolution = solutions[0]?.lesson;
7488
- if (topSolution?.what_worked) {
7489
- lines.push(`**⚡ Most likely fix:**`);
7490
- lines.push(`\`\`\``);
7491
- lines.push(topSolution.what_worked.slice(0, 500));
7492
- lines.push(`\`\`\``);
7493
- lines.push('');
7644
+ if (failures.length > 0) {
7645
+ lines.push(`**Root causes & failure chain:**`);
7646
+ failures.forEach((c, i) => {
7647
+ const l = c.lesson;
7648
+ const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
7649
+ lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
7650
+ if (l.what_failed) lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
7651
+ });
7652
+ lines.push('');
7653
+ }
7654
+
7655
+ if (solutions.length > 0) {
7656
+ lines.push(`**Solutions that worked before:**`);
7657
+ solutions.forEach((c, i) => {
7658
+ const l = c.lesson;
7659
+ const date = new Date(l.ts).toLocaleDateString('de-DE');
7660
+ lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}×`);
7661
+ if (l.what_worked) lines.push(` ${l.what_worked.slice(0, 200)}`);
7662
+ });
7663
+ lines.push('');
7664
+ }
7665
+
7666
+ const topSolution = solutions[0]?.lesson;
7667
+ if (topSolution?.what_worked) {
7668
+ lines.push(`**⚡ Most likely fix:**`);
7669
+ lines.push(`\`\`\``);
7670
+ lines.push(topSolution.what_worked.slice(0, 500));
7671
+ lines.push(`\`\`\``);
7672
+ lines.push('');
7673
+ }
7494
7674
  }
7495
7675
 
7496
7676
  lines.push(`💡 After applying: \`learn_from_attempts(topic="fix:${tokens[0] ?? 'issue'}", outcome="success", ...)\``);
7677
+ if (ckgDeduped.length > 0) lines.push(`🕸️ Explore graph: \`ckg_inspect(concept="${tokens[0] ?? 'fix'}")\``);
7497
7678
  return lines.join('\n');
7498
7679
  }
7499
7680
 
7500
- // ── v0.6 Cognitive Cache: knowledge_decay ────────────────────────────────
7681
+
7501
7682
  case 'knowledge_decay': {
7502
7683
  const { instance_id, min_age_days = 0, show_top = 20 } = args as {
7503
7684
  instance_id: string; min_age_days?: number; show_top?: number;
@@ -7908,194 +8089,218 @@ smart_recall(
7908
8089
  return lines.filter(l => l !== '').join('\n');
7909
8090
  }
7910
8091
 
7911
- // ── v0.9 compact_recover — fast context recovery ──────────────────────────
7912
- case 'compact_recover': {
7913
- const { instance_id, focus = '' } = args as { instance_id: string; focus?: string };
8092
+ // ── Layer 1: brain_search ─────────────────────────────────────────────────
8093
+ case 'brain_search': {
8094
+ const { instance_id, query, limit = 15 } = args as { instance_id: string; query: string; limit?: number };
7914
8095
  const redis = await getConnection(instance_id);
7915
8096
 
7916
- const lines: string[] = ['⚡ **Context Recovered**', ''];
8097
+ // BM25+ over ALL brain key namespaces
8098
+ const allMatches = await keywordSearch(
8099
+ redis,
8100
+ [
8101
+ 'cachly:lesson:best:*',
8102
+ 'cachly:ctx:*',
8103
+ 'cachly:idx:*',
8104
+ 'cachly:session:last',
8105
+ 'cachly:session:handoff',
8106
+ 'cachly:roadmap:*',
8107
+ 'cachly:ckg:node:*',
8108
+ ],
8109
+ query,
8110
+ limit,
8111
+ );
7917
8112
 
7918
- // 1. Latest checkpoint
7919
- const checkpointRaw = await redis.get('cachly:session:checkpoint');
7920
- if (checkpointRaw) {
7921
- try {
7922
- const cp = JSON.parse(checkpointRaw) as {
7923
- ts: string; task: string; files_touched?: string[]; next_step?: string; provider?: string;
7924
- };
7925
- const ago = Math.round((Date.now() - new Date(cp.ts).getTime()) / 60000);
7926
- const agoStr = ago < 60 ? `${ago}m ago` : `${Math.round(ago / 60)}h ago`;
7927
- lines.push(`📌 **Last checkpoint** (${agoStr}): ${cp.task}`);
7928
- if (cp.files_touched && cp.files_touched.length > 0) {
7929
- lines.push(` Files: ${cp.files_touched.slice(0, 5).join(', ')}${cp.files_touched.length > 5 ? '…' : ''}`);
7930
- }
7931
- if (cp.next_step) lines.push(`📍 **Resume with:** ${cp.next_step}`);
7932
- lines.push('');
7933
- } catch { /* ignore corrupt */ }
8113
+ if (allMatches.length === 0) {
8114
+ return [`🔎 **Brain Search: "${query}"**`, '', `No results found across all brain data.`, '', `💡 Try \`smart_recall\` or check \`list_remembered\`.`].join('\n');
7934
8115
  }
7935
8116
 
7936
- // 2. Handoff (if any, from a different window)
7937
- const handoffRaw = await redis.get('cachly:session:handoff');
7938
- if (handoffRaw) {
7939
- try {
7940
- const hf = JSON.parse(handoffRaw) as { ts: string; remaining_tasks?: string[]; blocked_on?: string };
7941
- const remaining = hf.remaining_tasks ?? [];
7942
- if (remaining.length > 0) {
7943
- lines.push(`⏳ **Remaining tasks:**`);
7944
- remaining.slice(0, 5).forEach(t => lines.push(` - ${t}`));
7945
- if (remaining.length > 5) lines.push(` …and ${remaining.length - 5} more`);
7946
- lines.push('');
7947
- }
7948
- if (hf.blocked_on) { lines.push(`🚫 **Blocked on:** ${hf.blocked_on}`, ''); }
7949
- } catch { /* ignore */ }
8117
+ const lines = [`🔎 **Brain Search: "${query}"** — ${allMatches.length} result${allMatches.length !== 1 ? 's' : ''} across all brain data\n`];
8118
+ for (const m of allMatches.slice(0, limit)) {
8119
+ const ns = m.key.startsWith('cachly:lesson:') ? '💡 lesson'
8120
+ : m.key.startsWith('cachly:ctx:') ? '📝 context'
8121
+ : m.key.startsWith('cachly:idx:') ? '📂 index'
8122
+ : m.key.startsWith('cachly:session:') ? '🕐 session'
8123
+ : m.key.startsWith('cachly:roadmap:') ? '🗺️ roadmap'
8124
+ : m.key.startsWith('cachly:ckg:node:') ? '🕸️ ckg-node'
8125
+ : '🗄️ data';
8126
+ const preview = m.content.slice(0, 280).replace(/\n/g, ' ');
8127
+ lines.push(`**${ns}** \`${m.key.split(':').slice(2).join(':')}\` _(BM25: ${m.score.toFixed(2)})_`);
8128
+ lines.push(`> ${preview}${m.content.length > 280 ? '…' : ''}\n`);
7950
8129
  }
8130
+ return lines.join('\n');
8131
+ }
7951
8132
 
7952
- // 3. Top recently recalled lessons (BM25 if focus, else recency)
7953
- const lessonKeys: string[] = [];
7954
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
7955
- await new Promise<void>((resolve, reject) => {
7956
- lStream.on('data', (batch: string[]) => lessonKeys.push(...batch));
7957
- lStream.on('end', resolve);
7958
- lStream.on('error', reject);
7959
- });
8133
+ // ── Layer 1: ckg_inspect ─────────────────────────────────────────────────
8134
+ case 'ckg_inspect': {
8135
+ const { instance_id, concept, max_hops = 2 } = args as { instance_id: string; concept: string; max_hops?: number };
8136
+ const redis = await getConnection(instance_id);
7960
8137
 
7961
- type RecoverLesson = { topic: string; outcome: string; what_worked: string; ts: string; recall_count?: number; tags?: string[] };
7962
- const allLessons: RecoverLesson[] = [];
7963
- for (const k of lessonKeys) {
7964
- const raw = await redis.get(k);
7965
- if (!raw) continue;
7966
- try { allLessons.push(JSON.parse(raw) as RecoverLesson); } catch { /* skip */ }
8138
+ const conceptId = ckgSlug(concept);
8139
+ const visited = new Set<string>();
8140
+ const allEdges: CKGEdge[] = [];
8141
+
8142
+ // BFS traversal of CKG
8143
+ const queue: Array<{ id: string; hop: number }> = [{ id: conceptId, hop: 0 }];
8144
+ while (queue.length > 0) {
8145
+ const { id, hop } = queue.shift()!;
8146
+ if (visited.has(id) || hop > max_hops) continue;
8147
+ visited.add(id);
8148
+
8149
+ const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${id}`);
8150
+ const toKeys = await redis.smembers(`cachly:ckg:idx:to:${id}`);
8151
+
8152
+ for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
8153
+ const raw = await redis.get(ek);
8154
+ if (!raw) continue;
8155
+ const edge: CKGEdge = JSON.parse(raw);
8156
+ allEdges.push(edge);
8157
+ if (hop < max_hops) {
8158
+ if (!visited.has(edge.from)) queue.push({ id: edge.from, hop: hop + 1 });
8159
+ if (!visited.has(edge.to)) queue.push({ id: edge.to, hop: hop + 1 });
8160
+ }
8161
+ }
7967
8162
  }
7968
8163
 
7969
- const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
7970
- let topLessons = focusTerms.length > 0
7971
- ? allLessons.filter(l =>
7972
- focusTerms.some(t => l.topic.toLowerCase().includes(t) || (l.tags ?? []).some((tag: string) => tag.toLowerCase().includes(t)))
7973
- ).slice(0, 5)
7974
- : allLessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0)).slice(0, 5);
7975
-
7976
- if (topLessons.length > 0) {
7977
- lines.push(`🧠 **Top lessons${focus ? ` for "${focus}"` : ''}:**`);
7978
- for (const l of topLessons) {
7979
- const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
7980
- lines.push(` ${icon} \`${l.topic}\` — ${l.what_worked.slice(0, 120)}${l.what_worked.length > 120 ? '…' : ''}`);
8164
+ if (allEdges.length === 0) {
8165
+ // Try fuzzy: scan for nodes matching the concept as substring
8166
+ const nodeKeys: string[] = [];
8167
+ const nStream = redis.scanStream({ match: `cachly:ckg:node:*${conceptId}*`, count: 100 });
8168
+ await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
8169
+ if (nodeKeys.length === 0) {
8170
+ return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No CKG nodes found. The graph builds automatically as you call \`learn_from_attempts\`.`, '', `💡 Once you have lessons stored, CKG edges will appear here.`].join('\n');
8171
+ }
8172
+ const nodeList = nodeKeys.slice(0, 10).map(k => ` • \`${k.replace('cachly:ckg:node:', '')}\``).join('\n');
8173
+ return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No edges found for \`${conceptId}\`, but found similar nodes:`, nodeList, '', `Try: \`ckg_inspect(concept="<exact-node-id>")\``].join('\n');
8174
+ }
8175
+
8176
+ // Sort by confidence desc, deduplicate
8177
+ const edgeSeen = new Set<string>();
8178
+ const unique = allEdges.filter(e => {
8179
+ const k = `${e.from}:${e.edgeType}:${e.to}`;
8180
+ if (edgeSeen.has(k)) return false;
8181
+ edgeSeen.add(k);
8182
+ return true;
8183
+ }).sort((a, b) => b.confidence - a.confidence);
8184
+
8185
+ const EDGE_ICON: Record<string, string> = { fixes: '🔧', requires: '🔗', 'co-occurs': '🔄', causes: '⚡', contradicts: '⚠️', degrades_under: '📉' };
8186
+
8187
+ const lines = [`🕸️ **CKG Inspect: "${concept}"** (${unique.length} edge${unique.length !== 1 ? 's' : ''}, ${visited.size} node${visited.size !== 1 ? 's' : ''} traversed)\n`];
8188
+
8189
+ // Group by edge type
8190
+ const byType = new Map<string, CKGEdge[]>();
8191
+ for (const e of unique) {
8192
+ if (!byType.has(e.edgeType)) byType.set(e.edgeType, []);
8193
+ byType.get(e.edgeType)!.push(e);
8194
+ }
8195
+ for (const [eType, edges] of byType) {
8196
+ const icon = EDGE_ICON[eType] ?? '→';
8197
+ lines.push(`**${icon} ${eType}** (${edges.length})`);
8198
+ for (const e of edges.slice(0, 8)) {
8199
+ const confPct = Math.round(e.confidence * 100);
8200
+ const bar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
8201
+ lines.push(` \`${e.from}\` → \`${e.to}\` ${bar} ${confPct}% (${e.successes.toFixed(1)}/${e.trials} trials)`);
7981
8202
  }
7982
8203
  lines.push('');
7983
8204
  }
7984
8205
 
7985
- lines.push(`💡 Call \`session_start\` for the full briefing, or resume work directly.`);
7986
- return lines.filter(l => l !== '').join('\n');
8206
+ lines.push(`💡 Expand: \`ckg_inspect(concept="${concept}", max_hops=3)\` | Predict: \`brain_predict(context="${concept}")\``);
8207
+ return lines.join('\n');
7987
8208
  }
7988
8209
 
7989
- // ── v0.9 brain_from_git — bulk-load git history as lessons ───────────────
7990
- case 'brain_from_git': {
7991
- const {
7992
- instance_id,
7993
- workspace_path,
7994
- days = 180,
7995
- max_commits = 200,
7996
- author = '',
7997
- } = args as { instance_id: string; workspace_path: string; days?: number; max_commits?: number; author?: string };
7998
-
8210
+ // ── Layer 4: brain_predict (PPE) ─────────────────────────────────────────
8211
+ case 'brain_predict': {
8212
+ const { instance_id, context: ctx, top_k = 5 } = args as { instance_id: string; context: string; top_k?: number };
7999
8213
  const redis = await getConnection(instance_id);
8000
- const { execSync } = await import('node:child_process');
8001
-
8002
- const clampedDays = Math.min(Math.max(1, days), 365);
8003
- const clampedMax = Math.min(Math.max(1, max_commits), 500);
8004
8214
 
8005
- const authorArg = author ? `--author="${author}"` : '';
8006
- let gitRaw = '';
8007
- try {
8008
- gitRaw = execSync(
8009
- `git -C "${workspace_path}" log --since="${clampedDays} days ago" ${authorArg} --oneline --format="%H|||%s|||%ai|||%ae" -n ${clampedMax}`,
8010
- { timeout: 15000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
8011
- ).trim();
8012
- } catch {
8013
- return `❌ **brain_from_git failed**: Could not read git log from \`${workspace_path}\`.\nMake sure the path exists and is a git repository.`;
8215
+ const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:]/g, ' ').split(/\s+/).filter(t => t.length > 2);
8216
+
8217
+ // Step 1: Find CKG nodes matching context tokens
8218
+ type Prediction = { concept: string; edgeType: string; target: string; confidence: number; lesson?: { what_worked?: string; topic: string } };
8219
+ const predictions: Prediction[] = [];
8220
+
8221
+ for (const token of ctxTokens.slice(0, 6)) {
8222
+ const nodeKeys: string[] = [];
8223
+ const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
8224
+ await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
8225
+
8226
+ for (const nk of nodeKeys.slice(0, 5)) {
8227
+ const nodeRaw = await redis.get(nk);
8228
+ if (!nodeRaw) continue;
8229
+ const node: CKGNode = JSON.parse(nodeRaw);
8230
+ const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
8231
+ for (const ek of edgeKeys.slice(0, 20)) {
8232
+ const edgeRaw = await redis.get(ek);
8233
+ if (!edgeRaw) continue;
8234
+ const edge: CKGEdge = JSON.parse(edgeRaw);
8235
+ // Only interested in fixes and co-occurs for prediction
8236
+ if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes') continue;
8237
+ const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
8238
+ const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
8239
+ predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
8240
+ }
8241
+ }
8014
8242
  }
8015
8243
 
8016
- if (!gitRaw) {
8017
- return `📭 **No commits found** in the last ${clampedDays} days${author ? ` by "${author}"` : ''}.\nTry increasing \`days\` or removing the \`author\` filter.`;
8244
+ // Step 2: Text-based fallback — scan lessons for matching topics
8245
+ const textPredictions: Array<{ topic: string; what_worked?: string; what_failed?: string; outcome: string; severity?: string; confidence: number }> = [];
8246
+ const lessonKeys: string[] = [];
8247
+ const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
8248
+ await new Promise<void>((res, rej) => { lStream.on('data', (b: string[]) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
8249
+ for (const k of lessonKeys) {
8250
+ const raw = await redis.get(k);
8251
+ if (!raw) continue;
8252
+ try {
8253
+ const l = JSON.parse(raw) as { topic: string; what_worked?: string; what_failed?: string; outcome: string; severity?: string; ts: string; verified_at?: string; recall_count?: number };
8254
+ const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
8255
+ const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
8256
+ if (score >= 1 && l.outcome !== 'failure') {
8257
+ const conf = calculateConfidence(l);
8258
+ textPredictions.push({ ...l, confidence: conf });
8259
+ }
8260
+ } catch { /* skip */ }
8018
8261
  }
8262
+ textPredictions.sort((a, b) => b.confidence - a.confidence);
8019
8263
 
8020
- const commitLines = gitRaw.split('\n');
8021
- const now = new Date();
8264
+ if (predictions.length === 0 && textPredictions.length === 0) {
8265
+ return [
8266
+ `🔮 **Brain Predict: "${ctx}"**`,
8267
+ ``,
8268
+ `No predictions yet — the brain hasn't seen this domain.`,
8269
+ ``,
8270
+ `💡 As you solve problems in this area and call \`learn_from_attempts\`, the CKG builds up and predictions become available.`,
8271
+ ].join('\n');
8272
+ }
8022
8273
 
8023
- const actionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|rewrite|deploy|feat|revert|break|merge|bump)\b/i;
8024
- const failRe = /\b(revert|broken|broke|breaking|rollback|undo|hotfix|crash|oops|wrong|mistake|bad)\b/i;
8274
+ const lines = [`🔮 **Brain Predict: "${ctx}"**\n`];
8025
8275
 
8026
- let stored = 0;
8027
- let skipped = 0;
8028
- let failures = 0;
8029
- const topLessons: string[] = [];
8030
-
8031
- for (const line of commitLines) {
8032
- const parts = line.split('|||');
8033
- if (parts.length < 2) continue;
8034
- const [hash, msg, dateStr] = parts;
8035
- if (!msg || !actionRe.test(msg)) continue;
8036
-
8037
- const isFail = failRe.test(msg);
8038
- const cleanMsg = msg
8039
- .replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert|hotfix|bump)[:(\s!]+/i, '')
8040
- .trim();
8041
- if (!cleanMsg || cleanMsg.length < 5) continue;
8042
-
8043
- const slug = cleanMsg
8044
- .toLowerCase()
8045
- .replace(/[^a-z0-9]+/g, '-')
8046
- .replace(/^-+|-+$/g, '')
8047
- .slice(0, 40);
8048
- if (!slug) continue;
8276
+ // CKG-based predictions
8277
+ if (predictions.length > 0) {
8278
+ const pSeen = new Set<string>();
8279
+ const pUniq = predictions.filter(p => { const k = `${p.concept}:${p.edgeType}:${p.target}`; if (pSeen.has(k)) return false; pSeen.add(k); return true; })
8280
+ .sort((a, b) => b.confidence - a.confidence).slice(0, top_k);
8049
8281
 
8050
- const topic = `git:${slug}`;
8051
- const key = `cachly:lesson:best:${topic}`;
8052
- const existing = await redis.get(key);
8053
- if (existing) { skipped++; continue; }
8054
-
8055
- const lesson = {
8056
- topic,
8057
- outcome: isFail ? ('failure' as const) : ('success' as const),
8058
- what_worked: isFail ? '' : cleanMsg.slice(0, 300),
8059
- what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
8060
- context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
8061
- severity: 'minor' as const,
8062
- ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
8063
- recall_count: 0,
8064
- auto_learned: true,
8065
- source: 'brain_from_git',
8066
- version: 3,
8067
- };
8068
-
8069
- await redis.set(key, JSON.stringify(lesson));
8070
- await redis.expire(key, 90 * 86400); // 90-day TTL for git-sourced lessons
8071
- stored++;
8072
- if (isFail) failures++;
8073
- if (topLessons.length < 5) topLessons.push(` ${isFail ? '❌' : '✅'} \`${topic}\` — ${cleanMsg.slice(0, 80)}`);
8282
+ lines.push(`### 🕸️ CKG Predictions (based on ${pUniq.length} known edges)`);
8283
+ for (const p of pUniq) {
8284
+ const confPct = Math.round(p.confidence * 100);
8285
+ const icon = p.edgeType === 'fixes' ? '🔧' : p.edgeType === 'co-occurs' ? '🔄' : '⚡';
8286
+ lines.push(`${icon} **${confPct}%** \`${p.concept}\` _${p.edgeType}_ \`${p.target}\``);
8287
+ if (p.lesson?.what_worked) lines.push(` ✅ ${p.lesson.what_worked.slice(0, 120)}`);
8288
+ }
8289
+ lines.push('');
8074
8290
  }
8075
8291
 
8076
- const lines: string[] = [
8077
- `🧠 **Brain bootstrapped from git history**`,
8078
- ``,
8079
- `📊 **Scanned:** ${commitLines.filter(l => l.trim()).length} commits over last ${clampedDays} days`,
8080
- `✅ **Success lessons stored:** ${stored - failures}`,
8081
- `❌ **Failure/revert lessons:** ${failures}`,
8082
- skipped > 0 ? `⏭️ **Already in brain (skipped):** ${skipped}` : '',
8083
- ``,
8084
- ];
8085
-
8086
- if (topLessons.length > 0) {
8087
- lines.push(`**Top lessons extracted:**`);
8088
- lines.push(...topLessons);
8089
- if (stored > 5) lines.push(` …and ${stored - 5} more`);
8292
+ // Text-based lesson predictions
8293
+ if (textPredictions.length > 0) {
8294
+ lines.push(`### 📚 Relevant Lessons (${Math.min(textPredictions.length, top_k)} pre-loaded)`);
8295
+ for (const l of textPredictions.slice(0, top_k)) {
8296
+ const confPct = Math.round(l.confidence * 100);
8297
+ lines.push(` ✅ **${confPct}%** \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 120)}`);
8298
+ }
8090
8299
  lines.push('');
8091
8300
  }
8092
8301
 
8093
- lines.push(
8094
- `💡 **Next step:** Call \`session_start(focus="...")\` to see these lessons in your briefing.`,
8095
- `🔄 **Re-run anytime** — duplicates are safely skipped.`,
8096
- );
8097
-
8098
- return lines.filter(l => l !== '').join('\n');
8302
+ lines.push(`💡 Outcome confirmed? \`learn_from_attempts(topic="fix:...", outcome="success", ...)\` → improves future predictions`);
8303
+ return lines.join('\n');
8099
8304
  }
8100
8305
 
8101
8306
  default: