diff --git a/Cargo.lock b/Cargo.lock index 4ac9ebe3..3468417e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1226,7 +1226,6 @@ dependencies = [ "axum", "clap", "futures-util", - "hive-bash-mcp", "hive-sh4re", "reqwest", "rmcp", diff --git a/hive-ag3nt/Cargo.toml b/hive-ag3nt/Cargo.toml index 76a01aea..ed5de53b 100644 --- a/hive-ag3nt/Cargo.toml +++ b/hive-ag3nt/Cargo.toml @@ -12,7 +12,6 @@ axum.workspace = true reqwest.workspace = true futures-util = "0.3" clap.workspace = true -hive-bash-mcp = { path = "../hive-bash-mcp" } hive-sh4re.workspace = true rmcp.workspace = true rusqlite.workspace = true diff --git a/hive-ag3nt/src/bash_tasks.rs b/hive-ag3nt/src/bash_tasks.rs new file mode 100644 index 00000000..ce026f32 --- /dev/null +++ b/hive-ag3nt/src/bash_tasks.rs @@ -0,0 +1,78 @@ +//! Read-only view of bash task state files for `get_loose_ends`. +//! +//! The task runner and MCP tools live in `hive-bash-mcp`. This module +//! only reads the JSON task files that the daemon writes — no subprocess +//! logic, no wake signals, no dep on the hive-bash-mcp crate. +//! +//! Task files live under `harness_dir/bash-tasks/.json`. The JSON +//! schema is owned by `hive-bash-mcp::protocol::TaskFile`; this module +//! duplicates only the fields the harness needs for `get_loose_ends`. + +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +/// Lifecycle state of a bash task (mirrors `hive_bash_mcp::protocol::TaskStatus`). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TaskStatus { + Pending, + Running, + Done, + TimedOut, + Interrupted, +} + +/// Minimal task metadata (mirrors `hive_bash_mcp::protocol::TaskFile`). +#[derive(Debug, Clone, Deserialize)] +pub struct TaskFile { + pub id: String, + pub cmd: String, + pub status: TaskStatus, + pub created_at: i64, +} + +fn tasks_dir() -> PathBuf { + let base = if let Some(p) = std::env::var_os("HYPERHIVE_HARNESS_DIR") { + PathBuf::from(p) + } else { + let state = std::env::var("HYPERHIVE_STATE_DIR").unwrap_or_default(); + let state_path = PathBuf::from(&state); + state_path + .parent() + .map(|p| p.join("harness")) + .unwrap_or_else(|| PathBuf::from(state)) + }; + base.join("bash-tasks") +} + +/// Read a task file by ID. Returns `None` if missing or unparseable. +#[must_use] +pub fn read_task(id: &str) -> Option { + let path = tasks_dir().join(format!("{id}.json")); + let s = std::fs::read_to_string(path).ok()?; + serde_json::from_str(&s).ok() +} + +/// Return all tasks currently in `Pending` or `Running` state. +#[must_use] +pub fn active_tasks() -> Vec { + let Ok(rd) = std::fs::read_dir(tasks_dir()) else { + return Vec::new(); + }; + let mut out = Vec::new(); + for entry in rd.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("json") { + continue; + } + let Some(id) = path.file_stem().and_then(|s| s.to_str()).map(str::to_owned) else { + continue; + }; + let Some(task) = read_task(&id) else { continue }; + if matches!(task.status, TaskStatus::Pending | TaskStatus::Running) { + out.push(task); + } + } + out +} diff --git a/hive-ag3nt/src/lib.rs b/hive-ag3nt/src/lib.rs index 34c04282..204e5917 100644 --- a/hive-ag3nt/src/lib.rs +++ b/hive-ag3nt/src/lib.rs @@ -1,6 +1,7 @@ //! Shared in-container harness code used by both `hive-ag3nt` (agent) and //! `hive-m1nd` (manager) binaries. +pub mod bash_tasks; pub mod client; pub mod events; pub mod forge_notify; diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 469930bd..1de441ba 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -739,7 +739,7 @@ impl AgentServer { let mut out = annotate_retries(render_loose_ends(&loose_ends), retries); // Append any local bash tasks still in pending/running state so // the agent sees all outstanding work in one call. - let active = hive_bash_mcp::runner::active_tasks(); + let active = crate::bash_tasks::active_tasks(); if !active.is_empty() { use std::fmt::Write as _; let _ = write!(out, "\n\n{} active bash task(s):", active.len());