use std::collections::BTreeMap;
use argonauts_core::TaskId;
use argonauts_core::events::goal::GoalFile;
use argonauts_core::events::run::{Party, RunMode};
use argonauts_core::events::task::{TaskKind, TaskLimit, TaskSpec, TaskState};
use argonauts_core::names::{ModelRef, RoleName};
use argonauts_core::settings::protocol::ForcedMode;
use crate::engine::host::Host;
use crate::error::{EngineError, EngineResult};
use crate::roles;
use crate::state::RunState;
use crate::state::tasks::TaskEntry;
use crate::understand::{approval, draft, round};
pub async fn goal_from_prompt(host: &Host) -> EngineResult<GoalFile> {
if let Some(approved) = host.read(|state| state.goal.approved.clone()) {
return Ok(approved);
}
if host.settings().captain.understand_first {
return Err(EngineError::Refused(
"captain.understand_first is on, so the goal waits for your approval".to_owned(),
));
}
prompt_is_the_goal(host).await
}
pub async fn prompt_is_the_goal(host: &Host) -> EngineResult<GoalFile> {
if let Some(approved) = host.read(|state| state.goal.approved.clone()) {
return Ok(approved);
}
let request = host.read(round::request)?;
round::begin(host)?;
let goal = draft::from_prompt(host.run_id(), &request);
let goal = round::write_draft(host, goal).await?;
approval::approve(host, goal.clone(), Party::Config)?;
Ok(goal)
}
pub fn task(state: &RunState, host: &Host) -> EngineResult<TaskSpec> {
let started = state
.run
.started
.as_ref()
.ok_or_else(|| EngineError::Refused("the run hasn't started".to_owned()))?;
let settings = host.settings();
let worker = settings.crew.main.clone();
let reviewers = if settings.captain.review {
vec![reviewer(host, &worker)?]
} else {
Vec::new()
};
let criteria = state
.goal
.approved
.as_ref()
.map(|goal| {
goal.criteria
.iter()
.map(|criterion| criterion.id.clone())
.collect()
})
.unwrap_or_default();
let words = crate::workflow::naming::words_of(started);
let title = words.lines().next().unwrap_or(words).trim().to_owned();
let item = crate::workflow::brief::item(state).or_else(|| {
let name = crate::workflow::brief::name_from(words);
Some(argonauts_core::labels::slug(&name, 3)).filter(|item| !item.is_empty())
});
let mut task = started.request.clone();
if !reviewers.is_empty() {
let names: Vec<String> = reviewers.iter().map(roles::display_name).collect();
task.push_str(&format!(
"\n\nArgonauts Code has {} review your change after you report.",
names.join(" and ")
));
}
Ok(TaskSpec {
id: TaskId::numbered(1),
title,
task,
kind: TaskKind::Captain,
owned_paths: Vec::new(),
read_only_paths: Vec::new(),
forbidden_paths: Vec::new(),
hidden_paths: Vec::new(),
depends_on: Vec::new(),
criteria,
worker: Some(worker),
reviewers,
limit: TaskLimit::default(),
predictions: None,
item,
})
}
fn reviewer(host: &Host, worker: &ModelRef) -> EngineResult<ModelRef> {
let provider = |model: &ModelRef| roles::provider(host, model);
let worker_provider = provider(worker);
let mut pool = roles::candidates(host.settings(), RoleName::Reviewer);
pool.extend(host.services().workers.available());
pool.into_iter()
.find(|model| {
model != worker
&& provider(model) != worker_provider
&& roles::conflict(RoleName::Reviewer, model, Some(worker), &provider).is_none()
})
.ok_or_else(|| {
EngineError::Refused(format!(
"captain.review needs a reviewer from another provider than {worker} \
({worker_provider}), and none is available"
))
})
}
pub fn pick_try(state: &RunState, tries: &[TaskId]) -> Option<TaskId> {
tries
.iter()
.enumerate()
.filter_map(|(order, id)| {
let record = state.tasks.get(id)?;
passed_visible_checks(record).then_some((record.tries, order, id))
})
.min_by_key(|(tries, order, _)| (*tries, *order))
.map(|(_, _, id)| id.clone())
}
fn passed_visible_checks(record: &TaskEntry) -> bool {
let out_of_play = matches!(
record.state,
TaskState::Failed | TaskState::NeedsYou | TaskState::Cancelled | TaskState::Deferred
);
if out_of_play || record.checks.is_empty() {
return false;
}
let mut newest = BTreeMap::new();
for check in &record.checks {
newest.insert(check.name.as_str(), check.passed);
}
newest.values().all(|passed| *passed)
}
pub fn moves_to_crew(state: &RunState, host: &Host) -> bool {
let captain_first = state
.run
.started
.as_ref()
.and_then(|started| started.captain_or_crew.as_ref())
.is_some_and(|captain_or_crew| captain_or_crew.move_to_crew);
let forced_captain = host.settings().captain_or_crew.force == Some(ForcedMode::Captain);
let needs_you =
state.tasks.by_id.values().any(|record| {
record.spec.kind == TaskKind::Captain && record.state == TaskState::NeedsYou
});
round::mode(state) == RunMode::Captain && captain_first && !forced_captain && needs_you
}
#[cfg(test)]
mod tests {
use argonauts_core::events::task::{CheckKind, CheckOutcome};
use super::*;
fn check(name: &str, passed: bool) -> CheckOutcome {
CheckOutcome {
name: name.to_owned(),
kind: CheckKind::Command,
passed,
command: None,
exit_code: Some(i32::from(!passed)),
duration_ms: 1,
summary: String::new(),
output_tail: String::new(),
violations: Vec::new(),
sandboxed: true,
skipped: false,
}
}
fn record(n: u32, state: TaskState, tries: u32, checks: Vec<CheckOutcome>) -> TaskEntry {
TaskEntry {
spec: TaskSpec {
id: TaskId::numbered(n),
title: format!("try {n}"),
task: "t".to_owned(),
kind: TaskKind::Captain,
owned_paths: Vec::new(),
read_only_paths: Vec::new(),
forbidden_paths: Vec::new(),
hidden_paths: Vec::new(),
depends_on: Vec::new(),
criteria: Vec::new(),
worker: None,
reviewers: Vec::new(),
limit: TaskLimit::default(),
predictions: None,
item: None,
},
state,
tries,
worker: None,
reassigned: 0,
worktree: None,
branch: None,
base: None,
commits: Vec::new(),
reports: Vec::new(),
checks,
reviews: Vec::new(),
fallbacks: 0,
debt: None,
}
}
fn state(records: Vec<TaskEntry>) -> RunState {
let mut state = RunState::default();
for record in records {
state.tasks.by_id.insert(record.spec.id.clone(), record);
}
state
}
#[test]
fn best_of_n_keeps_a_try_that_passed_the_checks() {
let state = state(vec![
record(1, TaskState::InReview, 1, vec![check("test", false)]),
record(
2,
TaskState::InReview,
2,
vec![check("test", false), check("test", true)],
),
record(
3,
TaskState::InReview,
1,
vec![check("test", true), check("lint", true)],
),
record(4, TaskState::Failed, 1, vec![check("test", true)]),
record(5, TaskState::InReview, 1, Vec::new()),
]);
let ids: Vec<TaskId> = (1..=5).map(TaskId::numbered).collect();
assert_eq!(pick_try(&state, &ids), Some(TaskId::numbered(3)));
assert_eq!(pick_try(&state, &ids[..2]), Some(TaskId::numbered(2)));
assert_eq!(pick_try(&state, &[ids[0].clone(), ids[3].clone()]), None);
assert_eq!(pick_try(&state, &[TaskId::numbered(9)]), None);
}
}