//! Auto-review engine — after edits, spawns a subagent that reviews AND //! automatically fixes issues using tool access (edit/write/grep). //! //! Flow: after an agent turn with edits completes: //! 1. Emit `WorkflowAgentUpdate(Running)` → visible in workflow sidebar //! 2. Run `git diff` to get the changed files //! 3. Spawn the subagent engine with Write-tier tools + directive to //! review & fix //! 4. The subagent finds issues and applies fixes using edit/write tools //! 5. Results stream as `TurnEvent::SystemNote` events //! 6. Emit `WorkflowAgentUpdate(Completed)` when done use std::collections::VecDeque; use std::path::PathBuf; use std::process::Command; use std::sync::{Arc, Mutex}; use tracing::{debug, info, instrument, warn}; use crate::subagent::context::SubagentContext; use crate::subagent::division::AccessTier; use crate::subagent::engine::run_agent; use crate::tools::ToolCtx; use crate::{AgentStatus, TurnEvent}; const REVIEW_AGENT_ID: &str = "auto-review"; /// Spawn a review subagent that reviews changes and auto-fixes issues. /// /// The subagent runs inline on the current background thread (no extra /// thread spawn) with its own tokio runtime and Write-tier tool access /// (edit, write, grep, read, glob). It receives the git diff as context /// and is directed to: /// 1. Read changed files /// 2. Check for typos, missing imports, syntax errors, logic bugs /// 3. Fix any issues found using edit/write tools /// 4. Report what was fixed /// /// All findings stream as TurnEvent events consumed by the TUI event loop. #[instrument(skip(turn_events))] pub fn spawn_background_review( workspace_roots: Vec, turn_events: Arc>>, api_key: String, model: String, api_base: Option, ) { let agent_id = REVIEW_AGENT_ID.to_string(); let agent_name = "Auto-Review".to_string(); std::thread::spawn(move || { let root = match workspace_roots.first() { Some(r) => r.clone(), None => { debug!("auto-review: no workspace root, skipping"); return; } }; info!("auto-review: starting"); // Mark Running in workflow panel push_event( &turn_events, TurnEvent::WorkflowAgentUpdate { agent_id: agent_id.clone(), agent_name: agent_name.clone(), status: AgentStatus::Running, }, ); // 1. Get the git diff to know what changed let diff = match get_git_diff(&root) { Ok(d) if !d.is_empty() => d, Ok(_) => { debug!("auto-review: no changes detected"); push_event( &turn_events, TurnEvent::WorkflowAgentUpdate { agent_id, agent_name, status: AgentStatus::Completed, }, ); return; } Err(e) => { debug!(error = %e, "auto-review: git diff failed"); push_event( &turn_events, TurnEvent::WorkflowAgentUpdate { agent_id, agent_name, status: AgentStatus::Failed(e), }, ); return; } }; // Truncate very large diffs for the prompt const MAX_DIFF_CHARS: usize = 5000; let truncated_diff = if diff.len() > MAX_DIFF_CHARS { push_event( &turn_events, TurnEvent::SystemNote { kind: "review".into(), message: format!( "📐 Diff is large ({} chars), reviewing first {} chars...", diff.len(), MAX_DIFF_CHARS ), }, ); format!( "{}...\n[diff truncated at {} characters]", &diff[..MAX_DIFF_CHARS], MAX_DIFF_CHARS ) } else { diff.to_string() }; // 2. Build directive: review AND fix issues using tools let directive = format!( "You are an auto-review subagent. Complete the following:\n\n\ 1. Review this git diff for:\n\ - Typos and spelling errors\n\ - Missing imports or undefined references\n\ - Syntax errors or type mismatches\n\ - Logic bugs or off-by-one errors\n\ - Missing error handling\n\ - Security issues\n\n\ 2. FIX any issues you find using the available tools:\n\ - `read` to check file contents\n\ - `edit` to fix specific text blocks\n\ - `write` to replace files if needed\n\ - `grep` to find related patterns\n\n\ 3. Be conservative: only fix CLEAR, CONFIRMED issues. \ Don't change logic, style, or formatting.\n\ 4. Report what you fixed at the end.\n\n\ Git diff of changes:\n\n```diff\n{truncated_diff}\n```" ); // 3. Emit progress note push_event( &turn_events, TurnEvent::SystemNote { kind: "review".into(), message: "🔍 Auto-review: examining and fixing issues...".into(), }, ); // 4. Build minimal ToolCtx let tool_ctx = ToolCtx::builder() .session_dir(root.join(".zesdex").join("sessions").join("auto-review")) .workspaces(workspace_roots.clone()) .turn_events(turn_events.clone()) .build(); // 5. Determine base URL let base_url = api_base.unwrap_or_else(|| { std::env::var("OPENAI_API_BASE") .unwrap_or_else(|_| "https://opencode.ai/zen/v1".to_string()) }); // 6. Build SubagentContext let subagent_ctx = SubagentContext::new( directive, tool_ctx.clone(), "write".to_string(), base_url, api_key, model, ); // 7. Run the subagent engine directly on this thread // (creates its own tokio runtime, calls run_agent with Write tools) let rt = match tokio::runtime::Runtime::new() { Ok(r) => r, Err(e) => { warn!(error = %e, "auto-review: failed to create tokio runtime"); push_event( &turn_events, TurnEvent::WorkflowAgentUpdate { agent_id, agent_name, status: AgentStatus::Failed(format!("runtime error: {e}")), }, ); return; } }; let result = rt.block_on(run_agent( subagent_ctx, "Auto-review and fix issues in the changed files", AccessTier::Write, tool_ctx, )); // 8. Report results match result { Ok(report) => { let trimmed = report.trim(); if trimmed.is_empty() || trimmed.contains("no issues") { push_event( &turn_events, TurnEvent::SystemNote { kind: "review".into(), message: "✅ Auto-review: no issues found.".into(), }, ); info!("auto-review: no issues found"); } else { push_event( &turn_events, TurnEvent::SystemNote { kind: "review_finding".into(), message: format!("📋 Auto-review complete:\n{}", trimmed), }, ); info!("auto-review: completed with findings"); } push_event( &turn_events, TurnEvent::WorkflowAgentUpdate { agent_id, agent_name, status: AgentStatus::Completed, }, ); } Err(e) => { warn!(error = %e, "auto-review subagent failed"); push_event( &turn_events, TurnEvent::SystemNote { kind: "review".into(), message: format!("⚠️ Auto-review encountered an error: {e}"), }, ); push_event( &turn_events, TurnEvent::WorkflowAgentUpdate { agent_id, agent_name, status: AgentStatus::Failed(e.to_string()), }, ); } } }); } /// Run `git diff` to get workspace changes (both staged and unstaged). fn get_git_diff(workspace_root: &PathBuf) -> Result { let git_dir = workspace_root.join(".git"); if !git_dir.exists() { return Err("not a git repository".to_string()); } let mut combined = String::new(); // Unstaged diff if let Ok(out) = Command::new("git") .args(["diff"]) .current_dir(workspace_root) .output() { let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string(); if !stdout.is_empty() { combined.push_str("=== Unstaged Changes ===\n"); combined.push_str(&stdout); combined.push('\n'); } } // Staged diff if let Ok(out) = Command::new("git") .args(["diff", "--cached"]) .current_dir(workspace_root) .output() { let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string(); if !stdout.is_empty() { combined.push_str("=== Staged Changes ===\n"); combined.push_str(&stdout); combined.push('\n'); } } if combined.is_empty() { return Err("no changes".to_string()); } Ok(combined) } /// Push a TurnEvent onto the shared event queue. fn push_event(queue: &Arc>>, event: TurnEvent) { if let Ok(mut q) = queue.lock() { q.push_back(event); } }