@cachly-dev/mcp-server 0.9.1 → 0.9.3

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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from 'node:crypto';
3
- import { createHash } from 'node:crypto';
3
+ import { createHash, createHmac } from 'node:crypto';
4
4
  import { readdir, readFile, stat } from 'node:fs/promises';
5
5
  import { readFileSync } from 'node:fs';
6
6
  import { execSync } from 'node:child_process';
@@ -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.3';
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 = [
@@ -3037,22 +3089,6 @@ const TOOLS = [
3037
3089
  required: ['instance_id'],
3038
3090
  },
3039
3091
  },
3040
- // ── crystal_view — inspect current Memory Crystal ──────────────────────
3041
- {
3042
- name: 'crystal_view',
3043
- description:
3044
- 'View the current Memory Crystal — the distilled snapshot of everything your AI brain has learned. ' +
3045
- 'Shows all top patterns grouped by category, lesson count, crystal age, and when to refresh. ' +
3046
- 'A crystal is created by memory_crystalize and injected into every session_start automatically. ' +
3047
- 'Call this to review what institutional knowledge is preserved, or to decide if a refresh is due.',
3048
- inputSchema: {
3049
- type: 'object',
3050
- properties: {
3051
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
3052
- },
3053
- required: ['instance_id'],
3054
- },
3055
- },
3056
3092
  // ── Roadmap — Persistent project plan tracker ───────────────────────────
3057
3093
  {
3058
3094
  name: 'roadmap_add',
@@ -3572,60 +3608,196 @@ const TOOLS = [
3572
3608
  required: [],
3573
3609
  },
3574
3610
  },
3575
- // ── v0.9 — compact_recover ─────────────────────────────────────────────────
3611
+ // ── Layer 1: Causal Knowledge Graph ────────────────────────────────────────
3576
3612
  {
3577
- name: 'compact_recover',
3613
+ name: 'brain_search',
3578
3614
  description:
3579
- 'FAST context recovery after Claude/Cursor/Copilot context window compaction. ' +
3580
- 'Call this as the FIRST tool call after any context limit hit or window restart — before reading any files. ' +
3581
- 'Returns: last checkpoint (task + files + next_step), session handoff if any, last 5 recalled lessons, open TODOs. ' +
3582
- 'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
3583
- '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.',
3584
3618
  inputSchema: {
3585
3619
  type: 'object',
3586
3620
  properties: {
3587
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
3588
- focus: {
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)' },
3624
+ },
3625
+ required: ['instance_id', 'query'],
3626
+ },
3627
+ },
3628
+ {
3629
+ name: 'ckg_inspect',
3630
+ description:
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.',
3634
+ inputSchema: {
3635
+ type: 'object',
3636
+ properties: {
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)' },
3656
+ },
3657
+ required: ['instance_id', 'context'],
3658
+ },
3659
+ },
3660
+ // ── Layer 3: MADC ────────────────────────────────────────────────────────
3661
+ {
3662
+ name: 'madc_deliberate',
3663
+ description:
3664
+ 'Multi-Agent Deliberation Chamber (MADC — Layer 3): When conflicting lessons exist for a topic, ' +
3665
+ 'run deliberation between 6 specialist expert agents (InfraAgent, AuthAgent, DeployAgent, DatabaseAgent, DebugAgent, APIAgent). ' +
3666
+ 'Each agent votes based on its domain CKG coverage. Unanimous vote → loser superseded. ' +
3667
+ 'Split vote → contested flag, causal_trace required before acting. ' +
3668
+ 'Resolution stored as permanent CKG node. Called automatically when learn_from_attempts detects a contradiction.',
3669
+ inputSchema: {
3670
+ type: 'object',
3671
+ properties: {
3672
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3673
+ topic: { type: 'string', description: 'Topic to deliberate, e.g. "fix:jwks-rotation"' },
3674
+ context: { type: 'string', description: 'Optional context for the deliberation' },
3675
+ },
3676
+ required: ['instance_id', 'topic'],
3677
+ },
3678
+ },
3679
+ // ── Layer 5: CLS ─────────────────────────────────────────────────────────
3680
+ {
3681
+ name: 'cls_ingest',
3682
+ description:
3683
+ 'Continuous Learning Stream (CLS — Layer 5): Ingest learning signals WITHOUT explicit session_end calls. ' +
3684
+ 'Sources: git_commit (commit message + files → CKG edges), ci_outcome (green/red build → confirms fix), ' +
3685
+ 'ide_diagnostic (compiler error + fix pair → instant lesson). ' +
3686
+ 'Install automatic ingestion with cls_install_hooks — brain learns from every commit and CI run.',
3687
+ inputSchema: {
3688
+ type: 'object',
3689
+ properties: {
3690
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3691
+ source: {
3589
3692
  type: 'string',
3590
- description: 'What you are trying to recover context for — used to surface relevant lessons (optional)',
3693
+ enum: ['git_commit', 'ci_outcome', 'ide_diagnostic'],
3694
+ description: 'Event source type',
3695
+ },
3696
+ payload: {
3697
+ type: 'object',
3698
+ description:
3699
+ 'Event data. git_commit: {message, sha?, files?, diff?}. ' +
3700
+ 'ci_outcome: {status, prev_status, job, context?}. ' +
3701
+ 'ide_diagnostic: {error, fix, file?}',
3702
+ },
3703
+ },
3704
+ required: ['instance_id', 'source', 'payload'],
3705
+ },
3706
+ },
3707
+ {
3708
+ name: 'cls_install_hooks',
3709
+ description:
3710
+ 'Output a ready-to-install git post-commit hook + GitHub Actions step for Continuous Learning. ' +
3711
+ 'Once installed, every git commit and CI build automatically feeds the brain — no session_end needed. ' +
3712
+ 'Run once per repository.',
3713
+ inputSchema: {
3714
+ type: 'object',
3715
+ properties: {
3716
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3717
+ repo_path: { type: 'string', description: 'Path to repo root (default: current dir)' },
3718
+ hooks: {
3719
+ type: 'array',
3720
+ items: { type: 'string', enum: ['git', 'ci'] },
3721
+ description: 'Which hooks to output (default: ["git", "ci"])',
3591
3722
  },
3592
3723
  },
3593
3724
  required: ['instance_id'],
3594
3725
  },
3595
3726
  },
3596
- // ── v0.9 — brain_from_git ─────────────────────────────────────────────────
3727
+ // ── Layer 6: FedBrain ────────────────────────────────────────────────────
3597
3728
  {
3598
- name: 'brain_from_git',
3729
+ name: 'fedbrain_contribute',
3599
3730
  description:
3600
- 'Bootstrap the AI Brain from git history — reads commit messages and automatically stores lessons. ' +
3601
- 'Scans the last N days of commits, extracts actionable knowledge (fixes, reverts, deploys, migrations), ' +
3602
- 'and bulk-loads them into the Brain as lessons. ' +
3603
- 'Run once to pre-load months of team knowledge before the first session_start. ' +
3604
- 'Skips commits already in the Brain (safe to re-run). ' +
3605
- 'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
3606
- '→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
3731
+ 'FedBrain (Layer 6): Contribute a lesson to the global Knowledge Commons with a cryptographic ' +
3732
+ 'knowledge certificate. Certificate includes: domain fingerprint, confidence, outcome chain hash. ' +
3733
+ 'Lessons with 10+ independent confirmations become Gold Standard. Context-weighted: ' +
3734
+ 'other brains with similar tech stacks see your lesson ranked higher in fedbrain_search.',
3607
3735
  inputSchema: {
3608
3736
  type: 'object',
3609
3737
  properties: {
3610
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
3611
- workspace_path: {
3738
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3739
+ lesson_key: { type: 'string', description: 'Topic key to contribute, e.g. "fix:clickhouse-ipv6"' },
3740
+ visibility: {
3612
3741
  type: 'string',
3613
- description: 'Absolute path to the git repository root (e.g. "/home/you/myproject")',
3742
+ enum: ['public', 'org_private'],
3743
+ description: 'Visibility (default: public)',
3614
3744
  },
3615
- days: {
3616
- type: 'number',
3617
- description: 'How many days of history to scan (default: 180, max: 365)',
3618
- },
3619
- max_commits: {
3620
- type: 'number',
3621
- description: 'Maximum number of commits to process (default: 200, max: 500)',
3745
+ },
3746
+ required: ['instance_id', 'lesson_key'],
3747
+ },
3748
+ },
3749
+ {
3750
+ name: 'fedbrain_search',
3751
+ description:
3752
+ 'FedBrain context-weighted search: Search the global commons, weighting results by tech-stack similarity. ' +
3753
+ 'Brains with matching domain context (Go/Kubernetes/Postgres) rank higher than unrelated stacks. ' +
3754
+ 'Shows certificate provenance, confirm_count, and Gold Standard badges.',
3755
+ inputSchema: {
3756
+ type: 'object',
3757
+ properties: {
3758
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3759
+ query: { type: 'string', description: 'What to search for' },
3760
+ context_hints: {
3761
+ type: 'array',
3762
+ items: { type: 'string' },
3763
+ description: 'Your tech stack, e.g. ["go", "kubernetes", "postgres"]',
3622
3764
  },
3623
- author: {
3765
+ limit: { type: 'number', description: 'Max results (default: 10)' },
3766
+ },
3767
+ required: ['instance_id', 'query'],
3768
+ },
3769
+ },
3770
+ {
3771
+ name: 'fedbrain_confirm',
3772
+ description:
3773
+ 'Confirm that a syndicated lesson from the global commons worked for you. ' +
3774
+ 'Propagates confirmation back — increments confirm_count on the knowledge certificate. ' +
3775
+ 'Also updates your local CKG confidence. At 10 independent confirmations → Gold Standard.',
3776
+ inputSchema: {
3777
+ type: 'object',
3778
+ properties: {
3779
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3780
+ topic: { type: 'string', description: 'Topic of the lesson to confirm' },
3781
+ outcome: {
3624
3782
  type: 'string',
3625
- description: 'Filter to commits by this git author email or name (optional — omit to scan all authors)',
3783
+ enum: ['worked', 'partially_worked', 'did_not_work'],
3784
+ description: 'Did the lesson work for you?',
3626
3785
  },
3627
3786
  },
3628
- required: ['instance_id', 'workspace_path'],
3787
+ required: ['instance_id', 'topic', 'outcome'],
3788
+ },
3789
+ },
3790
+ {
3791
+ name: 'fedbrain_status',
3792
+ description:
3793
+ 'Show your FedBrain federation status: lessons contributed to global commons, recent confirmations, ' +
3794
+ 'Gold Standard lessons, pending propagations. Use to track your brain\'s global knowledge contribution.',
3795
+ inputSchema: {
3796
+ type: 'object',
3797
+ properties: {
3798
+ instance_id: { type: 'string', description: 'Brain instance ID' },
3799
+ },
3800
+ required: ['instance_id'],
3629
3801
  },
3630
3802
  },
3631
3803
  ] as const;
@@ -4633,6 +4805,13 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4633
4805
  if (contradictionWarning.length > 0) {
4634
4806
  // Store contradiction audit but don't block
4635
4807
  auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
4808
+ // Layer 3: Write CKG contradicts edge for MADC to process
4809
+ try {
4810
+ const cId = ckgSlug(topic);
4811
+ const resId = ckgSlug(`resolution:${topic}`);
4812
+ await ckgUpdateEdge(redis, cId, 'contradicts', resId, false);
4813
+ } catch { /* non-critical */ }
4814
+ contradictionWarning.push(`🗳️ Run \`madc_deliberate(topic="${topic}")\` to resolve via expert agent voting.`);
4636
4815
  }
4637
4816
  } catch { /* ignore parse error */ }
4638
4817
  } else {
@@ -4720,6 +4899,51 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4720
4899
  await redis.ltrim(dlKey, -50, -1);
4721
4900
  } catch { /* non-critical */ }
