diff --git a/hive-ag3nt/prompts/agent.md b/hive-ag3nt/prompts/agent.md index 46a1eddc..88f48d03 100644 --- a/hive-ag3nt/prompts/agent.md +++ b/hive-ag3nt/prompts/agent.md @@ -9,12 +9,10 @@ Tools (hyperhive surface): Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `manager`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config. -Durable knowledge: write to `/agents/{label}/state/notes.md` (free-form) or any other path under `/agents/{label}/state/`. That directory is bind-mounted from the host and persists across container destroy/recreate — claude's `--continue` session only carries short-term context, but `/agents/{label}/state/` is forever. Read it back at the start of relevant turns to remember things across resets. - -Claude session (OAuth credentials) lives at `/root/.claude/` and persists across restarts. +Durable knowledge: write to `/state/notes.md` (free-form) or any other path under `/state/`. That directory is bind-mounted from the host and persists across container destroy/recreate — claude's `--continue` session only carries short-term context, but `/state/` is forever. Read it back at the start of relevant turns to remember things across resets. **Shared space**: `/shared` is accessible to all agents (read/write). Only put things here you're willing to lose — other agents may delete them. Use for explicit cross-agent communication or shared artifacts when appropriate. -Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis): write the payload to `/agents/{label}/state/` and `send` a short pointer ("dropped the cluster audit in /state/cluster-audit-2026-05.md, headline: 3 nodes over 80% mem"). The manager + operator can read your state from the host as `/agents/{label}/state/`. Sub-agent peers can't read each other's `/state/` directly — go through the manager if a payload needs to reach another sub-agent. +Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis): write the payload to `/state/` and `send` a short pointer ("dropped the cluster audit in /state/cluster-audit-2026-05.md, headline: 3 nodes over 80% mem"). The manager + operator can read your `/state/` from the host as `/agents/{label}/state/`. Sub-agent peers can't read each other's `/state/` directly — go through the manager if a payload needs to reach another sub-agent. When your inbox has a message, handle it and stop. Don't narrate intent — act. diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md index c08f5b8a..8a4d9452 100644 --- a/hive-ag3nt/prompts/manager.md +++ b/hive-ag3nt/prompts/manager.md @@ -4,12 +4,12 @@ Tools (hyperhive surface): - `mcp__hyperhive__recv(wait_seconds?)` — drain one more message from your inbox. Without `wait_seconds` (or with `0`) it returns immediately — a cheap inbox peek you can drop between actions. To **wait** when you have nothing else to do, call with a long wait (e.g. `wait_seconds: 180`, the max) — you'll wake instantly on new work, otherwise return after the timeout. Use that instead of ending the turn or sleeping in a Bash command. - `mcp__hyperhive__send(to, body)` — message an agent (by name), another peer, or the operator (`operator` surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). -- `mcp__hyperhive__request_spawn(name, description?)` — queue a brand-new sub-agent for operator approval (≤9 char name). Pass an optional `description` and it appears on the dashboard approval card — no need to send a separate message explaining the request. +- `mcp__hyperhive__request_spawn(name)` — queue a brand-new sub-agent for operator approval (≤9 char name). - `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent. No approval required. - `mcp__hyperhive__start(name)` — start a stopped sub-agent. No approval required. - `mcp__hyperhive__restart(name)` — stop + start a sub-agent. No approval required. - `mcp__hyperhive__update(name)` — rebuild a sub-agent (re-applies the current hyperhive flake + agent.nix, restarts the container). No approval required — idempotent. Use when you receive a `needs_update` system event. -- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` — submit a config change for any agent (`hm1nd` for self) for operator approval. Pass an optional `description` and it appears on the dashboard approval card so the operator knows what changed without opening the diff. At submit time hive-c0re fetches your commit into the agent's applied repo and pins it as `proposal/`; from that moment your proposed-side commit can be amended or force-pushed freely without changing what the operator will build. +- `mcp__hyperhive__request_apply_commit(agent, commit_ref)` — submit a config change for any agent (`hm1nd` for self) for operator approval. At submit time hive-c0re fetches your commit into the agent's applied repo and pins it as `proposal/`; from that moment your proposed-side commit can be amended or force-pushed freely without changing what the operator will build. - `mcp__hyperhive__ask_operator(question, options?, multi?, ttl_seconds?)` — surface a question on the dashboard. Returns immediately with a question id; the operator's answer arrives later as a system `operator_answered` event in your inbox. Options are advisory: the dashboard always lets the operator type a free-text answer in addition. Set `multi: true` to render options as checkboxes (operator can pick multiple); the answer comes back as `, `-separated. Set `ttl_seconds` to auto-cancel after a deadline — useful when the decision becomes moot if the operator hasn't responded in time; on expiry the answer is `[expired]`. Do not poll inside the same turn — finish the current work and react when the event lands. Approval boundary: lifecycle ops on *existing* sub-agents (`kill`, `start`, `restart`) are at your discretion — no operator approval. *Creating* a new agent (`request_spawn`) and *changing* any agent's config (`request_apply_commit`) still go through the approval queue. The operator only signs off on changes; you run the day-to-day. @@ -32,7 +32,7 @@ in { environment.systemPackages = [ matrixPkg ]; hyperhive.extraMcpServers.matrix = { command = "${matrixPkg}/bin/mcp-matrix"; - args = [ "--config" "/agents//state/matrix.toml" ]; # replace with the agent's label + args = [ "--config" "/state/matrix.toml" ]; allowedTools = [ "send_message" "join_room" ]; }; } diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index fa9e1fe7..8d6741b1 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -65,7 +65,7 @@ async fn main() -> Result<()> { .and_then(|s| s.parse::().ok()) .unwrap_or(DEFAULT_WEB_PORT); let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hive-ag3nt".into()); - let claude_dir = login::default_dir(); + let claude_dir = PathBuf::from(login::DEFAULT_CLAUDE_DIR); let initial = LoginState::from_dir(&claude_dir); tracing::info!(state = ?initial, claude_dir = %claude_dir.display(), "harness boot"); let login_state = Arc::new(Mutex::new(initial)); diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index aa700796..b56e9b4c 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -55,7 +55,7 @@ async fn main() -> Result<()> { .and_then(|s| s.parse::().ok()) .unwrap_or(DEFAULT_WEB_PORT); let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hm1nd".into()); - let claude_dir = login::default_dir(); + let claude_dir = PathBuf::from(login::DEFAULT_CLAUDE_DIR); let initial = LoginState::from_dir(&claude_dir); tracing::info!(state = ?initial, claude_dir = %claude_dir.display(), "hm1nd boot"); let login_state = Arc::new(Mutex::new(initial)); diff --git a/hive-ag3nt/src/events.rs b/hive-ag3nt/src/events.rs index c8b0b08e..a5c89d2f 100644 --- a/hive-ag3nt/src/events.rs +++ b/hive-ag3nt/src/events.rs @@ -20,18 +20,21 @@ const CHANNEL_CAPACITY: usize = 256; /// Max `LiveEvent`s the `Bus` returns from `history()` and keeps in /// sqlite. Older rows are vacuumed on a periodic sweep. const HISTORY_CAPACITY: usize = 2000; -/// Path to the persisted event db. Overridable via `HYPERHIVE_EVENTS_DB` -/// for dev / tests; otherwise derived from the agent's state dir. -fn events_db_path() -> PathBuf { - std::env::var_os("HYPERHIVE_EVENTS_DB") - .map_or_else(|| crate::paths::state_dir().join("hyperhive-events.sqlite"), PathBuf::from) -} +/// Default sqlite db path. Lives under `/state/` so it survives +/// destroy/recreate but goes away on purge. Overridable via the +/// `HYPERHIVE_EVENTS_DB` env var (used in tests and one-shot tools). +const DEFAULT_EVENTS_DB: &str = "/state/hyperhive-events.sqlite"; -/// Path to the persisted model file. Overridable via `HYPERHIVE_MODEL_FILE` -/// for dev / tests; otherwise derived from the agent's state dir. +/// Persisted model name file. Same lifecycle as the events db — +/// survives destroy/recreate, gone on purge. Empty / missing file +/// falls back to `DEFAULT_MODEL`. +const DEFAULT_MODEL_FILE: &str = "/state/hyperhive-model"; + +/// Path to the persisted model file. Overridable via +/// `HYPERHIVE_MODEL_FILE` for dev / tests. fn model_file_path() -> PathBuf { std::env::var_os("HYPERHIVE_MODEL_FILE") - .map_or_else(|| crate::paths::state_dir().join("hyperhive-model"), PathBuf::from) + .map_or_else(|| PathBuf::from(DEFAULT_MODEL_FILE), PathBuf::from) } fn load_model() -> Option { @@ -180,7 +183,7 @@ pub struct Bus { tx: Arc>, /// Persistent event log. `None` only if opening the sqlite db failed /// at construction — we keep going so the harness doesn't die on a - /// missing state dir mount in dev / test scenarios. + /// missing `/state/` mount in dev / test scenarios. store: Option>, /// Current turn-loop state + since-when (unix seconds). state: Arc>, @@ -197,11 +200,13 @@ pub struct Bus { } impl Bus { - /// Open the events db (path from `events_db_path()`). On failure, fall back - /// to a no-store bus — the harness still works, just without persistent history. + /// Open the default events db (`/state/hyperhive-events.sqlite`, or + /// `HYPERHIVE_EVENTS_DB`). On failure, fall back to a no-store bus — + /// the harness still works, just without persistent history. #[must_use] pub fn new() -> Self { - let path = events_db_path(); + let path = std::env::var_os("HYPERHIVE_EVENTS_DB") + .map_or_else(|| PathBuf::from(DEFAULT_EVENTS_DB), PathBuf::from); let store = match EventStore::open(&path) { Ok(s) => Some(Arc::new(s)), Err(e) => { @@ -242,8 +247,8 @@ impl Bus { } /// Switch the model for future turns. The current turn (if any) - /// keeps the model it was already running. Persisted to the agent's - /// state dir (`hyperhive-model`) so the override survives harness + /// keeps the model it was already running. Persisted to + /// `/state/hyperhive-model` so the override survives harness /// restart and container rebuild (gone on `--purge`, matching /// every other piece of agent state). pub fn set_model(&self, name: impl Into) { diff --git a/hive-ag3nt/src/lib.rs b/hive-ag3nt/src/lib.rs index 22ced069..d29f3938 100644 --- a/hive-ag3nt/src/lib.rs +++ b/hive-ag3nt/src/lib.rs @@ -6,7 +6,6 @@ pub mod events; pub mod login; pub mod login_session; pub mod mcp; -pub mod paths; pub mod plugins; pub mod turn; pub mod web_ui; diff --git a/hive-ag3nt/src/login.rs b/hive-ag3nt/src/login.rs index 68578dbb..208045b0 100644 --- a/hive-ag3nt/src/login.rs +++ b/hive-ag3nt/src/login.rs @@ -1,6 +1,6 @@ //! Login-state probe for the bind-mounted `~/.claude/` dir. The dir is -//! provided by hive-c0re and persists across container destroy/recreate so -//! OAuth tokens survive. +//! provided by hive-c0re (Phase 8 step 1) and persists across container +//! destroy/recreate so OAuth tokens survive. //! //! "Has session" today means "the dir contains at least one regular file." //! That's a heuristic: a fresh bind-mount starts empty, and `claude auth login` @@ -8,15 +8,11 @@ //! specific credentials filename, or run a no-op `claude` call) once the //! exact layout is locked in. -use std::path::{Path, PathBuf}; +use std::path::Path; -/// Returns the Claude credentials directory for this agent, derived from -/// `HIVE_LABEL`. Manager ("hm1nd") uses `/root/.claude`; sub-agents use -/// `/agents/{label}/claude`. Overridable via `HYPERHIVE_CLAUDE_DIR`. -#[must_use] -pub fn default_dir() -> PathBuf { - crate::paths::claude_dir() -} +/// Mount point of the per-agent Claude credentials dir inside the container. +/// Matches `hive_c0re::lifecycle::CONTAINER_CLAUDE_MOUNT`. +pub const DEFAULT_CLAUDE_DIR: &str = "/root/.claude"; /// Returns `true` if `dir` exists and contains any regular file. Used at /// startup to decide whether to enter the turn loop (logged in) or stay in diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 90a8e739..a31a791d 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -265,10 +265,6 @@ pub struct RequestSpawnArgs { /// New sub-agent name (≤9 chars). Queues a Spawn approval; the /// operator approves on the dashboard before the container is created. pub name: String, - /// Optional description shown on the dashboard approval card so the - /// operator knows what the new agent is for without a separate message. - #[serde(default)] - pub description: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] @@ -324,10 +320,6 @@ pub struct RequestApplyCommitArgs { pub agent: String, /// Git sha (full or short) pointing at the proposed `agent.nix`. pub commit_ref: String, - /// Optional description shown on the dashboard approval card so the - /// operator knows what the change does without opening the diff. - #[serde(default)] - pub description: Option, } #[derive(Debug, Clone)] @@ -403,10 +395,7 @@ impl ManagerServer { let name = args.name.clone(); run_tool_envelope("request_spawn", log, async move { let resp = self - .dispatch(hive_sh4re::ManagerRequest::RequestSpawn { - name: args.name, - description: args.description, - }) + .dispatch(hive_sh4re::ManagerRequest::RequestSpawn { name: args.name }) .await; format_ack( resp, @@ -532,7 +521,6 @@ impl ManagerServer { .dispatch(hive_sh4re::ManagerRequest::RequestApplyCommit { agent: args.agent, commit_ref: args.commit_ref, - description: args.description, }) .await; format_ack( diff --git a/hive-ag3nt/src/paths.rs b/hive-ag3nt/src/paths.rs deleted file mode 100644 index 0774f75a..00000000 --- a/hive-ag3nt/src/paths.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Per-agent path resolution for state and credential directories. -//! -//! Manager ("hm1nd") keeps `/state`; sub-agents use `/agents/{label}/state`. -//! Claude credentials are always at `/root/.claude` for all agents. -//! -//! Both paths can be overridden via env vars (`HYPERHIVE_STATE_DIR`, -//! `HYPERHIVE_CLAUDE_DIR`) for dev / test scenarios. - -use std::path::PathBuf; - -/// Container label of the manager. Sub-agents get `/agents/{label}/state`; -/// the manager keeps `/state`. Must match `hive-c0re::lifecycle::MANAGER_NAME`. -pub const MANAGER_NAME: &str = "hm1nd"; - -/// Durable state directory for the current agent. Reads `HYPERHIVE_STATE_DIR` -/// first; falls back to `/agents/{label}/state` for sub-agents or `/state` for -/// the manager / any unrecognised label. -#[must_use] -pub fn state_dir() -> PathBuf { - if let Some(p) = std::env::var_os("HYPERHIVE_STATE_DIR") { - return PathBuf::from(p); - } - let label = std::env::var("HIVE_LABEL").unwrap_or_default(); - if label == MANAGER_NAME || label.is_empty() { - PathBuf::from("/state") - } else { - PathBuf::from(format!("/agents/{label}/state")) - } -} - -/// Claude credentials directory for the current agent. Always `/root/.claude` -/// because the `claude` CLI reads `$HOME/.claude` (uid 0 → `/root`), and -/// hive-c0re binds the per-agent credentials dir there for every container. -/// Overridable via `HYPERHIVE_CLAUDE_DIR` for dev / test scenarios. -#[must_use] -pub fn claude_dir() -> PathBuf { - std::env::var_os("HYPERHIVE_CLAUDE_DIR") - .map_or_else(|| PathBuf::from("/root/.claude"), PathBuf::from) -} diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 651f2a72..89a614ce 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -230,13 +230,15 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result )); } let mut cmd = Command::new("claude"); - // Spawn inside the agent's state dir so relative paths in tool calls - // (Read foo.md, Bash ls, Write notes.md) land in the durable dir - // instead of wherever the harness systemd unit started. Falls back - // silently if the dir is missing (dev / test without the bind mount). - let state_dir = crate::paths::state_dir(); - if state_dir.is_dir() { - cmd.current_dir(&state_dir); + // Spawn inside /state so any path claude resolves relatively (Read + // foo.md, Bash ls, Write notes.md) lands in the agent's durable + // dir instead of wherever the harness systemd unit started. /state + // is bind-mounted RW from the host so survives destroy/recreate. + // Fall back silently if the dir is missing (dev / test setups + // running without the bind mount) — Command picks up the parent's + // cwd in that case. + if std::path::Path::new("/state").is_dir() { + cmd.current_dir("/state"); } cmd.arg("--print") .arg("--verbose") diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index ecaae207..7b075c03 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -684,9 +684,6 @@ 'new sub-agent — container will be created on approve'), ); } - if (a.description) { - li.append(el('div', { class: 'approval-description' }, a.description)); - } // Deny prompts the operator for an optional reason; the // submit handler stashes it into a hidden `note` input that // rides along on the POST and is surfaced to the manager via diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index 5f92e40c..27269eb1 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -258,7 +258,6 @@ code { } .approvals .row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4em; } .approvals form.inline { display: inline; margin-left: 0.4em; } -.approval-description { font-size: 0.85em; color: var(--fg-dim, #888); margin: 0.2em 0 0.4em 1.2em; } .approval-tabs { display: flex; gap: 0.4em; diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs index b5cbca99..995c355b 100644 --- a/hive-c0re/src/approvals.rs +++ b/hive-c0re/src/approvals.rs @@ -24,20 +24,6 @@ CREATE INDEX IF NOT EXISTS idx_approvals_pending ON approvals (id) WHERE status = 'pending'; "; -/// Add the `description` column to pre-description databases. Manager-supplied -/// note shown on the dashboard approval card at submission time (distinct from -/// `note` which is set on denial/failure). -fn ensure_description_column(conn: &Connection) -> Result<()> { - let has: bool = conn - .prepare("SELECT 1 FROM pragma_table_info('approvals') WHERE name = 'description'")? - .exists([])?; - if !has { - conn.execute_batch("ALTER TABLE approvals ADD COLUMN description TEXT;") - .context("add approvals.description column")?; - } - Ok(()) -} - /// Add the `kind` column to pre-Phase-8 databases. ALTER TABLE ADD COLUMN is /// idempotent here only via a column-existence check (sqlite doesn't support /// IF NOT EXISTS on ADD COLUMN). Defaults legacy rows to `apply_commit`, @@ -86,24 +72,21 @@ impl Approvals { .context("apply approvals schema")?; ensure_kind_column(&conn).context("migrate approvals.kind")?; ensure_fetched_sha_column(&conn).context("migrate approvals.fetched_sha")?; - ensure_description_column(&conn).context("migrate approvals.description")?; Ok(Self { conn: Mutex::new(conn), }) } - pub fn submit_kind( - &self, - agent: &str, - kind: ApprovalKind, - commit_ref: &str, - description: Option<&str>, - ) -> Result { + pub fn submit(&self, agent: &str, commit_ref: &str) -> Result { + self.submit_kind(agent, ApprovalKind::ApplyCommit, commit_ref) + } + + pub fn submit_kind(&self, agent: &str, kind: ApprovalKind, commit_ref: &str) -> Result { let conn = self.conn.lock().unwrap(); conn.execute( - "INSERT INTO approvals (agent, kind, commit_ref, requested_at, status, description) - VALUES (?1, ?2, ?3, ?4, 'pending', ?5)", - params![agent, kind_to_str(kind), commit_ref, now_unix(), description], + "INSERT INTO approvals (agent, kind, commit_ref, requested_at, status) + VALUES (?1, ?2, ?3, ?4, 'pending')", + params![agent, kind_to_str(kind), commit_ref, now_unix()], )?; Ok(conn.last_insert_rowid()) } @@ -124,7 +107,7 @@ impl Approvals { pub fn recent_resolved(&self, limit: u64) -> Result> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( - "SELECT id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha, description + "SELECT id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha FROM approvals WHERE status IN ('approved', 'denied', 'failed') ORDER BY resolved_at DESC, id DESC @@ -138,7 +121,7 @@ impl Approvals { pub fn pending(&self) -> Result> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( - "SELECT id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha, description + "SELECT id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha FROM approvals WHERE status = 'pending' ORDER BY id ASC", @@ -151,7 +134,7 @@ impl Approvals { pub fn get(&self, id: i64) -> Result> { let conn = self.conn.lock().unwrap(); conn.query_row( - "SELECT id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha, description + "SELECT id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha FROM approvals WHERE id = ?1", params![id], row_to_approval, @@ -164,9 +147,9 @@ impl Approvals { /// approval so the caller can run the action and pass the agent name. pub fn mark_approved(&self, id: i64) -> Result { let conn = self.conn.lock().unwrap(); - let current: Option<(String, String, String, i64, String, Option, Option)> = - conn.query_row( - "SELECT agent, kind, commit_ref, requested_at, status, fetched_sha, description + let current: Option<(String, String, String, i64, String, Option)> = conn + .query_row( + "SELECT agent, kind, commit_ref, requested_at, status, fetched_sha FROM approvals WHERE id = ?1", params![id], |row| { @@ -177,14 +160,11 @@ impl Approvals { row.get(3)?, row.get(4)?, row.get(5)?, - row.get(6)?, )) }, ) .optional()?; - let Some((agent, kind, commit_ref, requested_at, status, fetched_sha, description)) = - current - else { + let Some((agent, kind, commit_ref, requested_at, status, fetched_sha)) = current else { bail!("approval {id} not found"); }; if status != "pending" { @@ -205,7 +185,6 @@ impl Approvals { resolved_at: Some(resolved_at), note: None, fetched_sha, - description, }) } @@ -245,7 +224,7 @@ impl Approvals { } fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result { - // Column order: id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha, description. + // Column order: id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha. let kind: String = row.get(2)?; let kind = match kind.as_str() { "apply_commit" => ApprovalKind::ApplyCommit, @@ -282,7 +261,6 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result { resolved_at: row.get(6)?, note: row.get(7)?, fetched_sha: row.get(8)?, - description: row.get(9)?, }) } diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 3e31d57c..4fdb4171 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -237,9 +237,6 @@ struct ApprovalView { sha_short: Option, /// Pre-rendered syntax-coloured diff HTML, for `ApplyCommit` only. diff_html: Option, - /// Manager-supplied description shown on the approval card. - #[serde(skip_serializing_if = "Option::is_none")] - description: Option, } /// Replace silent `.unwrap_or_default()` on the data sources behind @@ -608,11 +605,10 @@ async fn build_approval_views(approvals: Vec) -> Vec { let diff = approval_diff(&a.agent, a.id).await; ApprovalView { id: a.id, - agent: a.agent.clone(), + agent: a.agent, kind: "apply_commit", sha_short: Some(sha), diff_html: Some(render_diff_lines(&diff)), - description: a.description, } } hive_sh4re::ApprovalKind::Spawn => ApprovalView { @@ -621,7 +617,6 @@ async fn build_approval_views(approvals: Vec) -> Vec { kind: "spawn", sha_short: None, diff_html: None, - description: a.description, }, }); } @@ -1070,7 +1065,7 @@ async fn post_request_spawn( match state .coord .approvals - .submit_kind(&name, hive_sh4re::ApprovalKind::Spawn, "", None) + .submit_kind(&name, hive_sh4re::ApprovalKind::Spawn, "") { Ok(id) => { tracing::info!(%id, %name, "operator: spawn approval queued via dashboard"); diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 760d4b43..2b9656da 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -742,22 +742,8 @@ fn set_nspawn_flags( let path = format!("/etc/nixos-containers/{container}.conf"); let original = std::fs::read_to_string(&path).with_context(|| format!("read {path}"))?; - - // Compute the in-container state mount point. Sub-agents get - // /agents//state; the manager keeps the legacy /state path. - // Claude credentials always land at /root/.claude for all agents so - // the `claude` CLI (which reads $HOME/.claude) finds them without any - // HOME override. - let notes_mount = if container == MANAGER_NAME { - CONTAINER_NOTES_MOUNT.to_owned() - } else { - let agent_name = container.strip_prefix(AGENT_PREFIX).unwrap_or(container); - format!("/agents/{agent_name}/state") - }; - let claude_mount = CONTAINER_CLAUDE_MOUNT; - let mut binds = format!( - "--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{claude_mount} --bind={notes}:{notes_mount} --bind={shared}:{CONTAINER_SHARED_MOUNT}", + "--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{CONTAINER_CLAUDE_MOUNT} --bind={notes}:{CONTAINER_NOTES_MOUNT} --bind={shared}:{CONTAINER_SHARED_MOUNT}", runtime = runtime_dir.display(), claude = claude_dir.display(), notes = notes_dir.display(), diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index aa74a53c..47c1854a 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -131,14 +131,12 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc) -> ManagerResp message: format!("{e:#}"), }, }, - ManagerRequest::RequestSpawn { name, description } => { + ManagerRequest::RequestSpawn { name } => { tracing::info!(%name, "manager: request_spawn"); - match coord.approvals.submit_kind( - name, - hive_sh4re::ApprovalKind::Spawn, - "", - description.as_deref(), - ) { + match coord + .approvals + .submit_kind(name, hive_sh4re::ApprovalKind::Spawn, "") + { Ok(id) => { tracing::info!(%id, %name, "spawn approval queued"); ManagerResponse::Ok @@ -259,13 +257,9 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc) -> ManagerResp }, } } - ManagerRequest::RequestApplyCommit { - agent, - commit_ref, - description, - } => { + ManagerRequest::RequestApplyCommit { agent, commit_ref } => { tracing::info!(%agent, %commit_ref, "manager: request_apply_commit"); - match submit_apply_commit(coord, agent, commit_ref, description.as_deref()).await { + match submit_apply_commit(coord, agent, commit_ref).await { Ok((id, sha)) => { tracing::info!(%id, %agent, manager_ref = %commit_ref, %sha, "approval queued + proposal tag planted"); ManagerResponse::Ok @@ -293,7 +287,6 @@ async fn submit_apply_commit( coord: &Arc, agent: &str, commit_ref: &str, - description: Option<&str>, ) -> anyhow::Result<(i64, String)> { let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(agent); let applied_dir = crate::coordinator::Coordinator::agent_applied_dir(agent); @@ -311,12 +304,7 @@ async fn submit_apply_commit( } let id = coord .approvals - .submit_kind( - agent, - hive_sh4re::ApprovalKind::ApplyCommit, - commit_ref, - description, - ) + .submit(agent, commit_ref) .map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?; let tag = format!("proposal/{id}"); let sha = match crate::lifecycle::git_fetch_to_tag( diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index c11a8948..1a16291b 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -106,7 +106,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { tracing::info!(%name, "request_spawn"); let id = coord .approvals - .submit_kind(name, hive_sh4re::ApprovalKind::Spawn, "", None)?; + .submit_kind(name, hive_sh4re::ApprovalKind::Spawn, "")?; tracing::info!(%id, %name, "spawn approval queued"); HostResponse::success() } diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 5a6fd0ce..ed810516 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -83,11 +83,6 @@ pub struct Approval { pub resolved_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub note: Option, - /// Optional free-text description the manager attached at submission - /// time — shown on the dashboard approval card so the operator can - /// understand the change without opening the diff. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option, } /// What action the approval, when granted, will trigger. @@ -419,9 +414,6 @@ pub enum ManagerRequest { /// agent of the same name already exists, the approval will fail. RequestSpawn { name: String, - /// Optional description shown on the dashboard approval card. - #[serde(default, skip_serializing_if = "Option::is_none")] - description: Option, }, /// Stop a sub-agent (graceful). Kill { @@ -447,10 +439,6 @@ pub enum ManagerRequest { RequestApplyCommit { agent: String, commit_ref: String, - /// Optional description shown on the dashboard approval card so the - /// operator knows what the change does without opening the diff. - #[serde(default, skip_serializing_if = "Option::is_none")] - description: Option, }, /// Ask the operator a question. Returns immediately with the queued /// question id; the operator's answer arrives later as a