diff --git a/chrs-code-edit/Cargo.toml b/chrs-code-edit/Cargo.toml new file mode 100644 index 00000000..3ed53b1e --- /dev/null +++ b/chrs-code-edit/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "chrs-code-edit" +version = "0.1.0" +edition = "2021" + +[dependencies] +git2 = "0.18" +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" diff --git a/chrs-code-edit/src/lib.rs b/chrs-code-edit/src/lib.rs new file mode 100644 index 00000000..fec936e8 --- /dev/null +++ b/chrs-code-edit/src/lib.rs @@ -0,0 +1,46 @@ +//! chrs-code-edit: Autonomous Git-based code editing for CHORUS. + +use git2::{Repository, BranchType}; +use std::path::Path; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum EditError { + #[error("Git error: {0}")] + Git(#[from] git2::Error), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), +} + +/// Manages isolated Git worktrees/branches for agent tasks. +pub struct WorktreeManager { + repo: Repository, +} + +impl WorktreeManager { + /// Open an existing repository. + pub fn open>(path: P) -> Result { + let repo = Repository::open(path)?; + Ok(Self { repo }) + } + + /// Create a new branch for a specific task. + /// + /// **Why**: Isolation ensures that agents do not conflict with each other + /// or the main branch while performing autonomous edits. + pub fn spawn_task_branch(&self, task_id: &str) -> Result { + let branch_name = format!("task/{}", task_id); + let head = self.repo.head()?.peel_to_commit()?; + self.repo.branch(&branch_name, &head, false)?; + println!("[CODE-EDIT] Spawned branch: {}", branch_name); + Ok(branch_name) + } + + /// Checkout a specific branch. + pub fn checkout_branch(&self, branch_name: &str) -> Result<(), EditError> { + let obj = self.repo.revparse_single(&format!("refs/heads/{}", branch_name))?; + self.repo.checkout_tree(&obj, None)?; + self.repo.set_head(&format!("refs/heads/{}", branch_name))?; + Ok(()) + } +} diff --git a/chrs-discovery/Cargo.toml b/chrs-discovery/Cargo.toml new file mode 100644 index 00000000..af2b3439 --- /dev/null +++ b/chrs-discovery/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "chrs-discovery" +version = "0.1.0" +edition = "2021" + +[dependencies] +libp2p = { version = "0.52", features = ["mdns", "tcp", "noise", "yamux", "tokio", "gossipsub", "macros"] } +tokio = { version = "1.0", features = ["full"] } +futures = "0.3" +thiserror = "1.0" +serde = { version = "1.0", features = ["derive"] } diff --git a/chrs-discovery/src/lib.rs b/chrs-discovery/src/lib.rs new file mode 100644 index 00000000..11c5d200 --- /dev/null +++ b/chrs-discovery/src/lib.rs @@ -0,0 +1,69 @@ +//! chrs-discovery: LibP2P-based peer discovery for CHORUS. + +use libp2p::{ + mdns, + swarm::{NetworkBehaviour, SwarmEvent}, + gossipsub, +}; +use futures::StreamExt; +use std::error::Error; + +#[derive(NetworkBehaviour)] +pub struct MyBehaviour { + pub mdns: mdns::tokio::Behaviour, + pub gossipsub: gossipsub::Behaviour, +} + +pub struct SwarmManager; + +impl SwarmManager { + /// Initialize a new LibP2P swarm with mDNS discovery. + /// + /// **Why**: Moving away from polling a database to real-time P2P discovery + /// reduces latency and allows CHORUS to scale to dynamic, broker-less environments. + pub async fn start_discovery_loop() -> Result<(), Box> { + let mut swarm = libp2p::SwarmBuilder::with_new_identity() + .with_tokio() + .with_tcp( + libp2p::tcp::Config::default(), + libp2p::noise::Config::new, + libp2p::yamux::Config::default, + )? + .with_behaviour(|key| { + let message_id_fn = |message: &gossipsub::Message| { + let mut s = std::collections::hash_map::DefaultHasher::new(); + use std::hash::Hasher; + std::hash::Hash::hash(&message.data, &mut s); + gossipsub::MessageId::from(s.finish().to_string()) + }; + + let gossipsub_config = gossipsub::ConfigBuilder::default() + .message_id_fn(message_id_fn) + .build()?; + + Ok(MyBehaviour { + mdns: mdns::tokio::Behaviour::new(mdns::Config::default(), key.public().to_peer_id())?, + gossipsub: gossipsub::Behaviour::new( + gossipsub::MessageAuthenticity::Signed(key.clone()), + gossipsub_config, + )?, + }) + })? + .build(); + + swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; + + println!("[DISCOVERY] Swarm started. Listening for peers via mDNS..."); + + loop { + match swarm.select_next_some().await { + SwarmEvent::Behaviour(MyBehaviourEvent::Mdns(mdns::Event::Discovered(list))) => { + for (peer_id, _multiaddr) in list { + println!("[DISCOVERY] mDNS discovered a new peer: {}", peer_id); + } + } + _ => {} + } + } + } +} diff --git a/chrs-observer/Cargo.toml b/chrs-observer/Cargo.toml new file mode 100644 index 00000000..af871f68 --- /dev/null +++ b/chrs-observer/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "chrs-observer" +version = "0.1.0" +edition = "2021" + +[dependencies] +ratatui = "0.26" +crossterm = "0.27" +tokio = { version = "1.0", features = ["full"] } +chrs-mail = { path = "../chrs-mail" } +chrs-bubble = { path = "../chrs-bubble" } +chrs-backbeat = { path = "../chrs-backbeat" } +chrono = "0.4" +serde_json = "1.0" diff --git a/chrs-observer/src/main.rs b/chrs-observer/src/main.rs new file mode 100644 index 00000000..5144465c --- /dev/null +++ b/chrs-observer/src/main.rs @@ -0,0 +1,103 @@ +//! chrs-observer: Real-time TUI dashboard for CHORUS. + +use ratatui::{ + backend::CrosstermBackend, + widgets::{Block, Borders, Paragraph, List, ListItem}, + layout::{Layout, Constraint, Direction}, + Terminal, +}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{error::Error, io, time::{Duration, Instant}}; + +struct App { + pulse_bpm: u32, + beat_index: u32, + logs: Vec, +} + +impl App { + fn new() -> App { + App { + pulse_bpm: 30, + beat_index: 0, + logs: vec!["[OBSERVER] Initialized.".into()], + } + } +} + +fn main() -> Result<(), Box> { + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Create app and run loop + let app = App::new(); + let res = run_app(&mut terminal, app); + + // Restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} + +fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { + let tick_rate = Duration::from_millis(250); + let mut last_tick = Instant::now(); + loop { + terminal.draw(|f| ui(f, &app))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + if let KeyCode::Char('q') = key.code { + return Ok(()); + } + } + } + if last_tick.elapsed() >= tick_rate { + // In a real app, we would update beat_index from chrs-backbeat here + last_tick = Instant::now(); + } + } +} + +fn ui(f: &mut ratatui::Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Length(3), + Constraint::Min(0), + ] + .as_ref(), + ) + .split(f.size()); + + let header = Paragraph::new(format!("CHORUS CLUSTER DASHBOARD | BPM: {} | BEAT: {}", app.pulse_bpm, app.beat_index)) + .block(Block::default().borders(Borders::ALL).title("Pulse")); + f.render_widget(header, chunks[0]); + + let logs: Vec = app.logs.iter().rev().map(|s| ListItem::new(s.as_str())).collect(); + let log_list = List::new(logs).block(Block::default().borders(Borders::ALL).title("Live Events")); + f.render_widget(log_list, chunks[1]); +}