use std::collections::BTreeMap;
use std::convert::Infallible;
use std::sync::{Mutex, MutexGuard, PoisonError};
use argonauts_core::TaskId;
use argonauts_core::asking::{Ask, AskLine, AskReply};
use argonauts_core::events::RunEventKind;
use argonauts_core::events::run::account_in;
use argonauts_core::events::session::{
AskedBy, CliAgent, CliAgentState, CliWorkflowRun, InputPartKind,
};
use argonauts_core::events::task::{
CheckOutcome, ReviewEvent, TaskKind, TaskLimit, TaskSpec, TaskState,
};
use argonauts_core::events::workflow::{RunNote, WorkflowEvent};
use argonauts_core::labels;
use argonauts_core::names::{ModelRef, RoleName, SessionKey};
use argonauts_core::required::{Required, brief, check};
use argonauts_core::session_runner::{Access, Asking, SessionSpec};
use argonauts_core::settings::crew::CrewRole;
use argonauts_core::sha256_hex;
use argonauts_core::steps::judgment::{ReviewComment, ReviewOutput, ReviewVerdict};
use argonauts_core::steps::plan::Severity;
use argonauts_core::views::cli_workflow_run_id;
use futures::StreamExt;
use futures::stream::FuturesUnordered;
use serde_json::{Value, json};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use crate::engine::host::{Fallbacks, Host, SessionPlan};
use crate::error::{EngineError, EngineResult};
use crate::roles::{display_name, provider};
use crate::task::machine::Trigger;
use crate::task::pipeline::{self, Until};
use crate::task::support::part;
use crate::task::{self, checks, worktree};
use crate::ultra::OwnCrew;
use crate::workflow::{StepAt, StepCall, StepDone};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OwnSession {
pub asking: Asking,
pub subagent_model: Option<String>,
}
impl OwnSession {
pub fn apply(&self, spec: &mut SessionSpec) {
spec.cli_workflow.ultracode = true;
spec.asking = Some(self.asking.clone());
if let Some(model) = &self.subagent_model {
spec.env.push((SUBAGENT_MODEL.to_owned(), model.clone()));
spec.env
.push((WORKFLOW_SUBAGENT_MODEL.to_owned(), model.clone()));
}
}
}
const SUBAGENT_MODEL: &str = "CLAUDE_CODE_SUBAGENT_MODEL";
const WORKFLOW_SUBAGENT_MODEL: &str = "CLAUDE_CODE_WORKFLOW_SUBAGENT_MODEL";
#[derive(Debug, Clone)]
struct Reviewed {
role: CrewRole,
label: String,
change: String,
accepted: bool,
said: String,
reviews: Vec<(String, ReviewOutput)>,
what: String,
}
struct Asks<'a> {
host: &'a Host,
at: &'a StepAt,
task: &'a TaskId,
name: &'a str,
crew: &'a OwnCrew,
request: &'a str,
token: String,
reviewed: Mutex<Vec<Reviewed>>,
rounds: Mutex<BTreeMap<String, u32>>,
asked: Mutex<BTreeMap<CrewRole, u32>>,
}
#[derive(Debug, Default)]
struct RecordedAsks {
reviewed: Vec<Reviewed>,
rounds: BTreeMap<String, u32>,
asked: BTreeMap<CrewRole, u32>,
}
fn recorded_asks(state: &crate::state::RunState, task: &TaskId) -> RecordedAsks {
let mut recorded = RecordedAsks::default();
let Some(record) = state.tasks.get(task) else {
return recorded;
};
let sessions = &state.tasks.progress(task).review_sessions;
for (event, key) in record.reviews.iter().zip(sessions) {
let (
ReviewEvent::Reviewed {
reviewer, review, ..
},
Some(key),
) = (event, key)
else {
continue;
};
let Some(session) = state.sessions.get(key) else {
continue;
};
let role = match session.started.role {
RoleName::Reviewer => CrewRole::Reviewer,
RoleName::Judge => CrewRole::Judge,
_ => continue,
};
let Some(label) = session.started.label.clone() else {
continue;
};
let asked = session
.asked
.as_ref()
.or(session.started.asked.as_ref())
.unwrap_or(reviewer);
let who = in_place_of(state, key, asked, reviewer);
let said = said_by(&who, review);
let same_ask = recorded
.reviewed
.last()
.is_some_and(|last| last.label == label && last.role == role);
if same_ask && let Some(last) = recorded.reviewed.last_mut() {
last.accepted &= review.verdict == ReviewVerdict::Approve;
last.said = format!("{}\n\n{said}", last.said);
last.reviews.push((who, review.clone()));
continue;
}
let (base, round) = round_in(&label);
let rounds = recorded.rounds.entry(base.to_owned()).or_default();
*rounds = (*rounds).max(round);
*recorded.asked.entry(role).or_default() += 1;
recorded.reviewed.push(Reviewed {
role,
label,
change: String::new(),
accepted: review.verdict == ReviewVerdict::Approve,
said,
reviews: vec![(who, review.clone())],
what: String::new(),
});
}
recorded
}
fn round_in(label: &str) -> (&str, u32) {
label
.rsplit_once(" · round ")
.and_then(|(base, round)| Some((base, round.parse().ok()?)))
.unwrap_or((label, 1))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct Verdict {
accepted: bool,
reasons: Vec<String>,
}
pub async fn run(
host: &Host,
call: &StepCall,
request: &str,
crew: &OwnCrew,
) -> EngineResult<StepDone> {
let at = &call.at;
let crew = &asked(host, crew);
let name = labels::id_slug(account_in(request).unwrap_or(request), 4);
let task =
TaskId::new(format!("task-{name}")).map_err(|err| EngineError::Refused(err.to_string()))?;
let item = labels::slug(&name, 3);
let listener = TcpListener::bind("127.0.0.1:0").await?;
let recorded = host.read(|state| recorded_asks(state, &task));
let asks = Asks {
host,
at,
task: &task,
name: &item,
crew,
request,
token: format!("{:032x}", rand::random::<u128>()),
reviewed: Mutex::new(recorded.reviewed),
rounds: Mutex::new(recorded.rounds),
asked: Mutex::new(recorded.asked),
};
note(host, call, format!("{name} · {}", engine_line(crew)))?;
let required =
crate::script::required::shared(&crate::script::required::for_run(host, request));
if host.read(|state| state.tasks.get(&task).is_none()) {
task::create(host, spec_for(host, &task, &name, request, crew, &required))?;
}
let roles = if crew.judge.is_empty() {
vec![CrewRole::Reviewer]
} else {
vec![CrewRole::Reviewer, CrewRole::Judge]
};
let own = OwnSession {
asking: Asking {
asks: argonauts_core::Asks::Never,
endpoint: listener.local_addr()?.to_string(),
token: asks.token.clone(),
tools: Vec::new(),
standing: None,
roles,
},
subagent_model: crew.worker.model.clone(),
};
host.shared().work_as_claude_codes_own(&task, own);
let reviewed = review_rounds(host, &task, at, &listener, &asks).await;
host.shared().done_as_claude_codes_own(&task);
let (state, verdict, rounds) = reviewed?;
if state != TaskState::InReview {
let state = format!("{state:?}").to_lowercase();
let said = argonauts_core::text::state_words(&state);
return Ok(StepDone::done(format!(
"{name} stopped before its checks passed: it is {said}"
)));
}
let lacked = lacked(host, &task, &required);
if !lacked.is_empty() {
note_left(host, call, lacked.join(" "))?;
}
let lacked: String = lacked.iter().map(|line| format!(" · {line}")).collect();
let (trigger, why) = if verdict.accepted {
(
Trigger::Approved,
"the crew's reviewers accepted the final change".to_owned(),
)
} else {
(
Trigger::ChangesRequestedAgain,
not_accepted(rounds, &verdict),
)
};
pipeline::transition(host, &task, trigger, &why)?;
let merged = pipeline::run_task(host, &task, at).await? == TaskState::Merged;
Ok(StepDone::done(if merged {
format!("{name} ran as Claude Code's own workflow · merged into the run{lacked}")
} else {
format!("{name} ran as Claude Code's own workflow · {why}{lacked}")
}))
}
async fn review_rounds(
host: &Host,
task: &TaskId,
at: &StepAt,
listener: &TcpListener,
asks: &Asks<'_>,
) -> EngineResult<(TaskState, Verdict, u32)> {
let most = host.settings().review.max_change_rounds;
let mut state = serving(
listener,
asks,
pipeline::drive(host, task, at, Until::ChecksPassed),
)
.await?;
let mut rounds = 0;
loop {
if state != TaskState::InReview {
return Ok((state, Verdict::default(), rounds));
}
let verdict = serving(listener, asks, gone_through(asks)).await?;
if verdict.accepted || rounds >= most {
return Ok((state, verdict, rounds));
}
rounds += 1;
let note = fixes_asked(rounds, &verdict.reasons, asks.left(CrewRole::Reviewer));
state = serving(listener, asks, pipeline::revise(host, task, at, ¬e)).await?;
}
}
async fn serving<T>(
listener: &TcpListener,
asks: &Asks<'_>,
work: impl std::future::Future<Output = T>,
) -> T {
tokio::select! {
done = work => done,
never = serve(listener, asks) => match never {},
}
}
fn fixes_asked(round: u32, reasons: &[String], asks_left: bool) -> String {
let then = if asks_left {
"Fix what they found, ask them again about the change, and when that is done report \
again with argonauts_report."
} else {
"Fix what they found and report again with argonauts_report. They were asked as many \
times as Argonauts Code allows, so don't ask them again: Argonauts Code goes through \
the final change itself."
};
format!(
"Round {round} of fixes: the crew's reviewers didn't accept the final change.\n\n{}\n\n\
{then}",
reasons.join("\n\n")
)
}
fn not_accepted(rounds: u32, verdict: &Verdict) -> String {
let after = match rounds {
0 => String::new(),
1 => " after a round of fixes".to_owned(),
n => format!(" after {n} rounds of fixes"),
};
let first = verdict
.reasons
.first()
.and_then(|said| said.lines().next())
.map(|line| format!(": {line}"))
.unwrap_or_default();
format!("the crew's reviewers didn't accept the final change{after}{first}")
}
fn lacked(host: &Host, task: &TaskId, required: &Required) -> Vec<String> {
if required.is_empty() {
return Vec::new();
}
let reported = host.read(|state| {
state
.sessions
.in_order()
.into_iter()
.filter(|session| session.task.as_ref() == Some(task))
.filter_map(|session| session.workflow.clone())
.next_back()
});
let Some(workflow) = reported else {
let listed = check::tasks(required, &[]);
return check::said(&check::steps(required, &[]), listed.as_ref());
};
let phases: Vec<&str> = workflow
.phases
.iter()
.map(|phase| phase.title.as_str())
.collect();
let found = check::steps(required, &phases);
let in_a_required_step = |phase: Option<u32>| {
workflow
.phases
.iter()
.find(|known| Some(known.index) == phase)
.is_some_and(|known| required.step(&known.title).is_some())
};
let task_of = |label: &str| label.rsplit(':').next().unwrap_or(label).to_owned();
let mut labels: Vec<&str> = Vec::new();
for agent in &workflow.agents {
let listed_task = task_of(&agent.label);
if !in_a_required_step(agent.phase)
&& !labels.iter().any(|known| task_of(known) == listed_task)
{
labels.push(&agent.label);
}
}
let listed = check::tasks(required, &labels);
check::said(&found, listed.as_ref())
}
pub fn engine_line(crew: &OwnCrew) -> String {
let mut line = "Claude Code's own workflow".to_owned();
if crew.worker.model.is_some() {
line.push_str(&format!(" · workers on {}", display_name(&crew.worker)));
}
if !crew.reviewer.is_empty() {
line.push_str(&format!(" · reviews by {}", names(&crew.reviewer)));
}
line
}
fn names(models: &[ModelRef]) -> String {
models
.iter()
.map(display_name)
.collect::<Vec<_>>()
.join(", ")
}
fn asked(host: &Host, crew: &OwnCrew) -> OwnCrew {
let available = host.services().workers.available();
let theirs = provider(host, &crew.worker);
let the_authors = |model: &ModelRef| provider(host, model) == theirs;
let per_task = host.settings().review.per_task as usize;
let reviewer = asked_models(
&crew.reviewer,
&available,
&|model| (the_authors(model), false),
per_task,
);
let reviewers: Vec<String> = reviewer.iter().map(|model| provider(host, model)).collect();
let after = |model: &ModelRef| {
(
the_authors(model),
reviewers.contains(&provider(host, model)),
)
};
OwnCrew {
judge: asked_models(&crew.judge, &available, &after, 1),
reviewer,
..crew.clone()
}
}
fn asked_models(
models: &[ModelRef],
available: &[ModelRef],
after: &dyn Fn(&ModelRef) -> (bool, bool),
wanted: usize,
) -> Vec<ModelRef> {
let mut models = models.to_vec();
if models.iter().any(|model| available.contains(model)) {
models.retain(|model| available.contains(model));
}
models.sort_by_key(|model| after(model));
models.truncate(wanted.max(1));
models
}
fn note(host: &Host, call: &StepCall, text: String) -> EngineResult<()> {
record_note(host, call, text, false)
}
pub(crate) fn note_left(host: &Host, call: &StepCall, text: String) -> EngineResult<()> {
record_note(host, call, text, true)
}
fn record_note(host: &Host, call: &StepCall, text: String, left: bool) -> EngineResult<()> {
let note = RunNote {
text,
workflow: Some(call.workflow().clone()),
left,
};
host.record(
None,
None,
RunEventKind::Workflow(WorkflowEvent::Note(note)),
)?;
Ok(())
}
fn spec_for(
host: &Host,
task: &TaskId,
name: &str,
request: &str,
crew: &OwnCrew,
required: &Required,
) -> TaskSpec {
let criteria = host.read(|state| {
state
.goal
.approved
.as_ref()
.map(|goal| goal.criteria.iter().map(|c| c.id.clone()).collect())
.unwrap_or_default()
});
TaskSpec {
id: task.clone(),
title: format!("build:{}", labels::slug(name, 3)),
task: instructions(request, crew, required, asks_allowed(host)),
kind: TaskKind::Build,
owned_paths: Vec::new(),
read_only_paths: Vec::new(),
forbidden_paths: Vec::new(),
hidden_paths: Vec::new(),
depends_on: Vec::new(),
criteria,
worker: Some(crew.orchestrator.clone()),
reviewers: Vec::new(),
limit: TaskLimit::default(),
predictions: None,
item: Some(labels::slug(name, 3)),
}
}
fn instructions(request: &str, crew: &OwnCrew, required: &Required, asks: u32) -> String {
let mut text = format!(
"{request}\n\nRun this as one workflow of your own with the Workflow tool: plan its \
phases and let its agents do the work. Start one Workflow call for the whole task, \
and no second one for reviews or a sign-off."
);
if crew.worker.model.is_some() {
text.push_str(&format!(
" Your agents run on {}.",
display_name(&crew.worker)
));
}
if let Some(yours) = brief::for_claude_code(required) {
text.push_str(&format!("\n\n{}", yours.trim_end()));
}
text.push_str(&format!(
"\n\nThe crew's reviewers are {}. At the review step, the agent that reviews calls \
mcp__argonauts_status__argonauts_ask_reviewer with its own label and what the change \
is meant to do, instead of reviewing it itself, and what they mark blocking is fixed.",
names(&crew.reviewer)
));
if !crew.judge.is_empty() {
text.push_str(&format!(
" Then the judge, {}, signs off through \
mcp__argonauts_status__argonauts_ask_judge.",
names(&crew.judge)
));
}
text.push_str(&format!(
" Each of them is asked at most {} in this task, and then its last answer stands. \
Argonauts Code goes through the final change with them itself before anything merges, \
so don't ask them again for that.",
times(asks)
));
text.push_str(
"\nAn agent that doesn't see one of those tools loads it with ToolSearch, such as \
select:mcp__argonauts_status__argonauts_ask_reviewer.",
);
text.push_str(REPORT_WHEN_DONE);
text
}
const REPORT_WHEN_DONE: &str = "\n\nYour workflow runs in the background. When it has ended, and \
not before, call mcp__argonauts_status__argonauts_report once with your report of the whole task. \
A report sent while the workflow still runs isn't taken.";
async fn serve(listener: &TcpListener, asks: &Asks<'_>) -> Infallible {
let mut answering = FuturesUnordered::new();
loop {
tokio::select! {
accepted = listener.accept() => {
if let Ok((stream, _)) = accepted {
answering.push(answer(stream, asks));
}
}
Some(()) = answering.next(), if !answering.is_empty() => {}
}
}
}
async fn answer(stream: TcpStream, asks: &Asks<'_>) {
let (read, mut write) = stream.into_split();
let mut line = String::new();
if BufReader::new(read).read_line(&mut line).await.is_err() {
return;
}
let reply = match serde_json::from_str::<AskLine>(line.trim()) {
Ok(asked) if asked.token == asks.token => match asked.ask {
Ask::Role { role, label, what } => {
match asks.asked_in_the_session(role, &label, &what).await {
Ok(text) => AskReply::text(text),
Err(err) => AskReply {
text: format!("Argonauts Code couldn't ask them: {err}"),
is_error: true,
},
}
}
_ => AskReply {
text: "This session can only ask the crew's roles.".to_owned(),
is_error: true,
},
},
_ => AskReply {
text: "Argonauts Code didn't take that ask.".to_owned(),
is_error: true,
},
};
if let Ok(text) = serde_json::to_string(&reply) {
let _ = write.write_all(format!("{text}\n").as_bytes()).await;
}
}
fn asks_allowed(host: &Host) -> u32 {
host.settings().review.max_change_rounds.saturating_add(1)
}
fn report_now(role: CrewRole, allowed: u32) -> String {
let who = match role {
CrewRole::Judge => "the judge",
_ => "the crew's reviewers",
};
format!(
"Argonauts Code has asked {who} {} about this task, as many as it allows, so it doesn't \
ask again and their last answer stands. Don't start another review or sign-off: end your \
workflow and report with mcp__argonauts_status__argonauts_report. Argonauts Code goes \
through the final change itself before anything merges.",
times(allowed)
)
}
fn times(n: u32) -> String {
match n {
1 => "once".to_owned(),
2 => "twice".to_owned(),
n => format!("{n} times"),
}
}
impl Asks<'_> {
fn left(&self, role: CrewRole) -> bool {
lock(&self.asked).get(&role).copied().unwrap_or_default() < asks_allowed(self.host)
}
async fn asked_in_the_session(
&self,
role: CrewRole,
label: &str,
what: &str,
) -> EngineResult<String> {
let allowed = asks_allowed(self.host);
let within = {
let mut asked = lock(&self.asked);
let asked = asked.entry(role).or_default();
let within = *asked < allowed;
if within {
*asked += 1;
}
within
};
if !within {
return Ok(report_now(role, allowed));
}
self.ask(role, label, what).await
}
async fn ask(&self, role: CrewRole, label: &str, what: &str) -> EngineResult<String> {
let host = self.host;
let models = match role {
CrewRole::Judge if !self.crew.judge.is_empty() => &self.crew.judge,
CrewRole::Judge | CrewRole::Reviewer => &self.crew.reviewer,
other => {
return Err(EngineError::Refused(format!(
"the {other} aren't asked this way"
)));
}
};
let change = change_of(host, self.task).await?;
if change.trim().is_empty() {
return Ok("There is no change in the folder yet, so nothing was reviewed.".to_owned());
}
let tree = worktree::ensure(host, self.task).await?;
let checks = checks_now(host, self.task).await?;
let (label, round) = self.round_of(role, label);
let earlier = self.earlier_findings(what);
let asked = Asked {
role,
label: &label,
round,
what,
earlier: earlier.as_deref(),
change: &change,
checks: checks.as_deref(),
};
let mut answers = Vec::new();
let mut reviews = Vec::new();
for model in models {
let (who, review) = self.one(model, &asked, &tree.path).await?;
answers.push(said_by(&who, &review));
reviews.push((who, review));
}
let said = answers.join("\n\n");
let accepted = reviews
.iter()
.all(|(_, review)| review.verdict == ReviewVerdict::Approve);
lock(&self.reviewed).push(Reviewed {
role,
label,
change: sha256_hex(change.as_bytes()),
accepted,
said: said.clone(),
reviews,
what: what.to_owned(),
});
Ok(said)
}
fn round_of(&self, role: CrewRole, given: &str) -> (String, u32) {
let label = match given.trim() {
"" => format!("{}:{}", role_word(role), self.name),
given => given.to_owned(),
};
let mut rounds = lock(&self.rounds);
let round = rounds.entry(label.clone()).or_default();
*round += 1;
let shown = match *round {
1 => label,
n => format!("{label} · round {n}"),
};
(shown, *round)
}
fn earlier_findings(&self, what: &str) -> Option<String> {
let reviewed = lock(&self.reviewed);
if reviewed.is_empty() {
return None;
}
let mut text =
"Earlier rounds on this change, oldest first. Judge the change as it is now \
against the request; what an answer settled isn't raised again."
.to_owned();
for (n, round) in reviewed.iter().enumerate() {
text.push_str(&format!("\n\n{}. {}", n + 1, round.label));
for (who, review) in &round.reviews {
let verdict = if review.verdict == ReviewVerdict::Approve {
"accepted it"
} else {
"didn't accept it"
};
text.push_str(&format!("\n{who} {verdict}: {}", review.summary));
for comment in &review.comments {
text.push_str(&format!("\n- {}: {}", weight(comment), comment.text));
}
}
let answered = reviewed.get(n + 1).map_or(what, |next| next.what.as_str());
if !answered.trim().is_empty() {
text.push_str(&format!("\nAnswered: {}", answered.trim()));
}
}
Some(text)
}
async fn one(
&self,
model: &ModelRef,
asked: &Asked<'_>,
workdir: &std::path::Path,
) -> EngineResult<(String, ReviewOutput)> {
let host = self.host;
let role = asked.role;
let mut parts = vec![
part(InputPartKind::Prompt, role_word(role), asked_of(role), None),
part(InputPartKind::Goal, "the request", self.request, None),
];
if !asked.what.trim().is_empty() {
parts.push(part(
InputPartKind::Message,
"what it does",
asked.what,
None,
));
}
if let Some(earlier) = asked.earlier {
parts.push(part(
InputPartKind::EarlierFindings,
"earlier findings",
earlier,
None,
));
}
if let Some(checks) = asked.checks {
parts.push(part(InputPartKind::CheckOutput, "the checks", checks, None));
}
parts.push(part(
InputPartKind::Evidence,
"the change",
asked.change,
None,
));
let mut builders = vec![self.crew.orchestrator.clone()];
if !builders.contains(&self.crew.worker) {
builders.push(self.crew.worker.clone());
}
let plan = SessionPlan {
at: Some(self.at.clone()),
role: role.fine_role(),
worker: model.clone(),
task: None,
access: Access::ReadOnly,
step: None,
workdir: workdir.to_path_buf(),
parts,
json_schema: Some(verdict_schema()),
resume: None,
slot: 0,
label: Some(asked.label.to_owned()),
reviews: false,
fresh: false,
independent_of: builders,
in_place_of: None,
fork_of: None,
};
let ran = host
.run_session_in_turn(plan, None, Fallbacks::Allowed, &|_| Ok(()))
.await?;
let answer = ran.end.structured.clone().unwrap_or(Value::Null);
let review = review_of(&answer);
let event = ReviewEvent::Reviewed {
reviewer: ran.worker.clone(),
blind: false,
round: asked.round,
review: review.clone(),
};
host.record(Some(self.task), Some(&ran.key), RunEventKind::Review(event))?;
let who = host.read(|state| in_place_of(state, &ran.key, model, &ran.worker));
Ok((who, review))
}
}
struct Asked<'a> {
role: CrewRole,
label: &'a str,
round: u32,
what: &'a str,
earlier: Option<&'a str>,
change: &'a str,
checks: Option<&'a str>,
}
async fn change_of(host: &Host, task: &TaskId) -> EngineResult<String> {
Ok(crate::change::of_task(host, task).await?.diff)
}
async fn checks_now(host: &Host, task: &TaskId) -> EngineResult<Option<String>> {
let commands = &host.settings().checks.run;
if commands.is_empty() {
return Ok(None);
}
let (copy, label) = checks::clean_copy_of_change(host, task).await?;
if let Some(output) = checks::setup(host, task, ©, &label).await? {
return Ok(Some(format!("checks.setup failed: {output}")));
}
let mut lines = Vec::new();
for command in commands {
let request = checks::request(host, task, command, ©, false)?;
let mut outcome = host.services().checks.run(request).await;
outcome
.command
.get_or_insert_with(|| command.command.clone());
lines.push(checked(&outcome));
}
Ok(Some(lines.join("\n\n")))
}
fn checked(outcome: &CheckOutcome) -> String {
let mut line = checks::result_line(outcome);
let tail = outcome.output_tail.trim();
if !outcome.passed && !tail.is_empty() {
line.push_str(&format!("\n{tail}"));
}
line
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
async fn gone_through(asks: &Asks<'_>) -> EngineResult<Verdict> {
let last = sha256_hex(change_of(asks.host, asks.task).await?.as_bytes());
let mut roles = vec![CrewRole::Reviewer];
if !asks.crew.judge.is_empty() {
roles.push(CrewRole::Judge);
}
let mut verdict = Verdict {
accepted: true,
reasons: Vec::new(),
};
for role in roles {
let read_last = |reviewed: &Reviewed| reviewed.role == role && reviewed.change == last;
let mut reviewed = lock(&asks.reviewed)
.iter()
.rev()
.find(|r| read_last(r))
.cloned();
if reviewed.is_none() {
asks.ask(role, "", "The final change, before it merges.")
.await?;
reviewed = lock(&asks.reviewed).last().cloned();
}
match reviewed {
Some(reviewed) if reviewed.accepted => {}
Some(reviewed) => {
verdict.accepted = false;
verdict.reasons.push(reviewed.said);
}
None => verdict.accepted = false,
}
}
Ok(verdict)
}
fn role_word(role: CrewRole) -> &'static str {
match role {
CrewRole::Judge => "sign-off",
_ => "review",
}
}
pub fn asked_of(role: CrewRole) -> &'static str {
match role {
CrewRole::Judge => {
"Decide whether the change below is ready to merge for the request, judged against \
the request alone. List each finding once and mark it blocking when the change \
can't merge without it (it leaves out part of the request, or breaks something), \
else a note. Set accept to true to sign it off when nothing blocking is left."
}
_ => {
"Review the change below for the request, judged against the request alone. List \
each finding once and mark it blocking when the change can't merge without it (it \
leaves out part of the request, or breaks something), else a note. Set accept to \
true when nothing blocking is left. Summary is one line."
}
}
}
pub fn verdict_schema() -> Value {
json!({
"type": "object",
"properties": {
"accept": {"type": "boolean"},
"summary": {"type": ["string", "null"]},
"findings": {
"type": ["array", "null"],
"items": {
"type": "object",
"properties": {
"text": {"type": "string"},
"blocking": {"type": "boolean"},
},
"required": ["text", "blocking"],
"additionalProperties": false,
},
},
},
"required": ["accept", "summary", "findings"],
"additionalProperties": false,
})
}
pub fn review_of(answer: &Value) -> ReviewOutput {
let readable = answer.get("accept").and_then(Value::as_bool);
let accept = readable == Some(true);
let comments: Vec<ReviewComment> = answer
.get("findings")
.and_then(Value::as_array)
.map(|findings| {
findings
.iter()
.filter_map(|finding| finding_of(finding, accept))
.collect()
})
.unwrap_or_default();
let blocking = comments
.iter()
.any(|comment| comment.severity == Severity::High);
let approved = readable.is_some() && !blocking && (accept || !comments.is_empty());
let summary = answer
.get("summary")
.and_then(Value::as_str)
.map(str::trim)
.filter(|summary| !summary.is_empty())
.map_or_else(
|| if approved { "Accepted" } else { "Not accepted" }.to_owned(),
str::to_owned,
);
ReviewOutput {
verdict: if approved {
ReviewVerdict::Approve
} else {
ReviewVerdict::RequestChanges
},
summary,
comments,
tamper_suspected: false,
}
}
fn finding_of(finding: &Value, accept: bool) -> Option<ReviewComment> {
let (text, blocking) = match finding {
Value::String(text) => (text.as_str(), !accept),
Value::Object(_) => (
finding.get("text").and_then(Value::as_str)?,
finding.get("blocking").and_then(Value::as_bool) != Some(false),
),
_ => return None,
};
let text = text.trim();
(!text.is_empty()).then(|| ReviewComment {
path: String::new(),
line: None,
severity: if blocking {
Severity::High
} else {
Severity::Low
},
text: text.to_owned(),
})
}
fn weight(comment: &ReviewComment) -> &'static str {
if comment.severity == Severity::High {
"blocking"
} else {
"note"
}
}
fn in_place_of(
state: &crate::state::RunState,
key: &argonauts_core::names::SessionKey,
asked: &ModelRef,
worker: &ModelRef,
) -> String {
let who = display_name(worker);
if worker == asked {
return who;
}
let since = state
.sessions
.get(key)
.map_or(u64::MAX, |record| record.since);
let why = state
.sessions
.in_order()
.into_iter()
.rev()
.filter(|record| record.since < since && record.started.worker == *asked)
.find_map(|record| record.end.as_ref())
.map(|end| format!(" ({})", crate::engine::job_fallback::in_words(&end.reason)))
.unwrap_or_default();
format!("{who}, in place of {}{why}", display_name(asked))
}
pub fn said(reviewer: &ModelRef, review: &ReviewOutput) -> String {
said_by(&display_name(reviewer), review)
}
fn said_by(who: &str, review: &ReviewOutput) -> String {
let mut text = match review.verdict {
ReviewVerdict::Approve => format!("{who} accepts it: {}", review.summary),
_ => format!("{who} asks for changes: {}", review.summary),
};
for comment in &review.comments {
text.push_str(&format!("\n- {}: {}", weight(comment), comment.text));
}
text
}
pub fn asked_by(host: &Host, plan: &SessionPlan) -> Option<AskedBy> {
let asked = matches!(plan.role, RoleName::Reviewer | RoleName::Judge) && plan.task.is_none();
let (at, label) = (plan.at.as_ref()?, plan.label.as_deref()?);
if !asked {
return None;
}
host.read(|state| {
let asking = state
.sessions
.in_order()
.into_iter()
.rev()
.find(|session| {
session.task.is_some()
&& session.end.is_none()
&& session.workflow.is_some()
&& session.started.workflow_step.as_ref() == Some(&at.step)
})?;
let workflow = asking.workflow.as_deref()?;
Some(asker_in(&asking.key, workflow, plan.role, label))
})
}
fn asker_in(
session: &SessionKey,
workflow: &CliWorkflowRun,
role: RoleName,
label: &str,
) -> AskedBy {
let at_work: Vec<&CliAgent> = workflow
.agents
.iter()
.filter(|agent| matches!(agent.state, CliAgentState::Running | CliAgentState::Other))
.collect();
let word = match role {
RoleName::Judge => "sign-off",
_ => "review",
};
let only = |found: Vec<&&CliAgent>| match found.as_slice() {
[one] => Some(one.label.clone()),
_ => None,
};
let agent = only(at_work.iter().collect())
.or_else(|| {
only(
at_work
.iter()
.filter(|agent| label_stem(&agent.label) == label_stem(label))
.collect(),
)
})
.or_else(|| {
only(
at_work
.iter()
.filter(|agent| agent.label.starts_with(word))
.collect(),
)
});
AskedBy {
session: session.clone(),
workflow: Some(cli_workflow_run_id(&workflow.id)),
agent,
}
}
pub fn label_stem(label: &str) -> &str {
fn numbered<'a>((stem, n): (&'a str, &str)) -> Option<&'a str> {
n.trim().parse::<u32>().is_ok().then_some(stem)
}
let label = label.trim();
label
.rsplit_once(" · round ")
.and_then(numbered)
.or_else(|| label.rsplit_once('#').and_then(numbered))
.unwrap_or(label)
.trim_end()
}
#[cfg(test)]
mod tests {
use super::*;
use argonauts_core::events::session::SessionEvent;
use argonauts_core::events::task::TaskEvent;
use crate::state::RunState;
use crate::state::tests::{envelope, session_started};
use crate::testutil::task_spec;
fn reported(agents: &[(&str, CliAgentState)]) -> CliWorkflowRun {
CliWorkflowRun {
id: "worx86tbc".to_owned(),
call: None,
name: "fix-money-rounding".to_owned(),
description: String::new(),
phases: Vec::new(),
agents: agents
.iter()
.zip(1..)
.map(|((label, state), index)| CliAgent {
index,
label: (*label).to_owned(),
state: *state,
..CliAgent::default()
})
.collect(),
status: argonauts_core::events::session::SubagentStatus::Running,
tokens: 0,
tool_calls: 0,
duration_ms: 0,
summary: None,
started_at_ms: None,
}
}
fn asker(workflow: &CliWorkflowRun, role: RoleName, label: &str) -> Option<String> {
asker_in(&SessionKey::numbered(1), workflow, role, label).agent
}
#[test]
fn the_asker_is_the_one_agent_at_work_whatever_its_label() {
use CliAgentState::{Done, Running};
let workflow = reported(&[
("build:fix-the-money", Done),
("review:fix-the-money#1", Done),
("sign-off:fix-the-money#1", Running),
]);
assert_eq!(
asker(&workflow, RoleName::Judge, "sign-off:fix-the-money").as_deref(),
Some("sign-off:fix-the-money#1")
);
let by = asker_in(
&SessionKey::numbered(1),
&workflow,
RoleName::Judge,
"anything",
);
assert_eq!(by.session, SessionKey::numbered(1));
assert_eq!(by.workflow, Some(cli_workflow_run_id("worx86tbc")));
assert_eq!(by.agent.as_deref(), Some("sign-off:fix-the-money#1"));
}
#[test]
fn with_several_at_work_the_label_then_the_role_says_which() {
use CliAgentState::Running;
let workflow = reported(&[
("build:fix-the-money", Running),
("review:fix-the-money#2", Running),
("sign-off:fix-the-money#1", Running),
]);
assert_eq!(
asker(
&workflow,
RoleName::Reviewer,
"review:fix-the-money · round 2"
)
.as_deref(),
Some("review:fix-the-money#2")
);
assert_eq!(
asker(&workflow, RoleName::Judge, "final").as_deref(),
Some("sign-off:fix-the-money#1")
);
let two_reviews = reported(&[("review:a", Running), ("review:b", Running)]);
assert_eq!(asker(&two_reviews, RoleName::Reviewer, "review:c"), None);
let none = reported(&[("build:a", CliAgentState::Done)]);
assert_eq!(asker(&none, RoleName::Reviewer, "review:a"), None);
}
#[test]
fn a_labels_stem_drops_either_rounds_number() {
assert_eq!(label_stem("review:money#2"), "review:money");
assert_eq!(label_stem("review:money · round 3"), "review:money");
assert_eq!(label_stem("review:money"), "review:money");
assert_eq!(label_stem("review:issue#x"), "review:issue#x");
}
fn review(verdict: ReviewVerdict, summary: &str) -> ReviewOutput {
ReviewOutput {
verdict,
summary: summary.to_owned(),
comments: vec![ReviewComment {
path: "money.rs".to_owned(),
line: None,
severity: Severity::High,
text: format!("{summary} in full"),
}],
tamper_suspected: false,
}
}
#[test]
fn a_relaunched_loop_counts_its_asks_from_the_log() {
let codex: ModelRef = "codex".parse().unwrap();
let flash: ModelRef = "antigravity-cli/flash".parse().unwrap();
let task = "task-fix-the-money";
let mut events = vec![envelope(
1,
Some(task),
None,
RunEventKind::Task(TaskEvent::Created(Box::new(task_spec(task, &[], None)))),
)];
let asks = [
(
"s-001",
&codex,
RoleName::Reviewer,
"review:money",
ReviewVerdict::RequestChanges,
),
(
"s-002",
&codex,
RoleName::Reviewer,
"review:money · round 2",
ReviewVerdict::RequestChanges,
),
(
"s-003",
&flash,
RoleName::Judge,
"sign-off:money",
ReviewVerdict::Approve,
),
];
for (n, (key, worker, role, label, verdict)) in asks.into_iter().enumerate() {
let mut started = session_started(worker, role);
started.label = Some(label.to_owned());
let seq = 2 + 2 * n as u64;
events.push(envelope(
seq,
None,
Some(key),
RunEventKind::Session(SessionEvent::Started(Box::new(started))),
));
let reviewed = ReviewEvent::Reviewed {
reviewer: worker.clone(),
blind: false,
round: 1,
review: review(verdict, &format!("finding {n}")),
};
events.push(envelope(
seq + 1,
Some(task),
Some(key),
RunEventKind::Review(reviewed),
));
}
let state = RunState::fold(&events);
let recorded = recorded_asks(&state, &TaskId::new(task).unwrap());
assert_eq!(
recorded.asked,
BTreeMap::from([(CrewRole::Reviewer, 2), (CrewRole::Judge, 1)])
);
assert_eq!(
recorded.rounds,
BTreeMap::from([
("review:money".to_owned(), 2),
("sign-off:money".to_owned(), 1)
])
);
let labels: Vec<&str> = recorded.reviewed.iter().map(|r| r.label.as_str()).collect();
assert_eq!(
labels,
["review:money", "review:money · round 2", "sign-off:money"]
);
assert!(!recorded.reviewed[1].accepted);
assert!(recorded.reviewed[2].accepted);
assert!(
recorded.reviewed[0]
.said
.contains("- blocking: finding 0 in full"),
"{}",
recorded.reviewed[0].said
);
assert_eq!(round_in("review:money · round 12"), ("review:money", 12));
assert_eq!(round_in("review:money"), ("review:money", 1));
}
#[test]
fn a_round_of_fixes_says_whether_the_reviewers_may_be_asked_again() {
let reasons = ["Codex asks for changes: the rounding".to_owned()];
assert!(fixes_asked(1, &reasons, true).contains("ask them again about the change"));
let spent = fixes_asked(2, &reasons, false);
assert!(spent.contains("don't ask them again"), "{spent}");
assert!(
!spent.contains("ask them again about the change"),
"{spent}"
);
}
fn crew(judge: &[&str]) -> OwnCrew {
OwnCrew {
orchestrator: "claude-code/fable-5-1".parse().unwrap(),
worker: "claude-code/opus-5-5".parse().unwrap(),
reviewer: vec!["astra".parse().unwrap()],
judge: judge.iter().map(|model| model.parse().unwrap()).collect(),
}
}
#[test]
fn the_orchestrator_is_told_to_send_reviews_through_argonauts() {
let text = instructions(
"add due dates to tasks",
&crew(&[]),
&Required::default(),
3,
);
assert!(text.starts_with("add due dates to tasks\n\n"));
assert!(text.contains("Workflow tool"));
assert!(text.contains("argonauts_ask_reviewer"));
assert!(!text.contains("argonauts_ask_judge"));
assert!(text.ends_with("A report sent while the workflow still runs isn't taken."));
let judged = instructions(
"add due dates",
&crew(&["claude-code/opus-5-5"]),
&Required::default(),
3,
);
assert!(judged.contains("argonauts_ask_judge"));
}
#[test]
fn the_orchestrator_is_asked_for_one_workflow_and_told_the_cap() {
let text = instructions(
"fix the rounding",
&crew(&["codex"]),
&Required::default(),
3,
);
assert!(
text.contains("Start one Workflow call for the whole task"),
"{text}"
);
assert!(text.contains("with its own label"), "{text}");
assert!(text.contains("at most 3 times in this task"), "{text}");
assert!(!text.contains("once more"), "{text}");
assert!(report_now(CrewRole::Judge, 3).starts_with(
"Argonauts Code has asked the judge 3 times about this task, as many as it allows"
));
}
#[test]
fn the_steps_you_require_and_your_tasks_go_with_the_request() {
let required = Required {
steps: vec![argonauts_core::required::RequiredStep {
name: "Benchmark".into(),
role: CrewRole::Worker,
does: Some("Run the benchmarks.".into()),
when: Some("last".into()),
}],
tasks: vec!["migrate the schema".into()],
};
let text = instructions("add due dates", &crew(&[]), &required, 3);
assert!(
text.contains(
"Your workflow must have a phase for each of these steps, by this name, \
where it says:\n- Benchmark, last, by the workers: Run the benchmarks."
),
"{text}"
);
assert!(
text.contains("1. migrate the schema (label: migrate-schema)"),
"{text}"
);
assert!(text.find("Benchmark") < text.find("argonauts_ask_reviewer"));
}
#[test]
fn asked_models_put_other_providers_first() {
let models: Vec<ModelRef> = ["claude-code/haiku-4-5", "codex", "antigravity-cli"]
.iter()
.map(|model| model.parse().unwrap())
.collect();
let claude = |model: &ModelRef| (model.agent == "claude-code", false);
let picked = asked_models(&models, &[], &claude, 1);
assert_eq!(picked, [models[1].clone()]);
let picked = asked_models(&models, &[], &claude, 3);
assert_eq!(
picked,
[models[1].clone(), models[2].clone(), models[0].clone()]
);
let picked = asked_models(&models, &[models[0].clone(), models[2].clone()], &claude, 1);
assert_eq!(picked, [models[2].clone()]);
assert!(asked_models(&[], &[], &claude, 1).is_empty());
let after_codex = |model: &ModelRef| (model.agent == "claude-code", model.agent == "codex");
assert_eq!(
asked_models(&models, &[], &after_codex, 1),
[models[2].clone()]
);
}
#[test]
fn a_change_that_comes_to_you_says_why_in_the_reviewers_words() {
let verdict = Verdict {
accepted: false,
reasons: vec!["Codex asks for changes: Past dates pass\n- a past date".into()],
};
assert_eq!(
not_accepted(2, &verdict),
"the crew's reviewers didn't accept the final change after 2 rounds of fixes: \
Codex asks for changes: Past dates pass"
);
assert_eq!(
not_accepted(0, &Verdict::default()),
"the crew's reviewers didn't accept the final change"
);
}
#[test]
fn a_check_reads_as_the_runner_words_it() {
let outcome = |passed: bool, summary: &str| CheckOutcome {
name: "tests".into(),
kind: argonauts_core::events::task::CheckKind::Command,
passed,
command: Some(vec!["python3".to_owned(), "-m".into(), "unittest".into()]),
exit_code: Some(i32::from(!passed)),
duration_ms: 3,
summary: summary.into(),
output_tail: "FAIL: test_past_due".into(),
violations: Vec::new(),
sandboxed: true,
skipped: false,
};
assert_eq!(
checked(&outcome(true, "Ran 8 tests · OK")),
"tests (python3 -m unittest) passed: Ran 8 tests · OK"
);
assert_eq!(
checked(&outcome(false, "1 failed")),
"tests (python3 -m unittest) failed: 1 failed\nFAIL: test_past_due"
);
assert_eq!(
checked(&outcome(true, "passed")),
"tests (python3 -m unittest) passed",
"a summary that only says passed isn't said twice"
);
}
#[test]
fn an_answer_reads_as_a_review() {
let review = review_of(&json!({"accept": false, "findings": ["past dates pass"]}));
assert_eq!(review.verdict, ReviewVerdict::RequestChanges);
assert_eq!(review.summary, "Not accepted");
let astra: ModelRef = "astra".parse().unwrap();
assert!(said(&astra, &review).ends_with("\n- blocking: past dates pass"));
assert_eq!(
review_of(&Value::Null).verdict,
ReviewVerdict::RequestChanges
);
assert_eq!(
review_of(&json!({"accept": false, "summary": "Not ready", "findings": null})).verdict,
ReviewVerdict::RequestChanges,
"a no with nothing named blocks on its summary"
);
}
#[test]
fn a_review_accepts_when_nothing_blocking_is_left() {
let notes = review_of(&json!({
"accept": false,
"summary": "Close; nothing must change",
"findings": [{"text": "a bare $ parses as zero", "blocking": false}],
}));
assert_eq!(notes.verdict, ReviewVerdict::Approve);
assert_eq!(notes.comments[0].severity, Severity::Low);
let blocking = review_of(&json!({
"accept": true,
"summary": "Fine",
"findings": [
{"text": "with_tax drops the tax", "blocking": true},
{"text": "a docstring is stale", "blocking": false},
],
}));
assert_eq!(blocking.verdict, ReviewVerdict::RequestChanges);
let codex: ModelRef = "codex".parse().unwrap();
assert_eq!(
said(&codex, &blocking),
"Codex asks for changes: Fine\n- blocking: with_tax drops the tax\n- note: a docstring is stale"
);
}
#[test]
fn a_subagent_model_and_the_tools_reach_the_session() {
let own = OwnSession {
asking: Asking {
asks: argonauts_core::Asks::Never,
endpoint: "127.0.0.1:9".into(),
token: "t".into(),
tools: Vec::new(),
standing: None,
roles: vec![CrewRole::Reviewer],
},
subagent_model: Some("opus-5-5".into()),
};
let mut spec = argonauts_testkit::runner::session_spec(
"claude-code/fable-5-1",
Access::Build,
std::path::Path::new("/tmp/work"),
);
own.apply(&mut spec);
assert!(spec.cli_workflow.ultracode);
assert_eq!(spec.asking.as_ref().unwrap().roles, [CrewRole::Reviewer]);
assert!(
spec.env
.contains(&(SUBAGENT_MODEL.to_owned(), "opus-5-5".to_owned()))
);
assert!(
spec.env
.contains(&(WORKFLOW_SUBAGENT_MODEL.to_owned(), "opus-5-5".to_owned()))
);
}
#[test]
fn no_worker_model_sets_no_subagent_model() {
let own = OwnSession {
asking: Asking {
asks: argonauts_core::Asks::Never,
endpoint: "127.0.0.1:9".into(),
token: "t".into(),
tools: Vec::new(),
standing: None,
roles: vec![CrewRole::Reviewer],
},
subagent_model: None,
};
let mut spec = argonauts_testkit::runner::session_spec(
"claude-code",
Access::Build,
std::path::Path::new("/tmp/work"),
);
own.apply(&mut spec);
assert!(
!spec
.env
.iter()
.any(|(name, _)| name == SUBAGENT_MODEL || name == WORKFLOW_SUBAGENT_MODEL),
"{:?}",
spec.env
);
}
}