//! 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 }