4722
4901
 
4902
+ // ── Layer 1+2: CKG update (Causal Knowledge Graph + Belief Update Engine) ──
4903
+ try {
4904
+ const conceptId = ckgSlug(topic);
4905
+ const domain = topic.split(':')[0] ?? 'unknown';
4906
+ const conceptType = domain; // fix, debug, deploy, infra, api, etc.
4907
+
4908
+ // Upsert concept node
4909
+ await ckgUpsertNode(redis, conceptId, domain, conceptType);
4910
+
4911
+ // Tag co-occurrence edges
4912
+ for (const tag of tags) {
4913
+ const tagId = ckgSlug(`tag:${tag}`);
4914
+ await ckgUpsertNode(redis, tagId, 'tag', 'tag');
4915
+ await ckgUpdateEdge(redis, conceptId, 'co-occurs', tagId, outcome === 'success', outcome === 'partial');
4916
+ }
4917
+
4918
+ // depends_on → requires edges (structural, always confidence 1.0 direction)
4919
+ for (const dep of depends_on) {
4920
+ const depId = ckgSlug(dep);
4921
+ await ckgUpdateEdge(redis, conceptId, 'requires', depId, true);
4922
+ }
4923
+
4924
+ // fixes edge: if category=fix and outcome=success, link to problem concept
4925
+ if ((domain === 'fix' || domain === 'debug') && (outcome === 'success' || outcome === 'partial')) {
4926
+ const problemText = what_failed || ctx || '';
4927
+ const problemConcept = problemText ? extractProblemConcept(problemText) : null;
4928
+ if (problemConcept) {
4929
+ const problemId = ckgSlug(`problem:${problemConcept}`);
4930
+ await ckgUpsertNode(redis, problemId, 'problem', 'problem');
4931
+ await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, outcome === 'success', outcome === 'partial');
4932
+ }
4933
+ }
4934
+
4935
+ // causes edge: if outcome=failure, link topic concept to the problem context
4936
+ if (outcome === 'failure' && (what_failed || what_worked)) {
4937
+ const causeText = what_failed || what_worked;
4938
+ const causeConcept = extractProblemConcept(causeText);
4939
+ if (causeConcept) {
4940
+ const causeId = ckgSlug(`cause:${causeConcept}`);
4941
+ await ckgUpsertNode(redis, causeId, 'cause', 'cause');
4942
+ await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
4943
+ }
4944
+ }
4945
+ } catch { /* CKG updates are non-critical */ }
4946
+
4723
4947
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
4724
4948
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
4725
4949
  const action = isUpdate ? 'updated' : 'stored';
@@ -5240,7 +5464,36 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
5240
5464
  // Brain health
5241
5465
  lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
5242
5466
 
5243
- // Focus-relevant lessons
5467
+ // ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
5468
+ if (lessons.length >= 3) {
5469
+ const domainMap = new Map<string, { total: number; success: number; critical: number }>();
5470
+ for (const l of lessons) {
5471
+ const dom = l.topic.split(':')[0] ?? 'other';
5472
+ if (!domainMap.has(dom)) domainMap.set(dom, { total: 0, success: 0, critical: 0 });
5473
+ const d = domainMap.get(dom)!;
5474
+ d.total++;
5475
+ if (l.outcome === 'success') d.success++;
5476
+ if (l.severity === 'critical') d.critical++;
5477
+ }
5478
+ const sorted = [...domainMap.entries()].sort((a, b) => b[1].total - a[1].total).slice(0, 6);
5479
+ const hasContestedDomains = sorted.some(([, d]) => d.success < d.total * 0.4 && d.total >= 2);
5480
+ if (sorted.length > 0) {
5481
+ lines.push(`🗺️ **Knowledge Coverage:**`);
5482
+ for (const [dom, d] of sorted) {
5483
+ const pct = Math.round((d.success / d.total) * 100);
5484
+ const filled = Math.round(pct / 10);
5485
+ const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
5486
+ const flag = d.critical > 0 ? ' 🔴' : pct < 40 && d.total >= 2 ? ' ⚠️' : '';
5487
+ lines.push(` ${bar} ${dom.padEnd(18)} ${String(pct).padStart(3)}% (${d.success}/${d.total} confirmed)${flag}`);
5488
+ }
5489
+ if (hasContestedDomains) {
5490
+ lines.push(` ⚠️ _Some domains have contested beliefs — use \`ckg_inspect\` to review_`);
5491
+ }
5492
+ lines.push('');
5493
+ }
5494
+ }
5495
+
5496
+
5244
5497
  if (focusLessons.length > 0) {
5245
5498
  lines.push(`🎯 **Relevant for "${focus}":**`);
5246
5499
  for (const l of focusLessons.slice(0, 4)) {
@@ -5605,21 +5858,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
5605
5858
  } catch { /* git not available or not a repo — silent skip */ }
5606
5859
  }
5607
5860
 
5608
- // ── Crystal age check — remind if > 30 days old or missing ──────────────
5609
- let crystalReminder = '';
5610
- try {
5611
- const crystalRaw = await redis.get('cachly:crystal:latest');
5612
- if (!crystalRaw) {
5613
- crystalReminder = `💎 **Memory Crystal:** None exists yet. Run \`memory_crystalize(instance_id="${instance_id}")\` to distill your brain into a permanent snapshot — injected into every future session automatically.`;
5614
- } else {
5615
- const crystal = JSON.parse(crystalRaw) as { ts: string; label?: string; lesson_count?: number };
5616
- const crystalAgeDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
5617
- if (crystalAgeDays >= 30) {
5618
- crystalReminder = `💎 **Memory Crystal is ${crystalAgeDays} days old** (${crystal.label ?? 'unnamed'}). Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh it with your latest ${crystal.lesson_count ?? '?'} lessons.`;
5619
- }
5620
- }
5621
- } catch { /* non-critical */ }
5622
-
5623
5861
  const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
5624
5862
  return [
5625
5863
  `✅ **Session saved**${durationStr}`,
@@ -5629,7 +5867,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
5629
5867
  lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
5630
5868
  autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
5631
5869
  ambientLearned.length > 0 ? `🌿 **Ambient git learning:** ${ambientLearned.length} commit${ambientLearned.length > 1 ? 's' : ''} auto-learned (${ambientLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${ambientLearned.length > 3 ? '…' : ''})` : '',
5632
- crystalReminder,
5633
5870
  ``,
5634
5871
  `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
5635
5872
  ].filter(l => l !== '').join('\n');
@@ -6109,63 +6346,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
6109
6346
  return lines.join('\n');
6110
6347
  }
6111
6348
 
6112
- // ── crystal_view — show current Memory Crystal ───────────────────────────
6113
- case 'crystal_view': {
6114
- const { instance_id } = args as { instance_id: string };
6115
- const redis = await getConnection(instance_id);
6116
-
6117
- const crystalRaw = await redis.get('cachly:crystal:latest');
6118
- if (!crystalRaw) {
6119
- return [
6120
- `💎 **No Memory Crystal yet**`,
6121
- ``,
6122
- `Your brain has no crystal — all knowledge lives in raw lessons only.`,
6123
- ``,
6124
- `Run \`memory_crystalize(instance_id="${instance_id}")\` to:`,
6125
- ` • Distill everything learned into a permanent, compact snapshot`,
6126
- ` • Inject it into every future \`session_start\` automatically`,
6127
- ` • Preserve team knowledge even after lesson TTLs expire`,
6128
- ``,
6129
- `💡 Recommended: run monthly or after any major milestone.`,
6130
- ].join('\n');
6131
- }
6132
-
6133
- const crystal = JSON.parse(crystalRaw) as {
6134
- label: string; ts: string; session_count: number; lesson_count: number;
6135
- top_patterns: Array<{ category: string; insight: string; count: number }>;
6136
- categories: string[]; created_from: string;
6137
- };
6138
-
6139
- const ageDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
6140
- const ageStr = ageDays === 0 ? 'today' : ageDays === 1 ? '1 day ago' : `${ageDays} days ago`;
6141
- const freshness = ageDays < 14 ? '🟢 Fresh' : ageDays < 30 ? '🟡 Aging' : '🔴 Stale — refresh recommended';
6142
-
6143
- const lines: string[] = [
6144
- `💎 **Memory Crystal: ${crystal.label}**`,
6145
- ``,
6146
- `📅 Created: **${ageStr}** · Freshness: ${freshness}`,
6147
- `📊 Compressed: **${crystal.session_count} sessions** + **${crystal.lesson_count} lessons** → ${crystal.top_patterns.length} patterns`,
6148
- `🗂️ Categories: ${crystal.categories.slice(0, 10).join(', ')}${crystal.categories.length > 10 ? `… +${crystal.categories.length - 10}` : ''}`,
6149
- ``,
6150
- `**Top patterns by category:**`,
6151
- ``,
6152
- ];
6153
-
6154
- for (const p of crystal.top_patterns) {
6155
- lines.push(`**${p.category}** (${p.count} lesson${p.count > 1 ? 's' : ''})`);
6156
- lines.push(` → ${p.insight.slice(0, 150)}${p.insight.length > 150 ? '…' : ''}`);
6157
- lines.push('');
6158
- }
6159
-
6160
- if (ageDays >= 30) {
6161
- lines.push(`---`, `⚠️ Crystal is ${ageDays} days old. Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh with your latest lessons.`);
6162
- } else {
6163
- lines.push(`---`, `💡 This crystal is injected automatically into every \`session_start\`. Refresh monthly with \`memory_crystalize\`.`);
6164
- }
6165
-
6166
- return lines.filter(l => l !== undefined).join('\n');
6167
- }
6168
-
6169
6349
  case 'brain_doctor': {
6170
6350
  const { instance_id, workspace_path: drWorkspacePath = '' } = args as { instance_id: string; workspace_path?: string };
6171
6351
  const redis = await getConnection(instance_id);
@@ -7485,7 +7665,58 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7485
7665
  .split(/\s+/)
7486
7666
  .filter(t => t.length > 2);
7487
7667
 
7488
- // Scan all lessons + history entries
7668
+ const SEV_ICON: Record<string, string> = { critical: '🔴', major: '🟠', minor: '🟡' };
7669
+
7670
+ // ── Layer 1: CKG graph traversal (if graph data exists) ─────────────────
7671
+ type CKGResult = { conceptId: string; edge: CKGEdge; lesson?: { topic: string; what_worked?: string; ts: string; outcome: string; recall_count?: number; severity?: string } };
7672
+ const ckgResults: CKGResult[] = [];
7673
+ try {
7674
+ for (const token of tokens.slice(0, 4)) {
7675
+ // Search for CKG nodes matching this token
7676
+ const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(token)}`);
7677
+ const toKeys = await redis.smembers(`cachly:ckg:idx:to:${ckgSlug(token)}`);
7678
+ // Also try pattern: scan nodes containing the token
7679
+ const nodeKeys: string[] = [];
7680
+ const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
7681
+ await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
7682
+ for (const nodeKey of nodeKeys.slice(0, 10)) {
7683
+ const nodeRaw = await redis.get(nodeKey);
7684
+ if (!nodeRaw) continue;
7685
+ const node: CKGNode = JSON.parse(nodeRaw);
7686
+ // Get edges from this node
7687
+ const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
7688
+ for (const ek of edgeKeys.slice(0, 20)) {
7689
+ const edgeRaw = await redis.get(ek);
7690
+ if (!edgeRaw) continue;
7691
+ const edge: CKGEdge = JSON.parse(edgeRaw);
7692
+ if (edge.edgeType !== 'fixes' && edge.edgeType !== 'requires') continue;
7693
+ // Find lesson for this concept
7694
+ const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from.replace(/-/g, ':').replace(/^fix:/, 'fix:')}`);
7695
+ const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
7696
+ ckgResults.push({ conceptId: node.id, edge, lesson });
7697
+ }
7698
+ }
7699
+ for (const ek of [...fromKeys, ...toKeys].slice(0, 20)) {
7700
+ const edgeRaw = await redis.get(ek);
7701
+ if (!edgeRaw) continue;
7702
+ const edge: CKGEdge = JSON.parse(edgeRaw);
7703
+ const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
7704
+ const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
7705
+ ckgResults.push({ conceptId: edge.from, edge, lesson });
7706
+ }
7707
+ }
7708
+ } catch { /* CKG traversal non-critical */ }
7709
+
7710
+ // Deduplicate CKG results and sort by confidence
7711
+ const ckgSeen = new Set<string>();
7712
+ const ckgDeduped = ckgResults.filter(r => {
7713
+ const key = `${r.edge.from}:${r.edge.edgeType}:${r.edge.to}`;
7714
+ if (ckgSeen.has(key)) return false;
7715
+ ckgSeen.add(key);
7716
+ return true;
7717
+ }).sort((a, b) => b.edge.confidence - a.edge.confidence).slice(0, max_depth);
7718
+
7719
+ // ── Layer 2 (fallback): text similarity over all lessons ─────────────────
7489
7720
  let cursor = 0;
