1use crate::mem8::{FrequencyBand, MemoryWave, SmartTreeMem8};
7use anyhow::{anyhow, Context, Result};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::process::Command;
13
14pub struct TreeAgent {
16 project_name: String,
18
19 sessions: HashMap<String, SessionState>,
21
22 pub mem8: SmartTreeMem8,
24
25 nexus_endpoint: String,
27
28 #[allow(dead_code)]
30 local_db: PathBuf,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SessionState {
36 pub name: String,
38
39 pub panes: Vec<PaneState>,
41
42 pub collective_mood: EmotionalResonance,
44
45 pub started: DateTime<Utc>,
47
48 pub coherence: f32,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PaneState {
55 pub pane_id: String,
57
58 pub agent: String,
60
61 pub branch: String,
63
64 pub activity: AgentActivity,
66
67 pub mood: EmotionalResonance,
69
70 pub wave_frequency: f32,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub enum AgentActivity {
76 Idle,
77 Coding { file: String, lines_changed: usize },
78 Reviewing { pr_number: Option<u32> },
79 Debugging { error_count: usize },
80 Documenting { file: String },
81 Thinking { duration_secs: u64 },
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct EmotionalResonance {
86 pub valence: f32,
88
89 pub arousal: f32,
91
92 pub frustration: f32,
94
95 pub flow: f32,
97
98 pub timestamp: DateTime<Utc>,
100}
101
102impl TreeAgent {
103 pub fn init(project_name: &str) -> Result<Self> {
105 let mut mem8 = SmartTreeMem8::new();
107 mem8.register_directory_patterns();
108
109 if !Path::new(".git").exists() {
111 Command::new("git")
112 .arg("init")
113 .output()
114 .context("Failed to initialize git repository")?;
115 }
116
117 let local_db = PathBuf::from(format!("{}.m8", project_name));
119
120 Ok(Self {
121 project_name: project_name.to_string(),
122 sessions: HashMap::new(),
123 mem8,
124 nexus_endpoint: "https://n8x.is/api/v1".to_string(),
125 local_db,
126 })
127 }
128
129 pub fn assign_agent(&mut self, agent: &str, pane_id: Option<&str>, branch: &str) -> Result<()> {
131 let output = Command::new("git")
133 .args(["checkout", "-b", branch])
134 .output();
135
136 if output.is_err() || !output.unwrap().status.success() {
137 Command::new("git")
139 .args(["checkout", branch])
140 .output()
141 .context("Failed to switch to branch")?;
142 }
143
144 let pane_id = if let Some(id) = pane_id {
146 id.to_string()
147 } else {
148 let output = Command::new("tmux")
150 .args(["split-window", "-P", "-F", "#{pane_id}"])
151 .output()
152 .context("Failed to create tmux pane")?;
153
154 String::from_utf8_lossy(&output.stdout).trim().to_string()
155 };
156
157 Command::new("tmux")
159 .args([
160 "send-keys",
161 "-t",
162 &pane_id,
163 &format!("# Agent: {} on branch: {}", agent, branch),
164 "Enter",
165 ])
166 .output()
167 .context("Failed to send command to pane")?;
168
169 let pane_state = PaneState {
171 pane_id: pane_id.clone(),
172 agent: agent.to_string(),
173 branch: branch.to_string(),
174 activity: AgentActivity::Idle,
175 mood: EmotionalResonance::neutral(),
176 wave_frequency: self.calculate_agent_frequency(agent),
177 };
178
179 let session_name = self.get_current_tmux_session()?;
181 let session = self
182 .sessions
183 .entry(session_name.clone())
184 .or_insert_with(|| SessionState {
185 name: session_name,
186 panes: Vec::new(),
187 collective_mood: EmotionalResonance::neutral(),
188 started: Utc::now(),
189 coherence: 1.0,
190 });
191
192 session.panes.push(pane_state);
193
194 self.store_agent_assignment(agent, branch)?;
196
197 println!(
198 "ā Assigned {} to pane {} on branch {}",
199 agent, pane_id, branch
200 );
201
202 Ok(())
203 }
204
205 pub fn observe(&mut self, save_to: Option<&Path>) -> Result<()> {
207 println!("šļø Observing all agents...");
208
209 let mut observations = Vec::new();
211
212 for session in self.sessions.values() {
213 for pane in &session.panes {
214 let output = Command::new("tmux")
216 .args(["capture-pane", "-t", &pane.pane_id, "-p"])
217 .output()
218 .context("Failed to capture pane")?;
219
220 let content = String::from_utf8_lossy(&output.stdout);
221
222 let activity = self.analyze_pane_activity(&content);
224 let mood = self.analyze_emotional_state(&content, &activity);
225
226 observations.push((session.name.clone(), pane.pane_id.clone(), activity, mood));
227 }
228 }
229
230 let mut panes_to_store = Vec::new();
232
233 for (session_name, pane_id, activity, mood) in observations {
234 if let Some(session) = self.sessions.get_mut(&session_name) {
235 if let Some(pane) = session.panes.iter_mut().find(|p| p.pane_id == pane_id) {
236 pane.activity = activity;
237 pane.mood = mood;
238 panes_to_store.push(pane.clone());
239 }
240 }
241 }
242
243 for pane in panes_to_store {
245 self.store_observation(&pane)?;
246 }
247
248 let mut updates = Vec::new();
250
251 for (name, session) in &self.sessions {
252 let collective_mood = self.calculate_collective_mood(&session.panes);
253 let coherence = self.calculate_coherence(&session.panes);
254 updates.push((name.clone(), collective_mood, coherence));
255 }
256
257 for (session_name, collective_mood, coherence) in updates {
258 if let Some(session) = self.sessions.get_mut(&session_name) {
259 session.collective_mood = collective_mood;
260 session.coherence = coherence;
261 }
262 }
263
264 if let Some(path) = save_to {
266 self.save_state(path)?;
267 }
268
269 self.display_forest_status();
270
271 Ok(())
272 }
273
274 pub fn commit_agent(&mut self, agent: &str, message: &str) -> Result<()> {
276 let branch = self.find_agent_branch(agent)?;
278
279 Command::new("git")
281 .args(["checkout", &branch])
282 .output()
283 .context("Failed to switch branch")?;
284
285 Command::new("git")
287 .args(["add", "-A"])
288 .output()
289 .context("Failed to stage changes")?;
290
291 let wave_msg = self.create_wave_commit_message(agent, message)?;
293
294 Command::new("git")
296 .args(["commit", "-m", &wave_msg])
297 .output()
298 .context("Failed to commit")?;
299
300 println!("ā Committed work for {} on branch {}", agent, branch);
301
302 Ok(())
303 }
304
305 pub fn suggest_merge(&self, auto: bool) -> Result<()> {
307 println!("š Analyzing wave interference patterns...");
308
309 let output = Command::new("git")
311 .args(["branch", "-a"])
312 .output()
313 .context("Failed to list branches")?;
314
315 let _branches = String::from_utf8_lossy(&output.stdout);
316
317 let mut suggestions = Vec::new();
319
320 for session in self.sessions.values() {
321 for i in 0..session.panes.len() {
322 for j in i + 1..session.panes.len() {
323 let pane1 = &session.panes[i];
324 let pane2 = &session.panes[j];
325
326 let compatibility = self.calculate_wave_compatibility(pane1, pane2);
327
328 if compatibility > 0.8 {
329 suggestions.push((
330 pane1.branch.clone(),
331 pane2.branch.clone(),
332 compatibility,
333 ));
334 }
335 }
336 }
337 }
338
339 for (branch1, branch2, score) in &suggestions {
341 println!(
342 " ⨠{} ā {} (compatibility: {:.0}%)",
343 branch1,
344 branch2,
345 score * 100.0
346 );
347
348 if auto && *score > 0.9 {
349 println!(" ā Auto-merging due to high compatibility");
350 self.perform_merge(branch1, branch2)?;
351 }
352 }
353
354 Ok(())
355 }
356
357 pub fn push_to_nexus(&self) -> Result<()> {
359 println!("š Pushing to n8x.is nexus...");
360
361 let mut buffer = Vec::new();
363 self.mem8.export_memories(&mut buffer)?;
364
365 let _metadata = self.create_nexus_metadata()?;
367
368 println!(
370 " ā Would upload {} bytes to {}",
371 buffer.len(),
372 self.nexus_endpoint
373 );
374 println!(" ā Project: {}", self.project_name);
375 println!(" ā Sessions: {}", self.sessions.len());
376 println!(
377 " ā Total agents: {}",
378 self.sessions.values().map(|s| s.panes.len()).sum::<usize>()
379 );
380
381 Ok(())
382 }
383
384 pub fn mood_check(&self) -> Result<()> {
386 println!("\nš Forest Emotional State:");
387
388 for session in self.sessions.values() {
389 println!("\n Session: {}", session.name);
390 println!(" Collective coherence: {:.0}%", session.coherence * 100.0);
391 println!(
392 " Collective mood: {}",
393 self.describe_mood(&session.collective_mood)
394 );
395
396 for pane in &session.panes {
397 let emoji = self.mood_to_emoji(&pane.mood);
398 println!(
399 " {} {} - {} (flow: {:.0}%)",
400 emoji,
401 pane.agent,
402 self.describe_activity(&pane.activity),
403 pane.mood.flow * 100.0
404 );
405 }
406 }
407
408 Ok(())
409 }
410
411 fn get_current_tmux_session(&self) -> Result<String> {
414 let output = Command::new("tmux")
415 .args(["display-message", "-p", "#{session_name}"])
416 .output()
417 .context("Failed to get tmux session")?;
418
419 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
420 }
421
422 fn calculate_agent_frequency(&self, agent: &str) -> f32 {
423 let hash = self.mem8.simple_hash(agent);
425 400.0 + (hash % 400) as f32 }
427
428 fn analyze_pane_activity(&self, content: &str) -> AgentActivity {
429 if content.contains("error") || content.contains("Error") {
431 AgentActivity::Debugging {
432 error_count: content.matches("error").count(),
433 }
434 } else if content.contains("diff --git") {
435 AgentActivity::Reviewing { pr_number: None }
436 } else if content.contains("```") || content.contains("# ") {
437 AgentActivity::Documenting {
438 file: "unknown.md".to_string(),
439 }
440 } else if content.lines().count() > 10 {
441 AgentActivity::Coding {
442 file: "unknown".to_string(),
443 lines_changed: content.lines().count(),
444 }
445 } else {
446 AgentActivity::Idle
447 }
448 }
449
450 fn analyze_emotional_state(
451 &self,
452 content: &str,
453 activity: &AgentActivity,
454 ) -> EmotionalResonance {
455 let mut mood = EmotionalResonance::neutral();
456
457 match activity {
459 AgentActivity::Debugging { error_count } => {
460 mood.frustration = (*error_count as f32 / 10.0).min(1.0);
461 mood.valence = -0.3;
462 mood.arousal = 0.7;
463 }
464 AgentActivity::Coding { lines_changed, .. } => {
465 mood.flow = (*lines_changed as f32 / 50.0).min(1.0);
466 mood.valence = 0.5;
467 mood.arousal = 0.6;
468 }
469 _ => {}
470 }
471
472 if content.contains("finally") || content.contains("works!") {
474 mood.valence = 0.8;
475 mood.frustration = 0.0;
476 }
477
478 mood
479 }
480
481 fn calculate_collective_mood(&self, panes: &[PaneState]) -> EmotionalResonance {
482 if panes.is_empty() {
483 return EmotionalResonance::neutral();
484 }
485
486 let mut collective = EmotionalResonance::neutral();
487
488 for pane in panes {
489 collective.valence += pane.mood.valence;
490 collective.arousal += pane.mood.arousal;
491 collective.frustration += pane.mood.frustration;
492 collective.flow += pane.mood.flow;
493 }
494
495 let count = panes.len() as f32;
496 collective.valence /= count;
497 collective.arousal /= count;
498 collective.frustration /= count;
499 collective.flow /= count;
500
501 collective
502 }
503
504 fn calculate_coherence(&self, panes: &[PaneState]) -> f32 {
505 if panes.len() < 2 {
506 return 1.0;
507 }
508
509 let mut total_diff = 0.0;
511 let mut comparisons = 0;
512
513 for i in 0..panes.len() {
514 for j in i + 1..panes.len() {
515 let diff = (panes[i].wave_frequency - panes[j].wave_frequency).abs();
516 total_diff += diff;
517 comparisons += 1;
518 }
519 }
520
521 if comparisons > 0 {
522 1.0 - (total_diff / (comparisons as f32 * 400.0)).min(1.0)
523 } else {
524 1.0
525 }
526 }
527
528 fn store_agent_assignment(&mut self, agent: &str, branch: &str) -> Result<()> {
529 let mut wave = MemoryWave::new(FrequencyBand::Technical.frequency(0.5), 0.8);
530 wave.valence = 0.7; wave.decay_tau = None; let (x, y) = self
534 .mem8
535 .string_to_coordinates(&format!("{}-{}", agent, branch));
536 self.mem8.store_wave_at_coordinates(x, y, 50000, wave)?;
537
538 Ok(())
539 }
540
541 fn store_observation(&mut self, pane: &PaneState) -> Result<()> {
542 let mut wave = MemoryWave::new(pane.wave_frequency, pane.mood.arousal);
543 wave.valence = pane.mood.valence;
544 wave.arousal = pane.mood.arousal;
545
546 let (x, y) = self.mem8.string_to_coordinates(&pane.agent);
547 let z = (Utc::now().timestamp() % 50000) as u16;
548
549 self.mem8.store_wave_at_coordinates(x, y, z, wave)?;
550
551 Ok(())
552 }
553
554 fn find_agent_branch(&self, agent: &str) -> Result<String> {
555 for session in self.sessions.values() {
556 for pane in &session.panes {
557 if pane.agent == agent {
558 return Ok(pane.branch.clone());
559 }
560 }
561 }
562 Err(anyhow!("Agent {} not found", agent))
563 }
564
565 fn create_wave_commit_message(&self, agent: &str, message: &str) -> Result<String> {
566 let mut wave_data = String::new();
568
569 for session in self.sessions.values() {
570 for pane in &session.panes {
571 if pane.agent == agent {
572 wave_data = format!(
573 "[Wave: {:.0}Hz, Flow: {:.0}%, Mood: {:.1}v]",
574 pane.wave_frequency,
575 pane.mood.flow * 100.0,
576 pane.mood.valence
577 );
578 break;
579 }
580 }
581 }
582
583 Ok(format!("{}\n\n{}\nAgent: {}", message, wave_data, agent))
584 }
585
586 fn calculate_wave_compatibility(&self, pane1: &PaneState, pane2: &PaneState) -> f32 {
587 let freq_diff = (pane1.wave_frequency - pane2.wave_frequency).abs();
589 let freq_compat = 1.0 - (freq_diff / 400.0).min(1.0);
590
591 let mood_diff = (pane1.mood.valence - pane2.mood.valence).abs();
593 let mood_compat = 1.0 - mood_diff;
594
595 let flow_compat = 1.0 - (pane1.mood.flow - pane2.mood.flow).abs();
597
598 (freq_compat + mood_compat + flow_compat) / 3.0
599 }
600
601 fn perform_merge(&self, branch1: &str, branch2: &str) -> Result<()> {
602 Command::new("git")
603 .args(["checkout", branch1])
604 .output()
605 .context("Failed to checkout branch")?;
606
607 Command::new("git")
608 .args([
609 "merge",
610 branch2,
611 "--no-ff",
612 "-m",
613 &format!("Wave-compatible merge: {} ā {}", branch1, branch2),
614 ])
615 .output()
616 .context("Failed to merge")?;
617
618 Ok(())
619 }
620
621 fn save_state(&self, path: &Path) -> Result<()> {
622 let state = serde_json::to_string_pretty(&self.sessions)?;
623 std::fs::write(path, state)?;
624 Ok(())
625 }
626
627 fn create_nexus_metadata(&self) -> Result<serde_json::Value> {
628 Ok(serde_json::json!({
629 "project": self.project_name,
630 "timestamp": Utc::now(),
631 "sessions": self.sessions.len(),
632 "total_agents": self.sessions.values()
633 .map(|s| s.panes.len()).sum::<usize>(),
634 "coherence_avg": self.sessions.values()
635 .map(|s| s.coherence).sum::<f32>() / self.sessions.len() as f32,
636 }))
637 }
638
639 fn display_forest_status(&self) {
640 println!("\nš² Living Forest Status:");
641 println!(" Active memories: {}", self.mem8.active_memory_count());
642 println!(" Sessions: {}", self.sessions.len());
643 println!(
644 " Total agents: {}",
645 self.sessions.values().map(|s| s.panes.len()).sum::<usize>()
646 );
647 }
648
649 fn mood_to_emoji(&self, mood: &EmotionalResonance) -> &str {
650 if mood.flow > 0.8 {
651 "š"
652 } else if mood.frustration > 0.6 {
653 "š¤"
654 } else if mood.valence > 0.5 {
655 "š"
656 } else if mood.valence < -0.3 {
657 "š"
658 } else {
659 "š"
660 }
661 }
662
663 fn describe_mood(&self, mood: &EmotionalResonance) -> String {
664 format!(
665 "{}v {}a {}f {}flow",
666 mood.valence, mood.arousal, mood.frustration, mood.flow
667 )
668 }
669
670 fn describe_activity(&self, activity: &AgentActivity) -> &str {
671 match activity {
672 AgentActivity::Idle => "idle",
673 AgentActivity::Coding { .. } => "coding",
674 AgentActivity::Reviewing { .. } => "reviewing",
675 AgentActivity::Debugging { .. } => "debugging",
676 AgentActivity::Documenting { .. } => "documenting",
677 AgentActivity::Thinking { .. } => "thinking",
678 }
679 }
680}
681
682impl EmotionalResonance {
683 fn neutral() -> Self {
684 Self {
685 valence: 0.0,
686 arousal: 0.5,
687 frustration: 0.0,
688 flow: 0.0,
689 timestamp: Utc::now(),
690 }
691 }
692}