use super::ChildResult;
use crate::config::{Action, Event};
use crate::prompt::{ChildDispatchRequest, Deps, Error, WORKER_ROLE, child_dispatch, role};
use crate::template::GitRunner;
use std::path::Path;
pub(super) const VERIFIER_ROLE: &str = "verifier";
pub(super) const FEEDBACK: &str = "verifier.feedback";
pub(super) fn verdict(cr: &ChildResult) -> Event {
let approved = cr.epitaph == crate::prompt::inbox::Epitaph::FinalResponse.as_str()
&& cr
.response
.as_deref()
.is_some_and(|r| r.trim_start().starts_with("APPROVE"));
if approved {
Event::VerifierApprove
} else {
Event::VerifierReject
}
}
pub(super) fn dispatch(
workspace: &Path,
agent_id: &str,
worktree: &Path,
worker: &ChildResult,
deps: &Deps<'_>,
) -> Result<(), Error> {
if already_gated(worktree, agent_id, &worker.terminal_ref, deps.git)? {
return Ok(());
}
let goal = verifier_goal(worker.response.as_deref().unwrap_or(""));
let req = ChildDispatchRequest {
repo: workspace,
parent_branch: agent_id,
parent_worktree: worktree,
role: VERIFIER_ROLE,
goal: &goal,
name: None,
fork_point: Some(&worker.terminal_ref),
pins: crate::prompt::PinnedDocs::none(),
};
child_dispatch::run_procedure(&req, deps.git, deps.clock, deps.id_gen, deps.launcher)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn execute(
action: &Action,
event: Event,
workspace: &Path,
agent_id: &str,
worktree: &Path,
verifier_cr: &ChildResult,
results: &[ChildResult],
deps: &Deps<'_>,
) -> Result<(), Error> {
match (event, action) {
(Event::VerifierApprove, Action::DeliverResult) => {
if let Some(worker) = find_gated_worker(results, verifier_cr, worktree, deps.git) {
super::deliver_result(worktree, agent_id, worker, deps.git)?;
}
consume(verifier_cr)
}
(Event::VerifierReject, Action::Dispatch { role, with, .. }) if role == WORKER_ROLE => {
reject(
workspace,
agent_id,
worktree,
verifier_cr,
results,
with.as_deref(),
deps,
)
}
_ => Err(Error::ActionUnsupported {
action: format!("{action:?}"),
event: event.as_str(),
}),
}
}
fn reject(
workspace: &Path,
agent_id: &str,
worktree: &Path,
verifier_cr: &ChildResult,
results: &[ChildResult],
with: Option<&str>,
deps: &Deps<'_>,
) -> Result<(), Error> {
let feedback = match with {
Some(FEEDBACK) | None => verifier_cr.response.clone().unwrap_or_default(),
Some(literal) => literal.to_string(),
};
let req = ChildDispatchRequest {
repo: workspace,
parent_branch: agent_id,
parent_worktree: worktree,
role: WORKER_ROLE,
goal: &feedback,
name: None,
fork_point: None,
pins: crate::prompt::PinnedDocs::none(),
};
child_dispatch::run_procedure(&req, deps.git, deps.clock, deps.id_gen, deps.launcher)?;
if let Some(worker) = find_gated_worker(results, verifier_cr, worktree, deps.git) {
consume(worker)?;
}
consume(verifier_cr)
}
fn find_gated_worker<'a>(
results: &'a [ChildResult],
verifier_cr: &ChildResult,
worktree: &Path,
git: &dyn GitRunner,
) -> Option<&'a ChildResult> {
results.iter().find(|cr| {
cr.child_id != verifier_cr.child_id
&& cr.path.exists()
&& is_ancestor(worktree, &cr.terminal_ref, &verifier_cr.terminal_ref, git)
})
}
fn already_gated(
worktree: &Path,
parent: &str,
worker_ref: &str,
git: &dyn GitRunner,
) -> Result<bool, Error> {
let pattern = format!("agents/{parent}-*");
let out = git
.run_capture(
worktree,
&["branch", "--list", "--format=%(refname:short)", &pattern],
)
.map_err(|source| Error::Git {
op: "gate branch list",
source,
})?;
for line in out.lines() {
let branch = line.trim();
if branch.is_empty() {
continue;
}
let id = branch.strip_prefix("agents/").unwrap_or(branch);
if role::derive(worktree, branch, id, git)?.as_deref() == Some(VERIFIER_ROLE)
&& is_ancestor(worktree, worker_ref, branch, git)
{
return Ok(true);
}
}
Ok(false)
}
#[cfg(test)]
mod tests;
fn is_ancestor(worktree: &Path, ancestor: &str, descendant: &str, git: &dyn GitRunner) -> bool {
git.run(
worktree,
&["merge-base", "--is-ancestor", ancestor, descendant],
)
.is_ok()
}
fn consume(cr: &ChildResult) -> Result<(), Error> {
std::fs::remove_file(&cr.path).map_err(Error::Io)
}
fn verifier_goal(worker_response: &str) -> String {
format!(
"You are a verifier judging a worker's completed work — its output below and the \
work products in your tree. Reply with a single leading verdict line: `APPROVE` if \
the work satisfies the task, or `REJECT` followed by specific, actionable feedback.\n\n\
Worker's final response:\n{worker_response}\n"
)
}