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:
co-authored by
Claude Opus 5
parent
f848248fac
commit
fe57ce4ed1
@@ -674,6 +674,51 @@ pub async fn retry_phase(
|
||||
/// card produced by `phase_summarizer` for a terminal-state phase.
|
||||
/// Returns 404 while the phase is still running / hasn't been
|
||||
/// summarized yet.
|
||||
/// `GET /api/missions/{id}/phases/{phase_id}/evaluations` — every completion
|
||||
/// verdict for a phase, newest first.
|
||||
///
|
||||
/// One row per pass. The `reason` is the operator-facing explanation of why a
|
||||
/// phase iterated (or stopped), and is the same text fed back to the agents as
|
||||
/// guidance for the following pass.
|
||||
pub async fn list_phase_evaluations(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path((id, phase_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<Vec<Value>>, ApiError> {
|
||||
// Scope check — same shape as get_phase_summary.
|
||||
let _ = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
use sqlx::Row;
|
||||
let rows = sqlx::query(
|
||||
"SELECT iteration, met, reason, model, error, created_at
|
||||
FROM mission_phase_evaluations
|
||||
WHERE mission_id = $1 AND phase_id = $2
|
||||
ORDER BY iteration DESC",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(phase_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
Ok(Json(
|
||||
rows.into_iter()
|
||||
.map(|r| {
|
||||
let created_at: time::OffsetDateTime = r.get("created_at");
|
||||
serde_json::json!({
|
||||
"iteration": r.get::<i32, _>("iteration"),
|
||||
"met": r.get::<bool, _>("met"),
|
||||
"reason": r.get::<String, _>("reason"),
|
||||
"model": r.get::<String, _>("model"),
|
||||
"error": r.get::<Option<String>, _>("error"),
|
||||
"created_at": created_at
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_phase_summary(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
|
||||
Reference in New Issue
Block a user