use std::io::{Read, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use claudius::chat::ChatConfig;
use utf8path::Path;
use sid_isnt_done::ralph::EXIT_TRANSPORT;
use sid_isnt_done::ralph::args::RunArgs;
use sid_isnt_done::ralph::host::SidRalphHost;
use sid_isnt_done::ralph::journal::generate_run_id;
use sid_isnt_done::ralph::runner::{
CONTROL_DIR_ENV, RunnerOptions, ShimRequest, call_control_dir, run_ralph,
};
const EXIT_USAGE: u8 = 64;
fn main() -> ExitCode {
let mut args = std::env::args();
let argv0 = args.next().unwrap_or_else(|| "ralph".to_string());
let name = std::path::Path::new(&argv0)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or(argv0.clone());
let args: Vec<String> = args.collect();
if name == "agent" || name == "judge" {
shim_main(name, args)
} else {
interpreter_main(args)
}
}
fn shim_main(name: String, args: Vec<String>) -> ExitCode {
let Ok(control_dir) = std::env::var(CONTROL_DIR_ENV) else {
eprintln!("{name}: {CONTROL_DIR_ENV} is not set; run me from inside a ralph run");
return ExitCode::from(EXIT_TRANSPORT as u8);
};
let mut context = Vec::new();
if std::io::stdin().read_to_end(&mut context).is_err() {
eprintln!("{name}: failed to read stdin");
return ExitCode::from(EXIT_TRANSPORT as u8);
}
let context = String::from_utf8_lossy(&context).into_owned();
let request = ShimRequest {
name,
args,
context,
};
match call_control_dir(&PathBuf::from(control_dir), &request) {
Ok(response) => {
if !response.stdout.is_empty() {
let mut stdout = std::io::stdout().lock();
let _ = stdout.write_all(response.stdout.as_bytes());
let _ = stdout.flush();
}
if !response.stderr.is_empty() {
eprintln!("{}", response.stderr.trim_end());
}
let code = response.exit.clamp(0, 255) as u8;
ExitCode::from(code)
}
Err(err) => {
eprintln!("{}: control socket failure: {err}", request.name);
ExitCode::from(EXIT_TRANSPORT as u8)
}
}
}
fn interpreter_main(argv: Vec<String>) -> ExitCode {
if argv
.first()
.is_some_and(|arg| arg == "-h" || arg == "--help")
{
println!(
"usage: ralph SCRIPT [--max-iters N] [--budget TOKENS] [--resume RUN_ID] [--] [ARGS...]"
);
return ExitCode::SUCCESS;
}
let args = match RunArgs::parse(&argv) {
Ok(args) => args,
Err(err) => {
eprintln!("ralph: {err}");
return ExitCode::from(EXIT_USAGE);
}
};
match run_interpreter(args) {
Ok(exit) => ExitCode::from(exit.clamp(0, 255) as u8),
Err(err) => {
eprintln!("ralph: {err}");
ExitCode::from(EXIT_TRANSPORT as u8)
}
}
}
fn run_interpreter(args: RunArgs) -> Result<i32, String> {
let workspace_root = Path::try_from(
std::env::current_dir()
.map_err(|err| format!("failed to determine the current working directory: {err}"))?,
)
.map_err(|err| format!("current working directory is not valid UTF-8: {err}"))?
.into_owned();
let config_root = resolve_config_root()?;
let script_path = resolve_script(&args.script, &workspace_root, &config_root)?;
let script_text = std::fs::read_to_string(&script_path)
.map_err(|err| format!("failed to read {}: {err}", script_path.display()))?;
let runs_root = PathBuf::from(workspace_root.as_str())
.join(".ralph")
.join("runs");
std::fs::create_dir_all(&runs_root).map_err(|err| {
format!(
"failed to create runs directory {}: {err}",
runs_root.display()
)
})?;
let (run_id, resume) = match args.resume.as_ref() {
Some(id) => {
if !runs_root.join(id).is_dir() {
return Err(format!("no such run to --resume: {id}"));
}
(id.clone(), true)
}
None => (generate_run_id(&runs_root), false),
};
let run_dir = runs_root.join(&run_id);
let interrupted = Arc::new(AtomicBool::new(false));
install_ctrlc_handler(Arc::clone(&interrupted))?;
let options = RunnerOptions {
run_id: run_id.clone(),
run_dir: run_dir.clone(),
workspace_root: Some(PathBuf::from(workspace_root.as_str())),
max_iters: args.max_iters,
budget_tokens: args.budget,
resume,
script_args: args.script_args.clone(),
};
let runtime = tokio::runtime::Runtime::new()
.map_err(|err| format!("failed to create the async runtime: {err}"))?;
let report = runtime.block_on(async move {
let host = SidRalphHost::new(
workspace_root,
config_root,
ChatConfig::new(),
run_dir,
Vec::new(),
run_id,
tokio::runtime::Handle::current(),
Arc::clone(&interrupted),
);
let run_interrupted = Arc::clone(&interrupted);
tokio::task::spawn_blocking(move || {
run_ralph(Box::new(host), options, &script_text, &[], run_interrupted)
})
.await
.map_err(|err| format!("ralph runner thread panicked: {err}"))?
})?;
eprint!("{}", report.summary(&args.script));
Ok(report.exit)
}
fn resolve_config_root() -> Result<Path<'static>, String> {
match std::env::var("SID_HOME") {
Ok(path) if !path.is_empty() => Ok(Path::new(&path).into_owned()),
Ok(_) | Err(std::env::VarError::NotPresent) => {
Err("SID_HOME must be set and non-empty".to_string())
}
Err(std::env::VarError::NotUnicode(_)) => Err("SID_HOME is not valid UTF-8".to_string()),
}
}
fn resolve_script(
script: &str,
workspace_root: &Path,
config_root: &Path,
) -> Result<PathBuf, String> {
let raw = PathBuf::from(script);
let candidates = if raw.is_absolute() {
vec![raw]
} else {
vec![
PathBuf::from(workspace_root.as_str()).join(script),
PathBuf::from(config_root.as_str()).join(script),
]
};
for candidate in &candidates {
if candidate.is_file() {
return Ok(candidate.clone());
}
}
Err(format!("no such script: {script}"))
}
fn install_ctrlc_handler(interrupted: Arc<AtomicBool>) -> Result<(), String> {
ctrlc::set_handler(move || {
interrupted.store(true, Ordering::Relaxed);
})
.map_err(|err| format!("failed to install the Ctrl-C handler: {err}"))
}