//! Legacy `report_completion` — retained for backwards compatibility and testing. use std::sync::Arc; use super::super::super::super::{AgentEvent, AgentStatus, CompletionReport}; use super::super::super::{AgentPool, composite_key}; impl AgentPool { /// Internal: report that an agent has finished work on a story. /// /// **Note:** This is no longer exposed as an MCP tool. The server now /// automatically runs completion gates when an agent process exits /// (see `run_server_owned_completion`). This method is retained for /// backwards compatibility and testing. /// /// - Rejects with an error if the worktree has uncommitted changes. /// - Runs acceptance gates (cargo clippy + cargo nextest run / cargo test). /// - Stores the `CompletionReport` on the agent record. /// - Transitions status to `Completed` (gates passed) or `Failed` (gates failed). /// - Emits a `Done` event so `wait_for_agent` unblocks. #[allow(dead_code)] pub async fn report_completion( &self, story_id: &str, agent_name: &str, summary: &str, ) -> Result { let key = composite_key(story_id, agent_name); // Verify agent exists, is Running, and grab its worktree path. let worktree_path = { let agents = self.agents.lock().map_err(|e| e.to_string())?; let agent = agents .get(&key) .ok_or_else(|| format!("No agent '{agent_name}' for story '{story_id}'"))?; if agent.status != AgentStatus::Running { return Err(format!( "Agent '{agent_name}' for story '{story_id}' is not running (status: {}). \ report_completion can only be called by a running agent.", agent.status )); } agent .worktree_info .as_ref() .map(|wt| wt.path.clone()) .ok_or_else(|| { format!( "Agent '{agent_name}' for story '{story_id}' has no worktree. \ Cannot run acceptance gates." ) })? }; let path = worktree_path.clone(); // Run gate checks in a blocking thread to avoid stalling the async runtime. let (gates_passed, gate_output) = tokio::task::spawn_blocking(move || { // Step 1: Reject if worktree is dirty. crate::agents::gates::check_uncommitted_changes(&path)?; // Step 2: Run clippy + tests and return (passed, output). crate::agents::gates::run_acceptance_gates(&path) }) .await .map_err(|e| format!("Gate check task panicked: {e}"))??; let report = CompletionReport { summary: summary.to_string(), gates_passed, gate_output, }; // Extract data for pipeline advance, then remove the entry so // completed agents never appear in list_agents. let ( tx, session_id, project_root_for_advance, wt_path_for_advance, merge_failure_reported_for_advance, session_id_for_advance, ) = { let mut agents = self.agents.lock().map_err(|e| e.to_string())?; let agent = agents.get_mut(&key).ok_or_else(|| { format!("Agent '{agent_name}' for story '{story_id}' disappeared during gate check") })?; agent.completion = Some(report.clone()); let tx = agent.tx.clone(); let sid = agent.session_id.clone(); let pr = agent.project_root.clone(); let wt = agent.worktree_info.as_ref().map(|w| w.path.clone()); let mfr = agent.merge_failure_reported; let sid_advance = agent.session_id.clone(); agents.remove(&key); (tx, sid, pr, wt, mfr, sid_advance) }; // Emit Done so wait_for_agent unblocks. let _ = tx.send(AgentEvent::Done { story_id: story_id.to_string(), agent_name: agent_name.to_string(), session_id, }); // Notify WebSocket clients that the agent is gone. Self::notify_agent_state_changed(&self.watcher_tx); // Advance the pipeline state machine in a background task. let pool_clone = Self { agents: Arc::clone(&self.agents), port: self.port, child_killers: Arc::clone(&self.child_killers), watcher_tx: self.watcher_tx.clone(), status_broadcaster: Arc::clone(&self.status_broadcaster), }; let sid = story_id.to_string(); let aname = agent_name.to_string(); let report_for_advance = report.clone(); tokio::spawn(async move { pool_clone .run_pipeline_advance( &sid, &aname, report_for_advance, project_root_for_advance, wt_path_for_advance, merge_failure_reported_for_advance, session_id_for_advance, ) .await; }); Ok(report) } }