From ab6524561948396e27c3268f4f27a0e8c5059fc1 Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 4 Jun 2026 19:47:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(#1298):=20add=20hivectl=20choom=20verb=20?= =?UTF-8?q?=E2=80=94=20interactive=20claude=20session=20in=20agent=20conta?= =?UTF-8?q?iner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hive-c0re/src/bin/hivectl.rs | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index 52e4eafd..0f1419b5 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -15,6 +15,8 @@ //! `hive-c0re` lib — single source of truth, no duplication. use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::os::unix::process::CommandExt as _; use anyhow::{Context as _, Result, bail}; use clap::{Parser, Subcommand}; @@ -69,6 +71,25 @@ enum Cmd { #[command(subcommand)] cmd: AgentsCmd, }, + /// Open an interactive Claude session inside an agent container. + /// + /// Replaces the current process with `machinectl shell h-` + /// running `claude --continue` — drops the operator straight into + /// the agent's live Claude session with its full loaded context and + /// persona. Requires root (same as all machinectl shell operations) + /// and the container must be running. + /// + /// Pass `--fresh` to start a new Claude session instead of continuing + /// the most recent one. + Choom { + /// Agent name (e.g. `damocles`, `iris`). + name: String, + /// Start a fresh Claude session instead of continuing the most + /// recent one. Without this flag `--continue` is passed to Claude + /// so the operator joins the agent's ongoing session context. + #[arg(long)] + fresh: bool, + }, } #[derive(Subcommand)] @@ -292,6 +313,7 @@ async fn main() -> Result<()> { AgentsCmd::Restart { name, socket } => agents_restart(&socket, &name).await, AgentsCmd::RestartAll { socket } => agents_restart_all(&socket).await, }, + Cmd::Choom { name, fresh } => choom(&name, fresh), } } @@ -304,6 +326,35 @@ fn is_agent(name: &str) -> bool { Coordinator::agent_state_root(name).exists() } +/// Drop into an interactive Claude session in the agent container. +/// +/// Replaces the current process (exec) with `machinectl shell h- +/// /run/current-system/sw/bin/claude [--continue]`. The `--continue` +/// flag is passed by default so the operator joins the agent's active +/// session; omit it via `--fresh` to start a blank session instead. +/// +/// `machinectl shell` inherits the caller's PTY, so the session is +/// fully interactive. Requires root and a running container. +fn choom(name: &str, fresh: bool) -> Result<()> { + if !is_agent(name) { + bail!( + "no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)" + ); + } + let container = hive_c0re::lifecycle::container_name(name); + let claude = "/run/current-system/sw/bin/claude"; + let mut cmd = std::process::Command::new("machinectl"); + cmd.arg("shell").arg(&container).arg(claude); + if !fresh { + cmd.arg("--continue"); + } + // exec() replaces the current process — we inherit stdin/stdout/stderr + // (the caller's PTY) so the Claude session is fully interactive. + // This call only returns on error. + let err = cmd.exec(); + Err(anyhow::anyhow!("exec machinectl: {err}")) +} + async fn forge_create_user(name: &str, password: Option<&str>, password_stdin: bool) -> Result<()> { if !hive_c0re::forge::is_present().await { bail!(