mod flush;
mod sweep;
use super::derive::{
died_mid_work, has_pending, is_message_from, is_pending_deposit, returned, transcript_line_from,
};
use super::{ScanError, ScanReport, cli_run, scan};
use crate::prompt::Clock;
use crate::prompt::inbox::{INBOX_DIR, Launcher, inbox_dir, parent_of, try_acquire};
use crate::prompt::step::{RESPONSE_FILE, STEPS_DIR};
use crate::template::GitRunner;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io;
use tempfile::TempDir;
pub(super) struct FixedClock;
impl Clock for FixedClock {
fn now_iso8601(&self) -> String {
"2026-07-11T00:00:00Z".into()
}
fn now_compact(&self) -> String {
unreachable!("scan never reads the compact clock")
}
}
#[derive(Default)]
pub(super) struct StubGit {
branches: String,
ls_tree: HashMap<String, String>,
tips: HashMap<String, String>,
fail_op: Option<&'static str>,
invocations: RefCell<Vec<Vec<String>>>,
}
impl StubGit {
pub(super) fn with_branches(names: &[&str]) -> Self {
Self {
branches: names.join("\n"),
..Self::default()
}
}
pub(super) fn ls_tree(mut self, branch: &str, listing: &str) -> Self {
self.ls_tree.insert(branch.to_string(), listing.to_string());
self
}
pub(super) fn tip(mut self, branch: &str, sha: &str) -> Self {
self.tips.insert(branch.to_string(), sha.to_string());
self
}
pub(super) fn failing(mut self, op: &'static str) -> Self {
self.fail_op = Some(op);
self
}
pub(super) fn asked_ls_tree_for(&self, branch: &str) -> bool {
let want = format!("agents/{branch}");
self.invocations
.borrow()
.iter()
.any(|a| a.first().map(String::as_str) == Some("ls-tree") && a.contains(&want))
}
}
impl GitRunner for StubGit {
fn run(&self, _dest: &std::path::Path, _args: &[&str]) -> io::Result<()> {
unreachable!("scan issues only run_capture")
}
fn run_capture(&self, _dest: &std::path::Path, args: &[&str]) -> io::Result<String> {
self.invocations
.borrow_mut()
.push(args.iter().map(|s| (*s).to_string()).collect());
match args.first().copied() {
Some("for-each-ref") => {
if self.fail_op == Some("branch") {
return Err(io::Error::other("enumeration boom"));
}
Ok(self
.branches
.lines()
.filter(|l| !l.is_empty())
.map(|id| format!("agents/{id}"))
.collect::<Vec<_>>()
.join("\n"))
}
Some("ls-tree") => {
if self.fail_op == Some("ls-tree") {
return Err(io::Error::other("ls-tree boom"));
}
let branch = args
.get(3)
.and_then(|b| b.strip_prefix("agents/"))
.unwrap_or("");
Ok(self.ls_tree.get(branch).cloned().unwrap_or_default())
}
Some("rev-parse") => {
if self.fail_op == Some("rev-parse") {
return Err(io::Error::other("rev-parse boom"));
}
let branch = args
.get(2)
.and_then(|b| b.strip_prefix("agents/"))
.unwrap_or("");
Ok(self
.tips
.get(branch)
.cloned()
.unwrap_or_else(|| "deadbeef".into()))
}
other => unreachable!("unexpected git op {other:?}"),
}
}
}
#[derive(Default)]
pub(super) struct StubLauncher {
invocations: RefCell<Vec<String>>,
}
impl StubLauncher {
pub(super) fn invocations(&self) -> Vec<String> {
self.invocations.borrow().clone()
}
}
impl Launcher for StubLauncher {
fn launch(&self, _workspace: &std::path::Path, agent_id: &str) -> io::Result<()> {
self.invocations.borrow_mut().push(agent_id.to_string());
Ok(())
}
}
pub(super) struct FailLauncher;
impl Launcher for FailLauncher {
fn launch(&self, _workspace: &std::path::Path, _agent_id: &str) -> io::Result<()> {
Err(io::Error::other("cannot spawn"))
}
}
pub(super) const PARENT: &str = "20260101-p1";
pub(super) const CHILD: &str = "20260101-p1-20260102-c1";
pub(super) fn deposit_msg(ws: &std::path::Path, agent: &str, name: &str) {
let dir = inbox_dir(ws, agent);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(name), "---\nfrom: x\n---\nhi").unwrap();
}
pub(super) fn write_response(ws: &std::path::Path, branch: &str, seq: &str, body: &str) {
let dir = ws.join(STEPS_DIR).join(branch).join(seq);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(RESPONSE_FILE), body).unwrap();
}
pub(super) const COMPLETE: &str = "{\"type\":\"finish\",\"reason\":\"stop\"}\n{\"type\":\"end\"}\n";
pub(super) const KILLED: &str = "{\"type\":\"finish\",\"reason\":\"stop\"}\n";
pub(super) const FAILED: &str = "{\"type\":\"error\",\"kind\":\"parse_input\",\"message\":\"user accepts only text content\"}\n{\"type\":\"end\"}\n";