Skip to content

Commit 31a4531

Browse files
V48 Gate 3 (implementation-only): infer + render the ReadyToFinish verdict's decision reason(s) in /deposit Telemetry
The DIV loop's iterate-vs-finish verdict already streams whole as the cross-phase validation/readyToFinish artifact (finalApproval, recommendation, criticalChecks, finalBlockers, finalWarnings, scores, summary) — but nothing surfaced WHY a run iterated or finished. - Activity builder: capture one ReadyToFinishVerdictView per DIV iteration (stamped with the latched iteration) with the decision reason(s) INFERRED: failed critical checks first (named — 'critical checks failed: requirements met, documentation complete'), then the concrete finalBlockers; a rejection with no structured reasons falls back to its summary; an approval carries its summary instead of reasons. - /deposit Telemetry: a readiness-verdict block under the header — headline ('iter N verdict · iterate (abort) · quality 0.81 · confidence 0.28 · 5 warnings' / 'ready to finish'), the reason list for rejections, the summary for approvals, and a one-line history of prior iterations' verdicts. Amber while iterating, emerald when approved. Verified: uapi 155 suites / 607 tests green (verdict capture, reason inference order, and summary fallback pinned); tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 061048d commit 31a4531

3 files changed

Lines changed: 137 additions & 0 deletions

File tree

