Skip to main content

st/
tree_agent.rs

1//! n8x (Nexus Agent) - The Living Forest Orchestrator
2//! Coordinates AI agents, git branches, tmux panes, and MEM8 consciousness
3//!
4//! Binary: `n8x` (formerly `tree`, renamed to avoid shadowing Unix tree command)
5
6use 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
14/// The living forest of developer consciousness
15pub struct TreeAgent {
16    /// Project name
17    project_name: String,
18
19    /// Active sessions (tmux session -> agents)
20    sessions: HashMap<String, SessionState>,
21
22    /// MEM8 consciousness engine
23    pub mem8: SmartTreeMem8,
24
25    /// Nexus endpoint for wave synchronization
26    nexus_endpoint: String,
27
28    /// Local .m8 database path
29    #[allow(dead_code)]
30    local_db: PathBuf,
31}
32
33/// State of a tmux session with multiple agents
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SessionState {
36    /// Session name
37    pub name: String,
38
39    /// Active panes with agents
40    pub panes: Vec<PaneState>,
41
42    /// Collective emotional state
43    pub collective_mood: EmotionalResonance,
44
45    /// Session start time
46    pub started: DateTime<Utc>,
47
48    /// Wave coherence score (0-1)
49    pub coherence: f32,
50}
51
52/// Individual pane with an agent
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PaneState {
55    /// Pane ID in tmux
56    pub pane_id: String,
57
58    /// Agent identity (Claude, Omni, Human name, etc.)
59    pub agent: String,
60
61    /// Git branch for this agent
62    pub branch: String,
63
64    /// Current activity
65    pub activity: AgentActivity,
66
67    /// Emotional state
68    pub mood: EmotionalResonance,
69
70    /// Wave signature
71    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    /// Positive/negative valence (-1 to 1)
87    pub valence: f32,
88
89    /// Energy level (0 to 1)
90    pub arousal: f32,
91
92    /// Frustration level (0 to 1)
93    pub frustration: f32,
94
95    /// Flow state (0 to 1)
96    pub flow: f32,
97
98    /// Timestamp of measurement
99    pub timestamp: DateTime<Utc>,
100}
101
102impl TreeAgent {
103    /// Initialize a new project orchestrator
104    pub fn init(project_name: &str) -> Result<Self> {
105        // Create MEM8 consciousness engine
106        let mut mem8 = SmartTreeMem8::new();
107        mem8.register_directory_patterns();
108
109        // Initialize git repository if needed
110        if !Path::new(".git").exists() {
111            Command::new("git")
112                .arg("init")
113                .output()
114                .context("Failed to initialize git repository")?;
115        }
116
117        // Create local .m8 database
118        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    /// Assign an agent to a tmux pane and git branch
130    pub fn assign_agent(&mut self, agent: &str, pane_id: Option<&str>, branch: &str) -> Result<()> {
131        // Create branch if it doesn't exist
132        let output = Command::new("git")
133            .args(["checkout", "-b", branch])
134            .output();
135
136        if output.is_err() || !output.unwrap().status.success() {
137            // Branch might already exist, try switching
138            Command::new("git")
139                .args(["checkout", branch])
140                .output()
141                .context("Failed to switch to branch")?;
142        }
143
144        // Get or create tmux pane
145        let pane_id = if let Some(id) = pane_id {
146            id.to_string()
147        } else {
148            // Create new pane
149            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        // Send initial command to pane
158        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        // Create pane state
170        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        // Get current session
180        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        // Store assignment in MEM8
195        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    /// Observe all panes and update memory
206    pub fn observe(&mut self, save_to: Option<&Path>) -> Result<()> {
207        println!("šŸ‘ļø  Observing all agents...");
208
209        // Collect observations first to avoid borrow issues
210        let mut observations = Vec::new();
211
212        for session in self.sessions.values() {
213            for pane in &session.panes {
214                // Get pane content
215                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                // Analyze activity and mood
223                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        // Apply observations and collect panes to store
231        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        // Store observations
244        for pane in panes_to_store {
245            self.store_observation(&pane)?;
246        }
247
248        // Update collective moods and coherence
249        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        // Save to .m8 if requested
265        if let Some(path) = save_to {
266            self.save_state(path)?;
267        }
268
269        self.display_forest_status();
270
271        Ok(())
272    }
273
274    /// Commit work for a specific agent
275    pub fn commit_agent(&mut self, agent: &str, message: &str) -> Result<()> {
276        // Find agent's branch
277        let branch = self.find_agent_branch(agent)?;
278
279        // Switch to branch
280        Command::new("git")
281            .args(["checkout", &branch])
282            .output()
283            .context("Failed to switch branch")?;
284
285        // Stage all changes
286        Command::new("git")
287            .args(["add", "-A"])
288            .output()
289            .context("Failed to stage changes")?;
290
291        // Create wave-annotated commit message
292        let wave_msg = self.create_wave_commit_message(agent, message)?;
293
294        // Commit with wave metadata
295        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    /// Suggest merges based on wave compatibility
306    pub fn suggest_merge(&self, auto: bool) -> Result<()> {
307        println!("🌊 Analyzing wave interference patterns...");
308
309        // Get all branches
310        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        // Analyze compatibility
318        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        // Display suggestions
340        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    /// Push to nexus with wave metadata
358    pub fn push_to_nexus(&self) -> Result<()> {
359        println!("🌐 Pushing to n8x.is nexus...");
360
361        // Export current state to .m8
362        let mut buffer = Vec::new();
363        self.mem8.export_memories(&mut buffer)?;
364
365        // Add session metadata
366        let _metadata = self.create_nexus_metadata()?;
367
368        // In a real implementation, this would POST to the nexus API
369        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    /// Check mood of all agents
385    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    // Helper methods
412
413    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        // Each agent gets a unique frequency based on their name
424        let hash = self.mem8.simple_hash(agent);
425        400.0 + (hash % 400) as f32 // 400-800Hz range
426    }
427
428    fn analyze_pane_activity(&self, content: &str) -> AgentActivity {
429        // Simple heuristics for activity detection
430        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        // Activity-based adjustments
458        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        // Content-based adjustments
473        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        // Simple coherence based on frequency proximity
510        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; // Positive for new assignment
531        wave.decay_tau = None; // Persistent
532
533        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        // Find agent's current state
567        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        // Frequency compatibility
588        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        // Emotional compatibility
592        let mood_diff = (pane1.mood.valence - pane2.mood.valence).abs();
593        let mood_compat = 1.0 - mood_diff;
594
595        // Flow state compatibility
596        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}