diff --git a/ecc2/src/main.rs b/ecc2/src/main.rs index 70f469b4..e6a36e76 100644 --- a/ecc2/src/main.rs +++ b/ecc2/src/main.rs @@ -103,7 +103,11 @@ enum Commands { lead_limit: usize, }, /// Show global coordination, backlog, and daemon policy status - CoordinationStatus, + CoordinationStatus { + /// Emit machine-readable JSON instead of the human summary + #[arg(long)] + json: bool, + }, /// Rebalance unread handoffs across lead teams with backed-up delegates RebalanceAll { /// Agent type for routed delegates @@ -460,9 +464,9 @@ async fn main() -> Result<()> { ); } } - Some(Commands::CoordinationStatus) => { + Some(Commands::CoordinationStatus { json }) => { let status = session::manager::get_coordination_status(&db, &cfg)?; - println!("{status}"); + println!("{}", format_coordination_status(&status, json)?); } Some(Commands::RebalanceAll { agent, @@ -670,6 +674,17 @@ fn short_session(session_id: &str) -> String { session_id.chars().take(8).collect() } +fn format_coordination_status( + status: &session::manager::CoordinationStatus, + json: bool, +) -> Result { + if json { + return Ok(serde_json::to_string_pretty(status)?); + } + + Ok(status.to_string()) +} + fn send_handoff_message( db: &session::store::StateStore, from_id: &str, @@ -965,11 +980,48 @@ mod tests { .expect("coordination-status should parse"); match cli.command { - Some(Commands::CoordinationStatus) => {} + Some(Commands::CoordinationStatus { json }) => assert!(!json), _ => panic!("expected coordination-status subcommand"), } } + #[test] + fn cli_parses_coordination_status_json_flag() { + let cli = Cli::try_parse_from(["ecc", "coordination-status", "--json"]) + .expect("coordination-status --json should parse"); + + match cli.command { + Some(Commands::CoordinationStatus { json }) => assert!(json), + _ => panic!("expected coordination-status subcommand"), + } + } + + #[test] + fn format_coordination_status_emits_json() { + let status = session::manager::CoordinationStatus { + backlog_leads: 2, + backlog_messages: 5, + absorbable_sessions: 1, + saturated_sessions: 1, + auto_dispatch_enabled: true, + auto_dispatch_limit_per_session: 4, + daemon_activity: session::store::DaemonActivity { + last_dispatch_routed: 3, + last_dispatch_deferred: 1, + last_dispatch_leads: 2, + ..Default::default() + }, + }; + + let rendered = + format_coordination_status(&status, true).expect("json formatting should succeed"); + let value: serde_json::Value = + serde_json::from_str(&rendered).expect("valid json should be emitted"); + assert_eq!(value["backlog_leads"], 2); + assert_eq!(value["backlog_messages"], 5); + assert_eq!(value["daemon_activity"]["last_dispatch_routed"], 3); + } + #[test] fn cli_parses_rebalance_team_command() { let cli = Cli::try_parse_from([ diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index 2d214141..90f924e7 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use serde::Serialize; use std::collections::{BTreeMap, HashSet}; use std::fmt; use std::path::{Path, PathBuf}; @@ -1094,6 +1095,7 @@ pub struct CoordinateBacklogOutcome { pub remaining_saturated_sessions: usize, } +#[derive(Debug, Clone, Serialize)] pub struct CoordinationStatus { pub backlog_leads: usize, pub backlog_messages: usize, diff --git a/ecc2/src/session/store.rs b/ecc2/src/session/store.rs index ecbf5081..cc1a73ff 100644 --- a/ecc2/src/session/store.rs +++ b/ecc2/src/session/store.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use rusqlite::{Connection, OptionalExtension}; +use serde::Serialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -13,7 +14,7 @@ pub struct StateStore { conn: Connection, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] pub struct DaemonActivity { pub last_dispatch_at: Option>, pub last_dispatch_routed: usize,