vtcode_core/subagents/
controller_verify.rs1#![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 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 self.find_spec("explorer").await
59 }
60 };
61
62 let spec = match verifier_spec {
63 Some(s) => s,
64 None => {
65 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 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 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}