From 3f448192a24add4fe4d3680560772bad12e100fb Mon Sep 17 00:00:00 2001 From: "Yifeng[Terry] Yu" <125581657+xiaojiou176@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:46:31 -0700 Subject: [PATCH] feat: surface run completion governance --- apps/dashboard/components/RunDetail.tsx | 46 +++++++++++++++++ .../RunDetailStatusContractCard.tsx | 49 +++++++++++++++++++ apps/dashboard/tests/rundetail.shared.tsx | 16 ++++++ apps/dashboard/tests/rundetail_core.suite.tsx | 22 ++++++++- 4 files changed, 132 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/components/RunDetail.tsx b/apps/dashboard/components/RunDetail.tsx index 3bcd68f..68cfb62 100644 --- a/apps/dashboard/components/RunDetail.tsx +++ b/apps/dashboard/components/RunDetail.tsx @@ -5,6 +5,7 @@ import EventTimeline from "./EventTimeline"; import { Card } from "./ui/card"; import { fetchAgentStatus, + fetchArtifact, fetchChainSpec, fetchEvents, fetchReports, @@ -78,6 +79,8 @@ export default function RunDetail({ const [toolCalls, setToolCalls] = useState([]); const [toolCallsError, setToolCallsError] = useState(""); const [toolCallsLoading, setToolCallsLoading] = useState(false); + const [planningContracts, setPlanningContracts] = useState>>([]); + const [planningContractsError, setPlanningContractsError] = useState(""); const [chainSpecError, setChainSpecError] = useState(""); const [chainSpecLoading, setChainSpecLoading] = useState(false); const [liveEnabled, setLiveEnabled] = useState(true); @@ -130,6 +133,12 @@ export default function RunDetail({ const schemaVersion = toStringOr(run?.manifest?.versions?.contracts_schema, "v1"); const evidenceHashes = toObject(run?.manifest?.evidence_hashes); const manifestArtifacts = toArray(run?.manifest?.artifacts as unknown[] | undefined); + const hasPlanningContractsArtifact = manifestArtifacts.some((item) => { + const record = toObject(item); + const name = toStringOr(record.name, ""); + const path = toStringOr(record.path, ""); + return name === "planning_worker_prompt_contracts" || path === "artifacts/planning_worker_prompt_contracts.json"; + }); const observability = toObject(run?.manifest?.observability); const summaryGroups = ["reports/", "events.jsonl", "contract.json", "other"]; const summary = summaryGroups.map((group) => { @@ -232,6 +241,41 @@ export default function RunDetail({ }; }, [run?.run_id]); + useEffect(() => { + let alive = true; + async function loadPlanningContracts() { + if (!run?.run_id || !hasPlanningContractsArtifact) { + if (alive) { + setPlanningContracts([]); + setPlanningContractsError(""); + } + return; + } + try { + const artifact = await fetchArtifact(run.run_id, "planning_worker_prompt_contracts.json"); + const rows = Array.isArray(artifact?.data) ? artifact.data : []; + if (alive) { + setPlanningContracts( + rows + .map((item) => (item && typeof item === "object" && !Array.isArray(item) ? (item as Record) : null)) + .filter((item): item is Record => item !== null), + ); + setPlanningContractsError(""); + } + } catch (err: unknown) { + if (alive) { + setPlanningContracts([]); + console.error(`[run-detail] load planning contracts failed: ${uiErrorDetail(err)}`); + setPlanningContractsError(sanitizeUiError(err, "Planning governance unavailable")); + } + } + } + void loadPlanningContracts(); + return () => { + alive = false; + }; + }, [hasPlanningContractsArtifact, run?.run_id]); + useEffect(() => { let alive = true; async function loadChainSpec() { @@ -445,6 +489,8 @@ export default function RunDetail({ pendingApprovals={pendingApprovals} evidenceHashes={evidenceHashes} manifestArtifacts={manifestArtifacts} + planningContracts={planningContracts} + planningContractsError={planningContractsError} onOpenLogs={() => handleFailedTerminalAction("logs")} onOpenReports={() => handleFailedTerminalAction("reports")} failedTerminalActionFeedback={failedTerminalActionFeedback} diff --git a/apps/dashboard/components/run-detail/RunDetailStatusContractCard.tsx b/apps/dashboard/components/run-detail/RunDetailStatusContractCard.tsx index 66a4dae..4379ccb 100644 --- a/apps/dashboard/components/run-detail/RunDetailStatusContractCard.tsx +++ b/apps/dashboard/components/run-detail/RunDetailStatusContractCard.tsx @@ -38,6 +38,8 @@ type RunDetailStatusContractCardProps = { pendingApprovals: EventRecord[]; evidenceHashes: Record; manifestArtifacts: unknown[]; + planningContracts: Array>; + planningContractsError: string; onOpenLogs: () => void; onOpenReports: () => void; failedTerminalActionFeedback: string; @@ -64,6 +66,8 @@ export default function RunDetailStatusContractCard({ pendingApprovals, evidenceHashes, manifestArtifacts, + planningContracts, + planningContractsError, onOpenLogs, onOpenReports, failedTerminalActionFeedback, @@ -94,6 +98,29 @@ export default function RunDetailStatusContractCard({ ? "Worker prompt contracts" : "", ].filter(Boolean); + const continuationOnIncomplete = Array.from( + new Set( + planningContracts + .map((contract) => toDisplayText(toObject(contract.continuation_policy).on_incomplete)) + .filter((value) => value !== "-"), + ), + ); + const continuationOnBlocked = Array.from( + new Set( + planningContracts + .map((contract) => toDisplayText(toObject(contract.continuation_policy).on_blocked)) + .filter((value) => value !== "-"), + ), + ); + const doneChecks = Array.from( + new Set( + planningContracts.flatMap((contract) => + toArray(toObject(contract.done_definition).acceptance_checks as unknown[] | null | undefined) + .map((value) => toDisplayText(value)) + .filter((value) => value !== "-"), + ), + ), + ); const roleBindingReadModel = run.role_binding_read_model; return ( @@ -238,6 +265,28 @@ export default function RunDetailStatusContractCard({ ) : null} + {planningContracts.length > 0 || planningContractsError ? ( +
+
Completion governance
+
Worker prompt contracts: {planningContracts.length}
+ {continuationOnIncomplete.length > 0 ? ( +
On incomplete: {continuationOnIncomplete.join(" / ")}
+ ) : null} + {continuationOnBlocked.length > 0 ? ( +
On blocked: {continuationOnBlocked.join(" / ")}
+ ) : null} + {doneChecks.length > 0 ? ( +
DoD checks: {doneChecks.join(" / ")}
+ ) : null} + {planningContractsError ? ( +
{planningContractsError}
+ ) : ( +
+ Derived from persisted worker prompt contracts. These summaries stay advisory; task_contract still owns execution authority. +
+ )} +
+ ) : null}
Manifest artifacts:
{artifactList.length} artifact{artifactList.length === 1 ? "" : "s"}
{lifecycleArtifactLabels.length > 0 ? ( diff --git a/apps/dashboard/tests/rundetail.shared.tsx b/apps/dashboard/tests/rundetail.shared.tsx index 31101e9..691eae6 100644 --- a/apps/dashboard/tests/rundetail.shared.tsx +++ b/apps/dashboard/tests/rundetail.shared.tsx @@ -8,6 +8,7 @@ type FetchOptions = { events?: any[]; reports?: any[]; chainSpec?: any; + planningContracts?: any[]; availableRuns?: any[]; agentStatus?: any[]; toolCalls?: any[]; @@ -17,10 +18,12 @@ type FetchOptions = { eventsOk?: boolean; reportsOk?: boolean; chainOk?: boolean; + planningContractsOk?: boolean; agentStatusOk?: boolean; toolCallsOk?: boolean; throwRuns?: boolean; throwChain?: boolean; + throwPlanningContracts?: boolean; throwReplay?: boolean; throwAgentStatus?: boolean; throwToolCalls?: boolean; @@ -32,6 +35,7 @@ export function mockFetchFactory(options: FetchOptions) { events = [], reports = [], chainSpec = null, + planningContracts = [], availableRuns = [], replayOk = true, replayStatus = 200, @@ -39,12 +43,14 @@ export function mockFetchFactory(options: FetchOptions) { eventsOk = true, reportsOk = true, chainOk = true, + planningContractsOk = true, agentStatus = [], toolCalls = [], agentStatusOk = true, toolCallsOk = true, throwRuns = false, throwChain = false, + throwPlanningContracts = false, throwReplay = false, throwAgentStatus = false, throwToolCalls = false, @@ -59,6 +65,16 @@ export function mockFetchFactory(options: FetchOptions) { if (url.includes("/api/runs/") && url.endsWith("/reports")) { return { ok: reportsOk, status: reportsOk ? 200 : 500, json: async () => reports }; } + if (url.includes("/api/runs/") && url.includes("/artifacts?name=planning_worker_prompt_contracts.json")) { + if (throwPlanningContracts) { + throw new Error("planning contracts failed"); + } + return { + ok: planningContractsOk, + status: planningContractsOk ? 200 : 500, + json: async () => ({ data: planningContracts }), + }; + } if (url.includes("/api/runs/") && url.includes("/artifacts?name=tool_calls.jsonl")) { if (throwToolCalls) { throw new Error("tool calls failed"); diff --git a/apps/dashboard/tests/rundetail_core.suite.tsx b/apps/dashboard/tests/rundetail_core.suite.tsx index 551b266..50b654e 100644 --- a/apps/dashboard/tests/rundetail_core.suite.tsx +++ b/apps/dashboard/tests/rundetail_core.suite.tsx @@ -215,7 +215,21 @@ describe("RunDetail core flows", () => { ], }, }; - const fetchMock = mockFetchFactory({ events: [], reports: [], availableRuns: [] }); + const fetchMock = mockFetchFactory({ + events: [], + reports: [], + availableRuns: [], + planningContracts: [ + { + prompt_contract_id: "worker-1", + done_definition: { acceptance_checks: ["repo_hygiene", "test_report"] }, + continuation_policy: { + on_incomplete: "reply_auditor_reprompt_and_continue_same_session", + on_blocked: "spawn_independent_temporary_unblock_task", + }, + }, + ], + }); // @ts-expect-error test override global.fetch = fetchMock; @@ -232,6 +246,12 @@ describe("RunDetail core flows", () => { expect(screen.getByText("Runtime binding: agents / cliproxyapi / gpt-5.4")).toBeInTheDocument(); expect(screen.getByText("Runtime capability: standard-provider-path")).toBeInTheDocument(); expect(screen.getByText("Tool execution: standard-provider-path / provider-path-required")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText("Worker prompt contracts: 1")).toBeInTheDocument(); + }); + expect(screen.getByText("On incomplete: reply_auditor_reprompt_and_continue_same_session")).toBeInTheDocument(); + expect(screen.getByText("On blocked: spawn_independent_temporary_unblock_task")).toBeInTheDocument(); + expect(screen.getByText("DoD checks: repo_hygiene / test_report")).toBeInTheDocument(); expect(screen.getByText("Lifecycle artifacts: Prompt artifact / Wave plan / Worker prompt contracts")).toBeInTheDocument(); expect( screen.getByText(