use crate::errors::CliError;
use crate::git;
use crate::output::format::OutputFormat;
use serde::Serialize;
use std::process::Command;
#[derive(Serialize)]
struct Check {
name: String,
passed: bool,
message: String,
}
pub fn run(json: bool) -> Result<(), CliError> {
let format = OutputFormat::detect(json);
let mut checks: Vec<Check> = Vec::new();
let mut all_passed = true;
let git_ok = git::is_git_repo();
checks.push(Check {
name: "git_repo".into(),
passed: git_ok,
message: if git_ok {
"Git repository found".into()
} else {
"Not a git repository. Run `git init`.".into()
},
});
if !git_ok {
all_passed = false;
}
let config_exists = std::path::Path::new("autoresearch.toml").exists();
checks.push(Check {
name: "config_file".into(),
passed: config_exists,
message: if config_exists {
"autoresearch.toml found".into()
} else {
"No autoresearch.toml. Run `autoresearch init`.".into()
},
});
if !config_exists {
all_passed = false;
}
let config = if config_exists {
match std::fs::read_to_string("autoresearch.toml") {
Ok(content) => match toml::from_str::<toml::Table>(&content) {
Ok(table) => {
checks.push(Check {
name: "config_valid".into(),
passed: true,
message: "autoresearch.toml parses correctly".into(),
});
Some(table)
}
Err(e) => {
checks.push(Check {
name: "config_valid".into(),
passed: false,
message: format!("Invalid TOML: {e}"),
});
all_passed = false;
None
}
},
Err(e) => {
checks.push(Check {
name: "config_valid".into(),
passed: false,
message: format!("Cannot read autoresearch.toml: {e}"),
});
all_passed = false;
None
}
}
} else {
None
};
if let Some(ref table) = config {
for field in &[
"target_file",
"eval_command",
"metric_name",
"metric_direction",
] {
let has_field = table
.get(*field)
.and_then(|v| v.as_str())
.is_some_and(|s| !s.is_empty());
checks.push(Check {
name: format!("config_{field}"),
passed: has_field,
message: if has_field {
format!(
"{field} = {:?}",
table.get(*field).and_then(|v| v.as_str()).unwrap()
)
} else {
format!("Missing required field: {field}")
},
});
if !has_field {
all_passed = false;
}
}
}
if let Some(ref table) = config {
if let Some(target) = table.get("target_file").and_then(|v| v.as_str()) {
let exists = std::path::Path::new(target).exists();
checks.push(Check {
name: "target_file_exists".into(),
passed: exists,
message: if exists {
format!("{target} exists")
} else {
format!("{target} not found — the agent won't have a file to modify")
},
});
if !exists {
all_passed = false;
}
}
}
if let Some(ref table) = config {
if let Some(eval_cmd) = table.get("eval_command").and_then(|v| v.as_str()) {
let result = Command::new("sh")
.args(["-c", eval_cmd])
.output();
match result {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout);
let has_number = stdout
.trim()
.lines()
.last()
.and_then(|line| {
line.split_whitespace()
.last()
.and_then(|w| w.parse::<f64>().ok())
})
.is_some();
checks.push(Check {
name: "eval_runs".into(),
passed: true,
message: format!(
"Eval command runs successfully (exit 0, output: {} bytes)",
stdout.len()
),
});
checks.push(Check {
name: "eval_metric_parseable".into(),
passed: has_number,
message: if has_number {
"Eval output contains a parseable number".into()
} else {
format!(
"Could not find a number in eval output. Last line: {:?}",
stdout.trim().lines().last().unwrap_or("")
)
},
});
if !has_number {
all_passed = false;
}
}
Ok(output) => {
checks.push(Check {
name: "eval_runs".into(),
passed: false,
message: format!(
"Eval command failed (exit {}). stderr: {}",
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stderr)
.chars()
.take(200)
.collect::<String>()
),
});
all_passed = false;
}
Err(e) => {
checks.push(Check {
name: "eval_runs".into(),
passed: false,
message: format!("Cannot execute eval command: {e}"),
});
all_passed = false;
}
}
}
}
if git_ok {
if let Some(ref table) = config {
let branch = table
.get("branch")
.and_then(|v| v.as_str())
.unwrap_or("autoresearch");
let exists = git::experiment_branch_exists(branch);
checks.push(Check {
name: "experiment_branch".into(),
passed: true, message: if exists {
format!("Branch '{branch}' exists with experiments")
} else {
format!("Branch '{branch}' will be created on first run")
},
});
}
}
let log_dir = std::path::Path::new(".autoresearch").exists();
checks.push(Check {
name: "log_directory".into(),
passed: true, message: if log_dir {
".autoresearch/ directory exists".into()
} else {
".autoresearch/ will be created on first record".into()
},
});
if git::is_loop_running() {
checks.push(Check {
name: "stale_lock".into(),
passed: false,
message: "Loop lock file exists (.autoresearch/loop.lock). If no loop is running, delete it.".into(),
});
all_passed = false;
}
let program_exists = std::path::Path::new("program.md").exists();
checks.push(Check {
name: "program_md".into(),
passed: program_exists,
message: if program_exists {
"program.md found — agent will read this for research direction".into()
} else {
"No program.md — agent will have no research direction guidance".into()
},
});
if !program_exists {
all_passed = false;
}
if git_ok {
let output = Command::new("git")
.args(["status", "--porcelain"])
.output()
.ok();
let clean = output
.as_ref()
.map(|o| o.stdout.is_empty())
.unwrap_or(false);
checks.push(Check {
name: "git_clean".into(),
passed: clean,
message: if clean {
"Working tree is clean".into()
} else {
"Uncommitted changes detected. Commit or stash before starting the loop.".into()
},
});
}
let passed_count = checks.iter().filter(|c| c.passed).count();
let total_count = checks.len();
match format {
OutputFormat::Json => {
let out = serde_json::json!({
"status": if all_passed { "success" } else { "issues_found" },
"data": {
"all_passed": all_passed,
"passed": passed_count,
"total": total_count,
"checks": checks,
},
"ready": all_passed,
});
println!("{}", serde_json::to_string_pretty(&out).unwrap());
}
OutputFormat::Table => {
println!("Autoresearch Doctor\n");
for check in &checks {
let icon = if check.passed { "+" } else { "!" };
println!(" [{icon}] {}: {}", check.name, check.message);
}
println!();
println!(
"{passed_count}/{total_count} checks passed. {}",
if all_passed {
"Ready to start!"
} else {
"Fix the issues above before starting the loop."
}
);
}
}
Ok(())
}