"use client"; import { useCallback, useEffect, useState } from "react"; import { Target } from "lucide-react"; import { getPhaseEvaluations, type CheckOutcome, type MissionPhase, type PhaseEvaluation, } from "@/lib/api/missions"; /** * Whether a verdict was verified, and how honestly we can say so. * * Three states, not two. A judge that attempted ten commands and executed none * — because the sandbox was unreachable — is not the same as a judge that ran * ten, and neither is the same as a phase with no repo to check. Collapsing * the middle case into "verified" is what the first version of this badge did. */ function VerificationNote({ checks }: { checks?: CheckOutcome[] }) { const all = checks ?? []; const ran = all.filter((c) => c.ran); const failed = ran.filter((c) => c.exit_code !== 0); if (ran.length > 0) { const detail = ran.map((c) => `${c.argv.join(" ")} → exit ${c.exit_code}`).join("\n"); return ( {` — verified by ${ran.length} check${ran.length === 1 ? "" : "s"}`} {failed.length ? ` (${failed.length} non-zero)` : ""} ); } if (all.length > 0) { // Attempted but nothing executed: a broken sandbox, or every command // refused. Say so — this is the case that used to read as "verified". return ( `${c.argv.join(" ")} → ${c.refused ? "refused" : "could not run"}`).join("\n")} > {` — could not verify (${all.length} attempted, 0 ran)`} ); } return ( {" — from agent claims only"} ); } /** * 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 * explains why the phase iterated or stopped, so it is what an operator needs * to decide whether the condition is written well. It is *not* what the agents * were told: they get a sanitized `guidance` that withholds the acceptance * text, so a pass cannot be satisfied by pasting the verdict back. * * Whether the judge verified anything is shown alongside the verdict — see * `VerificationNote`. An operator should never have to guess whether a verdict * rests on executed commands or on the agents' own account of themselves. */ export function PhaseGoalStrip({ missionId, phase, }: { missionId: string; phase: MissionPhase; }) { const [evals, setEvals] = useState([]); 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 (
Done when pass {pass} / {phase.max_iterations}

{phase.done_when}

{judging && ( Judging this pass… )} {latest && (
{latest.met ? "met" : "not met"} {latest.reason} {latest.error && ( // Distinguishes "judged incomplete" from "could not judge" — // an evaluator outage should not read as a verdict on the work. (evaluator error) )} {!latest.error && }
)} {evals.length > 1 && ( <> {expanded && (
    {evals.map((e) => (
  1. pass {e.iteration + 1} {e.met ? "met" : "not met"} {" "} — {e.reason}
  2. ))}
)} )}
); }