Implement next-gen modules: chrs-code-edit (Git), chrs-discovery (LibP2P), and chrs-observer (TUI)
This commit is contained in:
9
chrs-code-edit/Cargo.toml
Normal file
9
chrs-code-edit/Cargo.toml
Normal file
@@ -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"
|
||||||
46
chrs-code-edit/src/lib.rs
Normal file
46
chrs-code-edit/src/lib.rs
Normal file
@@ -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<P: AsRef<Path>>(path: P) -> Result<Self, EditError> {
|
||||||
|
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<String, EditError> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
11
chrs-discovery/Cargo.toml
Normal file
11
chrs-discovery/Cargo.toml
Normal file
@@ -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"] }
|
||||||
69
chrs-discovery/src/lib.rs
Normal file
69
chrs-discovery/src/lib.rs
Normal file
@@ -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<dyn Error>> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
chrs-observer/Cargo.toml
Normal file
14
chrs-observer/Cargo.toml
Normal file
@@ -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"
|
||||||
103
chrs-observer/src/main.rs
Normal file
103
chrs-observer/src/main.rs
Normal file
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn new() -> App {
|
||||||
|
App {
|
||||||
|
pulse_bpm: 30,
|
||||||
|
beat_index: 0,
|
||||||
|
logs: vec!["[OBSERVER] Initialized.".into()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
// 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<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, 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<ListItem> = 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]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user