1use anyhow::Result;
7use colored::Colorize;
8use std::path::PathBuf;
9use std::thread;
10use std::time::Duration;
11
12use crate::agents::AgentDef;
13use crate::backpressure::{self, BackpressureConfig, ValidationResult};
14use crate::commands::spawn::terminal::{self, Harness};
15use crate::storage::Storage;
16
17#[allow(clippy::too_many_arguments)]
19pub fn run(
20 project_root: Option<PathBuf>,
21 command: Option<&str>,
22 max_attempts: usize,
23 harness_arg: &str,
24 agent_type: &str,
25 session_name: Option<String>,
26 attach: bool,
27) -> Result<()> {
28 let storage = Storage::new(project_root.clone());
29
30 if !storage.is_initialized() {
31 anyhow::bail!("SCUD not initialized. Run: scud init");
32 }
33
34 terminal::check_tmux_available()?;
36
37 let working_dir = project_root
39 .clone()
40 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
41
42 let bp_config = if let Some(cmd) = command {
44 BackpressureConfig {
45 commands: vec![cmd.to_string()],
46 stop_on_failure: true,
47 timeout_secs: 300,
48 }
49 } else {
50 let config = BackpressureConfig::load(project_root.as_ref())?;
51 if config.commands.is_empty() {
52 anyhow::bail!(
53 "No test command provided and no backpressure commands configured.\n\
54 Use: scud test --command 'your test command'\n\
55 Or configure: scud config backpressure 'cargo test'"
56 );
57 }
58 config
59 };
60
61 let harness = Harness::parse(harness_arg)?;
63 terminal::find_harness_binary(harness)?;
64
65 let session_name = session_name.unwrap_or_else(|| "scud-test".to_string());
67
68 println!("{}", "SCUD Test & Fix".cyan().bold());
70 println!("{}", "═".repeat(50));
71 println!(
72 "{:<20} {}",
73 "Commands:".dimmed(),
74 bp_config.commands.join(", ").cyan()
75 );
76 println!(
77 "{:<20} {}",
78 "Max attempts:".dimmed(),
79 max_attempts.to_string().cyan()
80 );
81 println!(
82 "{:<20} {}",
83 "Repair agent:".dimmed(),
84 agent_type.cyan()
85 );
86 println!("{:<20} {}", "Harness:".dimmed(), harness.name().cyan());
87 println!();
88
89 for attempt in 1..=max_attempts {
91 println!(
92 "{} {}/{}",
93 "Attempt".blue().bold(),
94 attempt,
95 max_attempts
96 );
97 println!("{}", "-".repeat(40).blue());
98
99 println!(" {} Running tests...", "→".dimmed());
101 let result = backpressure::run_validation(&working_dir, &bp_config)?;
102
103 if result.all_passed {
104 println!();
105 println!(
106 "{} All tests passed on attempt {}!",
107 "✓".green().bold(),
108 attempt
109 );
110 return Ok(());
111 }
112
113 println!();
115 println!(" {} Tests failed:", "✗".red());
116 for failure in &result.failures {
117 println!(" - {}", failure.red());
118 }
119
120 let error_output = format_error_output(&result);
122
123 if attempt == max_attempts {
124 println!();
125 println!(
126 "{} Max attempts ({}) reached. Tests still failing.",
127 "!".red().bold(),
128 max_attempts
129 );
130 return Err(anyhow::anyhow!("Tests failed after {} attempts", max_attempts));
131 }
132
133 println!();
135 println!(
136 " {} Spawning {} agent to fix...",
137 "→".dimmed(),
138 agent_type
139 );
140
141 let repair_marker = working_dir
142 .join(".scud")
143 .join(format!("test-repair-complete-{}", attempt));
144
145 let _ = std::fs::remove_file(&repair_marker);
147
148 spawn_repair_agent(
149 &working_dir,
150 &session_name,
151 attempt,
152 harness,
153 agent_type,
154 &bp_config.commands,
155 &error_output,
156 &repair_marker,
157 )?;
158
159 println!(" {} Waiting for repair agent...", "→".dimmed());
161 wait_for_repair(&repair_marker, attach, &session_name)?;
162
163 println!();
164 }
165
166 Ok(())
167}
168
169fn format_error_output(result: &ValidationResult) -> String {
171 let mut output = String::new();
172
173 for cmd_result in &result.results {
174 if !cmd_result.passed {
175 output.push_str(&format!("Command: {}\n", cmd_result.command));
176 output.push_str(&format!("Exit code: {:?}\n", cmd_result.exit_code));
177 if !cmd_result.stdout.is_empty() {
178 output.push_str(&format!("Stdout:\n{}\n", cmd_result.stdout));
179 }
180 if !cmd_result.stderr.is_empty() {
181 output.push_str(&format!("Stderr:\n{}\n", cmd_result.stderr));
182 }
183 output.push('\n');
184 }
185 }
186
187 output
188}
189
190#[allow(clippy::too_many_arguments)]
192fn spawn_repair_agent(
193 working_dir: &std::path::Path,
194 session_name: &str,
195 attempt: usize,
196 default_harness: Harness,
197 agent_type: &str,
198 commands: &[String],
199 error_output: &str,
200 repair_marker: &std::path::Path,
201) -> Result<()> {
202 let (harness, model) = match AgentDef::try_load(agent_type, working_dir) {
204 Some(agent_def) => {
205 let h = agent_def.harness().unwrap_or(default_harness);
206 let m = agent_def.model().map(String::from);
207 (h, m)
208 }
209 None => {
210 println!(
211 " {} Agent '{}' not found, using defaults",
212 "!".yellow(),
213 agent_type
214 );
215 (default_harness, None)
216 }
217 };
218
219 let commands_str = commands.join(" && ");
220 let marker_path = repair_marker.display();
221
222 let prompt = format!(
223 r#"You are a repair agent fixing test/build failures.
224
225## Failed Command(s)
226{commands_str}
227
228## Error Output
229{error_output}
230
231## Your Mission
2321. Analyze the error output to understand what went wrong
2332. Read the relevant source files mentioned in the errors
2343. Fix the issues while preserving the intended functionality
2354. Run the test command to verify your fix: {commands_str}
236
237## Important
238- Focus on fixing the specific errors shown above
239- Don't refactor unrelated code
240- After each fix attempt, re-run the tests to verify
241- Keep trying until the tests pass
242
243## When Done
244When the tests pass, signal completion:
245```bash
246echo "SUCCESS" > {marker_path}
247```
248
249If you cannot fix the issue and need human help:
250```bash
251echo "BLOCKED: <reason>" > {marker_path}
252```
253"#,
254 commands_str = commands_str,
255 error_output = error_output,
256 marker_path = marker_path,
257 );
258
259 let window_name = format!("repair-{}", attempt);
260
261 terminal::spawn_terminal_with_harness_and_model(
262 &window_name,
263 &prompt,
264 working_dir,
265 session_name,
266 harness,
267 model.as_deref(),
268 )?;
269
270 let agent_info = if let Some(ref m) = model {
271 format!("{}:{}", harness.name(), m)
272 } else {
273 harness.name().to_string()
274 };
275
276 println!(
277 " {} Spawned: {} [{}] {}:{}",
278 "✓".green(),
279 window_name.cyan(),
280 agent_info.dimmed(),
281 session_name.dimmed(),
282 attempt
283 );
284
285 Ok(())
286}
287
288fn wait_for_repair(
290 marker_path: &std::path::Path,
291 attach: bool,
292 session_name: &str,
293) -> Result<()> {
294 if attach {
296 println!(
297 " {} Attaching to session. Mark completion with: echo SUCCESS > {}",
298 "→".dimmed(),
299 marker_path.display()
300 );
301 terminal::tmux_attach(session_name)?;
302
303 if marker_path.exists() {
305 let content = std::fs::read_to_string(marker_path)?;
306 std::fs::remove_file(marker_path)?;
307
308 if content.starts_with("BLOCKED") {
309 println!(" {} Agent reported: {}", "!".yellow(), content.trim());
310 }
311 }
312 return Ok(());
313 }
314
315 let timeout = Duration::from_secs(3600); let start = std::time::Instant::now();
318 let poll_interval = Duration::from_secs(5);
319
320 println!(
321 " {} Attach with: tmux attach -t {}",
322 "ℹ".dimmed(),
323 session_name
324 );
325
326 loop {
327 if start.elapsed() > timeout {
328 println!(
329 " {} Repair timed out after 1 hour",
330 "!".yellow()
331 );
332 return Ok(());
333 }
334
335 if marker_path.exists() {
336 let content = std::fs::read_to_string(marker_path)?;
337 std::fs::remove_file(marker_path)?;
338
339 if content.starts_with("SUCCESS") {
340 println!(" {} Repair agent completed", "✓".green());
341 } else if content.starts_with("BLOCKED") {
342 println!(
343 " {} Agent reported blocked: {}",
344 "!".yellow(),
345 content.trim()
346 );
347 } else {
349 println!(" {} Repair agent finished", "✓".green());
350 }
351
352 return Ok(());
353 }
354
355 thread::sleep(poll_interval);
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn test_format_error_output() {
365 let result = ValidationResult {
366 all_passed: false,
367 failures: vec!["cargo test".to_string()],
368 results: vec![backpressure::CommandResult {
369 command: "cargo test".to_string(),
370 passed: false,
371 exit_code: Some(1),
372 stdout: "running 2 tests".to_string(),
373 stderr: "error: test failed".to_string(),
374 duration_secs: 1.5,
375 }],
376 };
377
378 let output = format_error_output(&result);
379 assert!(output.contains("cargo test"));
380 assert!(output.contains("Exit code: Some(1)"));
381 assert!(output.contains("test failed"));
382 }
383}