diff --git a/CLAUDE.md b/CLAUDE.md index 0640096..fea64a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -337,7 +337,9 @@ loops over every stale container. Container row buttons (rendered per-state by `assets/app.js`): - Always: `↻ R3BU1LD` (calls `lifecycle::rebuild`), and for sub-agents - `DESTR0Y` (container removed, state + creds kept). + `DESTR0Y` (container removed, state + creds kept) + `PURG3` + (DESTR0Y plus wipes `/var/lib/hyperhive/{agents,applied}//`; + no undo). - Running: `↺ R3ST4RT` + (sub-agents only) `■ ST0P`. - Stopped: `▶ ST4RT`. - Stale marker: clickable `needs update ↻` badge (same target as rebuild diff --git a/TODO.md b/TODO.md index 5492d72..dc956e7 100644 --- a/TODO.md +++ b/TODO.md @@ -42,11 +42,18 @@ Pick anything from here when relevant. Cross-cutting design notes live in channel from the harness: idle when waiting on the inbox, thinking while claude's stream is open, compacting when `/compact` is in flight. Replaces the binary "harness alive — turn loop running" line. -- **Terminal: slash commands + tab-completion.** Operator-facing - in-terminal commands: `/help`, `/model`, `/compact`, `/clear`. Tab - completes command names + model names (cf. bitburner-agent's pattern). -- **Terminal: multi-line input.** Replace the single-line `` with - an auto-growing textarea; Enter sends, Shift+Enter newlines. +- **Terminal: slash commands beyond /help and /clear.** Operator-facing + in-terminal commands still to add: `/model`, `/compact`, `/cancel`. + Each needs harness-side support (model override, force compaction, + cancel current claude turn). +- **Terminal: bigger.** The 32em max-height is cramped on a 1080p+ + screen. Grow it (e.g. `min(70vh, 60em)`) so the live tail is the + main visual element of the page rather than a strip. +- **Terminal: sticky-bottom auto-scroll.** Today every appended row + scrolls to bottom, so the view shifts while the operator is reading + scrolled-up. Track whether the user is *already* at the bottom + (within a small threshold), and only auto-scroll when that's true. + Show a small "↓ N new" indicator when not at bottom; click to jump. - **Terminal: cancel-current-turn button.** Explicit "kill claude process for this turn" control. Harness needs to track the in-flight claude child PID and offer a `/cancel` endpoint that sends @@ -57,9 +64,6 @@ Pick anything from here when relevant. Cross-cutting design notes live in same session id. Surfaces as a slash command in the terminal + a toolbar button while the state badge is `idle`. Sets state to `compacting` during the run. -- **Visuals.** Frosted-glass backdrop blur on the terminal wrap, - per-event fade-in slide-up animation on new rows, badge pulse - animation on state-badge transitions. - **xterm.js terminal** embedded per-agent, attached to a PTY exposed by the harness. Pairs well with the unprivileged-container work — would let the operator drop into the container without `nixos-container root-login`. @@ -115,9 +119,6 @@ Pick anything from here when relevant. Cross-cutting design notes live in - **Container crash events.** Watch `container@*.service` via D-Bus, push `HelperEvent::ContainerCrash` to the manager's inbox so the manager can react (restart, escalate, etc.). -- **`destroy --purge`.** Today `destroy` keeps state by design; add an - opt-in flag (CLI + dashboard) to also wipe `/var/lib/hyperhive/agents//` - and `/var/lib/hyperhive/applied//`. ## Cleanup / docs diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index d16f3b8..4fdc692 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -167,6 +167,10 @@ ' ', form('/destroy/' + c.name, 'btn-destroy', 'DESTR0Y', 'destroy ' + c.name + '? container is removed; state + creds kept.'), + ' ', + form('/destroy/' + c.name, 'btn-destroy', 'PURG3', + 'PURGE ' + c.name + '? container, config history, claude creds, ' + + 'and /state/ notes are all WIPED. no undo.', { purge: 'on' }), ); } ul.append(li); diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 8e7dee9..069b640 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -132,22 +132,40 @@ fn finish_approval( /// kept, so recreating an agent of the same name reuses prior config + creds /// (no re-login). The ephemeral runtime dir under `/run/hyperhive/agents/` /// is cleared because its contents (the mcp socket) don't survive restarts -/// anyway. A future `--purge` path can wipe state when the operator opts in. +/// anyway. With `purge=true` the persistent trees are also wiped — config +/// history, claude creds, notes — there is no undo. /// Refuses the manager (declarative; would fight with the host's nixos config). -pub async fn destroy(coord: &Coordinator, name: &str) -> Result<()> { +pub async fn destroy(coord: &Coordinator, name: &str, purge: bool) -> Result<()> { if name == MANAGER_NAME || name == MANAGER_AGENT { bail!("refusing to destroy the manager ({name})"); } - tracing::info!(%name, "destroy"); + tracing::info!(%name, purge, "destroy"); lifecycle::destroy(name).await?; coord.unregister_agent(name); let runtime = Coordinator::agent_dir(name); if runtime.exists() { let _ = std::fs::remove_dir_all(&runtime); } - let _ = coord - .approvals - .fail_pending_for_agent(name, "agent destroyed"); + if purge { + for dir in [ + Coordinator::agent_state_root(name), + Coordinator::agent_applied_dir(name), + ] { + if dir.exists() + && let Err(e) = std::fs::remove_dir_all(&dir) + { + tracing::warn!(error = ?e, dir = %dir.display(), "purge: remove failed"); + } + } + } + let _ = coord.approvals.fail_pending_for_agent( + name, + if purge { + "agent purged" + } else { + "agent destroyed" + }, + ); coord.notify_manager(&HelperEvent::Destroyed { agent: name.to_owned(), }); diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index bcfed09..02b5bc9 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -424,8 +424,20 @@ fn strip_container_prefix(name: &str) -> String { .to_owned() } -async fn post_destroy(State(state): State, AxumPath(name): AxumPath) -> Response { - match actions::destroy(&state.coord, &name).await { +#[derive(Deserialize, Default)] +struct DestroyForm { + #[serde(default)] + purge: Option, +} + +async fn post_destroy( + State(state): State, + AxumPath(name): AxumPath, + Form(form): Form, +) -> Response { + // Checkbox semantics: any non-empty value (axum sends "on") = purge. + let purge = form.purge.as_deref().is_some_and(|v| !v.is_empty()); + match actions::destroy(&state.coord, &name, purge).await { Ok(()) => Redirect::to("/").into_response(), Err(e) => error_response(&format!("destroy {name} failed: {e:#}")), } diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index c78735b..6561703 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -56,8 +56,14 @@ enum Cmd { /// Stop a managed container (graceful). Kill { name: String }, /// Tear down a sub-agent container. Container is removed; persistent - /// state (config repos + Claude credentials) is kept by default. - Destroy { name: String }, + /// state (config repos + Claude credentials) is kept by default. Pass + /// `--purge` to also wipe the agent's state dirs (config + creds + + /// notes). No undo. + Destroy { + name: String, + #[arg(long)] + purge: bool, + }, /// Apply pending config to a managed container. Rebuild { name: String }, /// List managed containers. @@ -121,8 +127,8 @@ async fn main() -> Result<()> { Cmd::Kill { name } => { render(client::request(&cli.socket, HostRequest::Kill { name }).await?) } - Cmd::Destroy { name } => { - render(client::request(&cli.socket, HostRequest::Destroy { name }).await?) + Cmd::Destroy { name, purge } => { + render(client::request(&cli.socket, HostRequest::Destroy { name, purge }).await?) } Cmd::Rebuild { name } => { render(client::request(&cli.socket, HostRequest::Rebuild { name }).await?) diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index afe1985..9ce4df1 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -115,8 +115,8 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { }); HostResponse::success() } - HostRequest::Destroy { name } => { - actions::destroy(&coord, name).await?; + HostRequest::Destroy { name, purge } => { + actions::destroy(&coord, name, *purge).await?; HostResponse::success() } HostRequest::Rebuild { name } => { diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 3415d20..067d79e 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -27,8 +27,15 @@ pub enum HostRequest { /// Tear down a sub-agent container: stop + remove + drop the systemd /// drop-in, purge pending approvals. Persistent state (proposed/applied /// repos, Claude credentials) is KEPT by default — recreating the agent - /// with the same name reuses prior config + login. Manager not destroyable. - Destroy { name: String }, + /// with the same name reuses prior config + login. With `purge=true` + /// the agent's `/var/lib/hyperhive/{agents,applied}//` trees are + /// also wiped (config history + creds + notes gone forever). Manager + /// not destroyable. + Destroy { + name: String, + #[serde(default)] + purge: bool, + }, /// Apply pending config to a managed container. Rebuild { name: String }, /// List managed containers.