use anyhow::Result;
use colored::Colorize;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use crate::agents::AgentDef;
use crate::backpressure::{self, BackpressureConfig, ValidationResult};
use crate::commands::spawn::terminal::{self, Harness};
use crate::storage::Storage;
#[allow(clippy::too_many_arguments)]
pub fn run(
project_root: Option<PathBuf>,
command: Option<&str>,
max_attempts: usize,
harness_arg: &str,
agent_type: &str,
session_name: Option<String>,
attach: bool,
) -> Result<()> {
let storage = Storage::new(project_root.clone());
if !storage.is_initialized() {
anyhow::bail!("SCUD not initialized. Run: scud init");
}
terminal::check_tmux_available()?;
let working_dir = project_root
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let bp_config = if let Some(cmd) = command {
BackpressureConfig {
commands: vec![cmd.to_string()],
stop_on_failure: true,
timeout_secs: 300,
}
} else {
let config = BackpressureConfig::load(project_root.as_ref())?;
if config.commands.is_empty() {
anyhow::bail!(
"No test command provided and no backpressure commands configured.\n\
Use: scud test --command 'your test command'\n\
Or configure: scud config backpressure 'cargo test'"
);
}
config
};
let harness = Harness::parse(harness_arg)?;
terminal::find_harness_binary(harness)?;
let session_name = session_name.unwrap_or_else(|| "scud-test".to_string());
println!("{}", "SCUD Test & Fix".cyan().bold());
println!("{}", "═".repeat(50));
println!(
"{:<20} {}",
"Commands:".dimmed(),
bp_config.commands.join(", ").cyan()
);
println!(
"{:<20} {}",
"Max attempts:".dimmed(),
max_attempts.to_string().cyan()
);
println!("{:<20} {}", "Repair agent:".dimmed(), agent_type.cyan());
println!("{:<20} {}", "Harness:".dimmed(), harness.name().cyan());
println!();
for attempt in 1..=max_attempts {
println!("{} {}/{}", "Attempt".blue().bold(), attempt, max_attempts);
println!("{}", "-".repeat(40).blue());
println!(" {} Running tests...", "→".dimmed());
let result = backpressure::run_validation(&working_dir, &bp_config)?;
if result.all_passed {
println!();
println!(
"{} All tests passed on attempt {}!",
"✓".green().bold(),
attempt
);
return Ok(());
}
println!();
println!(" {} Tests failed:", "✗".red());
for failure in &result.failures {
println!(" - {}", failure.red());
}
let error_output = format_error_output(&result);
if attempt == max_attempts {
println!();
println!(
"{} Max attempts ({}) reached. Tests still failing.",
"!".red().bold(),
max_attempts
);
return Err(anyhow::anyhow!(
"Tests failed after {} attempts",
max_attempts
));
}
println!();
println!(" {} Spawning {} agent to fix...", "→".dimmed(), agent_type);
let repair_marker = working_dir
.join(".scud")
.join(format!("test-repair-complete-{}", attempt));
let _ = std::fs::remove_file(&repair_marker);
spawn_repair_agent(
&working_dir,
&session_name,
attempt,
harness,
agent_type,
&bp_config.commands,
&error_output,
&repair_marker,
)?;
println!(" {} Waiting for repair agent...", "→".dimmed());
wait_for_repair(&repair_marker, attach, &session_name)?;
println!();
}
Ok(())
}
fn format_error_output(result: &ValidationResult) -> String {
let mut output = String::new();
for cmd_result in &result.results {
if !cmd_result.passed {
output.push_str(&format!("Command: {}\n", cmd_result.command));
output.push_str(&format!("Exit code: {:?}\n", cmd_result.exit_code));
if !cmd_result.stdout.is_empty() {
output.push_str(&format!("Stdout:\n{}\n", cmd_result.stdout));
}
if !cmd_result.stderr.is_empty() {
output.push_str(&format!("Stderr:\n{}\n", cmd_result.stderr));
}
output.push('\n');
}
}
output
}
#[allow(clippy::too_many_arguments)]
fn spawn_repair_agent(
working_dir: &std::path::Path,
session_name: &str,
attempt: usize,
default_harness: Harness,
agent_type: &str,
commands: &[String],
error_output: &str,
repair_marker: &std::path::Path,
) -> Result<()> {
let (harness, model) = match AgentDef::try_load(agent_type, working_dir) {
Some(agent_def) => {
let h = agent_def.harness().unwrap_or(default_harness);
let m = agent_def.model().map(String::from);
(h, m)
}
None => {
println!(
" {} Agent '{}' not found, using defaults",
"!".yellow(),
agent_type
);
(default_harness, None)
}
};
let commands_str = commands.join(" && ");
let marker_path = repair_marker.display();
let prompt = format!(
r#"You are a repair agent fixing test/build failures.
## Failed Command(s)
{commands_str}
## Error Output
{error_output}
## Your Mission
1. Analyze the error output to understand what went wrong
2. Read the relevant source files mentioned in the errors
3. Fix the issues while preserving the intended functionality
4. Run the test command to verify your fix: {commands_str}
## Important
- Focus on fixing the specific errors shown above
- Don't refactor unrelated code
- After each fix attempt, re-run the tests to verify
- Keep trying until the tests pass
## When Done
When the tests pass, signal completion:
```bash
echo "SUCCESS" > {marker_path}
```
If you cannot fix the issue and need human help:
```bash
echo "BLOCKED: <reason>" > {marker_path}
```
"#,
commands_str = commands_str,
error_output = error_output,
marker_path = marker_path,
);
let window_name = format!("repair-{}", attempt);
let spawn_config = terminal::SpawnConfig {
task_id: &window_name,
prompt: &prompt,
working_dir,
session_name,
harness,
model: model.as_deref(),
task_list_id: None,
};
terminal::spawn_tmux_agent(&spawn_config)?;
let agent_info = if let Some(ref m) = model {
format!("{}:{}", harness.name(), m)
} else {
harness.name().to_string()
};
println!(
" {} Spawned: {} [{}] {}:{}",
"✓".green(),
window_name.cyan(),
agent_info.dimmed(),
session_name.dimmed(),
attempt
);
Ok(())
}
fn wait_for_repair(marker_path: &std::path::Path, attach: bool, session_name: &str) -> Result<()> {
if attach {
println!(
" {} Attaching to session. Mark completion with: echo SUCCESS > {}",
"→".dimmed(),
marker_path.display()
);
terminal::tmux_attach(session_name)?;
if marker_path.exists() {
let content = std::fs::read_to_string(marker_path)?;
std::fs::remove_file(marker_path)?;
if content.starts_with("BLOCKED") {
println!(" {} Agent reported: {}", "!".yellow(), content.trim());
}
}
return Ok(());
}
let timeout = Duration::from_secs(3600); let start = std::time::Instant::now();
let poll_interval = Duration::from_secs(5);
println!(
" {} Attach with: tmux attach -t {}",
"ℹ".dimmed(),
session_name
);
loop {
if start.elapsed() > timeout {
println!(" {} Repair timed out after 1 hour", "!".yellow());
return Ok(());
}
if marker_path.exists() {
let content = std::fs::read_to_string(marker_path)?;
std::fs::remove_file(marker_path)?;
if content.starts_with("SUCCESS") {
println!(" {} Repair agent completed", "✓".green());
} else if content.starts_with("BLOCKED") {
println!(
" {} Agent reported blocked: {}",
"!".yellow(),
content.trim()
);
} else {
println!(" {} Repair agent finished", "✓".green());
}
return Ok(());
}
thread::sleep(poll_interval);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_error_output() {
let result = ValidationResult {
all_passed: false,
failures: vec!["cargo test".to_string()],
results: vec![backpressure::CommandResult {
command: "cargo test".to_string(),
passed: false,
exit_code: Some(1),
stdout: "running 2 tests".to_string(),
stderr: "error: test failed".to_string(),
duration_secs: 1.5,
}],
};
let output = format_error_output(&result);
assert!(output.contains("cargo test"));
assert!(output.contains("Exit code: Some(1)"));
assert!(output.contains("test failed"));
}
}