"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 && }