diff --git a/server/src/agents/pool/query.rs b/server/src/agents/pool/query.rs index 8025ba4d..78318298 100644 --- a/server/src/agents/pool/query.rs +++ b/server/src/agents/pool/query.rs @@ -30,6 +30,19 @@ impl AgentPool { .collect()) } + /// Return story IDs of merge jobs currently in the `Running` state. + /// + /// Used by `list_agents` and `get_pipeline_status` to surface in-flight + /// deterministic merges that hold the merge lock but have no agent entry. + pub fn list_running_merges(&self) -> Result, String> { + let jobs = self.merge_jobs.lock().map_err(|e| e.to_string())?; + Ok(jobs + .values() + .filter(|job| matches!(job.status, crate::agents::merge::MergeJobStatus::Running)) + .map(|job| job.story_id.clone()) + .collect()) + } + /// List all agents with their status. pub fn list_agents(&self) -> Result, String> { let agents = self.agents.lock().map_err(|e| e.to_string())?; diff --git a/server/src/http/mcp/agent_tools/mod.rs b/server/src/http/mcp/agent_tools/mod.rs index ea59a27b..df3d5e03 100644 --- a/server/src/http/mcp/agent_tools/mod.rs +++ b/server/src/http/mcp/agent_tools/mod.rs @@ -87,25 +87,38 @@ pub(crate) async fn tool_stop_agent(args: &Value, ctx: &AppContext) -> Result Result { let project_root = ctx.services.agents.get_project_root(&ctx.state).ok(); let agents = ctx.services.agents.list_agents()?; - serde_json::to_string_pretty(&json!( - agents - .iter() - .filter(|a| { - project_root - .as_deref() - .map(|root| !crate::service::agents::is_archived(root, &a.story_id)) - .unwrap_or(true) - }) - .map(|a| json!({ + let mut entries: Vec = agents + .iter() + .filter(|a| { + project_root + .as_deref() + .map(|root| !crate::service::agents::is_archived(root, &a.story_id)) + .unwrap_or(true) + }) + .map(|a| { + json!({ "story_id": a.story_id, "agent_name": a.agent_name, "status": a.status.to_string(), "session_id": a.session_id, "worktree_path": a.worktree_path, - })) - .collect::>() - )) - .map_err(|e| format!("Serialization error: {e}")) + }) + }) + .collect(); + + // Append a synthetic entry for each deterministic merge holding the merge lock. + let running_merges = ctx.services.agents.list_running_merges()?; + for story_id in running_merges { + entries.push(json!({ + "story_id": story_id, + "agent_name": "deterministic-merge", + "status": "Running", + "session_id": null, + "worktree_path": null, + })); + } + + serde_json::to_string_pretty(&json!(entries)).map_err(|e| format!("Serialization error: {e}")) } /// Read agent session logs from disk and return a human-readable timeline. diff --git a/server/src/http/mcp/story_tools/story.rs b/server/src/http/mcp/story_tools/story.rs index bbd57d6e..373359a7 100644 --- a/server/src/http/mcp/story_tools/story.rs +++ b/server/src/http/mcp/story_tools/story.rs @@ -156,6 +156,7 @@ pub(crate) fn tool_list_upcoming(ctx: &AppContext) -> Result { pub(crate) fn tool_get_pipeline_status(ctx: &AppContext) -> Result { let state = load_pipeline_state(ctx)?; + let running_merges = ctx.services.agents.list_running_merges()?; fn map_items(items: &[crate::http::workflow::UpcomingStory], stage: &str) -> Vec { items @@ -203,6 +204,7 @@ pub(crate) fn tool_get_pipeline_status(ctx: &AppContext) -> Result