diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 49b83751..fb5af985 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -18,6 +18,7 @@ This document contains the help content for the `hivectl` command-line program. * [`hivectl gateway delete-user`↴](#hivectl-gateway-delete-user) * [`hivectl gateway list-users`↴](#hivectl-gateway-list-users) * [`hivectl agents`↴](#hivectl-agents) +* [`hivectl agents list`↴](#hivectl-agents-list) * [`hivectl agents restart`↴](#hivectl-agents-restart) * [`hivectl agents restart-all`↴](#hivectl-agents-restart-all) * [`hivectl wg`↴](#hivectl-wg) @@ -267,11 +268,24 @@ Agent container management. Requires the hive-c0re daemon to be running (connect ###### **Subcommands:** +* `list` — Show all managed agents with their status (running / needs-login / needs-update) and technical state (deployed sha, parent, pending reminders). The host roster overview; reuses the dashboard's per-agent aggregation. Requires the daemon running * `restart` — Stop and start a single agent container without rebuilding config. Useful for "kick the container" when the process is stuck or the container needs a clean restart without changing the NixOS config * `restart-all` — Stop and restart ALL managed agent containers in sequence. Iterates the live container list and restarts each one. Any per-agent failure is reported at the end rather than stopping mid-run, so all containers get a restart attempt +## `hivectl agents list` + +Show all managed agents with their status (running / needs-login / needs-update) and technical state (deployed sha, parent, pending reminders). The host roster overview; reuses the dashboard's per-agent aggregation. Requires the daemon running + +**Usage:** `hivectl agents list [OPTIONS]` + +###### **Options:** + +* `--json` — Emit the raw JSON rows instead of the padded table (for scripting). The table is the default human-readable shape + + + ## `hivectl agents restart` Stop and start a single agent container without rebuilding config. Useful for "kick the container" when the process is stuck or the container needs a clean restart without changing the NixOS config diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 51989da5..1ff0ac41 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -99,10 +99,21 @@ Container lifecycle shortcuts that go through the host admin socket. Requires the `hive-c0re` daemon to be running. ```bash +hivectl agents list # roster: every agent's status + technical state +hivectl agents list --json # same data as raw JSON rows (for scripting) hivectl agents restart iris # stop + start the `iris` container (no rebuild) hivectl agents restart-all # stop + start every managed agent container in sequence ``` +`list` prints a padded table with one row per managed agent — +`NAME STATUS REV PARENT REMIND`. STATUS collapses the health flags +(`running` / `stopped`, plus ` needs-login` / ` needs-update` when set); +REV is the first 12 chars of the agent's locked config sha; PARENT is its +place in the topology tree (`-` for a root agent); REMIND is the count of +pending reminders. It reuses the same per-agent aggregation the dashboard +renders, so the CLI roster and the web UI never drift. `--json` emits the +raw rows instead of the table. + `restart` is the manual equivalent of the MCP `restart` tool — useful when you need to kick a container from the host without going through the agent hierarchy. Failures on `restart-all` are collected and diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index c3a91d99..d65df71c 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -504,6 +504,16 @@ const DEFAULT_HOST_SOCKET: &str = "/run/hyperhive/host.sock"; #[derive(Subcommand)] enum AgentsCmd { + /// Show all managed agents with their status (running / needs-login / + /// needs-update) and technical state (deployed sha, parent, pending + /// reminders). The host roster overview; reuses the dashboard's + /// per-agent aggregation. Requires the daemon running. + List { + /// Emit the raw JSON rows instead of the padded table (for + /// scripting). The table is the default human-readable shape. + #[arg(long)] + json: bool, + }, /// Stop and start a single agent container without rebuilding config. /// Useful for "kick the container" when the process is stuck or the /// container needs a clean restart without changing the NixOS config. @@ -577,6 +587,7 @@ async fn main() -> Result<()> { GatewayCmd::ListUsers { file } => gateway_list_users(&file), }, Cmd::Agents { cmd } => match cmd { + AgentsCmd::List { json } => agents_list(&socket, json).await, AgentsCmd::Restart { name } => agents_restart(&socket, &name).await, AgentsCmd::RestartAll => agents_restart_all(&socket).await, }, @@ -1374,6 +1385,82 @@ async fn agents_restart(socket: &Path, name: &str) -> Result<()> { } } +/// `hivectl agents list` — fetch the per-agent status roster from the +/// daemon (`HostRequest::AgentStatus`) and render it as a padded table, +/// or the raw JSON rows with `--json`. Reuses the dashboard's +/// `ContainerView` aggregation, so the CLI and the web UI never drift. +async fn agents_list(socket: &Path, json: bool) -> Result<()> { + let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::AgentStatus) + .await + .with_context(|| format!("connect to daemon socket {}", socket.display()))?; + if !resp.ok { + bail!( + "agents list: {}", + resp.error.as_deref().unwrap_or("unknown error") + ); + } + let rows = resp.agent_statuses.unwrap_or_default(); + if json { + println!("{}", serde_json::to_string_pretty(&rows)?); + return Ok(()); + } + if rows.is_empty() { + println!("no managed agents found"); + return Ok(()); + } + // STATUS collapses the health flags into one space-separated token so + // the common case (`running`) stays short and anomalies stand out. + let status_of = |r: &hive_sh4re::AgentStatusRow| -> String { + let mut s = if r.running { "running" } else { "stopped" }.to_owned(); + if r.needs_login { + s.push_str(" needs-login"); + } + if r.needs_update { + s.push_str(" needs-update"); + } + s + }; + let headers = ["NAME", "STATUS", "REV", "PARENT", "REMIND"]; + let table: Vec<[String; 5]> = rows + .iter() + .map(|r| { + [ + r.name.clone(), + status_of(r), + r.deployed_sha.clone().unwrap_or_else(|| "-".to_owned()), + r.parent.clone().unwrap_or_else(|| "-".to_owned()), + if r.pending_reminders > 0 { + r.pending_reminders.to_string() + } else { + "-".to_owned() + }, + ] + }) + .collect(); + let mut widths = headers.map(str::len); + for row in &table { + for (i, cell) in row.iter().enumerate() { + widths[i] = widths[i].max(cell.len()); + } + } + let fmt_row = |cells: &[String]| -> String { + cells + .iter() + .enumerate() + .map(|(i, c)| format!("{c:>() + .join(" ") + .trim_end() + .to_owned() + }; + let header_cells: Vec = headers.iter().map(|h| (*h).to_owned()).collect(); + println!("{}", fmt_row(&header_cells)); + for row in &table { + println!("{}", fmt_row(row)); + } + Ok(()) +} + async fn agents_restart_all(socket: &Path) -> Result<()> { let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::RestartAll) .await diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 13ca9719..21402aec 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -139,6 +139,22 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { } HostRequest::Rebuild { name } => handle_rebuild(&coord, name).await?, HostRequest::List => HostResponse::list(lifecycle::list().await?), + HostRequest::AgentStatus => { + let rows = crate::container_view::build_all(&coord) + .await + .into_iter() + .map(|v| hive_sh4re::AgentStatusRow { + name: v.name, + running: v.running, + needs_update: v.needs_update, + needs_login: v.needs_login, + deployed_sha: v.deployed_sha, + pending_reminders: v.pending_reminders, + parent: v.parent, + }) + .collect(); + HostResponse::agent_statuses(rows) + } // The hive domain is injected into c0re's service env by // hive-c0re.nix (`HYPERHIVE_HIVE_DOMAIN`); surface it so the // operator CLI can fill in this hive's own identity. @@ -246,6 +262,7 @@ async fn handle_restart_all() -> Result { agents: Some(ok_agents), approvals: None, domain: None, + agent_statuses: None, }) } } @@ -414,6 +431,7 @@ fn finish_lifecycle(ok_items: Vec, errors: &[String]) -> HostResponse { agents: Some(ok_items), approvals: None, domain: None, + agent_statuses: None, } } } diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index b2457862..be7a8e7a 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -44,6 +44,11 @@ pub enum HostRequest { Rebuild { name: String }, /// List managed containers. List, + /// List managed agents with their full status + technical state + /// (running / needs-login / needs-update / deployed sha / parent / + /// pending reminders) — the `hivectl agents list` roster view. + /// Reuses the dashboard's per-agent `ContainerView` aggregation. + AgentStatus, /// Report this hive's canonical DNS domain /// (`services.hyperhive.domain`), or `None` when unset. Lets the /// operator CLI fill in the hive's own identity (e.g. the federation @@ -140,6 +145,11 @@ pub struct HostResponse { /// when the domain is unset (no `services.hyperhive.domain`). #[serde(default, skip_serializing_if = "Option::is_none")] pub domain: Option, + /// `AgentStatus` result — one row per managed agent with its + /// running/health flags + technical state. `None` for every other + /// request kind. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_statuses: Option>, } /// One row in the approval queue. `commit_ref` is overloaded per @@ -236,6 +246,7 @@ impl HostResponse { agents: None, approvals: None, domain: None, + agent_statuses: None, } } @@ -247,6 +258,7 @@ impl HostResponse { agents: None, approvals: None, domain: None, + agent_statuses: None, } } @@ -258,6 +270,7 @@ impl HostResponse { agents: Some(agents), approvals: None, domain: None, + agent_statuses: None, } } @@ -269,6 +282,7 @@ impl HostResponse { agents: None, approvals: Some(approvals), domain: None, + agent_statuses: None, } } @@ -281,6 +295,20 @@ impl HostResponse { agents: None, approvals: None, domain, + agent_statuses: None, + } + } + + /// `AgentStatus` result — one row per managed agent. + #[must_use] + pub fn agent_statuses(rows: Vec) -> Self { + Self { + ok: true, + error: None, + agents: None, + approvals: None, + domain: None, + agent_statuses: Some(rows), } } } @@ -836,6 +864,34 @@ pub struct ContainerInfo { pub running: bool, } +/// One row in a `HostRequest::AgentStatus` result — the operator-CLI +/// projection of the dashboard's per-agent `ContainerView`. Carries the +/// agent's running/health flags plus the technical state an operator +/// wants in a roster overview (`hivectl agents list`). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentStatusRow { + /// Logical agent name (no `h-` prefix). + pub name: String, + /// Whether the container is currently running. + pub running: bool, + /// Config commit is pending — the locked rev differs from the + /// agent's proposed/applied config (a rebuild would change it). + pub needs_update: bool, + /// The agent has no live claude session and is parked waiting for + /// the operator's re-auth flow. + pub needs_login: bool, + /// First 12 chars of the sha the meta flake currently has locked for + /// this agent's input. `None` when the agent has no locked rev yet. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deployed_sha: Option, + /// Count of this agent's pending reminders. + #[serde(default)] + pub pending_reminders: u64, + /// Parent in the topology tree. `None` marks a root-level agent. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent: Option, +} + /// Serde default for the `running` field; keeps wire backwards-compat /// with pre-running-field payloads. See /// `docs/conventions.md::Agent metadata`.