Skip to content

Commit 02ccf0f

Browse files
authored
feat: surface run completion governance (#75)
1 parent 3d32cd5 commit 02ccf0f

4 files changed

Lines changed: 132 additions & 1 deletion

File tree

apps/dashboard/components/RunDetail.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import EventTimeline from "./EventTimeline";
55
import { Card } from "./ui/card";
66
import {
77
fetchAgentStatus,
8+
fetchArtifact,
89
fetchChainSpec,
910
fetchEvents,
1011
fetchReports,
@@ -78,6 +79,8 @@ export default function RunDetail({
7879
const [toolCalls, setToolCalls] = useState<ToolCallRecord[]>([]);
7980
const [toolCallsError, setToolCallsError] = useState("");
8081
const [toolCallsLoading, setToolCallsLoading] = useState(false);
82+
const [planningContracts, setPlanningContracts] = useState<Array<Record<string, unknown>>>([]);
83+
const [planningContractsError, setPlanningContractsError] = useState("");
8184
const [chainSpecError, setChainSpecError] = useState("");
8285
const [chainSpecLoading, setChainSpecLoading] = useState(false);
8386
const [liveEnabled, setLiveEnabled] = useState(true);
@@ -130,6 +133,12 @@ export default function RunDetail({
130133
const schemaVersion = toStringOr(run?.manifest?.versions?.contracts_schema, "v1");
131134
const evidenceHashes = toObject(run?.manifest?.evidence_hashes);
132135
const manifestArtifacts = toArray(run?.manifest?.artifacts as unknown[] | undefined);
136+
const hasPlanningContractsArtifact = manifestArtifacts.some((item) => {
137+
const record = toObject(item);
138+
const name = toStringOr(record.name, "");
139+
const path = toStringOr(record.path, "");
140+
return name === "planning_worker_prompt_contracts" || path === "artifacts/planning_worker_prompt_contracts.json";
141+
});
133142
const observability = toObject(run?.manifest?.observability);
134143
const summaryGroups = ["reports/", "events.jsonl", "contract.json", "other"];
135144
const summary = summaryGroups.map((group) => {
@@ -232,6 +241,41 @@ export default function RunDetail({
232241
};
233242
}, [run?.run_id]);
234243

244+
useEffect(() => {
245+
let alive = true;
246+
async function loadPlanningContracts() {
247+
if (!run?.run_id || !hasPlanningContractsArtifact) {
248+
if (alive) {
249+
setPlanningContracts([]);
250+
setPlanningContractsError("");
251+
}
252+
return;
253+
}
254+
try {
255+
const artifact = await fetchArtifact(run.run_id, "planning_worker_prompt_contracts.json");
256+
const rows = Array.isArray(artifact?.data) ? artifact.data : [];
257+
if (alive) {
258+
setPlanningContracts(
259+
rows
260+
.map((item) => (item && typeof item === "object" && !Array.isArray(item) ? (item as Record<string, unknown>) : null))
261+
.filter((item): item is Record<string, unknown> => item !== null),
262+
);
263+
setPlanningContractsError("");
264+
}
265+
} catch (err: unknown) {
266+
if (alive) {
267+
setPlanningContracts([]);
268+
console.error(`[run-detail] load planning contracts failed: ${uiErrorDetail(err)}`);
269+
setPlanningContractsError(sanitizeUiError(err, "Planning governance unavailable"));
270+
}
271+
}
272+
}
273+
void loadPlanningContracts();
274+
return () => {
275+
alive = false;
276+
};
277+
}, [hasPlanningContractsArtifact, run?.run_id]);
278+
235279
useEffect(() => {
236280
let alive = true;
237281
async function loadChainSpec() {
@@ -445,6 +489,8 @@ export default function RunDetail({
445489
pendingApprovals={pendingApprovals}
446490
evidenceHashes={evidenceHashes}
447491
manifestArtifacts={manifestArtifacts}
492+
planningContracts={planningContracts}
493+
planningContractsError={planningContractsError}
448494
onOpenLogs={() => handleFailedTerminalAction("logs")}
449495
onOpenReports={() => handleFailedTerminalAction("reports")}
450496
failedTerminalActionFeedback={failedTerminalActionFeedback}

apps/dashboard/components/run-detail/RunDetailStatusContractCard.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type RunDetailStatusContractCardProps = {
3838
pendingApprovals: EventRecord[];
3939
evidenceHashes: Record<string, unknown>;
4040
manifestArtifacts: unknown[];
41+
planningContracts: Array<Record<string, unknown>>;
42+
planningContractsError: string;
4143
onOpenLogs: () => void;
4244
onOpenReports: () => void;
4345
failedTerminalActionFeedback: string;
@@ -64,6 +66,8 @@ export default function RunDetailStatusContractCard({
6466
pendingApprovals,
6567
evidenceHashes,
6668
manifestArtifacts,
69+
planningContracts,
70+
planningContractsError,
6771
onOpenLogs,
6872
onOpenReports,
6973
failedTerminalActionFeedback,
@@ -94,6 +98,29 @@ export default function RunDetailStatusContractCard({
9498
? "Worker prompt contracts"
9599
: "",
96100
].filter(Boolean);
101+
const continuationOnIncomplete = Array.from(
102+
new Set(
103+
planningContracts
104+
.map((contract) => toDisplayText(toObject(contract.continuation_policy).on_incomplete))
105+
.filter((value) => value !== "-"),
106+
),
107+
);
108+
const continuationOnBlocked = Array.from(
109+
new Set(
110+
planningContracts
111+
.map((contract) => toDisplayText(toObject(contract.continuation_policy).on_blocked))
112+
.filter((value) => value !== "-"),
113+
),
114+
);
115+
const doneChecks = Array.from(
116+
new Set(
117+
planningContracts.flatMap((contract) =>
118+
toArray(toObject(contract.done_definition).acceptance_checks as unknown[] | null | undefined)
119+
.map((value) => toDisplayText(value))
120+
.filter((value) => value !== "-"),
121+
),
122+
),
123+
);
97124
const roleBindingReadModel = run.role_binding_read_model;
98125

99126
return (
@@ -238,6 +265,28 @@ export default function RunDetailStatusContractCard({
238265
</div>
239266
</div>
240267
) : null}
268+
{planningContracts.length > 0 || planningContractsError ? (
269+
<div className="run-detail-section" data-testid="run-completion-governance-summary">
270+
<div className="mono run-detail-section-label">Completion governance</div>
271+
<div className="mono">Worker prompt contracts: {planningContracts.length}</div>
272+
{continuationOnIncomplete.length > 0 ? (
273+
<div className="mono">On incomplete: {continuationOnIncomplete.join(" / ")}</div>
274+
) : null}
275+
{continuationOnBlocked.length > 0 ? (
276+
<div className="mono">On blocked: {continuationOnBlocked.join(" / ")}</div>
277+
) : null}
278+
{doneChecks.length > 0 ? (
279+
<div className="mono">DoD checks: {doneChecks.join(" / ")}</div>
280+
) : null}
281+
{planningContractsError ? (
282+
<div className="mono muted">{planningContractsError}</div>
283+
) : (
284+
<div className="mono muted">
285+
Derived from persisted worker prompt contracts. These summaries stay advisory; task_contract still owns execution authority.
286+
</div>
287+
)}
288+
</div>
289+
) : null}
241290
<div className="mono">Manifest artifacts:</div>
242291
<div className="mono muted">{artifactList.length} artifact{artifactList.length === 1 ? "" : "s"}</div>
243292
{lifecycleArtifactLabels.length > 0 ? (

apps/dashboard/tests/rundetail.shared.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type FetchOptions = {
88
events?: any[];
99
reports?: any[];
1010
chainSpec?: any;
11+
planningContracts?: any[];
1112
availableRuns?: any[];
1213
agentStatus?: any[];
1314
toolCalls?: any[];
@@ -17,10 +18,12 @@ type FetchOptions = {
1718
eventsOk?: boolean;
1819
reportsOk?: boolean;
1920
chainOk?: boolean;
21+
planningContractsOk?: boolean;
2022
agentStatusOk?: boolean;
2123
toolCallsOk?: boolean;
2224
throwRuns?: boolean;
2325
throwChain?: boolean;
26+
throwPlanningContracts?: boolean;
2427
throwReplay?: boolean;
2528
throwAgentStatus?: boolean;
2629
throwToolCalls?: boolean;
@@ -32,19 +35,22 @@ export function mockFetchFactory(options: FetchOptions) {
3235
events = [],
3336
reports = [],
3437
chainSpec = null,
38+
planningContracts = [],
3539
availableRuns = [],
3640
replayOk = true,
3741
replayStatus = 200,
3842
runsOk = true,
3943
eventsOk = true,
4044
reportsOk = true,
4145
chainOk = true,
46+
planningContractsOk = true,
4247
agentStatus = [],
4348
toolCalls = [],
4449
agentStatusOk = true,
4550
toolCallsOk = true,
4651
throwRuns = false,
4752
throwChain = false,
53+
throwPlanningContracts = false,
4854
throwReplay = false,
4955
throwAgentStatus = false,
5056
throwToolCalls = false,
@@ -59,6 +65,16 @@ export function mockFetchFactory(options: FetchOptions) {
5965
if (url.includes("/api/runs/") && url.endsWith("/reports")) {
6066
return { ok: reportsOk, status: reportsOk ? 200 : 500, json: async () => reports };
6167
}
68+
if (url.includes("/api/runs/") && url.includes("/artifacts?name=planning_worker_prompt_contracts.json")) {
69+
if (throwPlanningContracts) {
70+
throw new Error("planning contracts failed");
71+
}
72+
return {
73+
ok: planningContractsOk,
74+
status: planningContractsOk ? 200 : 500,
75+
json: async () => ({ data: planningContracts }),
76+
};
77+
}
6278
if (url.includes("/api/runs/") && url.includes("/artifacts?name=tool_calls.jsonl")) {
6379
if (throwToolCalls) {
6480
throw new Error("tool calls failed");

apps/dashboard/tests/rundetail_core.suite.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,21 @@ describe("RunDetail core flows", () => {
215215
],
216216
},
217217
};
218-
const fetchMock = mockFetchFactory({ events: [], reports: [], availableRuns: [] });
218+
const fetchMock = mockFetchFactory({
219+
events: [],
220+
reports: [],
221+
availableRuns: [],
222+
planningContracts: [
223+
{
224+
prompt_contract_id: "worker-1",
225+
done_definition: { acceptance_checks: ["repo_hygiene", "test_report"] },
226+
continuation_policy: {
227+
on_incomplete: "reply_auditor_reprompt_and_continue_same_session",
228+
on_blocked: "spawn_independent_temporary_unblock_task",
229+
},
230+
},
231+
],
232+
});
219233
// @ts-expect-error test override
220234
global.fetch = fetchMock;
221235

@@ -232,6 +246,12 @@ describe("RunDetail core flows", () => {
232246
expect(screen.getByText("Runtime binding: agents / cliproxyapi / gpt-5.4")).toBeInTheDocument();
233247
expect(screen.getByText("Runtime capability: standard-provider-path")).toBeInTheDocument();
234248
expect(screen.getByText("Tool execution: standard-provider-path / provider-path-required")).toBeInTheDocument();
249+
await waitFor(() => {
250+
expect(screen.getByText("Worker prompt contracts: 1")).toBeInTheDocument();
251+
});
252+
expect(screen.getByText("On incomplete: reply_auditor_reprompt_and_continue_same_session")).toBeInTheDocument();
253+
expect(screen.getByText("On blocked: spawn_independent_temporary_unblock_task")).toBeInTheDocument();
254+
expect(screen.getByText("DoD checks: repo_hygiene / test_report")).toBeInTheDocument();
235255
expect(screen.getByText("Lifecycle artifacts: Prompt artifact / Wave plan / Worker prompt contracts")).toBeInTheDocument();
236256
expect(
237257
screen.getByText(

0 commit comments

Comments
 (0)