Skip to content

Commit 51f8aee

Browse files
V48 Gate 3 (impl-only): Visible Setup/Validation gates
Stamp finalApproval on deposit/read readyToFinish so DIV admits Finish and UI stops painting ITERATE (FINISH). Emit formal phase-decision rows when Setup host-env clone or Validation deterministic gate skips PTRR.
1 parent b75d8b1 commit 51f8aee

9 files changed

Lines changed: 335 additions & 27 deletions

File tree

apps/uapi/components/bitcode/pipeline/models/pipeline-run-activity.ts

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,27 @@ function iterationForPhase(
255255
: null;
256256
}
257257

258-
type FormalLogLineKind = 'llm' | 'tool';
258+
type FormalLogLineKind = 'llm' | 'tool' | 'decision';
259+
260+
function isPhaseDecisionPayload(payload: any, ns: string, key: string): boolean {
261+
const data = payload?.data;
262+
if (data && typeof data === 'object' && !Array.isArray(data)) {
263+
if ((data as { formalPhaseDecision?: unknown }).formalPhaseDecision === true) {
264+
return true;
265+
}
266+
if (
267+
typeof (data as { schema?: unknown }).schema === 'string' &&
268+
String((data as { schema: string }).schema).includes('phase-decision')
269+
) {
270+
return true;
271+
}
272+
}
273+
// Product gate + short-circuit decision stores (Setup/Validation/Finish).
274+
if (key === 'phaseDecision') return true;
275+
if (ns === 'validation' && key === 'readyToFinish') return true;
276+
if (ns === 'setup' && (key === 'phaseDecision' || key === 'cloneDecision')) return true;
277+
return false;
278+
}
259279

