use super::{AdvanceOutcome, run};
use crate::harness_root;
use crate::prompt::inbox::{self, AdvanceLauncher, baton};
use crate::prompt::resolve::{ConfigSource, resolve_worker};
use crate::prompt::{Deps, Error, RealSleeper, SpawnAdapter, SystemClock};
use crate::prompt::{NanoIdGen, tool::SpawnTool};
use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;
use std::sync::atomic::AtomicBool;
#[derive(Debug)]
pub enum AdvanceHandoff {
Done,
Exec(Command),
}
pub fn cli_run(
workspace: &Path,
agent_id: &str,
driver_target: &Path,
adapter_target: Option<&Path>,
stop: &AtomicBool,
) -> Result<AdvanceHandoff, Error> {
cli_run_with(
workspace,
agent_id,
std::env::var_os(baton::LOCK_FD_ENV).as_deref(),
driver_target,
adapter_target,
stop,
)
}
fn cli_run_with(
workspace: &Path,
agent_id: &str,
lease_env: Option<&OsStr>,
driver_target: &Path,
adapter_target: Option<&Path>,
stop: &AtomicBool,
) -> Result<AdvanceHandoff, Error> {
crate::workspace::require(workspace)?;
let inbox_dir = inbox::inbox_dir(workspace, agent_id);
let lease = match baton::take_lease(lease_env, &inbox_dir) {
Ok(Some(lease)) => lease,
Ok(None) => return Ok(AdvanceHandoff::Done),
Err(baton::LeaseError::Acquire(source)) => {
return Err(Error::ExecutorLock {
path: inbox_dir,
source,
});
}
Err(baton::LeaseError::Adopt(e)) => {
return Err(Error::LeaseAdopt {
agent: agent_id.to_string(),
detail: e.to_string(),
});
}
};
let roots = harness_root::resolve()?;
let tool_executor = SpawnTool::new(&roots.data, &SystemClock, driver_target);
let launcher = AdvanceLauncher::with_exe(driver_target.to_path_buf());
let deps = Deps {
adapter: &SpawnAdapter,
sleeper: &RealSleeper,
git: &crate::template::RealGit::new(),
clock: &SystemClock,
id_gen: &NanoIdGen,
tool_executor: &tool_executor,
config_root: &roots.config,
adapter_target,
stop,
launcher: &launcher,
};
let outcome = run(workspace, agent_id, Some(lease), &deps, &mut || {
resolve_worker(workspace, ConfigSource::Agent(agent_id), &deps)
})?;
handoff(driver_target, workspace, agent_id, outcome)
}
fn handoff(
exe: &Path,
workspace: &Path,
agent_id: &str,
outcome: AdvanceOutcome,
) -> Result<AdvanceHandoff, Error> {
match outcome {
AdvanceOutcome::ToolsPending(lease) => Ok(AdvanceHandoff::Exec(baton::successor_command(
exe, workspace, agent_id, lease,
)?)),
_ => Ok(AdvanceHandoff::Done),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prompt::inbox::{ExecutorLock, inbox_dir};
use tempfile::TempDir;
fn test_lease(dir: &Path) -> ExecutorLock {
inbox::try_acquire(dir).unwrap().expect("free lock")
}
fn td() -> &'static Path {
Path::new("lernie")
}
#[test]
fn a_non_workspace_is_refused_by_the_layout_guard() {
let ws = TempDir::new().unwrap();
let err = cli_run(
ws.path(),
"20260101-a1",
td(),
None,
&AtomicBool::new(false),
)
.unwrap_err();
assert!(matches!(err, Error::Layout(_)), "{err}");
}
#[test]
fn empty_workspace_is_nothing_to_do_via_production_wiring() {
let (_h, ws) = crate::workspace::fixture::workspace();
let out = cli_run(&ws, "20260101-a1", td(), None, &AtomicBool::new(false)).unwrap();
assert!(matches!(out, AdvanceHandoff::Done));
}
#[test]
fn held_lock_is_already_driven() {
let (_h, ws) = crate::workspace::fixture::workspace();
let _held = test_lease(&inbox_dir(&ws, "20260101-a1"));
let out = cli_run(&ws, "20260101-a1", td(), None, &AtomicBool::new(false)).unwrap();
assert!(matches!(out, AdvanceHandoff::Done));
}
#[test]
fn broken_inbox_surfaces_as_executor_lock_error() {
let (_h, ws) = crate::workspace::fixture::workspace();
std::fs::create_dir_all(ws.join("inbox")).unwrap();
std::fs::write(inbox_dir(&ws, "20260101-a1"), b"not a dir").unwrap();
let err = cli_run(&ws, "20260101-a1", td(), None, &AtomicBool::new(false)).unwrap_err();
assert!(matches!(err, Error::ExecutorLock { .. }), "{err}");
}
#[test]
fn bad_lease_env_is_declined_loudly_as_lease_adopt() {
let (_h, ws) = crate::workspace::fixture::workspace();
std::fs::create_dir_all(inbox_dir(&ws, "20260101-a1")).unwrap();
let err = cli_run_with(
&ws,
"20260101-a1",
Some(OsStr::new("not-an-fd")),
td(),
None,
&AtomicBool::new(false),
)
.unwrap_err();
assert!(matches!(err, Error::LeaseAdopt { .. }), "{err}");
}
#[test]
fn adopted_lease_env_drives_the_hop() {
let (_h, ws) = crate::workspace::fixture::workspace();
let dir = inbox_dir(&ws, "20260101-a1");
let lease = test_lease(&dir);
let fd = lease.as_raw_fd().to_string();
std::mem::forget(lease);
let out = cli_run_with(
&ws,
"20260101-a1",
Some(OsStr::new(&fd)),
td(),
None,
&AtomicBool::new(false),
)
.unwrap();
assert!(matches!(out, AdvanceHandoff::Done));
}
#[test]
fn a_warranted_hop_delivers_then_consults_the_resolver() {
let (_h, ws) = crate::workspace::fixture::workspace();
let agent = "20260101-a1";
let wt = crate::workspace::fixture::spawn_root(&ws, agent);
inbox::deposit(&ws, agent, "user", "hi", &SystemClock).unwrap();
let err = cli_run(&ws, agent, td(), None, &AtomicBool::new(false)).unwrap_err();
assert!(!err.to_string().is_empty());
assert!(wt.join("messages/001-user.md").exists());
}
#[test]
fn tools_pending_hands_off_as_a_prepared_exec() {
let ws = TempDir::new().unwrap();
let lease = test_lease(&inbox_dir(ws.path(), "20260101-a1"));
let out = handoff(
Path::new("/usr/bin/lernie"),
ws.path(),
"20260101-a1",
AdvanceOutcome::ToolsPending(lease),
)
.unwrap();
let AdvanceHandoff::Exec(cmd) = out else {
panic!("expected Exec");
};
let args: Vec<_> = cmd.get_args().map(|a| a.to_os_string()).collect();
assert_eq!(args[0], "advance");
assert!(
cmd.get_envs()
.any(|(k, v)| k == baton::LOCK_FD_ENV && v.is_some())
);
}
#[test]
fn non_tools_outcomes_hand_off_as_done() {
let ws = TempDir::new().unwrap();
let out = handoff(
Path::new("lernie"),
ws.path(),
"20260101-a1",
AdvanceOutcome::NothingToDo,
)
.unwrap();
assert!(matches!(out, AdvanceHandoff::Done));
}
}