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
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Target } from "lucide-react";
|
||||
|
||||
import {
|
||||
getPhaseEvaluations,
|
||||
type MissionPhase,
|
||||
type PhaseEvaluation,
|
||||
} from "@/lib/api/missions";
|
||||
|
||||
/**
|
||||
* The completion condition on a phase, plus how the last pass was judged.
|
||||
*
|
||||
* Renders nothing for phases without a `done_when` — most missions don't have
|
||||
* one, and an empty row per phase would be noise.
|
||||
*
|
||||
* The evaluator's `reason` is deliberately the most prominent thing here: it
|
||||
* is both the explanation of why the phase iterated (or stopped) and the exact
|
||||
* guidance handed to the agents for the next pass, so it is what an operator
|
||||
* needs to decide whether the condition is written well.
|
||||
*/
|
||||
export function PhaseGoalStrip({
|
||||
missionId,
|
||||
phase,
|
||||
}: {
|
||||
missionId: string;
|
||||
phase: MissionPhase;
|
||||
}) {
|
||||
const [evals, setEvals] = useState<PhaseEvaluation[]>([]);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setEvals(await getPhaseEvaluations(missionId, phase.id));
|
||||
} catch {
|
||||
// A phase that has never been judged has no rows; not an error state.
|
||||
}
|
||||
}, [missionId, phase.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!phase.done_when) return;
|
||||
void load();
|
||||
// Poll only while there is something to wait for.
|
||||
if (phase.status !== "running" && phase.status !== "evaluating") return;
|
||||
const t = setInterval(() => void load(), 5000);
|
||||
return () => clearInterval(t);
|
||||
}, [load, phase.done_when, phase.status]);
|
||||
|
||||
if (!phase.done_when) return null;
|
||||
|
||||
const latest = evals[0];
|
||||
const pass = phase.iteration + 1;
|
||||
const judging = phase.status === "evaluating";
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: "8px 10px",
|
||||
borderRadius: 10,
|
||||
background: "#0d0d10",
|
||||
border: "1px solid #1c1c22",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<Target aria-hidden size={12} color="#e8b465" />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: "uppercase",
|
||||
color: "#6a6a72",
|
||||
}}
|
||||
>
|
||||
Done when
|
||||
</span>
|
||||
<span style={{ marginLeft: "auto", fontSize: 10, color: "#6a6a72" }}>
|
||||
pass {pass} / {phase.max_iterations}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p style={{ margin: 0, fontSize: 12, color: "#c8c8d0", lineHeight: 1.45 }}>
|
||||
{phase.done_when}
|
||||
</p>
|
||||
|
||||
{judging && (
|
||||
<span style={{ fontSize: 11, color: "#e8b465" }}>
|
||||
Judging this pass…
|
||||
</span>
|
||||
)}
|
||||
|
||||
{latest && (
|
||||
<div style={{ display: "flex", gap: 6, alignItems: "flex-start" }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
color: latest.met ? "#5fd08a" : "#e8b465",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{latest.met ? "met" : "not met"}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: "#8a8a92", lineHeight: 1.45 }}>
|
||||
{latest.reason}
|
||||
{latest.error && (
|
||||
// Distinguishes "judged incomplete" from "could not judge" —
|
||||
// an evaluator outage should not read as a verdict on the work.
|
||||
<em style={{ color: "#ff8a7a" }}> (evaluator error)</em>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{evals.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
cursor: "pointer",
|
||||
fontSize: 10,
|
||||
color: "#6a6a72",
|
||||
}}
|
||||
>
|
||||
{expanded ? "hide" : `show all ${evals.length} passes`}
|
||||
</button>
|
||||
{expanded && (
|
||||
<ol
|
||||
style={{
|
||||
margin: 0,
|
||||
paddingLeft: 16,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{evals.map((e) => (
|
||||
<li
|
||||
key={e.iteration}
|
||||
style={{ fontSize: 11, color: "#8a8a92", lineHeight: 1.4 }}
|
||||
>
|
||||
<span
|
||||
style={{ color: e.met ? "#5fd08a" : "#e8b465", fontWeight: 600 }}
|
||||
>
|
||||
pass {e.iteration + 1} {e.met ? "met" : "not met"}
|
||||
</span>{" "}
|
||||
— {e.reason}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user