use crate::config;
use crate::error::{EngineError, Result};
use crate::events::{Event, EventKind};
use crate::types::{
BackendKind, MissionConfig, MissionState, Plan, ReviewerIndependence, Role, RunResult,
};
#[derive(Debug, PartialEq, Eq)]
pub struct CompletionBlocked {
pub milestone_id: String,
pub detail: String,
}
pub fn check_completion(
state: &MissionState,
events: &[Event],
milestone_id: Option<&str>,
) -> std::result::Result<(), CompletionBlocked> {
let Some(policy) = state
.mission
.reviewer_independence
.filter(|p| !p.is_empty())
else {
return Ok(());
};
for milestone in state
.mission
.milestones
.iter()
.filter(|m| milestone_id.is_none_or(|id| id == m.id))
{
let blocked = |detail| CompletionBlocked {
milestone_id: milestone.id.clone(),
detail,
};
let round = events
.iter()
.rposition(|event| {
matches!(&event.kind,
EventKind::MilestoneValidating { milestone_id } if milestone_id == &milestone.id
)
})
.ok_or_else(|| blocked("no recorded validation round for this milestone".into()))?;
let belongs = |id: &str| milestone.features.iter().any(|feature| feature.id == id);
let round_events = &events[round + 1..];
if round_events
.iter()
.any(|event| matches!(event.kind, EventKind::PlanRevised { .. }))
{
let reviewed = crate::reducer::fold(&events[..=round])
.map_err(|error| blocked(format!("cannot reconstruct reviewed plan: {error}")))?;
let context = |state: &MissionState| {
serde_json::to_value((
&state.mission.goal,
&state.mission.validation_contract,
&state.mission.command_grants,
&state.mission.touch_set,
state
.mission
.milestones
.iter()
.find(|m| m.id == milestone.id)
.map(|m| {
(
&m.title,
m.features
.iter()
.map(|f| (&f.title, &f.spec, &f.validation_criteria))
.collect::<Vec<_>>(),
)
}),
))
.map_err(|error| blocked(format!("cannot compare reviewed plan: {error}")))
};
if context(&reviewed)? != context(state)? {
return Err(blocked(
"plan revision changed the reviewed milestone context".into(),
));
}
}
if round_events.iter().any(|event| match &event.kind {
EventKind::FeatureStarted { feature_id }
| EventKind::FeatureProgress { feature_id, .. }
| EventKind::FeatureCompleted { feature_id, .. }
| EventKind::FeatureFailed { feature_id, .. } => belongs(feature_id),
EventKind::WorkerSpawned {
role: Role::Worker,
feature_id,
..
} => feature_id.as_deref().is_none_or(belongs),
EventKind::FixFeatureCreated { milestone_id, .. }
| EventKind::MilestoneStarted { milestone_id, .. }
| EventKind::ValidatorTamper { milestone_id, .. } => milestone_id == &milestone.id,
_ => false,
}) {
return Err(blocked(
"review evidence is stale after work, plan or checkout-integrity changes".into(),
));
}
for role in [Role::ValidatorScrutiny, Role::ValidatorFunctional] {
if !policy.requires(role) {
continue;
}
let run_id = round_events
.iter()
.rev()
.find_map(|event| match &event.kind {
EventKind::WorkerSpawned {
run_id,
role: run_role,
milestone_id: Some(id),
..
} if *run_role == role && id == &milestone.id => Some(run_id),
_ => None,
});
let run = run_id
.and_then(|id| state.runs.get(id))
.filter(|run| run.result == Some(RunResult::Pass))
.ok_or_else(|| {
blocked(format!(
"{role:?} has no successful review of the latest milestone work"
))
})?;
let backend = run.backend.ok_or_else(|| {
blocked(format!(
"{role:?} review has no recorded backend provenance"
))
})?;
check_dispatch(state, role, backend, &run.model).map_err(blocked)?;
}
}
Ok(())
}
pub fn model_family(backend: BackendKind, model: &str) -> Option<&'static str> {
let model = model.trim().to_ascii_lowercase();
let claude = ["opus", "sonnet", "haiku", "fable"]
.iter()
.any(|name| model == *name || model.starts_with(&format!("claude-{name}-")));
let gpt = model == "codex" || model.starts_with("gpt-5");
match backend {
BackendKind::Claude | BackendKind::Droid | BackendKind::Cursor if claude => {
Some("anthropic-claude")
}
BackendKind::Codex | BackendKind::Cursor if gpt => Some("openai-gpt"),
BackendKind::Droid if model.starts_with("accounts/fireworks/models/glm-") => {
Some("zai-glm")
}
BackendKind::Kimi
if matches!(
model.as_str(),
"kimi-code/k3"
| "kimi-code/kimi-for-coding"
| "kimi-code/kimi-for-coding-highspeed"
) =>
{
Some("moonshot-kimi")
}
_ => None,
}
}
pub fn configured_policy(cfg: &MissionConfig) -> Option<ReviewerIndependence> {
(!cfg.reviewer_independence.is_empty()).then_some(cfg.reviewer_independence)
}
pub fn pin_plan(plan: &mut Plan, policy: Option<ReviewerIndependence>) -> Result<()> {
if plan.reviewer_independence.is_some() && plan.reviewer_independence != policy {
return Err(EngineError::Config(
"plan reviewerIndependence differs from the operator policy".into(),
));
}
plan.reviewer_independence = policy;
Ok(())
}
pub fn validate_config(cfg: &MissionConfig) -> Result<()> {
let policy = cfg.reviewer_independence;
if policy.is_empty() {
return Ok(());
}
for (role, skipped) in [
(Role::ValidatorScrutiny, cfg.skip_scrutiny),
(Role::ValidatorFunctional, cfg.skip_functional),
] {
if !policy.requires(role) {
continue;
}
if skipped {
return Err(EngineError::Config(format!(
"reviewerIndependence requires {role:?}; its skip flag must be false"
)));
}
let kind = cfg.backend_kind(role);
let model = config::effective_model(role, kind, &cfg.role(role).model);
let reviewer = model_family(kind, &model).ok_or_else(|| {
EngineError::Config(format!(
"reviewerIndependence: {role:?} model family is unknown"
))
})?;
let workers = if cfg.worker_candidates.is_empty() {
vec![(cfg.backend_kind(Role::Worker), cfg.worker.model.as_str())]
} else {
cfg.worker_candidates
.iter()
.map(|candidate| {
config::parse_backend(Some(&candidate.backend))
.map(|kind| (kind, candidate.model.as_str()))
.map_err(|_| EngineError::Config("unknown worker candidate backend".into()))
})
.collect::<Result<Vec<_>>>()?
};
for (kind, model) in workers {
let effective = config::effective_model(Role::Worker, kind, model);
let worker = model_family(kind, &effective).ok_or_else(|| {
EngineError::Config("reviewerIndependence: worker model family is unknown".into())
})?;
if worker == reviewer {
return Err(EngineError::Config(format!(
"reviewerIndependence: {role:?} and worker share family {worker}"
)));
}
}
}
Ok(())
}
pub fn check_dispatch(
state: &MissionState,
role: Role,
backend: BackendKind,
model: &str,
) -> std::result::Result<Option<String>, String> {
if !state
.mission
.reviewer_independence
.is_some_and(|p| p.requires(role))
{
return Ok(None);
}
let reviewer = model_family(backend, model)
.ok_or_else(|| format!("{role:?} resolved model family is unknown"))?;
let mut workers = 0;
for run in state.runs.values().filter(|run| run.role == Role::Worker) {
workers += 1;
let family = run
.backend
.and_then(|kind| model_family(kind, &run.model))
.ok_or_else(|| format!("worker run {} has unknown model family", run.id))?;
if reviewer == family {
return Err(format!(
"{role:?} resolved to {reviewer}, the same family as worker run {}",
run.id
));
}
}
if workers == 0 {
return Err("no recorded worker provenance is available".into());
}
Ok(Some(format!(
"{role:?}: {} model {model:?} ({reviewer}) differs from all {} recorded worker attempts",
backend.as_str(),
workers
)))
}