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#[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 ¤t {
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}