260280
function classifyFormalLogLine(payload: any): FormalLogLineKind | null {
261281
const type = String(payload?.type || '');
@@ -264,12 +284,13 @@ function classifyFormalLogLine(payload: any): FormalLogLineKind | null {
264284
const ns = String(payload?.namespace || '');
265285
const key = String(payload?.key || '');
266286

267-
// The rich telemetry renders ONLY the ultimate LLM-call layer and Tool uses —
268-
// each with its complete hierarchy (LLM: Phase/Agent/Step/Failsafe/Thinkings;
269-
// Tool: Phase/Agent/Step + tool). Everything else (informational status, phase
270-
// banners, completion/error notices) advances the rolling context but never
271-
// becomes a row; run completion is surfaced by the processing indicator and
272-
// errors by the log's error banner, not by accordion rows.
287+
// Formal rows:
288+
// • LLM call — Thinkings substep output (full hierarchy)
289+
// • Tool use — tool result/error
290+
// • Decision — phase gate / short-circuit agent completion when no LLM/tool
291+
// fired (Setup host-env clone, Validation deterministic ready-to-finish).
292+
// Product law: Setup and Validation must remain visible even when conditionals
293+
// skip PTRR agents.
273294

274295
// Formal LLM call: the Thinkings substep output is canonical (full hierarchy).
275296
if (type === 'generation' || streamType === 'generation') return 'llm';
@@ -279,6 +300,9 @@ function classifyFormalLogLine(payload: any): FormalLogLineKind | null {
279300
if (type === 'tool-use' || streamType === 'tool-use') return 'tool';
280301
if ((ns === 'tool' || ns === 'tools') && (key === 'result' || key === 'error')) return 'tool';
281302

303+
// Formal phase decision (deterministic short-circuit / gate).
304+
if (isPhaseDecisionPayload(payload, ns, key)) return 'decision';
305+
282306
return null;
283307
}
284308

@@ -458,15 +482,33 @@ export function buildPipelineRunActivityFromEvents(
458482
!(payload.data as Record<string, unknown>).contentWithheld
459483
) {
460484
const verdict = payload.data as Record<string, any>;
485+
const recommendation =
486+
typeof verdict.recommendation === 'string' ? verdict.recommendation : null;
487+
// Normalize deposit admit: recommendation finish/complete ⇒ finalApproval.
488+
const finalApproval =
489+
typeof verdict.finalApproval === 'boolean'
490+
? verdict.finalApproval
491+
: verdict.readyToFinish === true ||
492+
verdict.ready === true ||
493+
verdict.passed === true ||
494+
String(recommendation || '').toLowerCase() === 'finish' ||
495+
String(recommendation || '').toLowerCase() === 'complete'
496+
? true
497+
: typeof verdict.finalApproval === 'boolean'
498+
? verdict.finalApproval
499+
: null;
461500
readyToFinishVerdicts.push({
462501
iteration: rollingContext.iteration ?? null,
463-
finalApproval: typeof verdict.finalApproval === 'boolean' ? verdict.finalApproval : null,
464-
recommendation: typeof verdict.recommendation === 'string' ? verdict.recommendation : null,
502+
finalApproval,
503+
recommendation,
465504
qualityScore: typeof verdict.qualityScore === 'number' ? verdict.qualityScore : null,
466505
overallConfidence:
467506
typeof verdict.overallConfidence === 'number' ? verdict.overallConfidence : null,
468507
warningsCount: Array.isArray(verdict.finalWarnings) ? verdict.finalWarnings.length : 0,
469-
reasons: inferReadyToFinishReasons(verdict),
508+
reasons: inferReadyToFinishReasons({
509+
...verdict,
510+
finalApproval: finalApproval === true,
511+
}),
470512
summary: typeof verdict.summary === 'string' ? verdict.summary : null,
471513
});
472514
}
@@ -538,6 +580,51 @@ export function buildPipelineRunActivityFromEvents(
538580
continue;
539581
}
540582

583+
if (kind === 'decision') {
584+
// Deterministic Setup/Validation/Finish decisions (no LLM/tool). Surface
585+
// as a generation-shaped row so existing pill rendering applies.
586+
const data =
587+
payload?.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
588+
? (payload.data as Record<string, unknown>)
589+
: {};
590+
const own = readEventExecutionState(payload);
591+
const phase = String(
592+
data.phase || own.phase || rollingContext.phase || ns || 'setup',
593+
);
594+
const merged: ExecContext = {
595+
phase,
596+
agent: String(
597+
data.agent || own.agent || rollingContext.agent || key || 'phase-decision',
598+
),
599+
step: String(data.step || own.step || rollingContext.step || 'decide'),
600+
failsafe: String(
601+
data.failsafe || own.failsafe || 'deterministic-gate',
602+
),
603+
generation: String(data.generation || own.generation || 'structure'),
604+
iteration: iterationForPhase(phase, rollingContext.iteration),
605+
};
606+
const text = String(
607+
data.summary ||
608+
data.message ||
609+
payload?.message ||
610+
payload?.status?.message ||
611+
'Phase decision recorded.',
612+
);
613+
// Advance rolling hierarchy so later rows inherit Validation/Setup.
614+
rollingContext.phase = merged.phase;
615+
rollingContext.agent = merged.agent;
616+
rollingContext.step = merged.step;
617+
pushRow(
618+
text,
619+
stampExecutionState(
620+
merged,
621+
{ ...payload, type: 'generation', message: text },
622+
{ phaseDecision: true },
623+
),
624+
);
625+
continue;
626+
}
627+
541628
if (kind === 'tool') {
542629
const acc = toolByNode.get(nodeId) || {};
543630
const toolName = resolvedToolName || (key === 'error' ? 'tool (failed)' : 'tool');

apps/uapi/components/deposits/DepositSynthesisTelemetry/DepositSynthesisTelemetry.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,14 @@ export function DepositSynthesisTelemetry({
173173
const verdicts = synthesisActivity.readyToFinishVerdicts;
174174
const latest = verdicts[verdicts.length - 1];
175175
const prior = verdicts.slice(0, -1);
176-
const approved = latest.finalApproval === true;
176+
// Deposit gate may stamp recommendation:'finish' without
177+
// finalApproval on older runs — treat both as admit.
178+
const approved =
179+
latest.finalApproval === true ||
180+
String(latest.recommendation || "").toLowerCase() ===
181+
"finish" ||
182+
String(latest.recommendation || "").toLowerCase() ===
183+
"complete";
177184
return (
178185
<div
179186
data-testid="deposit-telemetry-readiness-verdict"

apps/uapi/tests/pipelineExecutionLogTelemetryUx.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,58 @@ describe('buildPipelineRunActivityFromEvents — mode latch + latest context', (
262262
expect(snapshot.latestContext).toBeNull();
263263
});
264264

265+
it('surfaces Setup/Validation phase decisions as formal rows (no LLM/tool)', () => {
266+
const phaseEvents = [
267+
{
268+
id: 'setup-decision',
269+
event: {
270+
type: 'status',
271+
namespace: 'setup',
272+
key: 'phaseDecision',
273+
data: {
274+
formalPhaseDecision: true,
275+
phase: 'setup',
276+
agent: 'clone-vcs-repository',
277+
step: 'decide',
278+
failsafe: 'host-env-clone',
279+
generation: 'structure',
280+
summary:
281+
'Setup clone complete (setup-in-box-branch-shallow): working tree ready at revision for this run.',
282+
message:
283+
'Setup clone complete (setup-in-box-branch-shallow): working tree ready at revision for this run.',
284+
},
285+
},
286+
created_at: '2026-07-01T00:00:01.000Z',
287+
},
288+
{
289+
id: 'validation-ready',
290+
event: {
291+
type: 'status',
292+
namespace: 'validation',
293+
key: 'readyToFinish',
294+
data: {
295+
formalPhaseDecision: true,
296+
finalApproval: true,
297+
recommendation: 'finish',
298+
summary: 'Deposit synthesis ready to finish.',
299+
message: 'Deposit synthesis ready to finish.',
300+
phase: 'validation',
301+
agent: 'ready-to-finish-asset-packs-synthesis-deposit-pipeline',
302+
step: 'decide',
303+
},
304+
},
305+
created_at: '2026-07-01T00:00:10.000Z',
306+
},
307+
];
308+
const snapshot = buildPipelineRunActivityFromEvents(phaseEvents, null, [], null);
309+
const texts = Object.keys(snapshot.outputDetails);
310+
expect(texts.some((t) => /Setup clone complete/i.test(t))).toBe(true);
311+
expect(texts.some((t) => /ready to finish/i.test(t))).toBe(true);
312+
expect(snapshot.readyToFinishVerdicts).toHaveLength(1);
313+
expect(snapshot.readyToFinishVerdicts[0].finalApproval).toBe(true);
314+
expect(snapshot.readyToFinishVerdicts[0].recommendation).toBe('finish');
315+
});
316+
265317
it('de-dupes formal rows that arrive twice (sandbox legacy + host bridge)', () => {
266318
const duped = [
267319
...events,

packages/asset-packs-pipelines/syntheses/deposit/src/__tests__/deposit-ready-to-finish-agent.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,17 @@ describe('deposit-ready-to-finish-agent', () => {
115115
const out = await runReady({}, exec);
116116
expect(Array.isArray(out.options?.[0]?.absolutes)).toBe(true);
117117
expect(out.options[0].absolutes.length).toBeGreaterThan(0);
118-
expect(exec.get('validation', 'readyToFinish')).toBeTruthy();
118+
expect(out.readyToFinish).toBe(true);
119+
expect(out.finalApproval).toBe(true);
120+
expect(exec.get('validation', 'readyToFinish')).toMatchObject({
121+
finalApproval: true,
122+
recommendation: 'finish',
123+
formalPhaseDecision: true,
124+
});
125+
expect(exec.get('validation', 'phaseDecision')).toMatchObject({
126+
phase: 'validation',
127+
formalPhaseDecision: true,
128+
});
119129
});
120130

121131
it('flags obfuscation path violations', async () => {

packages/asset-packs-pipelines/syntheses/deposit/src/agents/finish/deposit-finish-synthesize-run-agent.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ export default async function runDepositFinishSynthesizeRunAgent(input: any, exe
5656
},
5757
selectable: true,
5858
})),
59-
readyToPresent: Boolean(ready?.recommendation === 'finish' || ready?.readyToFinish !== false),
59+
readyToPresent: Boolean(
60+
ready?.finalApproval === true ||
61+
ready?.readyToFinish === true ||
62+
ready?.ready === true ||
63+
ready?.recommendation === 'finish' ||
64+
ready?.recommendation === 'complete',
65+
),
6066
validationSummary: ready?.summary ?? null,
6167
};
6268

@@ -101,9 +107,25 @@ export default async function runDepositFinishSynthesizeRunAgent(input: any, exe
101107

102108
storeCrossPhaseArtifact(execution, 'finish', 'completion', completion);
103109
storeCrossPhaseArtifact(execution, 'finish', 'selectionEnvelope', selectionEnvelope);
110+
const finishMessage = `Synthesize deposit AssetPacks finished with ${selectionEnvelope.options.length} option(s) for selection.`;
104111
storeCrossPhaseArtifact(execution, 'finish', 'summary', {
105112
optionCount: selectionEnvelope.options.length,
106-
message: `Synthesize deposit AssetPacks finished with ${selectionEnvelope.options.length} option(s) for selection.`,
113+
message: finishMessage,
114+
});
115+
// Formal Finish phase decision so the log always shows Finish closed even when
116+
// Finish agents are mostly deterministic packaging (no long LLM trail).
117+
storeCrossPhaseArtifact(execution, 'finish', 'phaseDecision', {
118+
schema: 'bitcode.pipeline.phase-decision',
119+
formalPhaseDecision: true,
120+
phase: 'finish',
121+
agent: 'finish-synthesize-asset-packs-for-deposit-run',
122+
step: 'decide',
123+
failsafe: 'selection-envelope',
124+
generation: 'structure',
125+
summary: finishMessage,
126+
message: finishMessage,
127+
optionCount: selectionEnvelope.options.length,
128+
readyToPresent: selectionEnvelope.readyToPresent,
107129
});
108130

109131
return {

packages/asset-packs-pipelines/syntheses/deposit/src/agents/validation/deposit-ready-to-finish-agent.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,21 +377,51 @@ export default async function runDepositReadyToFinishAgent(input: any, execution
377377
: merged.recommendation
378378
: merged.recommendation;
379379

380+
const readyToFinish =
381+
recommendation === 'complete' && hardIssues.length === 0;
380382
const result = {
381383
...merged,
382384
issues: structureReady ? hardIssues : filteredMergedIssues,
383385
recommendation,
384-
readyToFinish: recommendation === 'complete' && hardIssues.length === 0,
386+
readyToFinish,
387+
// SDIVF DIV gate + deposit telemetry require finalApproval (not only
388+
// recommendation:'finish'). Without this, UI paints ITERATE (FINISH) and
389+
// the base loop never sees validation:readyToFinish.finalApproval.
390+
finalApproval: readyToFinish,
391+
ready: readyToFinish,
392+
passed: readyToFinish,
385393
};
386394

395+
const readinessSummary = readyToFinish
396+
? qualitativePtrrRan
397+
? 'Deposit synthesis ready to finish.'
398+
: 'Deposit synthesis ready to finish (Validation deterministic admit: measured options + required absolutes; qualitative PTRR skipped).'
399+
: `Deposit synthesis not ready: ${result.issues.slice(0, 5).join('; ')}`;
400+
387401
storeCrossPhaseArtifact(execution, 'validation/implementation', 'issues', result.issues);
388402
storeCrossPhaseArtifact(execution, 'validation', 'depositQuality', result);
403+
// Cross-phase gate artifact — dual-written to stream; UI treats as formal
404+
// phase decision when no qualitative PTRR rows exist (F19 extension).
389405
storeCrossPhaseArtifact(execution, 'validation', 'readyToFinish', {
390-
recommendation: result.readyToFinish ? 'finish' : 'revise',
391-
summary: result.readyToFinish
392-
? 'Deposit synthesis ready to finish.'
393-
: `Deposit synthesis not ready: ${result.issues.slice(0, 5).join('; ')}`,
406+
schema: 'bitcode.deposit.validation.ready-to-finish',
407+
recommendation: readyToFinish ? 'finish' : 'revise',
408+
finalApproval: readyToFinish,
409+
ready: readyToFinish,
410+
passed: readyToFinish,
411+
readyToFinish,
412+
summary: readinessSummary,
413+
message: readinessSummary,
394414
issues: result.issues,
415+
qualityScore: result.qualityScore ?? agentOutput?.qualityScore ?? null,
416+
// Hierarchy for formal telemetry row when PTRR was skipped.
417+
phase: 'validation',
418+
agent: 'ready-to-finish-asset-packs-synthesis-deposit-pipeline',
419+
step: qualitativePtrrRan ? 'try' : 'decide',
420+
failsafe: qualitativePtrrRan ? 'prepare' : 'deterministic-gate',
421+
generation: qualitativePtrrRan ? 'reason' : 'structure',
422+
formalPhaseDecision: true,
423+
qualitativePtrrRan,
424+
qualitativePtrrSkipped: !qualitativePtrrRan && structureReady,
395425
});
396426
// Always record whether qualitative PTRR ran or was short-circuited (every run).
397427
storeCrossPhaseArtifact(execution, 'validation', 'gateDecision', {
@@ -411,8 +441,24 @@ export default async function runDepositReadyToFinishAgent(input: any, execution
411441
qualitativePtrrError,
412442
recommendation: result.recommendation,
413443
readyToFinish: result.readyToFinish,
444+
finalApproval: readyToFinish,
414445
qualityScore: result.qualityScore ?? agentOutput?.qualityScore ?? null,
415446
});
447+
// Explicit phase-decision store so Setup/Validation short-circuits always
448+
// produce a formal telemetry row even when no LLM/tool fire.
449+
storeCrossPhaseArtifact(execution, 'validation', 'phaseDecision', {
450+
schema: 'bitcode.pipeline.phase-decision',
451+
formalPhaseDecision: true,
452+
phase: 'validation',
453+
agent: 'ready-to-finish-asset-packs-synthesis-deposit-pipeline',
454+
step: qualitativePtrrRan ? 'try' : 'decide',
455+
failsafe: qualitativePtrrRan ? 'prepare' : 'deterministic-gate',
456+
generation: 'structure',
457+
summary: readinessSummary,
458+
message: readinessSummary,
459+
finalApproval: readyToFinish,
460+
recommendation: readyToFinish ? 'finish' : 'revise',
461+
});
416462
storeCrossPhaseArtifact(execution, 'implementation', 'options', packs);
417463
storeCrossPhaseArtifact(execution, 'implementation', 'assetPacks', packs);
418464

0 commit comments

Comments
 (0)