From 3c853c2b58ce7ba5e9df1af94c87f9412d0148f8 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 1 Jun 2026 13:46:31 +0200 Subject: [PATCH] feat: include active bash tasks in get_loose_ends output --- hive-ag3nt/src/bash_runner.rs | 26 ++++++++++++++++++++++++++ hive-ag3nt/src/mcp.rs | 27 ++++++++++++++++++++------- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/hive-ag3nt/src/bash_runner.rs b/hive-ag3nt/src/bash_runner.rs index 8a907070..99b7b6b7 100644 --- a/hive-ag3nt/src/bash_runner.rs +++ b/hive-ag3nt/src/bash_runner.rs @@ -157,6 +157,32 @@ pub fn read_task(id: &str) -> Option { // Public API used by MCP tools // --------------------------------------------------------------------------- +/// Return all tasks in `Pending` or `Running` state. Used by the +/// `get_loose_ends` tool to surface active background work alongside +/// broker-side items (questions, reminders). Silently skips unreadable +/// or unparseable files. +#[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 +} + /// Create a new pending task and write it to disk. Returns the task ID /// the MCP tool should return to claude. The runner will pick it up /// within the next poll interval (~200ms). diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index ee156366..0ada63b3 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -654,17 +654,30 @@ impl AgentServer { description = "List loose ends pending against this agent: unanswered questions \ where you are the asker (waiting on someone) or the target (someone's waiting on \ you), pending reminders you scheduled, plus — for the manager only — pending \ - approvals you submitted that the operator hasn't acted on yet. Cheap server-side \ - sweep, no args. Useful at turn start to remember what you owe / what's owed to \ - you without scrolling inbox history. Output is a short bulleted list with ids, \ - ages in seconds, and the relevant context. Each `question` or `reminder` row \ - can be cancelled by passing its id + kind to `cancel_loose_end`. Empty result \ - is reported clearly." + approvals you submitted that the operator hasn't acted on yet. Also lists any \ + local bash tasks still in pending or running state. Cheap sweep, no args. Useful \ + at turn start to remember what you owe / what's owed to you without scrolling \ + inbox history. Output is a short bulleted list with ids, ages in seconds, and \ + the relevant context. Each `question` or `reminder` row can be cancelled by \ + passing its id + kind to `cancel_loose_end`. Empty result is reported clearly." )] async fn get_loose_ends(&self) -> String { run_tool_envelope("get_loose_ends", String::new(), async move { let (resp, retries) = self.dispatch(hive_sh4re::AgentRequest::GetLooseEnds { agent: None }).await; - annotate_retries(format_loose_ends(resp), retries) + let mut out = annotate_retries(format_loose_ends(resp), retries); + // Append any local bash tasks still in pending/running state so + // the agent sees all outstanding work in one call. + let active = crate::bash_runner::active_tasks(); + if !active.is_empty() { + use std::fmt::Write as _; + let _ = write!(out, "\n\n{} active bash task(s):", active.len()); + for task in &active { + let age = crate::serve_common::now_unix() - task.created_at; + let _ = write!(out, "\n- `{}` status={:?}, cmd: `{}`, age {}s", + task.id, task.status, task.cmd, age); + } + } + out }) .await }