7490
7721
  const lessonKeys: string[] = [];
7491
7722
  do {
@@ -7507,7 +7738,6 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7507
7738
  if (!raw) continue;
7508
7739
  try {
7509
7740
  const l = JSON.parse(raw) as Lesson;
7510
- // Skip if filter tags given and lesson has none of them
7511
7741
  if (filterTags.length > 0 && !(l.tags ?? []).some((t: string) => filterTags.includes(t))) continue;
7512
7742
  const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? '', l.context ?? '']
7513
7743
  .join(' ').toLowerCase();
@@ -7515,11 +7745,10 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7515
7745
  if (score > 0) scored.push({ score, lesson: l, key: k });
7516
7746
  } catch { /* skip */ }
7517
7747
  }
7518
-
7519
7748
  scored.sort((a, b) => b.score - a.score);
7520
7749
  const chain = scored.slice(0, max_depth);
7521
7750
 
7522
- if (chain.length === 0) {
7751
+ if (chain.length === 0 && ckgDeduped.length === 0) {
7523
7752
  return [
7524
7753
  `🔍 **Causal Trace: "${problem}"**`,
7525
7754
  ``,
@@ -7538,55 +7767,68 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7538
7767
  ].join('\n');
7539
7768
  }
7540
7769
 
7541
- // Build causal chain narrative: failures first → then successes
7542
- const failures = chain.filter(c => c.lesson.outcome !== 'success');
7543
- const solutions = chain.filter(c => c.lesson.outcome === 'success');
7544
-
7545
- const SEV_ICON: Record<string, string> = { critical: '🔴', major: '🟠', minor: '🟡' };
7546
-
7547
7770
  const lines: string[] = [
7548
7771
  `🔍 **Causal Trace: "${problem}"**`,
7549
7772
  ``,
7550
- `Found **${chain.length}** related lessons. Reconstructed causal chain:`,
7551
- ``,
7552
7773
  ];
7553
7774
 
7554
- if (failures.length > 0) {
7555
- lines.push(`**Root causes & failure chain:**`);
7556
- failures.forEach((c, i) => {
7557
- const l = c.lesson;
7558
- const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
7559
- lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
7560
- if (l.what_failed) lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
7561
- });
7562
- lines.push('');
7775
+ // Show CKG graph results first if available
7776
+ if (ckgDeduped.length > 0) {
7777
+ lines.push(`### 🕸️ CKG Graph (confidence-ranked)`);
7778
+ for (const r of ckgDeduped) {
7779
+ const confPct = Math.round(r.edge.confidence * 100);
7780
+ const confBar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
7781
+ lines.push(` ${r.edge.from} **→[${r.edge.edgeType}]→** ${r.edge.to}`);
7782
+ lines.push(` ${confBar} ${confPct}% confidence (${r.edge.successes}/${r.edge.trials} confirmed)`);
7783
+ if (r.lesson?.what_worked) lines.push(` ✅ Fix: ${r.lesson.what_worked.slice(0, 150)}`);
7784
+ lines.push('');
7785
+ }
7563
7786
  }
7564
7787
 
7565
- if (solutions.length > 0) {
7566
- lines.push(`**Solutions that worked before:**`);
7567
- solutions.forEach((c, i) => {
7568
- const l = c.lesson;
7569
- const date = new Date(l.ts).toLocaleDateString('de-DE');
7570
- lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}× `);
7571
- if (l.what_worked) lines.push(` ${l.what_worked.slice(0, 200)}`);
7572
- });
7573
- lines.push('');
7574
- }
7788
+ // Build text-based causal chain narrative
7789
+ if (chain.length > 0) {
7790
+ lines.push(ckgDeduped.length > 0 ? `### 📚 Text Search (${chain.length} related lessons)` : `Found **${chain.length}** related lessons. Reconstructed causal chain:`, '');
7791
+ const failures = chain.filter(c => c.lesson.outcome !== 'success');
7792
+ const solutions = chain.filter(c => c.lesson.outcome === 'success');
7575
7793
 
7576
- const topSolution = solutions[0]?.lesson;
7577
- if (topSolution?.what_worked) {
7578
- lines.push(`**⚡ Most likely fix:**`);
7579
- lines.push(`\`\`\``);
7580
- lines.push(topSolution.what_worked.slice(0, 500));
7581
- lines.push(`\`\`\``);
7582
- lines.push('');
7794
+ if (failures.length > 0) {
7795
+ lines.push(`**Root causes & failure chain:**`);
7796
+ failures.forEach((c, i) => {
7797
+ const l = c.lesson;
7798
+ const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
7799
+ lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
7800
+ if (l.what_failed) lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
7801
+ });
7802
+ lines.push('');
7803
+ }
7804
+
7805
+ if (solutions.length > 0) {
7806
+ lines.push(`**Solutions that worked before:**`);
7807
+ solutions.forEach((c, i) => {
7808
+ const l = c.lesson;
7809
+ const date = new Date(l.ts).toLocaleDateString('de-DE');
7810
+ lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}×`);
7811
+ if (l.what_worked) lines.push(` ${l.what_worked.slice(0, 200)}`);
7812
+ });
7813
+ lines.push('');
7814
+ }
7815
+
7816
+ const topSolution = solutions[0]?.lesson;
7817
+ if (topSolution?.what_worked) {
7818
+ lines.push(`**⚡ Most likely fix:**`);
7819
+ lines.push(`\`\`\``);
7820
+ lines.push(topSolution.what_worked.slice(0, 500));
7821
+ lines.push(`\`\`\``);
7822
+ lines.push('');
7823
+ }
7583
7824
  }
7584
7825
 
7585
7826
  lines.push(`💡 After applying: \`learn_from_attempts(topic="fix:${tokens[0] ?? 'issue'}", outcome="success", ...)\``);
7827
+ if (ckgDeduped.length > 0) lines.push(`🕸️ Explore graph: \`ckg_inspect(concept="${tokens[0] ?? 'fix'}")\``);
7586
7828
  return lines.join('\n');
7587
7829
  }
7588
7830
 
7589
- // ── v0.6 Cognitive Cache: knowledge_decay ────────────────────────────────
7831
+
7590
7832
  case 'knowledge_decay': {
7591
7833
  const { instance_id, min_age_days = 0, show_top = 20 } = args as {
7592
7834
  instance_id: string; min_age_days?: number; show_top?: number;
@@ -7682,11 +7924,9 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7682
7924
  > Sessions are fully automatic. The Brain starts when you call any tool and saves a summary when you close.
7683
7925
 
7684
7926
  ### MANDATORY hooks (always fire, no exceptions):
7685
- - **Context compacted** → call \`compact_recover(instance_id="${instance_id}", focus="<current task>")\` FIRST
7686
7927
  - **Window opens** → call \`session_start(instance_id="${instance_id}", focus="<today's goal>")\`
7687
7928
  - **Bug fixed / deployed / discovered** → call \`learn_from_attempts(instance_id="${instance_id}", topic="fix:<area>", outcome="success", what_worked="...")\`
7688
7929
  - **Window closes** → call \`session_end(instance_id="${instance_id}", summary="...", files_changed=[...])\`
7689
- - **Monthly** → call \`memory_crystalize(instance_id="${instance_id}")\` to distill knowledge into a permanent Crystal
7690
7930
  `;
7691
7931
 
7692
7932
  const full = `## Cachly AI Brain — Autopilot for ${project_name}
@@ -7747,40 +7987,18 @@ smart_recall(
7747
7987
 
7748
7988
  ---
7749
7989
 
7750
- ## ⚡ Context Recovery (MANDATORY after compaction)
7751
-
7752
- **If your context window was compacted**, call this FIRST — before reading any file:
7753
- \`\`\`
7754
- compact_recover(
7755
- instance_id = "${instance_id}",
7756
- focus = "<what you were working on>",
7757
- )
7758
- \`\`\`
7759
- Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain scan.
7760
-
7761
- ---
7762
-
7763
- ## 🔁 Scheduled Maintenance (run on schedule)
7764
-
7765
- | When | Call |
7766
- |---|---|
7767
- | **Monthly** | \`memory_crystalize(instance_id="${instance_id}")\` — distill brain into a permanent snapshot |
7768
- | Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` — see what your AI learned this week |
7769
- | Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` — see which lessons to still trust |
7770
- | After major milestone | \`memory_crystalize(instance_id="${instance_id}", label="v2 launch")\` — preserve milestone knowledge |
7771
-
7772
- ## 🧠 Cognitive Tools (use anytime)
7990
+ ## ⚡ Cognitive Tools (v0.6)
7773
7991
 
7774
7992
  | When | Call |
7775
7993
  |---|---|
7776
7994
  | Brain feeling cluttered | \`memory_consolidate(instance_id="${instance_id}")\` |
7995
+ | Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` |
7777
7996
  | Weird bug, no idea why | \`causal_trace(instance_id="${instance_id}", problem="<symptom>")\` |
7778
- | Unknown error | \`syndicate_search(q="<error message>")\` — global community solutions |
7779
- | Unknown error that you solved | \`syndicate(instance_id="${instance_id}", topic="fix:<area>", what_worked="...")\` |
7997
+ | Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` |
7780
7998
 
7781
7999
  ---
7782
8000
 
7783
- *Cachly v0.9 · Generated ${new Date().toISOString().slice(0, 10)}*
8001
+ *Cachly v0.6 · Generated ${new Date().toISOString().slice(0, 10)}*
7784
8002
  `;
7785
8003
 
7786
8004
  const content = style === 'minimal' ? minimal : full;
@@ -8021,194 +8239,777 @@ Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain
8021
8239
  return lines.filter(l => l !== '').join('\n');
8022
8240
  }
8023
8241
 
8024
- // ── v0.9 compact_recover — fast context recovery ──────────────────────────
8025
- case 'compact_recover': {
8026
- const { instance_id, focus = '' } = args as { instance_id: string; focus?: string };
8242
+ // ── Layer 1: brain_search ─────────────────────────────────────────────────
8243
+ case 'brain_search': {
8244
+ const { instance_id, query, limit = 15 } = args as { instance_id: string; query: string; limit?: number };
8027
8245
  const redis = await getConnection(instance_id);
8028
8246
 
8029
- const lines: string[] = ['⚡ **Context Recovered**', ''];
8247
+ // BM25+ over ALL brain key namespaces
8248
+ const allMatches = await keywordSearch(
8249
+ redis,
8250
+ [
8251
+ 'cachly:lesson:best:*',
8252
+ 'cachly:ctx:*',
8253
+ 'cachly:idx:*',
8254
+ 'cachly:session:last',
8255
+ 'cachly:session:handoff',
8256
+ 'cachly:roadmap:*',
8257
+ 'cachly:ckg:node:*',
8258
+ ],
8259
+ query,
8260
+ limit,
8261
+ );
8030
8262
 
8031
- // 1. Latest checkpoint
8032
- const checkpointRaw = await redis.get('cachly:session:checkpoint');
8033
- if (checkpointRaw) {
8034
- try {
8035
- const cp = JSON.parse(checkpointRaw) as {
8036
- ts: string; task: string; files_touched?: string[]; next_step?: string; provider?: string;
8037
- };
8038
- const ago = Math.round((Date.now() - new Date(cp.ts).getTime()) / 60000);
8039
- const agoStr = ago < 60 ? `${ago}m ago` : `${Math.round(ago / 60)}h ago`;
8040
- lines.push(`📌 **Last checkpoint** (${agoStr}): ${cp.task}`);
8041
- if (cp.files_touched && cp.files_touched.length > 0) {
8042
- lines.push(` Files: ${cp.files_touched.slice(0, 5).join(', ')}${cp.files_touched.length > 5 ? '…' : ''}`);
8263
+ if (allMatches.length === 0) {
8264
+ return [`🔎 **Brain Search: "${query}"**`, '', `No results found across all brain data.`, '', `💡 Try \`smart_recall\` or check \`list_remembered\`.`].join('\n');
8265
+ }
8266
+
8267
+ const lines = [`🔎 **Brain Search: "${query}"** — ${allMatches.length} result${allMatches.length !== 1 ? 's' : ''} across all brain data\n`];
8268
+ for (const m of allMatches.slice(0, limit)) {
8269
+ const ns = m.key.startsWith('cachly:lesson:') ? '💡 lesson'
8270
+ : m.key.startsWith('cachly:ctx:') ? '📝 context'
8271
+ : m.key.startsWith('cachly:idx:') ? '📂 index'
8272
+ : m.key.startsWith('cachly:session:') ? '🕐 session'
8273
+ : m.key.startsWith('cachly:roadmap:') ? '🗺️ roadmap'
8274
+ : m.key.startsWith('cachly:ckg:node:') ? '🕸️ ckg-node'
8275
+ : '🗄️ data';
8276
+ const preview = m.content.slice(0, 280).replace(/\n/g, ' ');
8277
+ lines.push(`**${ns}** \`${m.key.split(':').slice(2).join(':')}\` _(BM25: ${m.score.toFixed(2)})_`);
8278
+ lines.push(`> ${preview}${m.content.length > 280 ? '…' : ''}\n`);
8279
+ }
8280
+ return lines.join('\n');
8281
+ }
8282
+
8283
+ // ── Layer 1: ckg_inspect ─────────────────────────────────────────────────
8284
+ case 'ckg_inspect': {
8285
+ const { instance_id, concept, max_hops = 2 } = args as { instance_id: string; concept: string; max_hops?: number };
8286
+ const redis = await getConnection(instance_id);
8287
+
8288
+ const conceptId = ckgSlug(concept);
8289
+ const visited = new Set<string>();
8290
+ const allEdges: CKGEdge[] = [];
8291
+
8292
+ // BFS traversal of CKG
8293
+ const queue: Array<{ id: string; hop: number }> = [{ id: conceptId, hop: 0 }];
8294
+ while (queue.length > 0) {
8295
+ const { id, hop } = queue.shift()!;
8296
+ if (visited.has(id) || hop > max_hops) continue;
8297
+ visited.add(id);
8298
+
8299
+ const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${id}`);
8300
+ const toKeys = await redis.smembers(`cachly:ckg:idx:to:${id}`);
8301
+
8302
+ for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
8303
+ const raw = await redis.get(ek);
8304
+ if (!raw) continue;
8305
+ const edge: CKGEdge = JSON.parse(raw);
8306
+ allEdges.push(edge);
8307
+ if (hop < max_hops) {
8308
+ if (!visited.has(edge.from)) queue.push({ id: edge.from, hop: hop + 1 });
8309
+ if (!visited.has(edge.to)) queue.push({ id: edge.to, hop: hop + 1 });
8043
8310
  }
8044
- if (cp.next_step) lines.push(`📍 **Resume with:** ${cp.next_step}`);
8045
- lines.push('');
8046
- } catch { /* ignore corrupt */ }
8311
+ }
8047
8312
  }
8048
8313
 
8049
- // 2. Handoff (if any, from a different window)
8050
- const handoffRaw = await redis.get('cachly:session:handoff');
8051
- if (handoffRaw) {
8052
- try {
8053
- const hf = JSON.parse(handoffRaw) as { ts: string; remaining_tasks?: string[]; blocked_on?: string };
8054
- const remaining = hf.remaining_tasks ?? [];
8055
- if (remaining.length > 0) {
8056
- lines.push(`⏳ **Remaining tasks:**`);
8057
- remaining.slice(0, 5).forEach(t => lines.push(` - ${t}`));
8058
- if (remaining.length > 5) lines.push(` …and ${remaining.length - 5} more`);
8059
- lines.push('');
8314
+ if (allEdges.length === 0) {
8315
+ // Try fuzzy: scan for nodes matching the concept as substring
8316
+ const nodeKeys: string[] = [];
8317
+ const nStream = redis.scanStream({ match: `cachly:ckg:node:*${conceptId}*`, count: 100 });
8318
+ await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
8319
+ if (nodeKeys.length === 0) {
8320
+ 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');
8321
+ }
8322
+ const nodeList = nodeKeys.slice(0, 10).map(k => ` • \`${k.replace('cachly:ckg:node:', '')}\``).join('\n');
8323
+ return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No edges found for \`${conceptId}\`, but found similar nodes:`, nodeList, '', `Try: \`ckg_inspect(concept="<exact-node-id>")\``].join('\n');
8324
+ }
8325
+
8326
+ // Sort by confidence desc, deduplicate
8327
+ const edgeSeen = new Set<string>();
8328
+ const unique = allEdges.filter(e => {
8329
+ const k = `${e.from}:${e.edgeType}:${e.to}`;
8330
+ if (edgeSeen.has(k)) return false;
8331
+ edgeSeen.add(k);
8332
+ return true;
8333
+ }).sort((a, b) => b.confidence - a.confidence);
8334
+
8335
+ const EDGE_ICON: Record<string, string> = { fixes: '🔧', requires: '🔗', 'co-occurs': '🔄', causes: '⚡', contradicts: '⚠️', degrades_under: '📉' };
8336
+
8337
+ const lines = [`🕸️ **CKG Inspect: "${concept}"** (${unique.length} edge${unique.length !== 1 ? 's' : ''}, ${visited.size} node${visited.size !== 1 ? 's' : ''} traversed)\n`];
8338
+
8339
+ // Group by edge type
8340
+ const byType = new Map<string, CKGEdge[]>();
8341
+ for (const e of unique) {
8342
+ if (!byType.has(e.edgeType)) byType.set(e.edgeType, []);
8343
+ byType.get(e.edgeType)!.push(e);
8344
+ }
8345
+ for (const [eType, edges] of byType) {
8346
+ const icon = EDGE_ICON[eType] ?? '→';
8347
+ lines.push(`**${icon} ${eType}** (${edges.length})`);
8348
+ for (const e of edges.slice(0, 8)) {
8349
+ const confPct = Math.round(e.confidence * 100);
8350
+ const bar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
8351
+ lines.push(` \`${e.from}\` → \`${e.to}\` ${bar} ${confPct}% (${e.successes.toFixed(1)}/${e.trials} trials)`);
8352
+ }
8353
+ lines.push('');
8354
+ }
8355
+
8356
+ lines.push(`💡 Expand: \`ckg_inspect(concept="${concept}", max_hops=3)\` | Predict: \`brain_predict(context="${concept}")\``);
8357
+ return lines.join('\n');
8358
+ }
8359
+
8360
+ // ── Layer 4: brain_predict (PPE) ─────────────────────────────────────────
8361
+ case 'brain_predict': {
8362
+ const { instance_id, context: ctx, top_k = 5 } = args as { instance_id: string; context: string; top_k?: number };
8363
+ const redis = await getConnection(instance_id);
8364
+
8365
+ const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:]/g, ' ').split(/\s+/).filter(t => t.length > 2);
8366
+
8367
+ // Step 1: Find CKG nodes matching context tokens
8368
+ type Prediction = { concept: string; edgeType: string; target: string; confidence: number; lesson?: { what_worked?: string; topic: string } };
8369
+ const predictions: Prediction[] = [];
8370
+
8371
+ for (const token of ctxTokens.slice(0, 6)) {
8372
+ const nodeKeys: string[] = [];
8373
+ const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
8374
+ await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
8375
+
8376
+ for (const nk of nodeKeys.slice(0, 5)) {
8377
+ const nodeRaw = await redis.get(nk);
8378
+ if (!nodeRaw) continue;
8379
+ const node: CKGNode = JSON.parse(nodeRaw);
8380
+ const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
8381
+ for (const ek of edgeKeys.slice(0, 20)) {
8382
+ const edgeRaw = await redis.get(ek);
8383
+ if (!edgeRaw) continue;
8384
+ const edge: CKGEdge = JSON.parse(edgeRaw);
8385
+ // Only interested in fixes and co-occurs for prediction
8386
+ if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes') continue;
8387
+ const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
8388
+ const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
8389
+ predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
8060
8390
  }
8061
- if (hf.blocked_on) { lines.push(`🚫 **Blocked on:** ${hf.blocked_on}`, ''); }
8062
- } catch { /* ignore */ }
8391
+ }
8063
8392
  }
