feat(missions): surface goal conditions and per-pass verdicts in the UI

Makes the completion evaluator usable and observable.

- GET /api/missions/{id}/phases/{phase_id}/evaluations returns every verdict
  for a phase, newest pass first, scoped like the summary endpoint.
- MissionPhase gains done_when / max_iterations / iteration, so the phase card
  can show what the phase is working toward and which pass it is on.
- PhaseStatus gains 'evaluating' (amber) -- the state between "runs finished"
  and "phase done" that only conditioned phases enter.
- New PhaseGoalStrip renders on the phase card, and renders NOTHING for phases
  without a condition so unconditioned missions look exactly as before. It
  polls only while the phase is running or being judged.
- Mission wizard step 2 gains the condition + a max-passes field.

Two deliberate emphases in the UI:

The evaluator's `reason` is the most prominent element, because it is both the
explanation of why a phase iterated and the literal text handed back to the
agents as guidance -- it is what tells an operator whether the condition is
written well.

The hint copy states the constraint that actually governs whether a condition
works: the judge cannot run commands, it only reads what the agents wrote, so
the condition has to be provable from their output. "cargo test reported 0
failures" works; "the code is well factored" does not. Getting this wrong is
the difference between a phase that converges and one that burns every pass.

An evaluator error is rendered distinctly from a negative verdict, so a judge
outage doesn't read as a judgement on the work.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-30 13:10:57 -07:00
co-authored by Claude Opus 5
parent f848248fac
commit fe57ce4ed1
7 changed files with 322 additions and 1 deletions
+11
View File
@@ -59,6 +59,13 @@ pub struct MissionPhase {
pub order_idx: i32,
pub status: String,
pub config: Value,
/// Completion condition. `None` = the phase completes as soon as its runs
/// finish, with no evaluation (the pre-conditions behaviour).
pub done_when: Option<String>,
/// Upper bound on passes; 1 means run once.
pub max_iterations: i32,
/// Which pass the phase is on, 0-based.
pub iteration: i32,
#[serde(with = "time::serde::rfc3339::option")]
pub started_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option")]
@@ -410,6 +417,7 @@ pub async fn phases_for(pool: &PgPool, mission_id: Uuid) -> Result<Vec<MissionPh
use sqlx::Row;
let rows = sqlx::query(
"SELECT id, mission_id, kind, order_idx, status, config,
done_when, max_iterations, iteration,
started_at, completed_at
FROM mission_phases WHERE mission_id = $1
ORDER BY order_idx ASC",
@@ -426,6 +434,9 @@ pub async fn phases_for(pool: &PgPool, mission_id: Uuid) -> Result<Vec<MissionPh
order_idx: r.get("order_idx"),
status: r.get("status"),
config: r.get("config"),
done_when: r.get("done_when"),
max_iterations: r.get("max_iterations"),
iteration: r.get("iteration"),
started_at: r.get("started_at"),
completed_at: r.get("completed_at"),
})