mod derive;
use super::deposit::Epitaph;
use super::{
AdvanceLauncher, Launcher, ProbeOutcome, deposit_result, inbox_dir, parent_of, probe_and_launch,
};
use crate::prompt::{Clock, SystemClock};
use crate::template::{GitRunner, RealGit};
use derive::{
agent_branches, branch_tip, died_mid_work, has_pending, inbox_agents, is_driven, returned,
};
use std::io;
use std::path::PathBuf;
#[derive(Debug, thiserror::Error)]
pub enum ScanError {
#[error("git {op}: {source}")]
Git {
op: &'static str,
#[source]
source: io::Error,
},
#[error(transparent)]
Layout(#[from] crate::workspace::LayoutError),
#[error(transparent)]
Deposit(#[from] super::DepositError),
#[error("probe executor lock at {path}: {source}")]
Probe {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("flush (probe-and-launch) for {agent}: {source}")]
Flush {
agent: String,
#[source]
source: io::Error,
},
#[error("read inbox root {path}: {source}")]
InboxRoot {
path: PathBuf,
#[source]
source: io::Error,
},
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ScanReport {
pub silent_deaths: Vec<String>,
pub swept: Vec<String>,
pub flushed: Vec<String>,
pub inboxes_without_branch: Vec<String>,
}
pub fn scan(
workspace: &std::path::Path,
git: &dyn GitRunner,
clock: &dyn Clock,
launcher: &dyn Launcher,
) -> Result<ScanReport, ScanError> {
let mut report = ScanReport::default();
let agents = agent_branches(workspace, git)?;
sweep(workspace, git, clock, &agents, &mut report)?;
flush(workspace, launcher, &agents, &mut report)?;
Ok(report)
}
pub fn cli_run(
workspace: &std::path::Path,
driver_target: &std::path::Path,
) -> Result<ScanReport, ScanError> {
crate::workspace::require(workspace)?;
let launcher = AdvanceLauncher::with_exe(driver_target.to_path_buf());
scan(workspace, &RealGit::new(), &SystemClock, &launcher)
}
impl std::fmt::Display for ScanReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let named = match self.silent_deaths.is_empty() {
true => String::new(),
false => format!(" ({})", self.silent_deaths.join(", ")),
};
write!(
f,
"silent deaths: {}{named}; died deposits swept: {}; drivers launched: {}; \
inboxes with no agent branch: {}",
self.silent_deaths.len(),
self.swept.len(),
self.flushed.len(),
self.inboxes_without_branch.len()
)
}
}
fn sweep(
workspace: &std::path::Path,
git: &dyn GitRunner,
clock: &dyn Clock,
agents: &[String],
report: &mut ScanReport,
) -> Result<(), ScanError> {
for branch in agents.iter().cloned() {
if is_driven(workspace, &branch)? {
continue;
}
let died = died_mid_work(workspace, &branch);
let parent = parent_of(&branch).filter(|p| agents.contains(p));
let child_never = match &parent {
Some(parent) => !returned(workspace, git, parent, &branch)?,
None => false,
};
if died || child_never {
report.silent_deaths.push(branch.clone());
}
if child_never {
let parent = parent.expect("child_never implies a parent");
let tip = branch_tip(workspace, git, &branch)?;
deposit_result(
workspace,
&parent,
&branch,
Epitaph::Died,
&tip,
None,
clock,
git,
)?;
report.swept.push(branch);
}
}
Ok(())
}
fn flush(
workspace: &std::path::Path,
launcher: &dyn Launcher,
agents: &[String],
report: &mut ScanReport,
) -> Result<(), ScanError> {
for agent in inbox_agents(workspace)? {
if !has_pending(&inbox_dir(workspace, &agent)) {
continue;
}
if !agents.contains(&agent) {
report.inboxes_without_branch.push(agent);
continue;
}
match probe_and_launch(workspace, &agent, launcher).map_err(|source| ScanError::Flush {
agent: agent.clone(),
source,
})? {
ProbeOutcome::Launched => report.flushed.push(agent),
ProbeOutcome::Busy => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests;