@cachly-dev/mcp-server 0.9.6 → 0.9.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
@@ -47,7 +47,7 @@ import { Redis } from 'ioredis';
47
47
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
48
48
  let JWT = process.env.CACHLY_JWT ?? '';
49
49
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
50
- const CURRENT_VERSION = '0.9.6';
50
+ const CURRENT_VERSION = '0.9.7';
51
51
  // ── Default Instance Resolution (for Smithery & single-credential setups) ────
52
52
  // When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
53
53
  // When neither is set, we auto-fetch the first running instance once per process.
@@ -3804,6 +3804,68 @@ const TOOLS = [
3804
3804
  required: ['instance_id'],
3805
3805
  },
3806
3806
  },
3807
+ {
3808
+ name: 'crystal_view',
3809
+ description: 'Inspect the current Memory Crystal — the compressed wisdom distilled from all past sessions. ' +
3810
+ 'Shows top patterns per category, lesson count, and when the crystal was last refreshed. ' +
3811
+ 'Call after session_start when you want to quickly see accumulated wisdom across all past work.',
3812
+ inputSchema: {
3813
+ type: 'object',
3814
+ properties: {
3815
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3816
+ show_raw: { type: 'boolean', description: 'Include raw JSON crystal data (default: false)' },
3817
+ },
3818
+ required: ['instance_id'],
3819
+ },
3820
+ },
3821
+ {
3822
+ name: 'compact_recover',
3823
+ description: 'Call FIRST after any context limit hit / compaction. Reconstructs full context from Memory Crystal + ' +
3824
+ 'recent sessions + WIP registry + open failures. Returns a condensed briefing so the new context ' +
3825
+ 'window starts exactly where the previous one left off — no lost progress.',
3826
+ inputSchema: {
3827
+ type: 'object',
3828
+ properties: {
3829
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3830
+ focus: { type: 'string', description: 'What you were working on (helps filter relevant context)' },
3831
+ },
3832
+ required: ['instance_id'],
3833
+ },
3834
+ },
3835
+ {
3836
+ name: 'brain_from_git',
3837
+ description: 'Bootstrap brain lessons from git history. Parses commit messages and infers fix/feature/refactor ' +
3838
+ 'lessons automatically. Great for onboarding an existing codebase — run once and the brain instantly ' +
3839
+ 'knows your team\'s accumulated patterns. Supports limit and branch options.',
3840
+ inputSchema: {
3841
+ type: 'object',
3842
+ properties: {
3843
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3844
+ repo_path: { type: 'string', description: 'Path to git repository (default: current directory)' },
3845
+ limit: { type: 'number', description: 'Max commits to process (default: 100, max: 500)' },
3846
+ branch: { type: 'string', description: 'Git branch to parse (default: current branch / HEAD)' },
3847
+ since: { type: 'string', description: 'Only commits after this date, e.g. "2024-01-01" (optional)' },
3848
+ },
3849
+ required: ['instance_id'],
3850
+ },
3851
+ },
3852
+ {
3853
+ name: 'brain_predict_failures',
3854
+ description: 'Pre-deploy failure prediction with probability percentages. Given a change context (e.g. ' +
3855
+ '"upgrading Keycloak 21→24" or "deploying Redis 7 to prod"), returns the top likely failure modes ' +
3856
+ 'ranked by probability, with pre-loaded fixes. Uses CKG causal edges + lesson history. ' +
3857
+ 'Call before any significant deploy, migration, or infrastructure change.',
3858
+ inputSchema: {
3859
+ type: 'object',
3860
+ properties: {
3861
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3862
+ context: { type: 'string', description: 'What you are about to do, e.g. "upgrading Keycloak 21 to 24"' },
3863
+ top_k: { type: 'number', description: 'Number of failure predictions to return (default: 5)' },
3864
+ format: { type: 'string', enum: ['brief', 'detailed'], description: 'Output format (default: detailed)' },
3865
+ },
3866
+ required: ['instance_id', 'context'],
3867
+ },
3868
+ },
3807
3869
  ];
3808
3870
  // ── Handlers ──────────────────────────────────────────────────────────────────
3809
3871
  function formatInstance(inst) {
@@ -8442,6 +8504,387 @@ smart_recall(
8442
8504
  lines.push('', '---', `**Contribute:** \`fedbrain_contribute(lesson_key="fix:...")\``, `**Search:** \`fedbrain_search(query="...")\``, `**Confirm:** \`fedbrain_confirm(topic="...", outcome="worked")\``);
8443
8505
  return lines.join('\n');
8444
8506
  }
8507
+ // ── crystal_view ──────────────────────────────────────────────────────────
8508
+ case 'crystal_view': {
8509
+ const { instance_id, show_raw = false } = args;
8510
+ const redis = await getConnection(instance_id);
8511
+ const raw = await redis.get('cachly:crystal:latest');
8512
+ if (!raw) {
8513
+ return [
8514
+ `💎 **Memory Crystal: not yet created**`, '',
8515
+ `No crystal found. Create one with \`memory_crystalize()\` to compress your accumulated wisdom.`,
8516
+ '', `💡 Tip: run \`memory_crystalize\` monthly for best results.`,
8517
+ ].join('\n');
8518
+ }
8519
+ const crystal = JSON.parse(raw);
8520
+ const age = Math.floor((Date.now() - new Date(crystal.ts).getTime()) / 86400000);
8521
+ const freshEmoji = age <= 7 ? '🟢' : age <= 30 ? '🟡' : '🔴';
8522
+ const lines = [
8523
+ `💎 **Memory Crystal: ${crystal.label}**`, '',
8524
+ `📅 Created: ${new Date(crystal.ts).toLocaleDateString('de-DE')} (${age}d ago ${freshEmoji})`,
8525
+ `📊 Compressed from: ${crystal.created_from}`,
8526
+ `🗂️ Categories: ${crystal.categories.slice(0, 10).map(c => `\`${c}\``).join(', ')}${crystal.categories.length > 10 ? ` +${crystal.categories.length - 10} more` : ''}`,
8527
+ '',
8528
+ `**🔑 Top patterns (${crystal.top_patterns.length}):**`,
8529
+ ];
8530
+ for (const p of crystal.top_patterns) {
8531
+ lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 110)}`);
8532
+ }
8533
+ if (age > 30) {
8534
+ lines.push('', `⚠️ Crystal is ${age}d old — run \`memory_crystalize()\` to refresh it.`);
8535
+ }
8536
+ if (show_raw) {
8537
+ lines.push('', '```json', JSON.stringify(crystal, null, 2), '```');
8538
+ }
8539
+ lines.push('', `💡 Refresh: \`memory_crystalize()\` | Recover: \`compact_recover(instance_id="...")\``);
8540
+ return lines.join('\n');
8541
+ }
8542
+ // ── compact_recover ───────────────────────────────────────────────────────
8543
+ case 'compact_recover': {
8544
+ const { instance_id, focus = '' } = args;
8545
+ const redis = await getConnection(instance_id);
8546
+ const lines = [`🔁 **Compact Recovery Briefing**\n`];
8547
+ lines.push(`> *Call this first after any context limit hit. Reconstructs where you left off.*\n`);
8548
+ // 1. Memory Crystal
8549
+ const crystalRaw = await redis.get('cachly:crystal:latest');
8550
+ if (crystalRaw) {
8551
+ const crystal = JSON.parse(crystalRaw);
8552
+ lines.push(`### 💎 Memory Crystal: ${crystal.label}`);
8553
+ lines.push(`Compressed from ${crystal.session_count} sessions, ${crystal.lesson_count} lessons.`);
8554
+ const topN = focus
8555
+ ? crystal.top_patterns.filter(p => p.category.toLowerCase().includes(focus.toLowerCase()) || p.insight.toLowerCase().includes(focus.toLowerCase())).slice(0, 4)
8556
+ : crystal.top_patterns.slice(0, 4);
8557
+ for (const p of topN)
8558
+ lines.push(` • **${p.category}**: ${p.insight.slice(0, 100)}`);
8559
+ lines.push('');
8560
+ }
8561
+ // 2. Last session summary
8562
+ const lastSession = await redis.get('cachly:session:last');
8563
+ if (lastSession) {
8564
+ const sess = JSON.parse(lastSession);
8565
+ lines.push(`### 🕐 Last Session`);
8566
+ if (sess.focus)
8567
+ lines.push(`Focus: _${sess.focus}_`);
8568
+ if (sess.summary)
8569
+ lines.push(`Summary: ${sess.summary.slice(0, 300)}`);
8570
+ lines.push('');
8571
+ }
8572
+ // 3. Session handoff
8573
+ const handoff = await redis.get('cachly:session:handoff');
8574
+ if (handoff) {
8575
+ const h = JSON.parse(handoff);
8576
+ lines.push(`### 📋 Handoff (from last window)`);
8577
+ if (h.context_summary)
8578
+ lines.push(`Context: ${h.context_summary.slice(0, 200)}`);
8579
+ if (h.remaining_tasks?.length) {
8580
+ lines.push(`Remaining tasks:`);
8581
+ for (const t of h.remaining_tasks.slice(0, 5))
8582
+ lines.push(` • ${t}`);
8583
+ }
8584
+ if (h.instructions)
8585
+ lines.push(`⚠️ Instructions: ${h.instructions.slice(0, 200)}`);
8586
+ if (h.blocked_on)
8587
+ lines.push(`🚧 Blocked on: ${h.blocked_on}`);
8588
+ lines.push('');
8589
+ }
8590
+ // 4. WIP registry
8591
+ const wipRaw = await redis.get('cachly:ctx:wip-registry');
8592
+ if (wipRaw) {
8593
+ const wip = JSON.parse(wipRaw);
8594
+ if (wip.content) {
8595
+ lines.push(`### 🔧 WIP Registry`);
8596
+ lines.push(wip.content.slice(0, 400));
8597
+ lines.push('');
8598
+ }
8599
+ }
8600
+ // 5. Open failures (roadmap with status=blocked/in_progress)
8601
+ const roadmapKeys = [];
8602
+ const rStream = redis.scanStream({ match: 'cachly:roadmap:*', count: 100 });
8603
+ await new Promise((res, rej) => { rStream.on('data', (b) => roadmapKeys.push(...b)); rStream.on('end', res); rStream.on('error', rej); });
8604
+ const openItems = [];
8605
+ for (const k of roadmapKeys.slice(0, 30)) {
8606
+ const r = await redis.get(k);
8607
+ if (!r)
8608
+ continue;
8609
+ try {
8610
+ const item = JSON.parse(r);
8611
+ if (item.status === 'in_progress' || item.status === 'blocked')
8612
+ openItems.push({ title: item.title ?? k, status: item.status, priority: item.priority });
8613
+ }
8614
+ catch { /* skip */ }
8615
+ }
8616
+ if (openItems.length > 0) {
8617
+ lines.push(`### 🚧 Open Items`);
8618
+ for (const i of openItems.slice(0, 5))
8619
+ lines.push(` • [${i.status}] ${i.title}`);
8620
+ lines.push('');
8621
+ }
8622
+ // 6. Focus-relevant lessons
8623
+ if (focus) {
8624
+ const lessonKeys = [];
8625
+ const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
8626
+ await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
8627
+ const relevant = [];
8628
+ for (const k of lessonKeys) {
8629
+ const r = await redis.get(k);
8630
+ if (!r)
8631
+ continue;
8632
+ try {
8633
+ const l = JSON.parse(r);
8634
+ if (l.topic.toLowerCase().includes(focus.toLowerCase()) && l.what_worked) {
8635
+ relevant.push({ topic: l.topic, what_worked: l.what_worked });
8636
+ }
8637
+ }
8638
+ catch { /* skip */ }
8639
+ }
8640
+ if (relevant.length > 0) {
8641
+ lines.push(`### 💡 Relevant Lessons for "${focus}"`);
8642
+ for (const l of relevant.slice(0, 4))
8643
+ lines.push(` • **${l.topic}**: ${l.what_worked.slice(0, 100)}`);
8644
+ lines.push('');
8645
+ }
8646
+ }
8647
+ if (lines.length <= 3) {
8648
+ lines.push(`_No brain data found. Start accumulating knowledge with \`learn_from_attempts\` and \`session_start\`._`);
8649
+ }
8650
+ lines.push(`---`, `🧠 Brain is ready. Continue your work — full context restored.`);
8651
+ return lines.join('\n');
8652
+ }
8653
+ // ── brain_from_git ────────────────────────────────────────────────────────
8654
+ case 'brain_from_git': {
8655
+ const { instance_id, repo_path = '.', limit = 100, branch = 'HEAD', since = '' } = args;
8656
+ const redis = await getConnection(instance_id);
8657
+ const { execSync } = await import('node:child_process');
8658
+ const { resolve } = await import('node:path');
8659
+ const repoDir = resolve(repo_path);
8660
+ const maxCommits = Math.min(Number(limit) || 100, 500);
8661
+ // Verify it's a git repo
8662
+ try {
8663
+ execSync('git rev-parse --git-dir', { cwd: repoDir, stdio: 'pipe' });
8664
+ }
8665
+ catch {
8666
+ return `❌ Not a git repository: \`${repoDir}\`. Pass \`repo_path\` pointing to a git checkout.`;
8667
+ }
8668
+ // Build git log command
8669
+ const sinceFlag = since ? `--since="${since}"` : '';
8670
+ const logCmd = `git log ${branch} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
8671
+ let logOutput = '';
8672
+ try {
8673
+ logOutput = execSync(logCmd, { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
8674
+ }
8675
+ catch (e) {
8676
+ return `❌ git log failed: ${e.message}. Check \`repo_path\` and \`branch\`.`;
8677
+ }
8678
+ const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
8679
+ const [sha, subject, date, author] = line.split('|||');
8680
+ return { sha: (sha ?? '').trim(), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
8681
+ });
8682
+ if (commits.length === 0) {
8683
+ return `⚠️ No commits found in \`${repoDir}\` on branch \`${branch}\`${since ? ` since ${since}` : ''}.`;
8684
+ }
8685
+ // Pattern classifiers
8686
+ const classifyCommit = (subject) => {
8687
+ const s = subject.toLowerCase();
8688
+ if (/\b(fix|fixed|fixes|bug|hotfix|patch|revert|resolve|closes? #\d+)\b/.test(s)) {
8689
+ const sev = /\b(critical|crash|security|auth|data loss|outage|prod|production)\b/.test(s) ? 'critical' : /\b(major|breaking|regression|hotfix)\b/.test(s) ? 'major' : 'minor';
8690
+ return { category: 'fix', outcome: 'success', severity: sev };
8691
+ }
8692
+ if (/\b(feat|feature|add|added|implement|new|introduce)\b/.test(s))
8693
+ return { category: 'feat', outcome: 'success', severity: 'minor' };
8694
+ if (/\b(refactor|clean|cleanup|improve|simplify|extract|rename)\b/.test(s))
8695
+ return { category: 'refactor', outcome: 'success', severity: 'minor' };
8696
+ if (/\b(perf|optimize|speed|cache|latency|memory|performance)\b/.test(s))
8697
+ return { category: 'perf', outcome: 'success', severity: 'major' };
8698
+ if (/\b(security|cve|auth|csrf|xss|sql|injection|sanitize|escape|encrypt)\b/.test(s))
8699
+ return { category: 'security', outcome: 'success', severity: 'critical' };
8700
+ if (/\b(deploy|ci|cd|build|docker|k8s|helm|infra|devops)\b/.test(s))
8701
+ return { category: 'deploy', outcome: 'success', severity: 'major' };
8702
+ if (/\b(test|spec|coverage|assert|mock|unit|integration)\b/.test(s))
8703
+ return { category: 'test', outcome: 'success', severity: 'minor' };
8704
+ return { category: 'chore', outcome: 'success', severity: 'minor' };
8705
+ };
8706
+ // Extract domain keywords from commit subject
8707
+ const extractDomain = (subject) => {
8708
+ const s = subject.toLowerCase();
8709
+ const tokens = s.replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !['that', 'this', 'with', 'from', 'when', 'into', 'also', 'some', 'were'].includes(t));
8710
+ return tokens.slice(0, 3).join('-') || 'general';
8711
+ };
8712
+ const ts = new Date().toISOString();
8713
+ let ingested = 0;
8714
+ let skipped = 0;
8715
+ const categoryCount = new Map();
8716
+ for (const commit of commits) {
8717
+ if (!commit.subject) {
8718
+ skipped++;
8719
+ continue;
8720
+ }
8721
+ const { category, outcome, severity } = classifyCommit(commit.subject);
8722
+ const domain = extractDomain(commit.subject);
8723
+ const topic = `${category}:${domain}`;
8724
+ categoryCount.set(category, (categoryCount.get(category) ?? 0) + 1);
8725
+ const lessonObj = {
8726
+ topic, outcome, severity,
8727
+ what_worked: commit.subject.slice(0, 200),
8728
+ what_failed: '',
8729
+ context: `git:${commit.sha.slice(0, 8)} by ${commit.author} on ${commit.date}`,
8730
+ file_paths: [], commands: [`git show ${commit.sha.slice(0, 8)}`],
8731
+ tags: ['brain_from_git', category, 'git-history'],
8732
+ depends_on: [], recall_count: 0, ts, verified_at: ts,
8733
+ confidence: 0.55, // lower confidence for auto-inferred lessons
8734
+ audit_trail: [{ ts, action: 'brain_from_git', sha: commit.sha.slice(0, 8) }],
8735
+ version: 3,
8736
+ };
8737
+ // Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
8738
+ const existing = await redis.get(`cachly:lesson:best:${topic}`);
8739
+ if (!existing) {
8740
+ await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj));
8741
+ await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
8742
+ }
8743
+ await redis.rpush('cachly:lessons:brain_from_git:all', JSON.stringify({ topic, sha: commit.sha.slice(0, 8), subject: commit.subject.slice(0, 60) }));
8744
+ await redis.ltrim('cachly:lessons:brain_from_git:all', -500, -1);
8745
+ // Update CKG
8746
+ const conceptId = ckgSlug(topic);
8747
+ await ckgUpsertNode(redis, conceptId, category, 'git-derived');
8748
+ ingested++;
8749
+ }
8750
+ const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
8751
+ return [
8752
+ `🔁 **brain_from_git: ${repoDir}**`, '',
8753
+ `📂 Branch: \`${branch}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
8754
+ ``,
8755
+ `**Breakdown by category:**`,
8756
+ categoryBreakdown,
8757
+ ``,
8758
+ `💡 New lessons are stored with confidence 0.55 (auto-inferred).`,
8759
+ `💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
8760
+ `🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
8761
+ ].join('\n');
8762
+ }
8763
+ // ── brain_predict_failures ─────────────────────────────────────────────────
8764
+ case 'brain_predict_failures': {
8765
+ const { instance_id, context: ctx, top_k = 5, format = 'detailed' } = args;
8766
+ const redis = await getConnection(instance_id);
8767
+ const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:.]/g, ' ').split(/\s+/).filter(t => t.length > 2);
8768
+ const failures = [];
8769
+ // Step 1: CKG — find 'causes' and 'degrades_under' edges from context tokens
8770
+ for (const token of ctxTokens.slice(0, 8)) {
8771
+ const nodeKeys = [];
8772
+ const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
8773
+ await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
8774
+ for (const nk of nodeKeys.slice(0, 5)) {
8775
+ const nodeRaw = await redis.get(nk);
8776
+ if (!nodeRaw)
8777
+ continue;
8778
+ const node = JSON.parse(nodeRaw);
8779
+ const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
8780
+ for (const ek of edgeKeys.slice(0, 20)) {
8781
+ const edgeRaw = await redis.get(ek);
8782
+ if (!edgeRaw)
8783
+ continue;
8784
+ const edge = JSON.parse(edgeRaw);
8785
+ if (edge.edgeType !== 'causes' && edge.edgeType !== 'degrades_under')
8786
+ continue;
8787
+ // Look up fix for this failure from CKG 'fixes' edges
8788
+ const fixEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${edge.to}`);
8789
+ let fix;
8790
+ for (const fek of fixEdgeKeys.slice(0, 10)) {
8791
+ const feRaw = await redis.get(fek);
8792
+ if (!feRaw)
8793
+ continue;
8794
+ const fe = JSON.parse(feRaw);
8795
+ if (fe.edgeType === 'fixes') {
8796
+ const lessonRaw = await redis.get(`cachly:lesson:best:${fe.from}`);
8797
+ if (lessonRaw) {
8798
+ const lesson = JSON.parse(lessonRaw);
8799
+ fix = lesson.what_worked?.slice(0, 120);
8800
+ break;
8801
+ }
8802
+ }
8803
+ }
8804
+ failures.push({
8805
+ concept: node.id,
8806
+ failure: edge.to.replace(/-/g, ' '),
8807
+ probability: edge.confidence,
8808
+ fix,
8809
+ source: 'ckg',
8810
+ });
8811
+ }
8812
+ }
8813
+ }
8814
+ // Step 2: Lesson history — find failure-outcome lessons matching context
8815
+ const lessonKeys = [];
8816
+ const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 300 });
8817
+ await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
8818
+ for (const k of lessonKeys.slice(0, 100)) {
8819
+ const r = await redis.get(k);
8820
+ if (!r)
8821
+ continue;
8822
+ try {
8823
+ const l = JSON.parse(r);
8824
+ if (l.outcome !== 'failure' && l.outcome !== 'partial')
8825
+ continue;
8826
+ if (!l.what_failed)
8827
+ continue;
8828
+ const haystack = `${l.topic} ${l.what_failed}`.toLowerCase();
8829
+ const matchScore = ctxTokens.filter(t => haystack.includes(t)).length / Math.max(1, ctxTokens.length);
8830
+ if (matchScore < 0.15)
8831
+ continue;
8832
+ const sevBoost = l.severity === 'critical' ? 0.15 : l.severity === 'major' ? 0.05 : 0;
8833
+ failures.push({
8834
+ concept: l.topic,
8835
+ failure: l.what_failed.slice(0, 80),
8836
+ probability: Math.min(0.97, (l.confidence ?? 0.5) * matchScore * 1.5 + sevBoost),
8837
+ fix: l.what_worked?.slice(0, 120),
8838
+ topic: l.topic,
8839
+ source: 'lesson',
8840
+ });
8841
+ }
8842
+ catch { /* skip */ }
8843
+ }
8844
+ if (failures.length === 0) {
8845
+ return [
8846
+ `🔮 **Failure Prediction: "${ctx}"**`, '',
8847
+ `No known failure patterns found for this context.`,
8848
+ `💡 The brain learns from every \`learn_from_attempts(outcome="failure")\` call.`,
8849
+ `🔍 Try: \`brain_predict(context="${ctx}")\` for broader predictions.`,
8850
+ ].join('\n');
8851
+ }
8852
+ // Deduplicate and rank by probability
8853
+ const seen = new Set();
8854
+ const ranked = failures.filter(f => {
8855
+ const k = f.failure.slice(0, 40);
8856
+ if (seen.has(k))
8857
+ return false;
8858
+ seen.add(k);
8859
+ return true;
8860
+ }).sort((a, b) => b.probability - a.probability).slice(0, Number(top_k));
8861
+ const lines = [`🔮 **Failure Prediction for: "${ctx}"**\n`];
8862
+ lines.push(`> Pre-deploy failure analysis based on ${failures.length} patterns. Ranked by probability.\n`);
8863
+ for (let i = 0; i < ranked.length; i++) {
8864
+ const f = ranked[i];
8865
+ const pct = Math.round(f.probability * 100);
8866
+ const bar = '█'.repeat(Math.round(pct / 10)) + '░'.repeat(10 - Math.round(pct / 10));
8867
+ const icon = pct >= 70 ? '🔴' : pct >= 40 ? '🟡' : '🟢';
8868
+ lines.push(`${icon} **${i + 1}. ${f.failure}**`);
8869
+ lines.push(` Probability: ${bar} **${pct}%** (${f.source === 'ckg' ? 'CKG causal edge' : 'lesson history'})`);
8870
+ if (format === 'detailed' && f.fix) {
8871
+ lines.push(` ✅ Pre-loaded fix: _${f.fix}_`);
8872
+ }
8873
+ if (format === 'detailed' && f.topic) {
8874
+ lines.push(` 📚 Lesson: \`${f.topic}\``);
8875
+ }
8876
+ lines.push('');
8877
+ }
8878
+ const highRisk = ranked.filter(f => f.probability >= 0.6);
8879
+ if (highRisk.length > 0) {
8880
+ lines.push(`⚠️ **${highRisk.length} high-risk failure${highRisk.length > 1 ? 's' : ''} detected** (≥60% probability). Review fixes before proceeding.`);
8881
+ }
8882
+ else {
8883
+ lines.push(`✅ No high-risk failures detected. Proceed with caution and monitor closely.`);
8884
+ }
8885
+ lines.push('', `💡 After deploy: \`learn_from_attempts(topic="deploy:...", outcome="success|failure")\` to improve future predictions.`);
8886
+ return lines.join('\n');
8887
+ }
8445
8888
  default:
8446
8889
  return null;
8447
8890
  }