use super::ScanError;
use crate::prompt::inbox::{INBOX_DIR, inbox_dir, try_acquire};
use crate::prompt::step::latest_step_outcome;
use crate::provider::segment::Outcome;
use crate::template::GitRunner;
use crate::workspace;
use std::io;
use std::path::Path;
const MESSAGES_DIR: &str = "messages";
const MESSAGE_EXT: &str = "md";
pub(super) fn agent_branches(
workspace: &Path,
git: &dyn GitRunner,
) -> Result<Vec<String>, ScanError> {
workspace::agent_ids(workspace, git).map_err(|source| ScanError::Git {
op: "for-each-ref agents/",
source,
})
}
pub(super) fn is_driven(workspace: &Path, branch: &str) -> Result<bool, ScanError> {
let dir = inbox_dir(workspace, branch);
match try_acquire(&dir).map_err(|source| ScanError::Probe {
path: dir.clone(),
source,
})? {
Some(_guard) => Ok(false),
None => Ok(true),
}
}
pub(super) fn returned(
workspace: &Path,
git: &dyn GitRunner,
parent: &str,
child: &str,
) -> Result<bool, ScanError> {
if returned_mark_exists(workspace, git, child) || has_inbox_message(workspace, parent, child) {
return Ok(true);
}
transcript_has(workspace, git, parent, child)
}
fn returned_mark_exists(workspace: &Path, git: &dyn GitRunner, child: &str) -> bool {
let mark = crate::prompt::inbox::deposit::returned_ref(child);
git.run(
&workspace::repo_git(workspace),
&["show-ref", "--verify", "--quiet", &mark],
)
.is_ok()
}
fn has_inbox_message(workspace: &Path, parent: &str, child: &str) -> bool {
let dir = inbox_dir(workspace, parent);
let Ok(rd) = std::fs::read_dir(&dir) else {
return false;
};
rd.flatten()
.any(|e| is_message_from(&e.file_name().to_string_lossy(), child))
}
pub(super) fn is_message_from(name: &str, sender: &str) -> bool {
let prefix = format!("{sender}-");
let suffix = format!(".{MESSAGE_EXT}");
name.strip_prefix(&prefix)
.and_then(|m| m.strip_suffix(&suffix))
.is_some_and(|seq| seq.parse::<u32>().is_ok())
}
fn transcript_has(
ws: &Path,
git: &dyn GitRunner,
parent: &str,
child: &str,
) -> Result<bool, ScanError> {
let parent_ref = workspace::agent_ref(parent);
let out = git
.run_capture(
&workspace::repo_git(ws),
&[
"ls-tree",
"-r",
"--name-only",
parent_ref.as_str(),
"--",
MESSAGES_DIR,
],
)
.map_err(|source| ScanError::Git {
op: "ls-tree messages",
source,
})?;
Ok(out.lines().any(|line| transcript_line_from(line, child)))
}
pub(super) fn transcript_line_from(line: &str, sender: &str) -> bool {
let file = line.rsplit('/').next().unwrap_or(line);
let Some(stem) = file.strip_suffix(&format!(".{MESSAGE_EXT}")) else {
return false;
};
match stem.split_once('-') {
Some((seq, rest)) => seq.parse::<u32>().is_ok() && rest == sender,
None => false,
}
}
pub(super) fn died_mid_work(workspace: &Path, branch: &str) -> bool {
matches!(
latest_step_outcome(workspace, branch),
Some(Outcome::NoTerminal | Outcome::Failed)
)
}
pub(super) fn branch_tip(
ws: &Path,
git: &dyn GitRunner,
branch: &str,
) -> Result<String, ScanError> {
let branch_ref = workspace::agent_ref(branch);
let out = git
.run_capture(
&workspace::repo_git(ws),
&["rev-parse", "--verify", branch_ref.as_str()],
)
.map_err(|source| ScanError::Git {
op: "rev-parse branch tip",
source,
})?;
Ok(out.trim().to_string())
}
pub(super) fn inbox_agents(workspace: &Path) -> Result<Vec<String>, ScanError> {
let root = workspace.join(INBOX_DIR);
let rd = match std::fs::read_dir(&root) {
Ok(rd) => rd,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(source) => return Err(ScanError::InboxRoot { path: root, source }),
};
let mut agents: Vec<String> = rd
.flatten()
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
agents.sort();
Ok(agents)
}
pub(super) fn has_pending(dir: &Path) -> bool {
let Ok(rd) = std::fs::read_dir(dir) else {
return false;
};
rd.flatten().any(|e| {
let name = e.file_name().to_string_lossy().into_owned();
is_pending_deposit(&name)
})
}
pub(super) fn is_pending_deposit(name: &str) -> bool {
let Some(stem) = name.strip_suffix(&format!(".{MESSAGE_EXT}")) else {
return false;
};
match stem.rsplit_once('-') {
Some((sender, seq)) => {
seq.parse::<u32>().is_ok() && !sender.is_empty() && !sender.starts_with('.')
}
None => false,
}
}