Skip to main content

vtcode_core/subagents/
controller_verify.rs

1#![allow(
2    unused_imports,
3    reason = "Intentional compatibility, platform, or test-only suppression."
4)]
5use anyhow::{Context, Result, anyhow, bail};
6use chrono::Utc;
7use futures::future::select_all;
8use std::collections::VecDeque;
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12use tokio::sync::{Notify, RwLock};
13
14use crate::config::VTCodeConfig;
15use crate::config::types::ReasoningEffortLevel;
16use crate::core::agent::runner::{AgentRunner, RunnerSettings};
17use crate::core::agent::task::Task;
18use crate::core::threads::{ThreadBootstrap, ThreadId, ThreadRuntimeHandle, ThreadSnapshot};
19use crate::hooks::{LifecycleHookEngine, SessionStartTrigger};
20use crate::llm::provider::Message;
21use crate::tools::exec_session::ExecSessionManager;
22use crate::tools::pty::{PtyManager, PtySize};
23use crate::utils::session_archive::{SessionArchive, find_session_by_identifier};
24use vtcode_config::SubagentSpec;
25use vtcode_config::auth::OpenAIChatGptAuthHandle;
26
27use self::background::*;
28use self::config::*;
29use self::constants::*;
30use self::discovery::discover_controller_subagents;
31use self::model::*;
32use vtcode_config::subagents::SUBAGENT_HARD_CONCURRENCY_LIMIT;
33
34#[allow(
35    unused_imports,
36    reason = "Intentional compatibility, platform, or test-only suppression."
37)]
38use super::*;
39
40impl SubagentController {
41    /// Verify a proposed change by spawning a read-only verifier sub-agent.
42    ///
43    /// The verifier re-reads the affected files and either approves or rejects
44    /// the change. On rejection, the caller can retry the mutation up to N times.
45    ///
46    /// This implements the propose/verify separation from Osmani's loop
47    /// engineering pattern: the proposer and verifier are independent agents
48    /// with no shared context bias.
49    pub async fn verify_proposed_change(
50        &self,
51        diff_description: &str,
52        file_paths: &[PathBuf],
53    ) -> Result<VerificationResult> {
54        let verifier_spec = match self.find_spec("verifier").await {
55            Some(s) => Some(s),
56            None => {
57                // Fall back to a synthetic read-only spec if no verifier agent is defined.
58                self.find_spec("explorer").await
59            }
60        };
61
62        let spec = match verifier_spec {
63            Some(s) => s,
64            None => {
65                // No verifier or explorer agent available. Reject by default
66                // (fail-closed) — unverified changes must not be merged.
67                tracing::warn!("No verifier agent found; rejecting change without verification");
68                return Ok(VerificationResult {
69                    approved: false,
70                    issues: vec!["No verifier agent available to review this change.".to_string()],
71                    reasoning: "No verifier agent available; rejected (fail-closed).".to_string(),
72                });
73            }
74        };
75
76        let files_list = file_paths
77            .iter()
78            .map(|p| format!("- {}", p.display()))
79            .collect::<Vec<_>>()
80            .join("\n");
81
82        let prompt = format!(
83            "Review a proposed change before it is merged. You did not write it, so judge it only \
84             from the files as they are now.\n\n\
85             ## Diff Description\n{diff_description}\n\n\
86             ## Affected Files\n{files_list}\n\n\
87             Read each affected file and check that the change does what the description says, \
88             handles errors, and follows the surrounding code's conventions. Ignore problems that \
89             predate the change.\n\n\
90             Your reply is parsed by the harness: put each problem on its own line as \
91             `- ISSUE: <path>:<line> <description>`, and end with exactly one line, \
92             `Decision: APPROVED` or `Decision: REJECTED`. Any ISSUE line blocks the merge, so list \
93             only problems that must be fixed."
94        );
95
96        let request = SpawnAgentRequest {
97            agent_type: Some(spec.name.clone()),
98            message: Some(prompt),
99            items: Vec::new(),
100            fork_context: false,
101            model: None,
102            reasoning_effort: None,
103            background: false,
104            max_turns: Some(3),
105        };
106
107        let status = self.spawn_custom(spec.clone(), request).await?;
108        let entry = self.wait(std::slice::from_ref(&status.id), Some(60_000)).await?;
109
110        match entry {
111            Some(entry) if entry.status == SubagentStatus::Completed => {
112                let summary = entry.summary.unwrap_or_default();
113                let issues = extract_issues_from_summary(&summary);
114                // An explicit decision line wins (a non-clean value fails
115                // closed); keyword heuristics cover verifiers that do not follow
116                // the requested format. Any ISSUE line blocks approval.
117                let approved = match parse_verifier_decision(&summary) {
118                    Some(decision) => decision && issues.is_empty(),
119                    None => heuristic_verifier_approval(&summary, &issues),
120                };
121
122                Ok(VerificationResult { approved, issues, reasoning: summary })
123            }
124            Some(entry) => {
125                let error = entry.error.unwrap_or_default();
126                tracing::warn!(
127                    error = %error,
128                    "Verifier sub-agent failed; rejecting change (fail-closed)"
129                );
130                Ok(VerificationResult {
131                    approved: false,
132                    issues: vec![format!("Verifier agent error: {error}")],
133                    reasoning: format!("Verifier failed: {error}"),
134                })
135            }
136            None => {
137                tracing::warn!("Verifier sub-agent timed out; rejecting change (fail-closed)");
138                Ok(VerificationResult {
139                    approved: false,
140                    issues: vec!["Verifier agent timed out after 60s.".to_string()],
141                    reasoning: "Verifier timed out.".to_string(),
142                })
143            }
144        }
145    }
146
147    /// Reconcile a worktree-isolated child: diff → verify → merge → cleanup.
148    ///
149    /// Uses `WorktreeReconciler::reconcile` in a single `spawn_blocking` call
150    /// with the canonical [`HeuristicDiffVerifier`](crate::git::HeuristicDiffVerifier).
151    /// This avoids borrowing `&self` across spawn boundaries, which would make
152    /// the parent `tokio::spawn` future non-Send due to the recursive spawn
153    /// chain through `verify_proposed_change`.
154    pub(super) async fn run_worktree_reconciliation(&self, child_id: &str, wt_path: &std::path::Path, wt_name: &str) {
155        let ws = self.config.workspace_root.clone();
156        let wt_name_owned = wt_name.to_string();
157        let wt_path_owned = wt_path.to_path_buf();
158
159        let result = tokio::task::spawn_blocking(move || {
160            let reconciler = crate::git::WorktreeReconciler::new(&ws, "main");
161            let verifier: Box<dyn crate::git::DiffVerifier + Send + Sync> = Box::new(crate::git::HeuristicDiffVerifier);
162            reconciler.reconcile(&wt_name_owned, &wt_path_owned, verifier.as_ref())
163        })
164        .await;
165
166        match result {
167            Ok(Ok(rr)) if rr.approved && rr.merged => {
168                tracing::info!(
169                    child_id,
170                    worktree = %wt_name,
171                    reasoning = %rr.reasoning,
172                    "Worktree reconciled and merged"
173                );
174            }
175            Ok(Ok(rr)) if !rr.approved => {
176                tracing::warn!(
177                    child_id,
178                    worktree = %wt_name,
179                    issues = ?rr.issues,
180                    "Verifier rejected worktree changes; skipping merge"
181                );
182            }
183            Ok(Ok(rr)) => {
184                tracing::info!(
185                    child_id,
186                    worktree = %wt_name,
187                    reasoning = %rr.reasoning,
188                    "Worktree reconciliation completed (no merge needed)"
189                );
190            }
191            Ok(Err(err)) => {
192                tracing::warn!(
193                    child_id,
194                    worktree = %wt_name,
195                    error = %err,
196                    "Worktree reconciliation failed"
197                );
198            }
199            Err(err) => {
200                tracing::warn!(
201                    child_id,
202                    worktree = %wt_name,
203                    error = %err,
204                    "Reconciliation spawn_blocking panicked"
205                );
206            }
207        }
208    }
209}