uapi/app/deposit/DepositPageClient.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,60 @@ export default function DepositPageClient() {
14631463
</span>
14641464
</div>
14651465
</div>
1466+
{synthesisActivity.readyToFinishVerdicts.length > 0 &&
1467+
(() => {
1468+
const verdicts = synthesisActivity.readyToFinishVerdicts;
1469+
const latest = verdicts[verdicts.length - 1];
1470+
const prior = verdicts.slice(0, -1);
1471+
const approved = latest.finalApproval === true;
1472+
return (
1473+
<div
1474+
data-testid="deposit-telemetry-readiness-verdict"
1475+
className={`mt-3 border px-3 py-2 text-xs leading-5 ${
1476+
approved
1477+
? "border-emerald-300/20 bg-emerald-300/5 text-emerald-100/90"
1478+
: "border-amber-300/20 bg-amber-300/5 text-amber-100/90"
1479+
}`}
1480+
>
1481+
<p className="font-mono text-[0.62rem] uppercase tracking-[0.16em]">
1482+
{`iter ${latest.iteration ?? "—"} verdict · `}
1483+
{approved
1484+
? "ready to finish"
1485+
: `iterate${latest.recommendation ? ` (${latest.recommendation})` : ""}`}
1486+
{typeof latest.qualityScore === "number" &&
1487+
` · quality ${latest.qualityScore.toFixed(2)}`}
1488+
{typeof latest.overallConfidence === "number" &&
1489+
` · confidence ${latest.overallConfidence.toFixed(2)}`}
1490+
{latest.warningsCount > 0 && ` · ${latest.warningsCount} warnings`}
1491+
</p>
1492+
{approved
1493+
? latest.summary && (
1494+
<p className="mt-1 max-w-4xl text-neutral-300">{latest.summary}</p>
1495+
)
1496+
: latest.reasons.length > 0 && (
1497+
<ul className="mt-1 max-w-4xl list-disc space-y-1 pl-4 text-neutral-300">
1498+
{latest.reasons.map((reason, index) => (
1499+
<li key={index}>{reason}</li>
1500+
))}
1501+
</ul>
1502+
)}
1503+
{prior.length > 0 && (
1504+
<p className="mt-2 font-mono text-[0.6rem] uppercase tracking-[0.14em] text-neutral-500">
1505+
{prior
1506+
.map(
1507+
(verdict) =>
1508+
`iter ${verdict.iteration ?? "—"}: ${
1509+
verdict.finalApproval === true
1510+
? "ready"
1511+
: `iterate (${verdict.recommendation ?? "not approved"}, ${verdict.reasons.length} reasons)`
1512+
}`,
1513+
)
1514+
.join(" · ")}
1515+
</p>
1516+
)}
1517+
</div>
1518+
);
1519+
})()}
14661520
<div className="mt-4 min-w-0">
14671521
<PipelineExecutionLog
14681522
output={synthesisActivity.output}

uapi/app/terminal/terminal-run-activity.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ export interface TerminalRunActivitySnapshot {
3434
* the finish phase begins. Drives the header's 'iter N' marker.
3535
*/
3636
currentIteration: number | null;
37+
/**
38+
* One ReadyToFinish verdict per DIV iteration (chronological), captured
39+
* from the cross-phase validation/readyToFinish artifact with its decision
40+
* reason(s) INFERRED — failed critical checks first, then the concrete
41+
* final blockers; an approval carries its summary instead. Drives the
42+
* Telemetry readiness-verdict rendering.
43+
*/
44+
readyToFinishVerdicts: ReadyToFinishVerdictView[];
3745
/**
3846
* The CURRENT active call-chain: the rolling Phase→Agent→Step→Failsafe→
3947
* Generation context after the last streamed event — drives live header
@@ -48,6 +56,55 @@ export interface TerminalRunActivitySnapshot {
4856
} | null;
4957
}
5058

59+
export interface ReadyToFinishVerdictView {
60+
/** DIV iteration the verdict gated (1-based; null when never latched). */
61+
iteration: number | null;
62+
finalApproval: boolean | null;
63+
recommendation: string | null;
64+
qualityScore: number | null;
65+
overallConfidence: number | null;
66+
warningsCount: number;
67+
/** Inferred decision reason(s): failed critical checks, then blockers. */
68+
reasons: string[];
69+
summary: string | null;
70+
}
71+
72+
const CRITICAL_CHECK_LABELS: Record<string, string> = {
73+
requirementsMet: 'requirements met',
74+
testsPass: 'tests pass',
75+
noSecurityIssues: 'no security issues',
76+
documentationComplete: 'documentation complete',
77+
performanceAcceptable: 'performance acceptable',
78+
};
79+
80+
/**
81+
* Infer the decision reason(s) behind a ReadyToFinish verdict. A rejection's
82+
* reasons are its failed critical checks (named) followed by its concrete
83+
* finalBlockers; an approval needs no reasons beyond its summary. Falls back
84+
* to the summary when a rejection carries no structured reasons.
85+
*/
86+
export function inferReadyToFinishReasons(verdict: any): string[] {
87+
if (!verdict || typeof verdict !== 'object') return [];
88+
if (verdict.finalApproval === true) return [];
89+
const reasons: string[] = [];
90+
const checks =
91+
verdict.criticalChecks && typeof verdict.criticalChecks === 'object' ? verdict.criticalChecks : {};
92+
const failedChecks = Object.entries(checks)
93+
.filter(([, passed]) => passed === false)
94+
.map(([check]) => CRITICAL_CHECK_LABELS[check] || check);
95+
if (failedChecks.length) reasons.push(`critical checks failed: ${failedChecks.join(', ')}`);
96+
const blockers = Array.isArray(verdict.finalBlockers)
97+
? verdict.finalBlockers.filter(
98+
(blocker: unknown): blocker is string => typeof blocker === 'string' && blocker.trim().length > 0,
99+
)
100+
: [];
101+
reasons.push(...blockers);
102+
if (!reasons.length && typeof verdict.summary === 'string' && verdict.summary.trim()) {
103+
reasons.push(verdict.summary.trim());
104+
}
105+
return reasons;
106+
}
107+
51108
export type MockRunActivitySnapshot = {
52109
output: string;
53110
outputDetails: Record<string, any>;
@@ -225,6 +282,7 @@ export function buildTerminalRunActivityFromEvents(
225282
// become rows; every other event just advances the rolling context.
226283
const rollingContext: ExecContext = {};
227284
const toolByNode = new Map<string, { name?: string; input?: unknown }>();
285+
const readyToFinishVerdicts: ReadyToFinishVerdictView[] = [];
228286
let rowSeq = 0;
229287
// Pipeline mode latched from the 'synthesize-asset-packs'/'mode' store —
230288
// stamped onto subsequent rows (the processing indicator's 'While
@@ -305,6 +363,29 @@ export function buildTerminalRunActivityFromEvents(
305363
const candidate = payload.data.trim().toLowerCase();
306364
if (candidate === 'deposit' || candidate === 'read') pipelineMode = candidate;
307365
}
366+
367+
// Capture each iteration's ReadyToFinish verdict (the cross-phase
368+
// validation/readyToFinish artifact) with its inferred decision reasons.
369+
if (
370+
ns === 'validation' &&
371+
key === 'readyToFinish' &&
372+
payload?.data &&
373+
typeof payload.data === 'object' &&
374+
!(payload.data as Record<string, unknown>).contentWithheld
375+
) {
376+
const verdict = payload.data as Record<string, any>;
377+
readyToFinishVerdicts.push({
378+
iteration: rollingContext.iteration ?? null,
379+
finalApproval: typeof verdict.finalApproval === 'boolean' ? verdict.finalApproval : null,
380+
recommendation: typeof verdict.recommendation === 'string' ? verdict.recommendation : null,
381+
qualityScore: typeof verdict.qualityScore === 'number' ? verdict.qualityScore : null,
382+
overallConfidence:
383+
typeof verdict.overallConfidence === 'number' ? verdict.overallConfidence : null,
384+
warningsCount: Array.isArray(verdict.finalWarnings) ? verdict.finalWarnings.length : 0,
385+
reasons: inferReadyToFinishReasons(verdict),
386+
summary: typeof verdict.summary === 'string' ? verdict.summary : null,
387+
});
388+
}
308389
if ((ns === 'tool' || ns === 'tools') && nodeId) {
309390
if (key === 'name' && typeof payload?.data === 'string') {
310391
const acc = toolByNode.get(nodeId) || {};
@@ -401,6 +482,7 @@ export function buildTerminalRunActivityFromEvents(
401482
}
402483
: null,
403484
currentIteration: rollingContext.iteration ?? null,
485+
readyToFinishVerdicts,
404486
};
405487
}
406488

@@ -433,5 +515,6 @@ export function buildTerminalRunActivityFromMock(
433515
mode: null,
434516
latestContext: null,
435517
currentIteration: null,
518+
readyToFinishVerdicts: [],
436519
};
437520
}
3.49 KB
Binary file not shown.

0 commit comments

Comments
 (0)