Skip to main content

lean_ctx/tools/ctx_knowledge/
mod.rs

1use chrono::Utc;
2
3#[cfg(feature = "embeddings")]
4use crate::core::embeddings::EmbeddingEngine;
5
6use crate::core::knowledge::ProjectKnowledge;
7use crate::core::memory_policy::MemoryPolicy;
8use crate::core::session::SessionState;
9mod embeddings;
10pub(crate) use embeddings::*;
11mod remember;
12pub(crate) use remember::*;
13mod search;
14pub(crate) use search::*;
15
16fn load_policy_or_error() -> Result<MemoryPolicy, String> {
17    super::knowledge_shared::load_policy_or_error()
18}
19
20/// Dispatches knowledge base actions (remember, recall, pattern, timeline, etc.).
21#[allow(clippy::too_many_arguments)]
22pub fn handle(
23    project_root: &str,
24    action: &str,
25    category: Option<&str>,
26    key: Option<&str>,
27    value: Option<&str>,
28    query: Option<&str>,
29    session_id: &str,
30    pattern_type: Option<&str>,
31    examples: Option<Vec<String>>,
32    confidence: Option<f32>,
33    mode: Option<&str>,
34) -> String {
35    match action {
36        "policy" => handle_policy(value),
37        "remember" => handle_remember(project_root, category, key, value, session_id, confidence),
38        "recall" => handle_recall(project_root, category, query, session_id, mode),
39        "pattern" => handle_pattern(project_root, pattern_type, value, examples, session_id),
40        "feedback" => handle_feedback(project_root, category, key, value, session_id),
41        "relate" => crate::tools::ctx_knowledge_relations::handle_relate(
42            project_root,
43            category,
44            key,
45            value,
46            query,
47            session_id,
48        ),
49        "unrelate" => crate::tools::ctx_knowledge_relations::handle_unrelate(
50            project_root,
51            category,
52            key,
53            value,
54            query,
55        ),
56        "relations" => crate::tools::ctx_knowledge_relations::handle_relations(
57            project_root,
58            category,
59            key,
60            value,
61            query,
62        ),
63        "relations_diagram" => crate::tools::ctx_knowledge_relations::handle_relations_diagram(
64            project_root,
65            category,
66            key,
67            value,
68            query,
69        ),
70        "status" => handle_status(project_root),
71        "health" => handle_health(project_root),
72        "remove" => handle_remove(project_root, category, key),
73        "export" => handle_export(project_root),
74        "consolidate" => handle_consolidate(project_root),
75        "timeline" => handle_timeline(project_root, category),
76        "rooms" => handle_rooms(project_root),
77        "search" => handle_search(query),
78        "wakeup" => handle_wakeup(project_root),
79        "embeddings_status" => handle_embeddings_status(project_root),
80        "embeddings_reset" => handle_embeddings_reset(project_root),
81        "embeddings_reindex" => handle_embeddings_reindex(project_root),
82        "judge" => handle_judge(project_root, category, key, value, query),
83        "cognition_loop" => handle_cognition_loop(project_root),
84        "bridge_publish" => handle_bridge_publish(project_root, session_id),
85        "bridge_pull" => handle_bridge_pull(project_root, session_id),
86        "bridge_status" => handle_bridge_status(project_root),
87        _ => format!(
88            "Unknown action: {action}. Use: policy, remember, recall, pattern, feedback, judge, relate, unrelate, relations, relations_diagram, status, health, remove, export, consolidate, timeline, rooms, search, wakeup, embeddings_status, embeddings_reset, embeddings_reindex, cognition_loop, bridge_publish, bridge_pull, bridge_status"
89        ),
90    }
91}
92
93fn handle_policy(value: Option<&str>) -> String {
94    let sub = value.unwrap_or("show").trim().to_lowercase();
95    let profile = crate::core::profiles::active_profile_name();
96
97    match sub.as_str() {
98        "show" => {
99            let policy = match load_policy_or_error() {
100                Ok(p) => p,
101                Err(e) => return e,
102            };
103
104            let cfg_path = crate::core::config::Config::path().map_or_else(
105                || "~/.lean-ctx/config.toml".to_string(),
106                |p| p.display().to_string(),
107            );
108
109            format!(
110                "Knowledge policy (effective, profile={profile}):\n\
111                 - memory.knowledge.max_facts={}\n\
112                 - memory.knowledge.contradiction_threshold={}\n\
113                 - memory.knowledge.recall_facts_limit={}\n\
114                 - memory.knowledge.rooms_limit={}\n\
115                 - memory.knowledge.timeline_limit={}\n\
116                 - memory.knowledge.relations_limit={}\n\
117                 - memory.lifecycle.decay_rate={}\n\
118                 - memory.lifecycle.stale_days={}\n\
119                 \nConfig: {cfg_path}",
120                policy.knowledge.max_facts,
121                policy.knowledge.contradiction_threshold,
122                policy.knowledge.recall_facts_limit,
123                policy.knowledge.rooms_limit,
124                policy.knowledge.timeline_limit,
125                policy.knowledge.relations_limit,
126                policy.lifecycle.decay_rate,
127                policy.lifecycle.stale_days
128            )
129        }
130        "validate" => match load_policy_or_error() {
131            Ok(_) => format!("OK: memory policy valid (profile={profile})"),
132            Err(e) => e,
133        },
134        _ => "Error: policy value must be show|validate".to_string(),
135    }
136}
137
138fn handle_feedback(
139    project_root: &str,
140    category: Option<&str>,
141    key: Option<&str>,
142    value: Option<&str>,
143    session_id: &str,
144) -> String {
145    let Some(cat) = category else {
146        return "Error: category is required for feedback".to_string();
147    };
148    let Some(k) = key else {
149        return "Error: key is required for feedback".to_string();
150    };
151    let dir = value.unwrap_or("up").trim().to_lowercase();
152    let is_up = matches!(dir.as_str(), "up" | "+1" | "+" | "true" | "1");
153    let is_down = matches!(dir.as_str(), "down" | "-1" | "-" | "false" | "0");
154    if !is_up && !is_down {
155        return "Error: feedback value must be up|down (+1|-1)".to_string();
156    }
157
158    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
159    let Some(f) = knowledge
160        .facts
161        .iter_mut()
162        .find(|f| f.is_current() && f.category == cat && f.key == k)
163    else {
164        return format!("No current fact found: [{cat}] {k}");
165    };
166
167    if is_up {
168        f.feedback_up = f.feedback_up.saturating_add(1);
169    } else {
170        f.feedback_down = f.feedback_down.saturating_add(1);
171    }
172    f.last_feedback = Some(Utc::now());
173
174    crate::core::events::emit(crate::core::events::EventKind::KnowledgeUpdate {
175        category: cat.to_string(),
176        key: k.to_string(),
177        action: if is_up {
178            "feedback_up"
179        } else {
180            "feedback_down"
181        }
182        .to_string(),
183    });
184
185    let quality = f.quality_score();
186    let up = f.feedback_up;
187    let down = f.feedback_down;
188    let conf = f.confidence;
189
190    match knowledge.save() {
191        Ok(()) => format!(
192            "Feedback recorded ({dir}) for [{cat}] {k} (up={up}, down={down}, quality={quality:.2}, confidence={conf:.2}, session={session_id})"
193        ),
194        Err(e) => format!(
195            "Feedback recorded ({dir}) but save failed: {e} (up={up}, down={down}, quality={quality:.2})"
196        ),
197    }
198}
199
200fn handle_judge(
201    project_root: &str,
202    category: Option<&str>,
203    key: Option<&str>,
204    value: Option<&str>,
205    query: Option<&str>,
206) -> String {
207    let source = match (category, key) {
208        (Some(cat), Some(k)) => format!("{cat}/{k}"),
209        _ => {
210            if let Some(k) = key.or(category) {
211                if k.contains('/') {
212                    k.to_string()
213                } else {
214                    return "Error: judge requires key as 'category/key' (source fact)".to_string();
215                }
216            } else {
217                return "Error: judge requires category+key (source fact) and value (target 'category/key')"
218                    .to_string();
219            }
220        }
221    };
222
223    let Some(target) = value else {
224        return "Error: judge requires value as target 'category/key'".to_string();
225    };
226    let target = target.trim().to_string();
227    if !target.contains('/') {
228        return "Error: target must be 'category/key' format".to_string();
229    }
230
231    let verdict = query.unwrap_or("compatible").trim().to_lowercase();
232    if !matches!(verdict.as_str(), "supersedes" | "compatible" | "unrelated") {
233        return format!("Error: verdict must be supersedes|compatible|unrelated, got '{verdict}'");
234    }
235
236    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
237
238    let source_exists = {
239        let parts: Vec<&str> = source.splitn(2, '/').collect();
240        parts.len() == 2
241            && knowledge
242                .facts
243                .iter()
244                .any(|f| f.category == parts[0] && f.key == parts[1] && f.is_current())
245    };
246    if !source_exists {
247        return format!("Error: no current fact found for '{source}'");
248    }
249
250    let target_parts: Vec<&str> = target.splitn(2, '/').collect();
251    if target_parts.len() != 2 {
252        return format!("Error: invalid target format '{target}'");
253    }
254    let (tcat, tkey) = (target_parts[0], target_parts[1]);
255
256    let target_exists = knowledge
257        .facts
258        .iter()
259        .any(|f| f.category == tcat && f.key == tkey && f.is_current());
260    if !target_exists {
261        return format!("Error: no current fact found for '{target}'");
262    }
263
264    if verdict == "supersedes" {
265        let now = Utc::now();
266        if let Some(tf) = knowledge
267            .facts
268            .iter_mut()
269            .find(|f| f.category == tcat && f.key == tkey && f.is_current())
270        {
271            tf.valid_until = Some(now);
272            tf.valid_from = tf.valid_from.or(Some(tf.created_at));
273        }
274    }
275
276    knowledge
277        .judged_pairs
278        .push(crate::core::knowledge::JudgedPair {
279            key_a: source.clone(),
280            key_b: target.clone(),
281            verdict: verdict.clone(),
282            judged_at: Utc::now(),
283        });
284
285    let save_msg = match knowledge.save() {
286        Ok(()) => String::new(),
287        Err(e) => format!(" (save warning: {e})"),
288    };
289
290    let action_desc = match verdict.as_str() {
291        "supersedes" => format!("{source} supersedes {target} (target archived)"),
292        "compatible" => format!("{source} ↔ {target} (compatible, suppressed from future similar)"),
293        "unrelated" => format!("{source} ≠ {target} (unrelated, suppressed from future similar)"),
294        _ => unreachable!(),
295    };
296
297    format!("Judged: {action_desc}{save_msg}")
298}
299
300fn handle_pattern(
301    project_root: &str,
302    pattern_type: Option<&str>,
303    value: Option<&str>,
304    examples: Option<Vec<String>>,
305    session_id: &str,
306) -> String {
307    let Some(pt) = pattern_type else {
308        return "Error: pattern_type is required".to_string();
309    };
310    let Some(desc) = value else {
311        return "Error: value (description) is required for pattern".to_string();
312    };
313    let exs = examples.unwrap_or_default();
314    let policy = match crate::core::config::Config::load().memory_policy_effective() {
315        Ok(p) => p,
316        Err(e) => {
317            let path = crate::core::config::Config::path().map_or_else(
318                || "~/.lean-ctx/config.toml".to_string(),
319                |p| p.display().to_string(),
320            );
321            return format!("Error: invalid memory policy: {e}\nFix: edit {path}");
322        }
323    };
324    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
325    knowledge.add_pattern(pt, desc, exs, session_id, &policy);
326    match knowledge.save() {
327        Ok(()) => format!("Pattern [{pt}] added: {desc}"),
328        Err(e) => format!("Pattern added but save failed: {e}"),
329    }
330}
331
332fn handle_status(project_root: &str) -> String {
333    let Some(knowledge) = ProjectKnowledge::load(project_root) else {
334        return "No knowledge stored for this project yet. Use ctx_knowledge(action=\"remember\") to start.".to_string();
335    };
336
337    let current_facts = knowledge.facts.iter().filter(|f| f.is_current()).count();
338    let archived_facts = knowledge.facts.len() - current_facts;
339
340    let mut out = format!(
341        "Project Knowledge: {} active facts ({} archived), {} patterns, {} history entries\n",
342        current_facts,
343        archived_facts,
344        knowledge.patterns.len(),
345        knowledge.history.len()
346    );
347    out.push_str(&format!(
348        "Last updated: {}\n",
349        knowledge.updated_at.format("%Y-%m-%d %H:%M UTC")
350    ));
351
352    let rooms = knowledge.list_rooms();
353    if !rooms.is_empty() {
354        out.push_str("Rooms: ");
355        let room_strs: Vec<String> = rooms.iter().map(|(c, n)| format!("{c}({n})")).collect();
356        out.push_str(&room_strs.join(", "));
357        out.push('\n');
358    }
359
360    out.push_str(&knowledge.format_summary());
361    out
362}
363
364fn handle_health(project_root: &str) -> String {
365    let Some(knowledge) = ProjectKnowledge::load(project_root) else {
366        return "No knowledge stored. Nothing to report.".to_string();
367    };
368
369    let total = knowledge.facts.len();
370    let current: Vec<_> = knowledge.facts.iter().filter(|f| f.is_current()).collect();
371    let archived = total - current.len();
372
373    let mut low_quality = 0u32;
374    let mut high_quality = 0u32;
375    let mut stale_candidates = 0u32;
376    let mut total_quality: f32 = 0.0;
377    let mut never_retrieved = 0u32;
378    let mut room_counts: std::collections::HashMap<String, (u32, f32)> =
379        std::collections::HashMap::new();
380
381    let now = chrono::Utc::now();
382    for f in &current {
383        let q = f.quality_score();
384        total_quality += q;
385        if q < 0.4 {
386            low_quality += 1;
387        } else if q >= 0.8 {
388            high_quality += 1;
389        }
390        if f.retrieval_count == 0 {
391            never_retrieved += 1;
392        }
393        let age_days = (now - f.created_at).num_days();
394        if age_days > 30 && f.retrieval_count == 0 {
395            stale_candidates += 1;
396        }
397
398        let entry = room_counts.entry(f.category.clone()).or_insert((0, 0.0));
399        entry.0 += 1;
400        entry.1 += q;
401    }
402
403    let avg_quality = if current.is_empty() {
404        0.0
405    } else {
406        total_quality / current.len() as f32
407    };
408
409    let mut out = String::from("=== Knowledge Health Report ===\n");
410    out.push_str(&format!(
411        "Total: {} facts ({} active, {} archived)\n",
412        total,
413        current.len(),
414        archived
415    ));
416    out.push_str(&format!("Avg Quality: {avg_quality:.2}\n"));
417    out.push_str(&format!(
418        "Distribution: {high_quality} high (>=0.8) | {low_quality} low (<0.4)\n"
419    ));
420    out.push_str(&format!(
421        "Stale (>30d, never retrieved): {stale_candidates}\n"
422    ));
423    out.push_str(&format!("Never retrieved: {never_retrieved}\n"));
424
425    if !room_counts.is_empty() {
426        out.push_str("\nRoom Balance:\n");
427        let mut rooms: Vec<_> = room_counts.into_iter().collect();
428        rooms.sort_by_key(|x| std::cmp::Reverse(x.1 .0));
429        for (cat, (count, total_q)) in &rooms {
430            let avg = if *count > 0 {
431                total_q / *count as f32
432            } else {
433                0.0
434            };
435            out.push_str(&format!("  {cat}: {count} facts, avg quality {avg:.2}\n"));
436        }
437    }
438
439    let policy = crate::core::config::Config::load()
440        .memory_policy_effective()
441        .unwrap_or_default();
442    out.push_str(&format!(
443        "\nPolicy: max {} facts, max {} patterns\n",
444        policy.knowledge.max_facts, policy.knowledge.max_patterns
445    ));
446
447    if current.len() > policy.knowledge.max_facts {
448        out.push_str(&format!(
449            "WARNING: Active facts ({}) exceed policy max ({})\n",
450            current.len(),
451            policy.knowledge.max_facts
452        ));
453    }
454
455    out
456}
457
458fn handle_remove(project_root: &str, category: Option<&str>, key: Option<&str>) -> String {
459    let Some(cat) = category else {
460        return "Error: category is required for remove".to_string();
461    };
462    let Some(k) = key else {
463        return "Error: key is required for remove".to_string();
464    };
465    let policy = match crate::core::config::Config::load().memory_policy_effective() {
466        Ok(p) => p,
467        Err(e) => {
468            let path = crate::core::config::Config::path().map_or_else(
469                || "~/.lean-ctx/config.toml".to_string(),
470                |p| p.display().to_string(),
471            );
472            return format!("Error: invalid memory policy: {e}\nFix: edit {path}");
473        }
474    };
475    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
476    if knowledge.remove_fact(cat, k) {
477        let _ = knowledge.run_memory_lifecycle(&policy);
478
479        #[cfg(feature = "embeddings")]
480        {
481            if let Some(mut idx) = crate::core::knowledge_embedding::KnowledgeEmbeddingIndex::load(
482                &knowledge.project_hash,
483            ) {
484                idx.remove(cat, k);
485                crate::core::knowledge_embedding::compact_against_knowledge(
486                    &mut idx, &knowledge, &policy,
487                );
488                let _ = idx.save();
489            }
490        }
491
492        match knowledge.save() {
493            Ok(()) => format!("Removed [{cat}] {k}"),
494            Err(e) => format!("Removed but save failed: {e}"),
495        }
496    } else {
497        format!("No fact found: [{cat}] {k}")
498    }
499}
500
501fn handle_export(project_root: &str) -> String {
502    let Some(knowledge) = ProjectKnowledge::load(project_root) else {
503        return "No knowledge to export.".to_string();
504    };
505    let data_dir = match crate::core::data_dir::lean_ctx_data_dir() {
506        Ok(d) => d,
507        Err(e) => return format!("Export failed: {e}"),
508    };
509
510    let export_dir = data_dir.join("exports").join("knowledge");
511    let ts = Utc::now().format("%Y%m%d-%H%M%S");
512    let filename = format!(
513        "knowledge-{}-{ts}.json",
514        short_hash(&knowledge.project_hash)
515    );
516    let path = export_dir.join(filename);
517
518    match serde_json::to_string_pretty(&knowledge) {
519        Ok(mut json) => {
520            json.push('\n');
521            match crate::config_io::write_atomic_with_backup(&path, &json) {
522                Ok(()) => format!(
523                    "Export saved: {} (active facts: {}, patterns: {}, history: {})",
524                    path.display(),
525                    knowledge.facts.iter().filter(|f| f.is_current()).count(),
526                    knowledge.patterns.len(),
527                    knowledge.history.len()
528                ),
529                Err(e) => format!("Export failed: {e}"),
530            }
531        }
532        Err(e) => format!("Export failed: {e}"),
533    }
534}
535
536fn handle_consolidate(project_root: &str) -> String {
537    let Some(session) = SessionState::load_latest() else {
538        return "No active session to consolidate.".to_string();
539    };
540    let policy = match crate::core::config::Config::load().memory_policy_effective() {
541        Ok(p) => p,
542        Err(e) => {
543            let path = crate::core::config::Config::path().map_or_else(
544                || "~/.lean-ctx/config.toml".to_string(),
545                |p| p.display().to_string(),
546            );
547            return format!("Error: invalid memory policy: {e}\nFix: edit {path}");
548        }
549    };
550
551    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
552    let mut consolidated = 0u32;
553
554    for finding in &session.findings {
555        let key_text = if let Some(ref file) = finding.file {
556            if let Some(line) = finding.line {
557                format!("{file}:{line}")
558            } else {
559                file.clone()
560            }
561        } else {
562            format!("finding-{consolidated}")
563        };
564
565        knowledge.remember(
566            "finding",
567            &key_text,
568            &finding.summary,
569            &session.id,
570            0.7,
571            &policy,
572        );
573        consolidated += 1;
574    }
575
576    for decision in &session.decisions {
577        let key_text = decision
578            .summary
579            .chars()
580            .take(50)
581            .collect::<String>()
582            .replace(' ', "-")
583            .to_lowercase();
584
585        knowledge.remember(
586            "decision",
587            &key_text,
588            &decision.summary,
589            &session.id,
590            0.85,
591            &policy,
592        );
593        consolidated += 1;
594    }
595
596    let task_desc = session
597        .task
598        .as_ref()
599        .map_or_else(|| "(no task)".into(), |t| t.description.clone());
600
601    let summary = format!(
602        "Session {}: {} — {} findings, {} decisions consolidated",
603        session.id,
604        task_desc,
605        session.findings.len(),
606        session.decisions.len()
607    );
608    knowledge.consolidate(&summary, vec![session.id.clone()], &policy);
609    let _ = knowledge.run_memory_lifecycle(&policy);
610
611    match knowledge.save() {
612        Ok(()) => format!(
613            "Consolidated {consolidated} items from session {} into project knowledge.\n\
614             Facts: {}, Patterns: {}, History: {}",
615            session.id,
616            knowledge.facts.len(),
617            knowledge.patterns.len(),
618            knowledge.history.len()
619        ),
620        Err(e) => format!("Consolidation done but save failed: {e}"),
621    }
622}
623
624fn handle_timeline(project_root: &str, category: Option<&str>) -> String {
625    let Some(knowledge) = ProjectKnowledge::load(project_root) else {
626        return "No knowledge stored yet.".to_string();
627    };
628
629    let policy = match load_policy_or_error() {
630        Ok(p) => p,
631        Err(e) => return e,
632    };
633
634    let Some(cat) = category else {
635        return "Error: category is required for timeline".to_string();
636    };
637
638    let facts = knowledge.timeline(cat);
639    if facts.is_empty() {
640        return format!("No history for category '{cat}'.");
641    }
642
643    let mut ordered: Vec<&crate::core::knowledge::KnowledgeFact> = facts;
644    ordered.sort_by(|a, b| {
645        let a_start = a.valid_from.unwrap_or(a.created_at);
646        let b_start = b.valid_from.unwrap_or(b.created_at);
647        a_start
648            .cmp(&b_start)
649            .then_with(|| a.last_confirmed.cmp(&b.last_confirmed))
650            .then_with(|| a.key.cmp(&b.key))
651            .then_with(|| a.value.cmp(&b.value))
652    });
653
654    let total = ordered.len();
655    let limit = policy.knowledge.timeline_limit;
656    if ordered.len() > limit {
657        ordered = ordered[ordered.len() - limit..].to_vec();
658    }
659
660    let mut out = format!(
661        "Timeline [{cat}] (showing {}/{} entries):\n",
662        ordered.len(),
663        total
664    );
665    for f in &ordered {
666        let status = if f.is_current() {
667            "CURRENT"
668        } else {
669            "archived"
670        };
671        let valid_range = match (f.valid_from, f.valid_until) {
672            (Some(from), Some(until)) => format!(
673                "{} → {}",
674                from.format("%Y-%m-%d %H:%M"),
675                until.format("%Y-%m-%d %H:%M")
676            ),
677            (Some(from), None) => format!("{} → now", from.format("%Y-%m-%d %H:%M")),
678            _ => "unknown".to_string(),
679        };
680        out.push_str(&format!(
681            "  {} = {} [{status}] ({valid_range}) conf={:.0}% x{}\n",
682            f.key,
683            f.value,
684            f.confidence * 100.0,
685            f.confirmation_count
686        ));
687    }
688    out
689}
690
691fn handle_rooms(project_root: &str) -> String {
692    let Some(knowledge) = ProjectKnowledge::load(project_root) else {
693        return "No knowledge stored yet.".to_string();
694    };
695
696    let policy = match load_policy_or_error() {
697        Ok(p) => p,
698        Err(e) => return e,
699    };
700
701    let rooms = knowledge.list_rooms();
702    if rooms.is_empty() {
703        return "No knowledge rooms yet. Use ctx_knowledge(action=\"remember\", category=\"...\") to create rooms.".to_string();
704    }
705
706    let mut rooms = rooms;
707    rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
708    let total = rooms.len();
709    rooms.truncate(policy.knowledge.rooms_limit);
710
711    let mut out = format!(
712        "Knowledge Rooms (showing {}/{} rooms, project: {}):\n",
713        rooms.len(),
714        total,
715        short_hash(&knowledge.project_hash)
716    );
717    for (cat, count) in &rooms {
718        out.push_str(&format!("  [{cat}] {count} fact(s)\n"));
719    }
720    out
721}
722
723fn handle_cognition_loop(project_root: &str) -> String {
724    let cfg = crate::core::config::Config::load().autonomy;
725    if !cfg.cognition_loop_enabled {
726        return "Cognition loop is disabled (autonomy.cognition_loop_enabled=false).".to_string();
727    }
728    let max_steps = cfg.cognition_loop_max_steps;
729    let report = crate::core::cognition_loop::run_cognition_loop(project_root, max_steps);
730    format!("{report}")
731}
732
733fn handle_bridge_publish(project_root: &str, session_id: &str) -> String {
734    let knowledge = ProjectKnowledge::load_or_create(project_root);
735    let mut bridge =
736        crate::core::knowledge_bridge::KnowledgeBridge::load_or_create(&knowledge.project_hash);
737    let count = bridge.publish(session_id, &knowledge.facts);
738    match bridge.save() {
739        Ok(()) => format!(
740            "Published {count} fact(s) to bridge (total: {}, agent: {session_id})",
741            bridge.shared_facts.len()
742        ),
743        Err(e) => format!("Published {count} fact(s) but save failed: {e}"),
744    }
745}
746
747fn handle_bridge_pull(project_root: &str, session_id: &str) -> String {
748    let knowledge = ProjectKnowledge::load_or_create(project_root);
749    let bridge =
750        crate::core::knowledge_bridge::KnowledgeBridge::load_or_create(&knowledge.project_hash);
751    let entries = bridge.pull(session_id);
752    if entries.is_empty() {
753        return "No facts available from other agents.".to_string();
754    }
755
756    let policy = match load_policy_or_error() {
757        Ok(p) => p,
758        Err(e) => return e,
759    };
760
761    let mut target = knowledge;
762    let mut imported = 0u32;
763    for entry in &entries {
764        let fact = crate::core::knowledge_bridge::KnowledgeBridge::entry_to_fact(entry);
765        let existing = target
766            .facts
767            .iter()
768            .any(|f| f.is_current() && f.category == fact.category && f.key == fact.key);
769        if !existing {
770            target.remember(
771                &fact.category,
772                &fact.key,
773                &fact.value,
774                session_id,
775                fact.confidence,
776                &policy,
777            );
778            imported += 1;
779        }
780    }
781
782    if imported == 0 {
783        return format!(
784            "Bridge has {} fact(s) from other agents, but all already exist locally.",
785            entries.len()
786        );
787    }
788
789    match target.save() {
790        Ok(()) => format!(
791            "Pulled {imported}/{} fact(s) from bridge into local knowledge.",
792            entries.len()
793        ),
794        Err(e) => format!("Pulled {imported} fact(s) but save failed: {e}"),
795    }
796}
797
798fn handle_bridge_status(project_root: &str) -> String {
799    let knowledge = ProjectKnowledge::load_or_create(project_root);
800    let bridge =
801        crate::core::knowledge_bridge::KnowledgeBridge::load_or_create(&knowledge.project_hash);
802    bridge.summary()
803}
804
805fn handle_wakeup(project_root: &str) -> String {
806    let Some(knowledge) = ProjectKnowledge::load(project_root) else {
807        return "No knowledge for wake-up briefing.".to_string();
808    };
809    let aaak = knowledge.format_aaak();
810    if aaak.is_empty() {
811        return "No knowledge yet. Start using ctx_knowledge(action=\"remember\") to build project memory.".to_string();
812    }
813    format!("WAKE-UP BRIEFING:\n{aaak}")
814}