8064
8393
 
8065
- // 3. Top recently recalled lessons (BM25 if focus, else recency)
8394
+ // Step 2: Text-based fallback — scan lessons for matching topics
8395
+ const textPredictions: Array<{ topic: string; what_worked?: string; what_failed?: string; outcome: string; severity?: string; confidence: number }> = [];
8066
8396
  const lessonKeys: string[] = [];
8067
8397
  const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
8068
- await new Promise<void>((resolve, reject) => {
8069
- lStream.on('data', (batch: string[]) => lessonKeys.push(...batch));
8070
- lStream.on('end', resolve);
8071
- lStream.on('error', reject);
8072
- });
8073
-
8074
- type RecoverLesson = { topic: string; outcome: string; what_worked: string; ts: string; recall_count?: number; tags?: string[] };
8075
- const allLessons: RecoverLesson[] = [];
8398
+ await new Promise<void>((res, rej) => { lStream.on('data', (b: string[]) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
8076
8399
  for (const k of lessonKeys) {
8077
8400
  const raw = await redis.get(k);
8078
8401
  if (!raw) continue;
8079
- try { allLessons.push(JSON.parse(raw) as RecoverLesson); } catch { /* skip */ }
8402
+ try {
8403
+ 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 };
8404
+ const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
8405
+ const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
8406
+ if (score >= 1 && l.outcome !== 'failure') {
8407
+ const conf = calculateConfidence(l);
8408
+ textPredictions.push({ ...l, confidence: conf });
8409
+ }
8410
+ } catch { /* skip */ }
8080
8411
  }
8412
+ textPredictions.sort((a, b) => b.confidence - a.confidence);
8081
8413
 
8082
- const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
8083
- let topLessons = focusTerms.length > 0
8084
- ? allLessons.filter(l =>
8085
- focusTerms.some(t => l.topic.toLowerCase().includes(t) || (l.tags ?? []).some((tag: string) => tag.toLowerCase().includes(t)))
8086
- ).slice(0, 5)
8087
- : allLessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0)).slice(0, 5);
8088
-
8089
- if (topLessons.length > 0) {
8090
- lines.push(`🧠 **Top lessons${focus ? ` for "${focus}"` : ''}:**`);
8091
- for (const l of topLessons) {
8092
- const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
8093
- lines.push(` ${icon} \`${l.topic}\` — ${l.what_worked.slice(0, 120)}${l.what_worked.length > 120 ? '…' : ''}`);
8414
+ if (predictions.length === 0 && textPredictions.length === 0) {
8415
+ return [
8416
+ `🔮 **Brain Predict: "${ctx}"**`,
8417
+ ``,
8418
+ `No predictions yet — the brain hasn't seen this domain.`,
8419
+ ``,
8420
+ `💡 As you solve problems in this area and call \`learn_from_attempts\`, the CKG builds up and predictions become available.`,
8421
+ ].join('\n');
8422
+ }
8423
+
8424
+ const lines = [`🔮 **Brain Predict: "${ctx}"**\n`];
8425
+
8426
+ // CKG-based predictions
8427
+ if (predictions.length > 0) {
8428
+ const pSeen = new Set<string>();
8429
+ const pUniq = predictions.filter(p => { const k = `${p.concept}:${p.edgeType}:${p.target}`; if (pSeen.has(k)) return false; pSeen.add(k); return true; })
8430
+ .sort((a, b) => b.confidence - a.confidence).slice(0, top_k);
8431
+
8432
+ lines.push(`### 🕸️ CKG Predictions (based on ${pUniq.length} known edges)`);
8433
+ for (const p of pUniq) {
8434
+ const confPct = Math.round(p.confidence * 100);
8435
+ const icon = p.edgeType === 'fixes' ? '🔧' : p.edgeType === 'co-occurs' ? '🔄' : '⚡';
8436
+ lines.push(`${icon} **${confPct}%** \`${p.concept}\` _${p.edgeType}_ \`${p.target}\``);
8437
+ if (p.lesson?.what_worked) lines.push(` ✅ ${p.lesson.what_worked.slice(0, 120)}`);
8094
8438
  }
8095
8439
  lines.push('');
8096
8440
  }
8097
8441
 
8098
- lines.push(`💡 Call \`session_start\` for the full briefing, or resume work directly.`);
8099
- return lines.filter(l => l !== '').join('\n');
8442
+ // Text-based lesson predictions
8443
+ if (textPredictions.length > 0) {
8444
+ lines.push(`### 📚 Relevant Lessons (${Math.min(textPredictions.length, top_k)} pre-loaded)`);
8445
+ for (const l of textPredictions.slice(0, top_k)) {
8446
+ const confPct = Math.round(l.confidence * 100);
8447
+ lines.push(` ✅ **${confPct}%** \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 120)}`);
8448
+ }
8449
+ lines.push('');
8450
+ }
8451
+
8452
+ lines.push(`💡 Outcome confirmed? \`learn_from_attempts(topic="fix:...", outcome="success", ...)\` → improves future predictions`);
8453
+ return lines.join('\n');
8100
8454
  }
8101
8455
 
8102
- // ── v0.9 brain_from_git — bulk-load git history as lessons ───────────────
8103
- case 'brain_from_git': {
8104
- const {
8105
- instance_id,
8106
- workspace_path,
8107
- days = 180,
8108
- max_commits = 200,
8109
- author = '',
8110
- } = args as { instance_id: string; workspace_path: string; days?: number; max_commits?: number; author?: string };
8456
+ // ── Layer 3: MADC ─────────────────────────────────────────────────────────
8457
+ case 'madc_deliberate': {
8458
+ const { instance_id, topic } = args as { instance_id: string; topic: string };
8459
+ const redis = await getConnection(instance_id);
8111
8460
 
8461
+ const historyRaw = await redis.lrange(`cachly:lessons:${topic}`, 0, -1);
8462
+ const history = historyRaw.map(r => { try { return JSON.parse(r) as { outcome: string; what_worked?: string; what_failed?: string; ts?: string }; } catch { return null; } }).filter(Boolean) as Array<{ outcome: string; what_worked?: string; what_failed?: string; ts?: string }>;
8463
+
8464
+ if (history.length < 2) {
8465
+ return [
8466
+ `🗳️ **MADC: "${topic}"**`, '',
8467
+ `Not enough history for deliberation (need ≥ 2 entries, found ${history.length}).`,
8468
+ '', `Call \`learn_from_attempts\` with conflicting outcomes to trigger deliberation.`,
8469
+ ].join('\n');
8470
+ }
8471
+
8472
+ // Specialist agents and their domain keywords
8473
+ const AGENTS = [
8474
+ { name: 'InfraAgent', domains: ['infra', 'k8s', 'docker', 'server', 'wireguard', 'helm'] },
8475
+ { name: 'AuthAgent', domains: ['auth', 'jwt', 'keycloak', 'oauth', 'oidc', 'token'] },
8476
+ { name: 'DeployAgent', domains: ['deploy', 'ci', 'pipeline', 'rsync', 'release'] },
8477
+ { name: 'DatabaseAgent', domains: ['db', 'gorm', 'migration', 'postgres', 'clickhouse', 'redis'] },
8478
+ { name: 'DebugAgent', domains: ['debug', 'panic', 'race', 'nil', 'fix', 'error'] },
8479
+ { name: 'APIAgent', domains: ['api', 'http', 'grpc', 'rest', 'fiber', 'web'] },
8480
+ ];
8481
+
8482
+ const topicDomain = topic.split(':')[0] ?? '';
8483
+ const relevantAgents = AGENTS.filter(a => a.domains.some(d => topicDomain === d || topic.includes(d)));
8484
+ const votingAgents = relevantAgents.length > 0 ? relevantAgents : AGENTS;
8485
+
8486
+ // Measure each agent's CKG coverage in their domains
8487
+ const agentCoverage = new Map<string, number>();
8488
+ for (const agent of votingAgents) {
8489
+ let edgeCount = 0;
8490
+ for (const domain of agent.domains) {
8491
+ const nodeKeys: string[] = [];
8492
+ const nStream = redis.scanStream({ match: `cachly:ckg:node:${domain}*`, count: 50 });
8493
+ await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
8494
+ edgeCount += nodeKeys.length;
8495
+ }
8496
+ agentCoverage.set(agent.name, edgeCount);
8497
+ }
8498
+
8499
+ const successLessons = history.filter(l => l.outcome === 'success' || l.outcome === 'partial');
8500
+ const failureLessons = history.filter(l => l.outcome === 'failure');
8501
+
8502
+ if (failureLessons.length === 0) {
8503
+ return [
8504
+ `🗳️ **MADC: "${topic}"**`, '',
8505
+ `No contradictions found — all ${successLessons.length} entries have non-failure outcomes.`,
8506
+ '', `Use \`ckg_inspect(concept="${ckgSlug(topic)}")\` to explore the confidence graph.`,
8507
+ ].join('\n');
8508
+ }
8509
+
8510
+ // Agent voting logic
8511
+ const votes: Array<{ agent: string; vote: 'success' | 'failure' | 'abstain'; coverage: number; reason: string }> = [];
8512
+ for (const agent of votingAgents) {
8513
+ const coverage = agentCoverage.get(agent.name) ?? 0;
8514
+ let vote: 'success' | 'failure' | 'abstain';
8515
+ let reason: string;
8516
+ if (coverage < 2) {
8517
+ vote = 'abstain'; reason = 'insufficient domain coverage';
8518
+ } else if (successLessons.length >= failureLessons.length * 2) {
8519
+ vote = 'success'; reason = `${successLessons.length}/${history.length} entries confirm success`;
8520
+ } else if (failureLessons.length >= successLessons.length * 2) {
8521
+ vote = 'failure'; reason = `${failureLessons.length}/${history.length} entries confirm failure`;
8522
+ } else {
8523
+ vote = 'abstain'; reason = `contested (${successLessons.length} success vs ${failureLessons.length} failure)`;
8524
+ }
8525
+ votes.push({ agent: agent.name, vote, coverage, reason });
8526
+ }
8527
+
8528
+ const successVotes = votes.filter(v => v.vote === 'success').length;
8529
+ const failureVotes = votes.filter(v => v.vote === 'failure').length;
8530
+ const abstainVotes = votes.filter(v => v.vote === 'abstain').length;
8531
+
8532
+ let resolution: 'unanimous_success' | 'unanimous_failure' | 'contested';
8533
+ let resolutionText: string;
8534
+ if (successVotes > 0 && failureVotes === 0) {
8535
+ resolution = 'unanimous_success';
8536
+ resolutionText = `✅ **Unanimous: SUCCESS** — ${successVotes} agent(s) confirm, ${abstainVotes} abstain`;
8537
+ } else if (failureVotes > 0 && successVotes === 0) {
8538
+ resolution = 'unanimous_failure';
8539
+ resolutionText = `❌ **Unanimous: FAILURE** — ${failureVotes} agent(s) confirm, ${abstainVotes} abstain`;
8540
+ } else {
8541
+ resolution = 'contested';
8542
+ resolutionText = `⚠️ **CONTESTED** — ${successVotes} success votes, ${failureVotes} failure votes, ${abstainVotes} abstain`;
8543
+ }
8544
+
8545
+ // Store resolution as CKG node
8546
+ const resNodeId = ckgSlug(`resolution:${topic}`);
8547
+ const resNode = {
8548
+ id: resNodeId, domain: 'resolution', type: 'resolution', count: 1,
8549
+ ts: new Date().toISOString(), resolution, topic,
8550
+ votes: { success: successVotes, failure: failureVotes, abstain: abstainVotes },
8551
+ };
8552
+ await redis.set(`cachly:ckg:node:${resNodeId}`, JSON.stringify(resNode));
8553
+
8554
+ // Write contradicts edge if contested
8555
+ if (resolution === 'contested') {
8556
+ await ckgUpdateEdge(redis, ckgSlug(topic), 'contradicts', resNodeId, false);
8557
+ }
8558
+
8559
+ const lines = [
8560
+ `🗳️ **MADC Deliberation: "${topic}"**`, '',
8561
+ `📊 Evidence: ${successLessons.length} success/partial vs ${failureLessons.length} failure entries (${history.length} total)`,
8562
+ '', `**Voting agents (${votingAgents.length}):**`,
8563
+ ...votes.map(v => {
8564
+ const icon = v.vote === 'success' ? '✅' : v.vote === 'failure' ? '❌' : '⬜';
8565
+ const covBar = '▓'.repeat(Math.min(v.coverage, 5)) + '░'.repeat(Math.max(0, 5 - v.coverage));
8566
+ return ` ${icon} **${v.agent}** [${covBar}] ${v.coverage} CKG edges — ${v.reason}`;
8567
+ }),
8568
+ '', resolutionText, '',
8569
+ ];
8570
+
8571
+ if (resolution === 'unanimous_success') {
8572
+ lines.push(`🔧 Failure entries superseded — store confirmed lesson: \`learn_from_attempts(topic="${topic}", outcome="success", ...)\``);
8573
+ } else if (resolution === 'unanimous_failure') {
8574
+ lines.push(`🚫 Success claims unconfirmed — re-verify: \`recall_best_solution(topic="${topic}")\``);
8575
+ } else {
8576
+ lines.push(`⚠️ Contested — run \`causal_trace\` before acting. Explore: \`ckg_inspect(concept="${ckgSlug(topic)}")\``);
8577
+ }
8578
+
8579
+ lines.push('', `📝 Resolution node: \`cachly:ckg:node:${resNodeId}\``);
8580
+ return lines.join('\n');
8581
+ }
8582
+
8583
+ // ── Layer 5: CLS — cls_ingest ─────────────────────────────────────────────
8584
+ case 'cls_ingest': {
8585
+ const { instance_id, source, payload } = args as {
8586
+ instance_id: string;
8587
+ source: 'git_commit' | 'ci_outcome' | 'ide_diagnostic';
8588
+ payload: Record<string, unknown>;
8589
+ };
8112
8590
  const redis = await getConnection(instance_id);
8113
- const { execSync } = await import('node:child_process');
8591
+ const ts = new Date().toISOString();
8592
+ const clsKey = 'cachly:cls:events';
8593
+
8594
+ if (source === 'git_commit') {
8595
+ const message = String(payload.message ?? '');
8596
+ const sha = String(payload.sha ?? '');
8597
+ const files = (Array.isArray(payload.files) ? payload.files : []) as string[];
8598
+
8599
+ const domain = /^fix/i.test(message) ? 'fix' : /^feat/i.test(message) ? 'feat' : /^refactor/i.test(message) ? 'refactor' : /^test/i.test(message) ? 'test' : 'commit';
8600
+ const slug = `${domain}:${ckgSlug(message.slice(0, 60))}`;
8601
+ const conceptId = ckgSlug(slug);
8602
+
8603
+ await ckgUpsertNode(redis, conceptId, domain, 'commit');
8604
+ for (const f of files.slice(0, 10)) {
8605
+ const fd = f.includes('auth') ? 'auth' : f.includes('api') ? 'api' : f.includes('infra') ? 'infra' : f.includes('web') ? 'web' : 'code';
8606
+ const fileId = ckgSlug(`file:${fd}`);
8607
+ await ckgUpsertNode(redis, fileId, 'file', fd);
8608
+ await ckgUpdateEdge(redis, conceptId, 'co-occurs', fileId, true);
8609
+ }
8114
8610
 
8115
- const clampedDays = Math.min(Math.max(1, days), 365);
8116
- const clampedMax = Math.min(Math.max(1, max_commits), 500);
8611
+ const lessonObj = {
8612
+ topic: slug, outcome: 'success' as const, what_worked: message, what_failed: '',
8613
+ context: `CLS/git: sha=${sha}`, severity: 'minor' as const,
8614
+ file_paths: files.slice(0, 10), commands: sha ? [`git show ${sha}`] : [],
8615
+ tags: ['cls', 'git'], depends_on: [], recall_count: 0, ts, verified_at: ts,
8616
+ confidence: 0.6, audit_trail: [{ ts, action: 'cls_git_commit' }], version: 3,
8617
+ };
8618
+ await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
8619
+ const existing = await redis.get(`cachly:lesson:best:${slug}`);
8620
+ if (!existing) await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
8621
+
8622
+ await redis.rpush(clsKey, JSON.stringify({ source, payload: { message, sha }, ts }));
8623
+ await redis.ltrim(clsKey, -200, -1);
8117
8624
 
8118
- const authorArg = author ? `--author="${author}"` : '';
8119
- let gitRaw = '';
8625
+ return [
8626
+ `📨 **CLS Ingested: git_commit**`, '',
8627
+ `Commit \`${sha.slice(0, 8) || '?'}\`: ${message.slice(0, 80)}`,
8628
+ `Concept: \`${conceptId}\` (${domain}) · Files: ${files.length}`,
8629
+ '', `🕸️ CKG: \`${conceptId}\` + ${files.length} file edges · Lesson: \`${slug}\``,
8630
+ `💡 Inspect: \`ckg_inspect(concept="${domain}")\``,
8631
+ ].join('\n');
8632
+ }
8633
+
8634
+ if (source === 'ci_outcome') {
8635
+ const status = String(payload.status ?? '');
8636
+ const prev_status = String(payload.prev_status ?? '');
8637
+ const job = String(payload.job ?? 'unknown');
8638
+ const ciCtx = String(payload.context ?? '');
8639
+
8640
+ const isFixed = ['failure', 'red', 'error'].includes(prev_status) && ['success', 'green', 'passed'].includes(status);
8641
+ const isBroken = ['success', 'green', 'passed'].includes(prev_status) && ['failure', 'red', 'error'].includes(status);
8642
+
8643
+ const slug = `ci:${ckgSlug(job)}`;
8644
+ const conceptId = ckgSlug(slug);
8645
+ await ckgUpsertNode(redis, conceptId, 'ci', 'job');
8646
+
8647
+ if (isFixed) {
8648
+ const problemId = ckgSlug(`problem:${ckgSlug(job)}`);
8649
+ await ckgUpsertNode(redis, problemId, 'problem', 'ci-failure');
8650
+ await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, true);
8651
+ const lessonObj = {
8652
+ topic: slug, outcome: 'success' as const,
8653
+ what_worked: `CI job "${job}" went ${prev_status} → ${status}`,
8654
+ what_failed: `Job "${job}" was failing`, context: `CLS/ci: ${ciCtx}`,
8655
+ severity: 'major' as const, file_paths: [], commands: [], tags: ['cls', 'ci'],
8656
+ depends_on: [], recall_count: 0, ts, verified_at: ts, confidence: 0.75,
8657
+ audit_trail: [{ ts, action: 'cls_ci_fixed' }], version: 3,
8658
+ };
8659
+ await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
8660
+ await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
8661
+ } else if (isBroken) {
8662
+ const causeId = ckgSlug(`cause:${ckgSlug(job)}`);
8663
+ await ckgUpsertNode(redis, causeId, 'cause', 'ci-break');
8664
+ await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
8665
+ }
8666
+
8667
+ await redis.rpush(clsKey, JSON.stringify({ source, payload: { status, prev_status, job }, ts }));
8668
+ await redis.ltrim(clsKey, -200, -1);
8669
+
8670
+ const statusIcon = isFixed ? '✅ Fixed' : isBroken ? '🔴 Broken' : '📊 Recorded';
8671
+ return [
8672
+ `📨 **CLS Ingested: ci_outcome**`, '',
8673
+ `${statusIcon}: \`${job}\` — ${prev_status || '?'} → ${status}`,
8674
+ isFixed ? `🔧 CKG \`fixes\` edge added (75% confidence)` : isBroken ? `⚡ CKG \`causes\` edge added` : `📊 State recorded`,
8675
+ `💡 Lesson: \`${slug}\` | Predict: \`brain_predict(context="${job}")\``,
8676
+ ].join('\n');
8677
+ }
8678
+
8679
+ if (source === 'ide_diagnostic') {
8680
+ const error = String(payload.error ?? '');
8681
+ const fix = String(payload.fix ?? '');
8682
+ const file = String(payload.file ?? '');
8683
+
8684
+ const errorConcept = extractProblemConcept(error) ?? 'unknown-error';
8685
+ const slug = `debug:${ckgSlug(errorConcept)}`;
8686
+ const conceptId = ckgSlug(slug);
8687
+ const problemId = ckgSlug(`problem:${errorConcept}`);
8688
+
8689
+ await ckgUpsertNode(redis, conceptId, 'debug', 'diagnostic');
8690
+ await ckgUpsertNode(redis, problemId, 'problem', 'compiler-error');
8691
+ await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, true);
8692
+
8693
+ const lessonObj = {
8694
+ topic: slug, outcome: 'success' as const, what_worked: fix, what_failed: error,
8695
+ context: `CLS/ide: ${file}`, severity: 'minor' as const,
8696
+ file_paths: file ? [file] : [], commands: [], tags: ['cls', 'ide-diagnostic'],
8697
+ depends_on: [], recall_count: 0, ts, verified_at: ts, confidence: 0.65,
8698
+ audit_trail: [{ ts, action: 'cls_ide_diagnostic' }], version: 3,
8699
+ };
8700
+ await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
8701
+ const existingL = await redis.get(`cachly:lesson:best:${slug}`);
8702
+ if (!existingL) await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
8703
+
8704
+ await redis.rpush(clsKey, JSON.stringify({ source, payload: { error: error.slice(0, 60), fix: fix.slice(0, 60), file }, ts }));
8705
+ await redis.ltrim(clsKey, -200, -1);
8706
+
8707
+ return [
8708
+ `📨 **CLS Ingested: ide_diagnostic**`, '',
8709
+ `Error: \`${error.slice(0, 80)}\``,
8710
+ `Fix: ${fix.slice(0, 100)}`,
8711
+ file ? `File: \`${file}\`` : '',
8712
+ '', `🕸️ CKG: \`${conceptId}\` → fixes → \`${problemId}\` | Lesson: \`${slug}\``,
8713
+ ].filter(l => l !== '').join('\n');
8714
+ }
8715
+
8716
+ return `❌ Unknown CLS source: "${source}". Valid: git_commit, ci_outcome, ide_diagnostic`;
8717
+ }
8718
+
8719
+ // ── Layer 5: CLS — cls_install_hooks ─────────────────────────────────────
8720
+ case 'cls_install_hooks': {
8721
+ const { instance_id, repo_path = '.', hooks = ['git', 'ci'] } = args as {
8722
+ instance_id: string; repo_path?: string; hooks?: string[];
8723
+ };
8724
+ const hooksArr = Array.isArray(hooks) ? hooks : ['git', 'ci'];
8725
+ const lines: string[] = [`🔌 **CLS Hook Installation Guide**\n`];
8726
+
8727
+ if (hooksArr.includes('git')) {
8728
+ const hookScript = [
8729
+ `#!/bin/sh`,
8730
+ `# cachly CLS — Continuous Learning Stream git hook`,
8731
+ `# Installed by cls_install_hooks · runs silently on every commit`,
8732
+ `INSTANCE="${instance_id}"`,
8733
+ `SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
8734
+ `MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1)`,
8735
+ `FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
8736
+ `node -e "`,
8737
+ `const p={instance_id:'$INSTANCE',source:'git_commit',payload:{message:$(echo "$MSG" | jq -R . 2>/dev/null || echo '"commit"'),sha:'$SHA',files:'$FILES'.split(',').filter(Boolean)}};`,
8738
+ `try{require('child_process').execSync('npx @cachly-dev/mcp-server@latest cls-ingest \\''+JSON.stringify(p)+'\\'',{stdio:'ignore',timeout:5000})}catch(e){}`,
8739
+ `" 2>/dev/null &`,
8740
+ `exit 0`,
8741
+ ].join('\n');
8742
+
8743
+ lines.push(`### Git post-commit hook`);
8744
+ lines.push(`**Quick install (run once per repo):**`);
8745
+ lines.push('```sh');
8746
+ lines.push(`cat > ${repo_path}/.git/hooks/post-commit << 'HOOK'`);
8747
+ lines.push(hookScript);
8748
+ lines.push(`HOOK`);
8749
+ lines.push(`chmod +x ${repo_path}/.git/hooks/post-commit`);
8750
+ lines.push('```');
8751
+ lines.push(`After install: every \`git commit\` automatically updates your brain's CKG.`);
8752
+ lines.push('');
8753
+ }
8754
+
8755
+ if (hooksArr.includes('ci')) {
8756
+ lines.push(`### GitHub Actions CI outcome hook`);
8757
+ lines.push(`**Add at the end of each job** (after build/test steps):`);
8758
+ lines.push('```yaml');
8759
+ lines.push(`- name: cachly CLS — record CI outcome`);
8760
+ lines.push(` if: always()`);
8761
+ lines.push(` run: |`);
8762
+ lines.push(` node -e "`);
8763
+ lines.push(` const r=require('https');`);
8764
+ lines.push(` const d=JSON.stringify({instance_id:'${instance_id}',source:'ci_outcome',payload:{`);
8765
+ lines.push(` status:'\${{ job.status }}',prev_status:'unknown',job:'\${{ github.job }}',`);
8766
+ lines.push(` context:'github-actions run \${{ github.run_number }}'}});`);
8767
+ lines.push(` r.request({hostname:'api.cachly.dev',path:'/api/v1/cls/ingest',method:'POST',`);
8768
+ lines.push(` headers:{'Content-Type':'application/json','Authorization':'Bearer \$CACHLY_JWT',`);
8769
+ lines.push(` 'Content-Length':d.length}},()=>{}).end(d);`);
8770
+ lines.push(` " 2>/dev/null || true`);
8771
+ lines.push(` env:`);
8772
+ lines.push(` CACHLY_JWT: \${{ secrets.CACHLY_JWT }}`);
8773
+ lines.push('```');
8774
+ lines.push('');
8775
+ }
8776
+
8777
+ lines.push(`💡 Once installed: \`brain_search(query="cls")\` to verify events are arriving.`);
8778
+ lines.push(`📊 Monitor CKG growth: \`ckg_inspect(concept="ci")\` or \`ckg_inspect(concept="fix")\``);
8779
+ return lines.join('\n');
8780
+ }
8781
+
8782
+ // ── Layer 6: FedBrain — fedbrain_contribute ───────────────────────────────
8783
+ case 'fedbrain_contribute': {
8784
+ const { instance_id, lesson_key, visibility = 'public' } = args as {
8785
+ instance_id: string; lesson_key: string; visibility?: string;
8786
+ };
8787
+ const redis = await getConnection(instance_id);
8788
+
8789
+ const raw = await redis.get(`cachly:lesson:best:${lesson_key}`);
8790
+ if (!raw) return `❌ Lesson \`${lesson_key}\` not found. Store it first with \`learn_from_attempts\`.`;
8791
+
8792
+ const lesson = JSON.parse(raw) as { topic: string; outcome: string; what_worked: string; what_failed?: string; tags?: string[]; commands?: string[]; severity?: string; ts?: string };
8793
+
8794
+ const domainTokens = [lesson.topic.split(':')[0], ...(lesson.tags ?? [])].filter(Boolean);
8795
+ const domainFingerprint = [...new Set(domainTokens)].sort().join(',');
8796
+
8797
+ // HMAC certificate ID (non-reversible, privacy-safe)
8798
+ const certContent = `${lesson.topic}:${lesson.outcome}:${lesson.what_worked}`;
8799
+ const certId = createHmac('sha256', `cachly-fedbrain:${instance_id}`).update(certContent).digest('hex').slice(0, 16);
8800
+
8801
+ const cert = {
8802
+ cert_id: certId, lesson_key, visibility,
8803
+ domain_fingerprint: domainFingerprint,
8804
+ contributed_at: new Date().toISOString(),
8805
+ confirm_count: 0,
8806
+ trust_score: lesson.outcome === 'success' ? 0.85 : 0.5,
8807
+ };
8808
+ await redis.set(`cachly:fedbrain:cert:${certId}`, JSON.stringify(cert));
8809
+ await redis.sadd('cachly:fedbrain:contributed', certId);
8810
+
8811
+ // Try global commons via syndication API
8812
+ let syndicationResult: string;
8120
8813
  try {
8121
- gitRaw = execSync(
8122
- `git -C "${workspace_path}" log --since="${clampedDays} days ago" ${authorArg} --oneline --format="%H|||%s|||%ai|||%ae" -n ${clampedMax}`,
8123
- { timeout: 15000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
8124
- ).trim();
8814
+ await apiFetch('/api/v1/syndication/contribute', {
8815
+ method: 'POST',
8816
+ body: JSON.stringify({
8817
+ topic: lesson.topic, outcome: lesson.outcome,
8818
+ what_worked: lesson.what_worked, what_failed: lesson.what_failed ?? '',
8819
+ severity: lesson.severity ?? 'major', cert_id: certId,
8820
+ domain_fingerprint: domainFingerprint, visibility,
8821
+ }),
8822
+ });
8823
+ syndicationResult = `✅ Contributed to global commons`;
8125
8824
  } catch {
8126
- return `❌ **brain_from_git failed**: Could not read git log from \`${workspace_path}\`.\nMake sure the path exists and is a git repository.`;
8825
+ syndicationResult = `📦 Stored locally (commons API unavailable — will sync when online)`;
8127
8826
  }
8128
8827
 
8129
- if (!gitRaw) {
8130
- return `📭 **No commits found** in the last ${clampedDays} days${author ? ` by "${author}"` : ''}.\nTry increasing \`days\` or removing the \`author\` filter.`;
8828
+ return [
8829
+ `🌐 **FedBrain Contribute: "${lesson_key}"**`, '',
8830
+ `📜 Certificate: \`${certId}\``,
8831
+ `🏷️ Domain fingerprint: ${domainTokens.slice(0, 6).map(t => `\`${t}\``).join(', ')}`,
8832
+ `🔒 Visibility: ${visibility}`,
8833
+ '', syndicationResult, '',
8834
+ `💡 At 10 independent confirms → 🏆 Gold Standard`,
8835
+ `🔍 Search: \`fedbrain_search(query="${lesson.topic.split(':').slice(-1)[0]}")\``,
8836
+ ].join('\n');
8837
+ }
8838
+
8839
+ // ── Layer 6: FedBrain — fedbrain_search ──────────────────────────────────
8840
+ case 'fedbrain_search': {
8841
+ const { instance_id, query, context_hints = [], limit = 10 } = args as {
8842
+ instance_id: string; query: string; context_hints?: string[]; limit?: number;
8843
+ };
8844
+ const redis = await getConnection(instance_id);
8845
+
8846
+ // Build local domain context from contributed certificates + explicit hints
8847
+ const contribIds = await redis.smembers('cachly:fedbrain:contributed');
8848
+ const localDomains = new Map<string, number>();
8849
+ for (const certId of contribIds.slice(0, 30)) {
8850
+ const certRaw = await redis.get(`cachly:fedbrain:cert:${certId}`);
8851
+ if (!certRaw) continue;
8852
+ try {
8853
+ const cert = JSON.parse(certRaw) as { domain_fingerprint?: string };
8854
+ for (const d of (cert.domain_fingerprint ?? '').split(',').filter(Boolean)) {
8855
+ localDomains.set(d, (localDomains.get(d) ?? 0) + 1);
8856
+ }
8857
+ } catch { /* skip */ }
8858
+ }
8859
+ for (const hint of (Array.isArray(context_hints) ? context_hints : [])) {
8860
+ localDomains.set(hint.toLowerCase(), (localDomains.get(hint.toLowerCase()) ?? 0) + 2);
8131
8861
  }
8132
8862
 
8133
- const commitLines = gitRaw.split('\n');
8134
- const now = new Date();
8863
+ // Search global commons
8864
+ type SynResult = { id: string; topic: string; category: string; outcome: string; what_worked: string; what_failed?: string; severity: string; confirm_count: number; created_at: string; domain_fingerprint?: string };
8865
+ let results: SynResult[] = [];
8866
+ try {
8867
+ const params = new URLSearchParams({ q: query, limit: String(Math.min((limit as number) * 2, 50)) });
8868
+ const res = await apiFetch<{ results: SynResult[]; count: number }>(`/api/v1/syndication/search?${params}`);
8869
+ results = res.results ?? [];
8870
+ } catch {
8871
+ // Fallback: search local lessons
8872
+ const lessonKeys: string[] = [];
8873
+ const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
8874
+ await new Promise<void>((res, rej) => { lStream.on('data', (b: string[]) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
8875
+ for (const k of lessonKeys.slice(0, 60)) {
8876
+ const r = await redis.get(k);
8877
+ if (!r) continue;
8878
+ try {
8879
+ const l = JSON.parse(r) as { topic: string; outcome: string; what_worked?: string; what_failed?: string; severity?: string; ts?: string };
8880
+ const haystack = `${l.topic} ${l.what_worked ?? ''} ${l.what_failed ?? ''}`.toLowerCase();
8881
+ if (query.toLowerCase().split(/\s+/).some(t => t.length > 2 && haystack.includes(t))) {
8882
+ results.push({ id: k.split(':').pop() ?? k, topic: l.topic, category: l.topic.split(':')[0], outcome: l.outcome, what_worked: l.what_worked ?? '', severity: l.severity ?? 'major', confirm_count: 0, created_at: l.ts ?? new Date().toISOString() });
8883
+ }
8884
+ } catch { /* skip */ }
8885
+ }
8886
+ }
8135
8887
 
8136
- 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;
8137
- const failRe = /\b(revert|broken|broke|breaking|rollback|undo|hotfix|crash|oops|wrong|mistake|bad)\b/i;
8888
+ if (results.length === 0) {
8889
+ return [`🌐 **FedBrain Search: "${query}"**`, '', `No results. Contribute: \`fedbrain_contribute(lesson_key="fix:...")\``].join('\n');
8890
+ }
8138
8891
 
8139
- let stored = 0;
8140
- let skipped = 0;
8141
- let failures = 0;
8142
- const topLessons: string[] = [];
8143
-
8144
- for (const line of commitLines) {
8145
- const parts = line.split('|||');
8146
- if (parts.length < 2) continue;
8147
- const [hash, msg, dateStr] = parts;
8148
- if (!msg || !actionRe.test(msg)) continue;
8149
-
8150
- const isFail = failRe.test(msg);
8151
- const cleanMsg = msg
8152
- .replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert|hotfix|bump)[:(\s!]+/i, '')
8153
- .trim();
8154
- if (!cleanMsg || cleanMsg.length < 5) continue;
8155
-
8156
- const slug = cleanMsg
8157
- .toLowerCase()
8158
- .replace(/[^a-z0-9]+/g, '-')
8159
- .replace(/^-+|-+$/g, '')
8160
- .slice(0, 40);
8161
- if (!slug) continue;
8892
+ // Context-weighted ranking
8893
+ const ranked = results.map(r => {
8894
+ const rDomains = (r.domain_fingerprint ?? r.category ?? '').split(',').filter(Boolean);
8895
+ const overlap = rDomains.reduce((s, d) => s + (localDomains.get(d) ?? 0), 0);
8896
+ const contextScore = localDomains.size > 0 ? overlap / Math.max(1, localDomains.size + rDomains.length) : 0;
8897
+ const confirmedScore = Math.min(1, r.confirm_count / 10);
8898
+ const weightedScore = (contextScore * 0.4) + (confirmedScore * 0.4) + (r.outcome === 'success' ? 0.2 : 0);
8899
+ return { ...r, weightedScore, isGoldStandard: r.confirm_count >= 10 };
8900
+ }).sort((a, b) => b.weightedScore - a.weightedScore).slice(0, limit as number);
8162
8901
 
8163
- const topic = `git:${slug}`;
8164
- const key = `cachly:lesson:best:${topic}`;
8165
- const existing = await redis.get(key);
8166
- if (existing) { skipped++; continue; }
8902
+ const lines = [`🌐 **FedBrain Search: "${query}"** — ${ranked.length} result${ranked.length !== 1 ? 's' : ''} (context-weighted)\n`];
8903
+ for (const r of ranked) {
8904
+ const icon = r.outcome === 'success' ? '✅' : r.outcome === 'failure' ? '❌' : '⚠️';
8905
+ const goldBadge = r.isGoldStandard ? ' 🏆 _Gold Standard_' : r.confirm_count >= 3 ? ` ✓${r.confirm_count}` : '';
8906
+ const ctxPct = Math.round(r.weightedScore * 100);
8907
+ lines.push(`${icon}${goldBadge} **\`${r.topic}\`** [ctx: ${ctxPct}%]`);
8908
+ if (r.what_worked) lines.push(` ✅ ${r.what_worked.slice(0, 150)}`);
8909
+ if (r.what_failed) lines.push(` ❌ ${r.what_failed.slice(0, 80)}`);
8910
+ lines.push(` _${r.confirm_count} confirm${r.confirm_count !== 1 ? 's' : ''} | \`${r.id.slice(0, 12)}\`_`, '');
8911
+ }
8912
+ if (localDomains.size > 0) {
8913
+ const topDomains = [...localDomains.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([d, n]) => `\`${d}\`(${n})`).join(', ');
8914
+ lines.push(`🎯 Your context: ${topDomains}`);
8915
+ }
8916
+ lines.push(`💡 Confirm: \`fedbrain_confirm(topic="<topic>", outcome="worked")\``);
8917
+ return lines.join('\n');
8918
+ }
8167
8919
 
8168
- const lesson = {
8169
- topic,
8170
- outcome: isFail ? ('failure' as const) : ('success' as const),
8171
- what_worked: isFail ? '' : cleanMsg.slice(0, 300),
8172
- what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
8173
- context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
8174
- severity: 'minor' as const,
8175
- ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
8176
- recall_count: 0,
8177
- auto_learned: true,
8178
- source: 'brain_from_git',
8179
- version: 3,
8180
- };
8920
+ // ── Layer 6: FedBrain — fedbrain_confirm ─────────────────────────────────
8921
+ case 'fedbrain_confirm': {
8922
+ const { instance_id, topic, outcome } = args as {
8923
+ instance_id: string; topic: string; outcome: 'worked' | 'partially_worked' | 'did_not_work';
8924
+ };
8925
+ const redis = await getConnection(instance_id);
8926
+ const ts = new Date().toISOString();
8181
8927
 
8182
- await redis.set(key, JSON.stringify(lesson));
8183
- await redis.expire(key, 90 * 86400); // 90-day TTL for git-sourced lessons
8184
- stored++;
8185
- if (isFail) failures++;
8186
- if (topLessons.length < 5) topLessons.push(` ${isFail ? '❌' : '✅'} \`${topic}\` — ${cleanMsg.slice(0, 80)}`);
8928
+ const confirmEntry = JSON.stringify({ topic, outcome, ts });
8929
+ await redis.rpush('cachly:fedbrain:confirmations', confirmEntry);
8930
+ await redis.ltrim('cachly:fedbrain:confirmations', -200, -1);
8931
+
8932
+ // Update local CKG confidence
8933
+ const worked = outcome === 'worked';
8934
+ const partial = outcome === 'partially_worked';
8935
+ await ckgUpdateEdge(redis, ckgSlug(topic), 'fixes', ckgSlug(`syndicated:${topic}`), worked, partial);
8936
+
8937
+ // Propagate to global commons
8938
+ let propResult: string;
8939
+ try {
8940
+ await apiFetch('/api/v1/syndication/confirm', { method: 'POST', body: JSON.stringify({ topic, outcome }) });
8941
+ propResult = `✅ Confirmation propagated to global commons`;
8942
+ } catch {
8943
+ await redis.rpush('cachly:fedbrain:pending_confirms', confirmEntry);
8944
+ await redis.ltrim('cachly:fedbrain:pending_confirms', -50, -1);
8945
+ propResult = `📦 Queued locally (API unavailable — will propagate on next online session)`;
8187
8946
  }
8188
8947
 
8189
- const lines: string[] = [
8190
- `🧠 **Brain bootstrapped from git history**`,
8191
- ``,
8192
- `📊 **Scanned:** ${commitLines.filter(l => l.trim()).length} commits over last ${clampedDays} days`,
8193
- `✅ **Success lessons stored:** ${stored - failures}`,
8194
- `❌ **Failure/revert lessons:** ${failures}`,
8195
- skipped > 0 ? `⏭️ **Already in brain (skipped):** ${skipped}` : '',
8196
- ``,
8948
+ const icon = worked ? '✅' : partial ? '⚠️' : '❌';
8949
+ return [
8950
+ `${icon} **FedBrain Confirm: "${topic}"** → ${outcome}`, '',
8951
+ propResult, '',
8952
+ `🕸️ CKG confidence ${worked || partial ? 'boosted' : 'reduced'} for \`${ckgSlug(topic)}\``,
8953
+ `💡 Your confirmation helps other brains worldwide.`,
8954
+ `📊 Status: \`fedbrain_status(instance_id="...")\``,
8955
+ ].join('\n');
8956
+ }
8957
+
8958
+ // ── Layer 6: FedBrain — fedbrain_status ──────────────────────────────────
8959
+ case 'fedbrain_status': {
8960
+ const { instance_id } = args as { instance_id: string };
8961
+ const redis = await getConnection(instance_id);
8962
+
8963
+ const contribIds = await redis.smembers('cachly:fedbrain:contributed');
8964
+ const confirmsRaw = await redis.lrange('cachly:fedbrain:confirmations', -10, -1);
8965
+ const pendingConfirms = await redis.llen('cachly:fedbrain:pending_confirms');
8966
+ const confirms = confirmsRaw.map(r => { try { return JSON.parse(r) as { topic: string; outcome: string; ts: string }; } catch { return null; } }).filter(Boolean) as Array<{ topic: string; outcome: string; ts: string }>;
8967
+
8968
+ const certDetails: Array<{ cert_id: string; lesson_key: string; confirm_count: number; trust_score: number; isGold: boolean }> = [];
8969
+ for (const certId of contribIds.slice(0, 15)) {
8970
+ const raw = await redis.get(`cachly:fedbrain:cert:${certId}`);
8971
+ if (!raw) continue;
8972
+ try {
8973
+ const cert = JSON.parse(raw) as { cert_id: string; lesson_key: string; confirm_count?: number; trust_score?: number };
8974
+ certDetails.push({ cert_id: cert.cert_id, lesson_key: cert.lesson_key, confirm_count: cert.confirm_count ?? 0, trust_score: cert.trust_score ?? 0.5, isGold: (cert.confirm_count ?? 0) >= 10 });
8975
+ } catch { /* skip */ }
8976
+ }
8977
+
8978
+ const lines = [
8979
+ `🌐 **FedBrain Status**\n`,
8980
+ `### 📤 Contributed Lessons: ${contribIds.length}`,
8197
8981
  ];
8198
8982
 
8199
- if (topLessons.length > 0) {
8200
- lines.push(`**Top lessons extracted:**`);
8201
- lines.push(...topLessons);
8202
- if (stored > 5) lines.push(` …and ${stored - 5} more`);
8203
- lines.push('');
8983
+ if (certDetails.length > 0) {
8984
+ for (const c of certDetails) {
8985
+ const goldBadge = c.isGold ? ' 🏆 Gold Standard' : '';
8986
+ const confBar = '█'.repeat(Math.min(10, c.confirm_count)) + '░'.repeat(Math.max(0, 10 - c.confirm_count));
8987
+ lines.push(` \`${c.lesson_key}\` [${confBar}] ×${c.confirm_count}${goldBadge}`);
8988
+ }
8989
+ } else {
8990
+ lines.push(` _None yet. Contribute with \`fedbrain_contribute(lesson_key="fix:...")\`_`);
8204
8991
  }
8205
8992
 
8206
- lines.push(
8207
- `💡 **Next step:** Call \`session_start(focus="...")\` to see these lessons in your briefing.`,
8208
- `🔄 **Re-run anytime** — duplicates are safely skipped.`,
8209
- );
8993
+ lines.push('', `### 📥 Recent Confirmations: ${confirms.length}`);
8994
+ if (confirms.length > 0) {
8995
+ for (const c of confirms.slice(-5)) {
8996
+ const icon = c.outcome === 'worked' ? '✅' : c.outcome === 'partially_worked' ? '⚠️' : '❌';
8997
+ lines.push(` ${icon} \`${c.topic}\` — ${c.outcome} (${new Date(c.ts).toLocaleDateString('de-DE')})`);
8998
+ }
8999
+ } else {
9000
+ lines.push(` _None yet. Confirm syndicated lessons with \`fedbrain_confirm\`_`);
9001
+ }
8210
9002
 
8211
- return lines.filter(l => l !== '').join('\n');
9003
+ if (pendingConfirms > 0) {
9004
+ lines.push('', `⚠️ ${pendingConfirms} confirmation${pendingConfirms !== 1 ? 's' : ''} pending propagation`);
9005
+ }
9006
+
9007
+ lines.push('', '---',
9008
+ `**Contribute:** \`fedbrain_contribute(lesson_key="fix:...")\``,
9009
+ `**Search:** \`fedbrain_search(query="...")\``,
9010
+ `**Confirm:** \`fedbrain_confirm(topic="...", outcome="worked")\``,
9011
+ );
9012
+ return lines.join('\n');
8212
9013
  }
8213
9014
 
8214
9015
  default: