diff --git a/apps/dashboard/components/RunDetail.tsx b/apps/dashboard/components/RunDetail.tsx index 0e3254d..df56587 100644 --- a/apps/dashboard/components/RunDetail.tsx +++ b/apps/dashboard/components/RunDetail.tsx @@ -83,6 +83,8 @@ export default function RunDetail({ const [planningContractsError, setPlanningContractsError] = useState(""); const [unblockTasks, setUnblockTasks] = useState>>([]); const [unblockTasksError, setUnblockTasksError] = useState(""); + const [contextPackArtifact, setContextPackArtifact] = useState | null>(null); + const [harnessRequestArtifact, setHarnessRequestArtifact] = useState | null>(null); const [chainSpecError, setChainSpecError] = useState(""); const [chainSpecLoading, setChainSpecLoading] = useState(false); const [liveEnabled, setLiveEnabled] = useState(true); @@ -108,6 +110,9 @@ export default function RunDetail({ const reviewReport = reportsState.find((r) => r.name === "review_report.json")?.data; const taskResult = reportsState.find((r) => r.name === "task_result.json")?.data; const workReport = reportsState.find((r) => r.name === "work_report.json")?.data; + const completionGovernanceReport = toObject( + reportsState.find((r) => r.name === "completion_governance_report.json")?.data + ); const evidenceReport = reportsState.find((r) => r.name === "evidence_report.json")?.data; const incidentPack = reportsState.find((r) => r.name === "incident_pack.json")?.data; const proofPack = reportsState.find((r) => r.name === "proof_pack.json")?.data; @@ -147,6 +152,18 @@ export default function RunDetail({ const path = toStringOr(record.path, ""); return name === "planning_unblock_tasks" || path === "artifacts/planning_unblock_tasks.json"; }); + const hasContextPackArtifact = manifestArtifacts.some((item) => { + const record = toObject(item); + const name = toStringOr(record.name, ""); + const path = toStringOr(record.path, ""); + return name === "context_pack" || path === "artifacts/context_pack.json"; + }); + const hasHarnessRequestArtifact = manifestArtifacts.some((item) => { + const record = toObject(item); + const name = toStringOr(record.name, ""); + const path = toStringOr(record.path, ""); + return name === "harness_request" || path === "artifacts/harness_request.json"; + }); const observability = toObject(run?.manifest?.observability); const summaryGroups = ["reports/", "events.jsonl", "contract.json", "other"]; const summary = summaryGroups.map((group) => { @@ -319,6 +336,40 @@ export default function RunDetail({ }; }, [hasUnblockTasksArtifact, run?.run_id]); + useEffect(() => { + let alive = true; + async function loadGovernanceArtifacts() { + if (!run?.run_id) { + if (alive) { + setContextPackArtifact(null); + setHarnessRequestArtifact(null); + } + return; + } + const [contextPackRes, harnessRequestRes] = await Promise.allSettled([ + hasContextPackArtifact ? fetchArtifact(run.run_id, "context_pack.json") : Promise.resolve(null), + hasHarnessRequestArtifact ? fetchArtifact(run.run_id, "harness_request.json") : Promise.resolve(null), + ]); + if (!alive) { + return; + } + setContextPackArtifact( + contextPackRes.status === "fulfilled" && contextPackRes.value?.data && typeof contextPackRes.value.data === "object" + ? (contextPackRes.value.data as Record) + : null, + ); + setHarnessRequestArtifact( + harnessRequestRes.status === "fulfilled" && harnessRequestRes.value?.data && typeof harnessRequestRes.value.data === "object" + ? (harnessRequestRes.value.data as Record) + : null, + ); + } + void loadGovernanceArtifacts(); + return () => { + alive = false; + }; + }, [hasContextPackArtifact, hasHarnessRequestArtifact, run?.run_id]); + useEffect(() => { let alive = true; async function loadChainSpec() { @@ -532,10 +583,13 @@ export default function RunDetail({ pendingApprovals={pendingApprovals} evidenceHashes={evidenceHashes} manifestArtifacts={manifestArtifacts} + completionGovernanceReport={completionGovernanceReport} planningContracts={planningContracts} planningContractsError={planningContractsError} unblockTasks={unblockTasks} unblockTasksError={unblockTasksError} + contextPackArtifact={contextPackArtifact} + harnessRequestArtifact={harnessRequestArtifact} 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 be3477c..b579df0 100644 --- a/apps/dashboard/components/run-detail/RunDetailStatusContractCard.tsx +++ b/apps/dashboard/components/run-detail/RunDetailStatusContractCard.tsx @@ -38,10 +38,13 @@ type RunDetailStatusContractCardProps = { pendingApprovals: EventRecord[]; evidenceHashes: Record; manifestArtifacts: unknown[]; + completionGovernanceReport: Record; planningContracts: Array>; planningContractsError: string; unblockTasks: Array>; unblockTasksError: string; + contextPackArtifact: Record | null; + harnessRequestArtifact: Record | null; onOpenLogs: () => void; onOpenReports: () => void; failedTerminalActionFeedback: string; @@ -68,15 +71,19 @@ export default function RunDetailStatusContractCard({ pendingApprovals, evidenceHashes, manifestArtifacts, + completionGovernanceReport, planningContracts, planningContractsError, unblockTasks, unblockTasksError, + contextPackArtifact, + harnessRequestArtifact, onOpenLogs, onOpenReports, failedTerminalActionFeedback, }: RunDetailStatusContractCardProps) { const bindingReadModelCopy = getUiCopy(DEFAULT_UI_LOCALE).desktop.runDetail.bindingReadModel; + const completionGovernanceCopy = getUiCopy(DEFAULT_UI_LOCALE).desktop.runDetail.completionGovernance; const terminal = terminalStatus.toUpperCase(); const isTerminal = terminal === "FAILED" || terminal === "ERROR" || terminal === "SUCCESS" || terminal === "DONE" || terminal === "REJECTED"; const isFailedTerminal = terminal === "FAILED" || terminal === "ERROR" || terminal === "REJECTED"; @@ -129,6 +136,29 @@ export default function RunDetailStatusContractCard({ ), ); const roleBindingReadModel = run.role_binding_read_model; + const runtimeCompletionGovernance = toObject(completionGovernanceReport); + const hasRuntimeCompletionGovernance = Object.keys(runtimeCompletionGovernance).length > 0; + const runtimeDodChecker = toObject(runtimeCompletionGovernance.dod_checker); + const runtimeReplyAuditor = toObject(runtimeCompletionGovernance.reply_auditor); + const runtimeContinuationDecision = toObject(runtimeCompletionGovernance.continuation_decision); + const runtimeContextPack = toObject(runtimeCompletionGovernance.context_pack); + const runtimeHarnessRequest = toObject(runtimeCompletionGovernance.harness_request); + const contextPackRecord = toObject(contextPackArtifact); + const harnessRequestRecord = toObject(harnessRequestArtifact); + const runtimeDodRequiredChecks = Array.from( + new Set( + toArray(runtimeDodChecker.required_checks as unknown[] | null | undefined) + .map((value) => toDisplayText(value)) + .filter((value) => value !== "-"), + ), + ); + const runtimeDodUnmetChecks = Array.from( + new Set( + toArray(runtimeDodChecker.unmet_checks as unknown[] | null | undefined) + .map((value) => toDisplayText(value)) + .filter((value) => value !== "-"), + ), + ); const unblockTaskOwners = Array.from( new Set(unblockTasks.map((task) => toDisplayText(task.owner)).filter((value) => value !== "-")), ); @@ -281,36 +311,100 @@ export default function RunDetailStatusContractCard({ ) : null} - {planningContracts.length > 0 || planningContractsError || unblockTasks.length > 0 || unblockTasksError ? ( + {hasRuntimeCompletionGovernance || planningContracts.length > 0 || planningContractsError || unblockTasks.length > 0 || unblockTasksError ? (
-
Completion governance
-
Worker prompt contracts: {planningContracts.length}
- {unblockTasks.length > 0 ?
Unblock tasks: {unblockTasks.length}
: null} - {continuationOnIncomplete.length > 0 ? ( -
On incomplete: {continuationOnIncomplete.join(" / ")}
- ) : null} - {continuationOnBlocked.length > 0 ? ( -
On blocked: {continuationOnBlocked.join(" / ")}
- ) : null} - {doneChecks.length > 0 ? ( -
DoD checks: {doneChecks.join(" / ")}
- ) : null} - {unblockTaskOwners.length > 0 ? ( -
Unblock owner: {unblockTaskOwners.join(" / ")}
- ) : null} - {unblockTaskModes.length > 0 ? ( -
Unblock mode: {unblockTaskModes.join(" / ")}
+
{completionGovernanceCopy.title}
+ {hasRuntimeCompletionGovernance ? ( +
+
{completionGovernanceCopy.runtimeTitle}
+
{completionGovernanceCopy.overallVerdict}: {toDisplayText(runtimeCompletionGovernance.overall_verdict)}
+
{completionGovernanceCopy.reportAuthority}: {toDisplayText(runtimeCompletionGovernance.authority)}
+
{completionGovernanceCopy.reportSource}: {toDisplayText(runtimeCompletionGovernance.source)}
+
{completionGovernanceCopy.reportExecutionAuthority}: {toDisplayText(runtimeCompletionGovernance.execution_authority)}
+
{completionGovernanceCopy.dodChecker}: {toDisplayText(runtimeDodChecker.status)}
+ {toDisplayText(runtimeDodChecker.summary) !== "-" ? ( +
{completionGovernanceCopy.dodSummary}: {toDisplayText(runtimeDodChecker.summary)}
+ ) : null} + {runtimeDodRequiredChecks.length > 0 ? ( +
{completionGovernanceCopy.dodRequiredChecks}: {runtimeDodRequiredChecks.join(" / ")}
+ ) : null} + {runtimeDodUnmetChecks.length > 0 ? ( +
{completionGovernanceCopy.dodUnmetChecks}: {runtimeDodUnmetChecks.join(" / ")}
+ ) : null} +
{completionGovernanceCopy.replyAuditor}: {toDisplayText(runtimeReplyAuditor.status)}
+ {toDisplayText(runtimeReplyAuditor.summary) !== "-" ? ( +
{completionGovernanceCopy.replySummary}: {toDisplayText(runtimeReplyAuditor.summary)}
+ ) : null} +
{completionGovernanceCopy.continuationDecision}: {toDisplayText(runtimeContinuationDecision.selected_action)}
+ {toDisplayText(runtimeContinuationDecision.summary) !== "-" ? ( +
{completionGovernanceCopy.continuationSummary}: {toDisplayText(runtimeContinuationDecision.summary)}
+ ) : null} + {toDisplayText(runtimeContinuationDecision.action_source) !== "-" ? ( +
{completionGovernanceCopy.actionSource}: {toDisplayText(runtimeContinuationDecision.action_source)}
+ ) : null} + {toDisplayText(runtimeContinuationDecision.unblock_task_id) !== "-" ? ( +
{completionGovernanceCopy.selectedUnblockTask}: {toDisplayText(runtimeContinuationDecision.unblock_task_id)}
+ ) : null} +
{completionGovernanceCopy.contextPack}: {toDisplayText(runtimeContextPack.status)}
+ {toDisplayText(runtimeContextPack.summary) !== "-" ? ( +
{completionGovernanceCopy.contextPackSummary}: {toDisplayText(runtimeContextPack.summary)}
+ ) : null} + {toDisplayText(contextPackRecord.pack_id) !== "-" ? ( +
{completionGovernanceCopy.contextPackId}: {toDisplayText(contextPackRecord.pack_id)}
+ ) : null} + {toDisplayText(contextPackRecord.trigger_reason) !== "-" ? ( +
{completionGovernanceCopy.contextPackTrigger}: {toDisplayText(contextPackRecord.trigger_reason)}
+ ) : null} +
{completionGovernanceCopy.harnessRequest}: {toDisplayText(runtimeHarnessRequest.status)}
+ {toDisplayText(runtimeHarnessRequest.summary) !== "-" ? ( +
{completionGovernanceCopy.harnessRequestSummary}: {toDisplayText(runtimeHarnessRequest.summary)}
+ ) : null} + {toDisplayText(harnessRequestRecord.request_id) !== "-" ? ( +
{completionGovernanceCopy.harnessRequestId}: {toDisplayText(harnessRequestRecord.request_id)}
+ ) : null} + {toDisplayText(harnessRequestRecord.scope) !== "-" ? ( +
{completionGovernanceCopy.harnessRequestScope}: {toDisplayText(harnessRequestRecord.scope)}
+ ) : null} + {harnessRequestRecord.approval_required !== undefined ? ( +
{completionGovernanceCopy.harnessRequestApproval}: {toDisplayText(harnessRequestRecord.approval_required)}
+ ) : null} +
{completionGovernanceCopy.runtimeNote}
+
) : null} - {unblockTaskTriggers.length > 0 ? ( -
Unblock trigger: {unblockTaskTriggers.join(" / ")}
+ {planningContracts.length > 0 || planningContractsError || unblockTasks.length > 0 || unblockTasksError ? ( + <> + {hasRuntimeCompletionGovernance ? ( +
{completionGovernanceCopy.planningFallbackTitle}
+ ) : null} +
{completionGovernanceCopy.workerPromptContracts}: {planningContracts.length}
+ {unblockTasks.length > 0 ? ( +
{completionGovernanceCopy.unblockTasks}: {unblockTasks.length}
+ ) : null} + {continuationOnIncomplete.length > 0 ? ( +
{completionGovernanceCopy.onIncomplete}: {continuationOnIncomplete.join(" / ")}
+ ) : null} + {continuationOnBlocked.length > 0 ? ( +
{completionGovernanceCopy.onBlocked}: {continuationOnBlocked.join(" / ")}
+ ) : null} + {doneChecks.length > 0 ? ( +
{completionGovernanceCopy.doneChecks}: {doneChecks.join(" / ")}
+ ) : null} + {unblockTaskOwners.length > 0 ? ( +
{completionGovernanceCopy.unblockOwner}: {unblockTaskOwners.join(" / ")}
+ ) : null} + {unblockTaskModes.length > 0 ? ( +
{completionGovernanceCopy.unblockMode}: {unblockTaskModes.join(" / ")}
+ ) : null} + {unblockTaskTriggers.length > 0 ? ( +
{completionGovernanceCopy.unblockTrigger}: {unblockTaskTriggers.join(" / ")}
+ ) : null} + {planningContractsError || unblockTasksError ? ( +
{planningContractsError || unblockTasksError}
+ ) : ( +
{completionGovernanceCopy.advisoryNote}
+ )} + ) : null} - {planningContractsError || unblockTasksError ? ( -
{planningContractsError || unblockTasksError}
- ) : ( -
- Derived from persisted worker prompt contracts and unblock tasks. These summaries stay advisory; task_contract still owns execution authority. -
- )}
) : null}
Manifest artifacts:
diff --git a/apps/dashboard/tests/rundetail_core.suite.tsx b/apps/dashboard/tests/rundetail_core.suite.tsx index 3f16db1..55cc495 100644 --- a/apps/dashboard/tests/rundetail_core.suite.tsx +++ b/apps/dashboard/tests/rundetail_core.suite.tsx @@ -273,6 +273,190 @@ describe("RunDetail core flows", () => { ).toBeInTheDocument(); }); + it("prefers runtime completion governance report when one is present", async () => { + const run = { + run_id: "run_runtime_governance", + task_id: "task_runtime_governance", + status: "SUCCESS", + allowed_paths: ["README.md"], + contract: { task_id: "task_runtime_governance" }, + manifest: { + artifacts: [ + { name: "planning_worker_prompt_contracts", path: "artifacts/planning_worker_prompt_contracts.json" }, + { name: "planning_unblock_tasks", path: "artifacts/planning_unblock_tasks.json" }, + ], + }, + }; + const reports = [ + { + name: "completion_governance_report.json", + data: { + authority: "runtime-evaluated-read-back", + source: "reports/completion_governance_report.json", + execution_authority: "task_contract", + overall_verdict: "continue_required", + dod_checker: { + status: "failed", + summary: "Missing test_report before completion.", + required_checks: ["repo_hygiene", "test_report"], + unmet_checks: ["test_report"], + }, + reply_auditor: { + status: "needs_followup", + summary: "Reply stopped before verification evidence landed.", + }, + continuation_decision: { + selected_action: "reply_auditor_reprompt_and_continue_same_session", + summary: "Continue in the same session after the auditor reprompt.", + action_source: "reply_auditor", + unblock_task_id: "unblock-runtime-1", + }, + context_pack: { + status: "not_requested", + summary: "Fallback context pack stayed idle.", + }, + harness_request: { + status: "not_requested", + summary: "Harness escalation was not required.", + }, + }, + }, + ]; + 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", + }, + }, + ], + planningUnblockTasks: [ + { + unblock_task_id: "unblock-runtime-1", + owner: "L0", + mode: "independent_temporary_task", + trigger: "spawn_independent_temporary_unblock_task", + }, + ], + }); + // @ts-expect-error test override + global.fetch = fetchMock; + + render(); + await act(async () => { + await flushPromises(); + }); + + await waitFor(() => { + expect(screen.getByText("Runtime evaluator verdict")).toBeInTheDocument(); + }); + expect(screen.getByText("Overall verdict: continue_required")).toBeInTheDocument(); + expect(screen.getByText("DoD checker: failed")).toBeInTheDocument(); + expect(screen.getByText("Reply auditor: needs_followup")).toBeInTheDocument(); + expect(screen.getByText("Continuation decision: reply_auditor_reprompt_and_continue_same_session")).toBeInTheDocument(); + expect(screen.getByText("Context Pack: not_requested")).toBeInTheDocument(); + expect(screen.getByText("Harness Request: not_requested")).toBeInTheDocument(); + expect( + screen.getByText( + "Runtime-evaluated read-back: this report reflects the live completion evaluator. task_contract still owns execution authority; this report does not replace the contract.", + ), + ).toBeInTheDocument(); + expect(screen.getByText("Planning advisory fallback")).toBeInTheDocument(); + expect(screen.getByText("Worker prompt contracts: 1")).toBeInTheDocument(); + }); + + it("reads context pack and harness request artifacts when runtime governance produced them", async () => { + const run = { + run_id: "run_runtime_artifacts", + task_id: "task_runtime_artifacts", + status: "FAILURE", + allowed_paths: ["README.md"], + contract: { task_id: "task_runtime_artifacts" }, + manifest: { + artifacts: [ + { name: "context_pack", path: "artifacts/context_pack.json" }, + { name: "harness_request", path: "artifacts/harness_request.json" }, + ], + }, + }; + const reports = [ + { + name: "completion_governance_report.json", + data: { + authority: "runtime-evaluated-read-back", + source: "reports/completion_governance_report.json", + execution_authority: "task_contract", + overall_verdict: "continue_same_session", + dod_checker: { status: "failed", summary: "Need follow-up.", required_checks: ["repo_hygiene"], unmet_checks: ["run_status"] }, + reply_auditor: { status: "needs_follow_up", summary: "Reply was incomplete." }, + continuation_decision: { + selected_action: "reply_auditor_reprompt_and_continue_same_session", + summary: "Queued follow-up contract for the same session.", + action_source: "continuation_policy.on_incomplete", + }, + context_pack: { status: "generated", summary: "Generated ctx-pack-run_runtime_artifacts for fallback handoff." }, + harness_request: { status: "approval_required", summary: "Generated harness-run_runtime_artifacts with approval_required policy verdict." }, + }, + }, + ]; + const fetchMock = mockFetchFactory({ + events: [], + reports, + availableRuns: [], + }); + const baseFetch = fetchMock as unknown as typeof global.fetch; + global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("/api/runs/run_runtime_artifacts/artifacts?name=context_pack.json")) { + return { + ok: true, + status: 200, + json: async () => ({ + data: { + pack_id: "ctx-pack-run_runtime_artifacts", + trigger_reason: "contamination", + }, + }), + text: async () => "", + } as Response; + } + if (url.includes("/api/runs/run_runtime_artifacts/artifacts?name=harness_request.json")) { + return { + ok: true, + status: 200, + json: async () => ({ + data: { + request_id: "harness-run_runtime_artifacts", + scope: "project-local", + approval_required: true, + }, + }), + text: async () => "", + } as Response; + } + return baseFetch(input, init); + }; + + render(); + await act(async () => { + await flushPromises(); + }); + + await waitFor(() => { + expect(screen.getByText("Context Pack ID: ctx-pack-run_runtime_artifacts")).toBeInTheDocument(); + }); + expect(screen.getByText("Context Pack trigger: contamination")).toBeInTheDocument(); + expect(screen.getByText("Harness Request ID: harness-run_runtime_artifacts")).toBeInTheDocument(); + expect(screen.getByText("Harness Request scope: project-local")).toBeInTheDocument(); + expect(screen.getByText("Harness approval required: true")).toBeInTheDocument(); + }); + it("uses SSE transport and consumes stream events", async () => { const run = { run_id: "run_sse", diff --git a/apps/desktop/src/pages/RunDetailPage.tsx b/apps/desktop/src/pages/RunDetailPage.tsx index c8d8c93..1e26e86 100644 --- a/apps/desktop/src/pages/RunDetailPage.tsx +++ b/apps/desktop/src/pages/RunDetailPage.tsx @@ -126,6 +126,8 @@ export function RunDetailPage({ runId, onBack, onOpenCompare = () => {}, locale const [replayResult, setReplayResult] = useState | null>(null); const [planningContracts, setPlanningContracts] = useState>>([]); const [unblockTasks, setUnblockTasks] = useState>>([]); + const [contextPackArtifact, setContextPackArtifact] = useState | null>(null); + const [harnessRequestArtifact, setHarnessRequestArtifact] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); @@ -197,14 +199,28 @@ export function RunDetailPage({ runId, onBack, onOpenCompare = () => {}, locale const record = asRecord(item); return toStr(record.name, "") === "planning_unblock_tasks" || toStr(record.path, "") === "artifacts/planning_unblock_tasks.json"; }); + const hasContextPackArtifact = artifacts.some((item) => { + const record = asRecord(item); + return toStr(record.name, "") === "context_pack" || toStr(record.path, "") === "artifacts/context_pack.json"; + }); + const hasHarnessRequestArtifact = artifacts.some((item) => { + const record = asRecord(item); + return toStr(record.name, "") === "harness_request" || toStr(record.path, "") === "artifacts/harness_request.json"; + }); - const [planningContractsArtifactRes, unblockTasksArtifactRes] = await Promise.allSettled([ + const [planningContractsArtifactRes, unblockTasksArtifactRes, contextPackArtifactRes, harnessRequestArtifactRes] = await Promise.allSettled([ hasPlanningContractsArtifact ? fetchArtifact(runId, "planning_worker_prompt_contracts.json") : Promise.resolve(null), hasUnblockTasksArtifact ? fetchArtifact(runId, "planning_unblock_tasks.json") : Promise.resolve(null), + hasContextPackArtifact + ? fetchArtifact(runId, "context_pack.json") + : Promise.resolve(null), + hasHarnessRequestArtifact + ? fetchArtifact(runId, "harness_request.json") + : Promise.resolve(null), ]); if (loadToken !== loadTokenRef.current) { return; @@ -219,6 +235,16 @@ export function RunDetailPage({ runId, onBack, onOpenCompare = () => {}, locale ? toArr(unblockTasksArtifactRes.value.data as Array>) : [], ); + setContextPackArtifact( + contextPackArtifactRes.status === "fulfilled" && contextPackArtifactRes.value && contextPackArtifactRes.value.data && typeof contextPackArtifactRes.value.data === "object" + ? (contextPackArtifactRes.value.data as Record) + : null, + ); + setHarnessRequestArtifact( + harnessRequestArtifactRes.status === "fulfilled" && harnessRequestArtifactRes.value && harnessRequestArtifactRes.value.data && typeof harnessRequestArtifactRes.value.data === "object" + ? (harnessRequestArtifactRes.value.data as Record) + : null, + ); } catch (err) { setError(sanitizeUiError(err, "Run detail failed to load")); } finally { @@ -397,6 +423,7 @@ export function RunDetailPage({ runId, onBack, onOpenCompare = () => {}, locale const evidenceReport = reports.find(r => r.name === "evidence_report.json")?.data; const incidentPack = reports.find(r => r.name === "incident_pack.json")?.data as Record | undefined; const proofPack = reports.find(r => r.name === "proof_pack.json")?.data as Record | undefined; + const completionGovernanceReport = asRecord(reports.find(r => r.name === "completion_governance_report.json")?.data); const runCompareReport = asRecord(reports.find(r => r.name === "run_compare_report.json")?.data); const compareSummary = asRecord(runCompareReport.compare_summary); const chainReport = reports.find(r => r.name === "chain_report.json")?.data; @@ -437,6 +464,28 @@ export function RunDetailPage({ runId, onBack, onOpenCompare = () => {}, locale const unblockOwners = Array.from(new Set(unblockTasks.map((task) => toStr(asRecord(task).owner, "")).filter(Boolean))); const unblockModes = Array.from(new Set(unblockTasks.map((task) => toStr(asRecord(task).mode, "")).filter(Boolean))); const unblockTriggers = Array.from(new Set(unblockTasks.map((task) => toStr(asRecord(task).trigger, "")).filter(Boolean))); + const hasRuntimeCompletionGovernance = Object.keys(completionGovernanceReport).length > 0; + const runtimeDodChecker = asRecord(completionGovernanceReport.dod_checker); + const runtimeReplyAuditor = asRecord(completionGovernanceReport.reply_auditor); + const runtimeContinuationDecision = asRecord(completionGovernanceReport.continuation_decision); + const runtimeContextPack = asRecord(completionGovernanceReport.context_pack); + const runtimeHarnessRequest = asRecord(completionGovernanceReport.harness_request); + const contextPackRecord = asRecord(contextPackArtifact); + const harnessRequestRecord = asRecord(harnessRequestArtifact); + const runtimeDodRequiredChecks = Array.from( + new Set( + toArr(runtimeDodChecker.required_checks as unknown[] | null | undefined) + .map((item) => toStr(item, "")) + .filter(Boolean), + ), + ); + const runtimeDodUnmetChecks = Array.from( + new Set( + toArr(runtimeDodChecker.unmet_checks as unknown[] | null | undefined) + .map((item) => toStr(item, "")) + .filter(Boolean), + ), + ); const semanticType = outcomeSemantic(run.outcome_type, run.status, run.failure_class, run.failure_code); const outcomeSemanticText = outcomeSemanticLabel( run.outcome_type, @@ -546,34 +595,100 @@ export function RunDetailPage({ runId, onBack, onOpenCompare = () => {}, locale
{runDetailCopy.bindingReadModel.readOnlyNote}
) : null} - {planningContracts.length > 0 || unblockTasks.length > 0 ? ( + {hasRuntimeCompletionGovernance || planningContracts.length > 0 || unblockTasks.length > 0 ? (
{completionGovernanceCopy.title}
-
-
{completionGovernanceCopy.workerPromptContracts}{planningContracts.length}
- {unblockTasks.length > 0 ? ( -
{completionGovernanceCopy.unblockTasks}{unblockTasks.length}
- ) : null} - {continuationOnIncomplete.length > 0 ? ( -
{completionGovernanceCopy.onIncomplete}{continuationOnIncomplete.join(" / ")}
- ) : null} - {continuationOnBlocked.length > 0 ? ( -
{completionGovernanceCopy.onBlocked}{continuationOnBlocked.join(" / ")}
- ) : null} - {doneChecks.length > 0 ? ( -
{completionGovernanceCopy.doneChecks}{doneChecks.join(" / ")}
- ) : null} - {unblockOwners.length > 0 ? ( -
{completionGovernanceCopy.unblockOwner}{unblockOwners.join(" / ")}
- ) : null} - {unblockModes.length > 0 ? ( -
{completionGovernanceCopy.unblockMode}{unblockModes.join(" / ")}
- ) : null} - {unblockTriggers.length > 0 ? ( -
{completionGovernanceCopy.unblockTrigger}{unblockTriggers.join(" / ")}
- ) : null} -
-
{completionGovernanceCopy.advisoryNote}
+ {hasRuntimeCompletionGovernance ? ( + <> +
{completionGovernanceCopy.runtimeTitle}
+
+
{completionGovernanceCopy.overallVerdict}{toStr(completionGovernanceReport.overall_verdict)}
+
{completionGovernanceCopy.reportAuthority}{toStr(completionGovernanceReport.authority)}
+
{completionGovernanceCopy.reportSource}{toStr(completionGovernanceReport.source)}
+
{completionGovernanceCopy.reportExecutionAuthority}{toStr(completionGovernanceReport.execution_authority)}
+
{completionGovernanceCopy.dodChecker}{toStr(runtimeDodChecker.status)}
+ {toStr(runtimeDodChecker.summary, "") ? ( +
{completionGovernanceCopy.dodSummary}{toStr(runtimeDodChecker.summary, "")}
+ ) : null} + {runtimeDodRequiredChecks.length > 0 ? ( +
{completionGovernanceCopy.dodRequiredChecks}{runtimeDodRequiredChecks.join(" / ")}
+ ) : null} + {runtimeDodUnmetChecks.length > 0 ? ( +
{completionGovernanceCopy.dodUnmetChecks}{runtimeDodUnmetChecks.join(" / ")}
+ ) : null} +
{completionGovernanceCopy.replyAuditor}{toStr(runtimeReplyAuditor.status)}
+ {toStr(runtimeReplyAuditor.summary, "") ? ( +
{completionGovernanceCopy.replySummary}{toStr(runtimeReplyAuditor.summary, "")}
+ ) : null} +
{completionGovernanceCopy.continuationDecision}{toStr(runtimeContinuationDecision.selected_action)}
+ {toStr(runtimeContinuationDecision.summary, "") ? ( +
{completionGovernanceCopy.continuationSummary}{toStr(runtimeContinuationDecision.summary, "")}
+ ) : null} + {toStr(runtimeContinuationDecision.action_source, "") ? ( +
{completionGovernanceCopy.actionSource}{toStr(runtimeContinuationDecision.action_source, "")}
+ ) : null} + {toStr(runtimeContinuationDecision.unblock_task_id, "") ? ( +
{completionGovernanceCopy.selectedUnblockTask}{toStr(runtimeContinuationDecision.unblock_task_id, "")}
+ ) : null} +
{completionGovernanceCopy.contextPack}{toStr(runtimeContextPack.status)}
+ {toStr(runtimeContextPack.summary, "") ? ( +
{completionGovernanceCopy.contextPackSummary}{toStr(runtimeContextPack.summary, "")}
+ ) : null} + {toStr(contextPackRecord.pack_id, "") ? ( +
{completionGovernanceCopy.contextPackId}{toStr(contextPackRecord.pack_id, "")}
+ ) : null} + {toStr(contextPackRecord.trigger_reason, "") ? ( +
{completionGovernanceCopy.contextPackTrigger}{toStr(contextPackRecord.trigger_reason, "")}
+ ) : null} +
{completionGovernanceCopy.harnessRequest}{toStr(runtimeHarnessRequest.status)}
+ {toStr(runtimeHarnessRequest.summary, "") ? ( +
{completionGovernanceCopy.harnessRequestSummary}{toStr(runtimeHarnessRequest.summary, "")}
+ ) : null} + {toStr(harnessRequestRecord.request_id, "") ? ( +
{completionGovernanceCopy.harnessRequestId}{toStr(harnessRequestRecord.request_id, "")}
+ ) : null} + {toStr(harnessRequestRecord.scope, "") ? ( +
{completionGovernanceCopy.harnessRequestScope}{toStr(harnessRequestRecord.scope, "")}
+ ) : null} + {harnessRequestRecord.approval_required !== undefined ? ( +
{completionGovernanceCopy.harnessRequestApproval}{toStr(harnessRequestRecord.approval_required)}
+ ) : null} +
+
{completionGovernanceCopy.runtimeNote}
+ + ) : null} + {planningContracts.length > 0 || unblockTasks.length > 0 ? ( + <> + {hasRuntimeCompletionGovernance ? ( +
{completionGovernanceCopy.planningFallbackTitle}
+ ) : null} +
+
{completionGovernanceCopy.workerPromptContracts}{planningContracts.length}
+ {unblockTasks.length > 0 ? ( +
{completionGovernanceCopy.unblockTasks}{unblockTasks.length}
+ ) : null} + {continuationOnIncomplete.length > 0 ? ( +
{completionGovernanceCopy.onIncomplete}{continuationOnIncomplete.join(" / ")}
+ ) : null} + {continuationOnBlocked.length > 0 ? ( +
{completionGovernanceCopy.onBlocked}{continuationOnBlocked.join(" / ")}
+ ) : null} + {doneChecks.length > 0 ? ( +
{completionGovernanceCopy.doneChecks}{doneChecks.join(" / ")}
+ ) : null} + {unblockOwners.length > 0 ? ( +
{completionGovernanceCopy.unblockOwner}{unblockOwners.join(" / ")}
+ ) : null} + {unblockModes.length > 0 ? ( +
{completionGovernanceCopy.unblockMode}{unblockModes.join(" / ")}
+ ) : null} + {unblockTriggers.length > 0 ? ( +
{completionGovernanceCopy.unblockTrigger}{unblockTriggers.join(" / ")}
+ ) : null} +
+
{completionGovernanceCopy.advisoryNote}
+ + ) : null}
) : null} diff --git a/apps/desktop/src/pages/run_detail_page_controls.test.tsx b/apps/desktop/src/pages/run_detail_page_controls.test.tsx index e0bf67d..8d73df7 100644 --- a/apps/desktop/src/pages/run_detail_page_controls.test.tsx +++ b/apps/desktop/src/pages/run_detail_page_controls.test.tsx @@ -425,6 +425,172 @@ describe("RunDetailPage p0 controls", () => { expect(screen.getByText("L0")).toBeInTheDocument(); }); + it("prefers runtime completion governance read-back when a runtime report exists", async () => { + vi.mocked(fetchRun).mockResolvedValueOnce( + makeRun({ + manifest: { + artifacts: [ + { name: "planning_worker_prompt_contracts", path: "artifacts/planning_worker_prompt_contracts.json" }, + { name: "planning_unblock_tasks", path: "artifacts/planning_unblock_tasks.json" }, + ], + }, + }), + ); + vi.mocked(fetchReports).mockResolvedValueOnce([ + { + name: "completion_governance_report.json", + data: { + authority: "runtime-evaluated-read-back", + source: "reports/completion_governance_report.json", + execution_authority: "task_contract", + overall_verdict: "continue_required", + dod_checker: { + status: "failed", + summary: "Missing test_report before completion.", + required_checks: ["repo_hygiene", "test_report"], + unmet_checks: ["test_report"], + }, + reply_auditor: { + status: "needs_followup", + summary: "Reply stopped before verification evidence landed.", + }, + continuation_decision: { + selected_action: "reply_auditor_reprompt_and_continue_same_session", + summary: "Continue in the same session after the auditor reprompt.", + action_source: "reply_auditor", + unblock_task_id: "unblock-runtime-1", + }, + context_pack: { + status: "not_requested", + summary: "Fallback context pack stayed idle.", + }, + harness_request: { + status: "not_requested", + summary: "Harness escalation was not required.", + }, + }, + }, + ] as any); + vi.mocked(fetchArtifact) + .mockResolvedValueOnce({ + data: [ + { + prompt_contract_id: "worker-1", + continuation_policy: { + on_incomplete: "reply_auditor_reprompt_and_continue_same_session", + on_blocked: "spawn_independent_temporary_unblock_task", + }, + done_definition: { acceptance_checks: ["repo_hygiene", "test_report"] }, + }, + ], + } as any) + .mockResolvedValueOnce({ + data: [ + { + unblock_task_id: "unblock-runtime-1", + owner: "L0", + mode: "independent_temporary_task", + trigger: "spawn_independent_temporary_unblock_task", + }, + ], + } as any); + + render(); + + expect(await screen.findByRole("heading", { name: "run-001" })).toBeInTheDocument(); + expect(screen.getByText("Runtime evaluator verdict")).toBeInTheDocument(); + expect(screen.getByText("Overall verdict")).toBeInTheDocument(); + expect(screen.getByText("continue_required")).toBeInTheDocument(); + expect(screen.getByText("DoD checker")).toBeInTheDocument(); + expect(screen.getByText("failed")).toBeInTheDocument(); + expect(screen.getByText("Reply auditor")).toBeInTheDocument(); + expect(screen.getByText("needs_followup")).toBeInTheDocument(); + expect(screen.getByText("Continuation decision")).toBeInTheDocument(); + expect(screen.getByText("Context Pack")).toBeInTheDocument(); + expect(screen.getByText("Harness Request")).toBeInTheDocument(); + expect( + screen.getByText( + "Runtime-evaluated read-back: this report reflects the live completion evaluator. task_contract still owns execution authority; this report does not replace the contract.", + ), + ).toBeInTheDocument(); + expect(screen.getByText("Planning advisory fallback")).toBeInTheDocument(); + expect(screen.getByText("Worker prompt contracts")).toBeInTheDocument(); + }); + + it("reads context pack and harness request artifacts when runtime governance produced them", async () => { + vi.mocked(fetchRun).mockResolvedValueOnce( + makeRun({ + manifest: { + artifacts: [ + { name: "context_pack", path: "artifacts/context_pack.json" }, + { name: "harness_request", path: "artifacts/harness_request.json" }, + ], + }, + }), + ); + vi.mocked(fetchReports).mockResolvedValueOnce([ + { + name: "completion_governance_report.json", + data: { + authority: "runtime-evaluated-read-back", + source: "reports/completion_governance_report.json", + execution_authority: "task_contract", + overall_verdict: "continue_same_session", + dod_checker: { + status: "failed", + summary: "Need follow-up.", + required_checks: ["repo_hygiene"], + unmet_checks: ["run_status"], + }, + reply_auditor: { + status: "needs_follow_up", + summary: "Reply was incomplete.", + }, + continuation_decision: { + selected_action: "reply_auditor_reprompt_and_continue_same_session", + summary: "Queued follow-up contract for the same session.", + action_source: "continuation_policy.on_incomplete", + }, + context_pack: { + status: "generated", + summary: "Generated ctx-pack-run-001 for fallback handoff.", + }, + harness_request: { + status: "approval_required", + summary: "Generated harness-run-001 with approval_required policy verdict.", + }, + }, + }, + ] as any); + vi.mocked(fetchArtifact) + .mockResolvedValueOnce({ + data: { + pack_id: "ctx-pack-run-001", + trigger_reason: "contamination", + }, + } as any) + .mockResolvedValueOnce({ + data: { + request_id: "harness-run-001", + scope: "project-local", + approval_required: true, + }, + } as any); + + render(); + + expect(await screen.findByRole("heading", { name: "run-001" })).toBeInTheDocument(); + expect(screen.getByText("Context Pack ID")).toBeInTheDocument(); + expect(screen.getByText("ctx-pack-run-001")).toBeInTheDocument(); + expect(screen.getByText("Context Pack trigger")).toBeInTheDocument(); + expect(screen.getByText("contamination")).toBeInTheDocument(); + expect(screen.getByText("Harness Request ID")).toBeInTheDocument(); + expect(screen.getByText("harness-run-001")).toBeInTheDocument(); + expect(screen.getByText("Harness Request scope")).toBeInTheDocument(); + expect(screen.getByText("project-local")).toBeInTheDocument(); + expect(screen.getByText("Harness approval required")).toBeInTheDocument(); + }); + it("recovers from error state after retry load", async () => { const user = userEvent.setup(); vi.mocked(fetchRun).mockRejectedValueOnce(new Error("first fail")); diff --git a/apps/orchestrator/src/cortexpilot_orch/scheduler/completion_governance.py b/apps/orchestrator/src/cortexpilot_orch/scheduler/completion_governance.py new file mode 100644 index 0000000..b292805 --- /dev/null +++ b/apps/orchestrator/src/cortexpilot_orch/scheduler/completion_governance.py @@ -0,0 +1,458 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + + +def _load_json_artifact(run_dir: Path, filename: str) -> Any: + path = run_dir / "artifacts" / filename + if not path.exists(): + return None + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + + +def _normalize_records(payload: Any) -> list[dict[str, Any]]: + if not isinstance(payload, list): + return [] + normalized: list[dict[str, Any]] = [] + for item in payload: + if isinstance(item, dict): + normalized.append(dict(item)) + return normalized + + +def _coerce_gate_passed(task_result: dict[str, Any] | None, gate_name: str) -> bool: + if not isinstance(task_result, dict): + return False + gates = task_result.get("gates") + if not isinstance(gates, dict): + return False + gate = gates.get(gate_name) + return bool(isinstance(gate, dict) and gate.get("passed")) + + +def _collect_required_checks( + planning_contracts: list[dict[str, Any]], + *, + contract: dict[str, Any], +) -> list[str]: + required_checks: list[str] = [] + for planning_contract in planning_contracts: + done_definition = planning_contract.get("done_definition") + if not isinstance(done_definition, dict): + continue + checks = done_definition.get("acceptance_checks") + if not isinstance(checks, list): + continue + for item in checks: + value = str(item).strip() + if value and value not in required_checks: + required_checks.append(value) + if required_checks: + return required_checks + acceptance_tests = contract.get("acceptance_tests") + if isinstance(acceptance_tests, list) and acceptance_tests: + required_checks.extend(["test_report"]) + return required_checks or ["diff_gate", "policy_gate", "review_report", "test_report"] + + +def _check_required_item( + check_name: str, + *, + task_result: dict[str, Any] | None, + test_report: dict[str, Any] | None, + review_report: dict[str, Any] | None, + run_dir: Path, + status: str, +) -> tuple[bool, str]: + normalized = check_name.strip().lower() + if normalized in {"diff_gate"}: + return _coerce_gate_passed(task_result, "diff_gate"), check_name + if normalized in {"policy_gate", "repo_hygiene"}: + return _coerce_gate_passed(task_result, "policy_gate"), check_name + if normalized in {"review_gate", "review_report"}: + review_pass = _coerce_gate_passed(task_result, "review_gate") + verdict_pass = isinstance(review_report, dict) and str(review_report.get("verdict") or "").upper() == "PASS" + return review_pass and verdict_pass, check_name + if normalized in {"tests_gate", "test_report"}: + tests_pass = _coerce_gate_passed(task_result, "tests_gate") + report_pass = isinstance(test_report, dict) and str(test_report.get("status") or "").upper() == "PASS" + return tests_pass and report_pass, check_name + if normalized == "task_result": + return str(status).upper() == "SUCCESS", check_name + if normalized == "evidence_report": + return (run_dir / "reports" / "evidence_report.json").exists(), check_name + if normalized == "prompt_artifact": + return (run_dir / "artifacts" / "prompt_artifact.json").exists(), check_name + return False, f"unknown:{check_name}" + + +def _collect_policy_action( + planning_contracts: list[dict[str, Any]], + field_name: str, +) -> str: + for planning_contract in planning_contracts: + continuation_policy = planning_contract.get("continuation_policy") + if not isinstance(continuation_policy, dict): + continue + value = str(continuation_policy.get(field_name) or "").strip() + if value: + return value + return "" + + +def _extract_thread_id( + *, + contract: dict[str, Any], + task_result: dict[str, Any] | None, + run_dir: Path, +) -> str: + if isinstance(task_result, dict): + evidence_refs = task_result.get("evidence_refs") + if isinstance(evidence_refs, dict): + thread_id = str(evidence_refs.get("thread_id") or evidence_refs.get("codex_thread_id") or "").strip() + if thread_id: + return thread_id + assigned_agent = contract.get("assigned_agent") + if isinstance(assigned_agent, dict): + thread_id = str(assigned_agent.get("codex_thread_id") or "").strip() + if thread_id: + return thread_id + return run_dir.name + + +def _detect_context_pack_trigger( + *, + failure_reason: str, + reply_auditor: dict[str, Any], +) -> str: + normalized_reason = failure_reason.lower() + trigger_pairs = [ + ("context_pressure", ["context pressure", "context limit", "token limit"]), + ("contamination", ["contamination", "context contamination", "poisoned context"]), + ("role_switch", ["role switch", "handoff to another role"]), + ("phase_switch", ["phase switch", "stage switch", "next phase"]), + ("repetition", ["repetition", "repeat", "looping reply"]), + ("distortion", ["distortion", "garbled", "misread"]), + ] + for trigger, phrases in trigger_pairs: + if any(phrase in normalized_reason for phrase in phrases): + return trigger + signals = reply_auditor.get("signals") + if isinstance(signals, list): + normalized_signals = [str(item).strip().lower() for item in signals if str(item).strip()] + if any("repetition" in item for item in normalized_signals): + return "repetition" + if any("contamination" in item for item in normalized_signals): + return "contamination" + return "" + + +def _build_context_pack_artifact( + *, + contract: dict[str, Any], + run_dir: Path, + failure_reason: str, + continuation_summary: str, + reply_auditor: dict[str, Any], +) -> dict[str, Any] | None: + trigger_reason = _detect_context_pack_trigger( + failure_reason=failure_reason, + reply_auditor=reply_auditor, + ) + if not trigger_reason: + return None + assigned_agent = contract.get("assigned_agent") if isinstance(contract.get("assigned_agent"), dict) else {} + objective = str(contract.get("objective") or "").strip() or "Continue the current scope safely." + source_role = str(assigned_agent.get("role") or "WORKER").strip() or "WORKER" + thread_id = _extract_thread_id(contract=contract, task_result=None, run_dir=run_dir) + return { + "version": "v1", + "pack_id": f"ctx-pack-{run_dir.name}", + "role_scope": "L1", + "source_session_id": thread_id, + "source_role": source_role, + "trigger_reason": trigger_reason, + "global_state_summary": ( + f"The current run for '{objective}' hit a {trigger_reason} fallback condition and needs an explicit handoff." + ), + "actor_handoff_summary": continuation_summary, + "required_reads": [ + "contract.json", + "reports/task_result.json", + "reports/completion_governance_report.json", + ], + "optional_reads": [ + "reports/review_report.json", + "reports/test_report.json", + "events.jsonl", + ], + "conversation_exports": ["events.jsonl"], + "artifact_refs": [ + "reports/task_result.json", + "reports/completion_governance_report.json", + ], + } + + +def _derive_harness_request_artifact( + *, + contract: dict[str, Any], + task_result: dict[str, Any] | None, + run_dir: Path, +) -> tuple[dict[str, Any] | None, str]: + if not isinstance(task_result, dict): + return None, "not_requested" + gates = task_result.get("gates") + if not isinstance(gates, dict): + return None, "not_requested" + policy_gate = gates.get("policy_gate") + if not isinstance(policy_gate, dict): + return None, "not_requested" + violations = policy_gate.get("violations") + if not isinstance(violations, list) or not violations: + return None, "not_requested" + normalized_violations = [str(item).strip() for item in violations if str(item).strip()] + if not normalized_violations: + return None, "not_requested" + + project_level_signals = {"network_gate", "mcp_gate", "human_approval_required"} + scope = "project-local" if any(item in project_level_signals for item in normalized_violations) else "session-local" + approval_state = "approval_required" if scope == "project-local" else "auto_approved" + assigned_agent = contract.get("assigned_agent") if isinstance(contract.get("assigned_agent"), dict) else {} + runtime_options = contract.get("runtime_options") if isinstance(contract.get("runtime_options"), dict) else {} + + requested_capabilities = { + "skills": ["continuation-hardening"], + "mcp_servers": ["runtime-governance"] if "mcp_gate" in normalized_violations else [], + "permission_changes": [], + "runtime_bindings": [str(runtime_options.get("provider") or runtime_options.get("runner") or "codex")], + } + if "network_gate" in normalized_violations: + requested_capabilities["permission_changes"].append("network.allow") + if "tool_gate" in normalized_violations: + requested_capabilities["permission_changes"].append("tool.allow") + if "sampling_gate" in normalized_violations: + requested_capabilities["permission_changes"].append("sampling.allow") + if "human_approval_required" in normalized_violations: + requested_capabilities["permission_changes"].append("approval.resume") + + request = { + "version": "v1", + "request_id": f"harness-{run_dir.name}", + "scope": scope, + "requested_by": { + "role": str(assigned_agent.get("role") or "WORKER").strip() or "WORKER", + "agent_id": str(assigned_agent.get("agent_id") or "agent-1").strip() or "agent-1", + }, + "reason": ( + "Runtime completion governance detected policy-gate blockers and generated a harness evolution request " + f"for {', '.join(normalized_violations)}." + ), + "requested_capabilities": requested_capabilities, + "risk_level": "medium" if scope == "project-local" else "low", + "approval_required": scope != "session-local", + "rollback_plan": "Remove the temporary capability request and restore the current runtime/tool permission posture.", + "validation_plan": "Rerun repo hygiene, targeted runtime tests, and the affected operator read-back after apply.", + } + return request, approval_state + + +def _build_dod_checker( + *, + required_checks: list[str], + task_result: dict[str, Any] | None, + test_report: dict[str, Any] | None, + review_report: dict[str, Any] | None, + run_dir: Path, + status: str, +) -> dict[str, Any]: + unmet_checks: list[str] = [] + for check_name in required_checks: + ok, label = _check_required_item( + check_name, + task_result=task_result, + test_report=test_report, + review_report=review_report, + run_dir=run_dir, + status=status, + ) + if not ok: + unmet_checks.append(label) + if str(status).upper() != "SUCCESS" and "run_status" not in unmet_checks: + unmet_checks.append("run_status") + if unmet_checks: + return { + "status": "failed", + "summary": "Required completion checks are still missing or failed.", + "required_checks": required_checks, + "unmet_checks": unmet_checks, + } + return { + "status": "passed", + "summary": "All completion checks required by the current run passed.", + "required_checks": required_checks, + "unmet_checks": [], + } + + +def _build_reply_auditor( + *, + status: str, + failure_reason: str, + dod_checker: dict[str, Any], + unblock_tasks: list[dict[str, Any]], +) -> dict[str, Any]: + signals: list[str] = [] + if str(status).upper() != "SUCCESS": + signals.append("run_status_not_success") + if failure_reason: + signals.append("failure_reason_present") + if dod_checker.get("status") != "passed": + signals.append("dod_unmet") + if unblock_tasks and signals: + return { + "status": "blocked", + "summary": "The run ended with blocker signals and an unblock path is available.", + "signals": signals, + } + if signals: + return { + "status": "needs_follow_up", + "summary": "The run still needs follow-up before it can be treated as complete.", + "signals": signals, + } + return { + "status": "accepted", + "summary": "The run result passed the current reply audit checks.", + "signals": [], + } + + +def _queue_unblock_tasks( + unblock_tasks: list[dict[str, Any]], +) -> tuple[list[dict[str, Any]] | None, str]: + if not unblock_tasks: + return None, "" + updated: list[dict[str, Any]] = [] + selected_id = "" + for index, task in enumerate(unblock_tasks): + task_copy = dict(task) + if index == 0: + task_copy["status"] = "queued" + selected_id = str(task_copy.get("unblock_task_id") or "") + updated.append(task_copy) + return updated, selected_id + + +def evaluate_completion_governance( + *, + contract: dict[str, Any], + run_dir: Path, + task_result: dict[str, Any] | None, + test_report: dict[str, Any] | None, + review_report: dict[str, Any] | None, + status: str, + failure_reason: str, + generated_at: str, +) -> tuple[dict[str, Any], list[dict[str, Any]] | None]: + planning_contracts = _normalize_records(_load_json_artifact(run_dir, "planning_worker_prompt_contracts.json")) + unblock_tasks = _normalize_records(_load_json_artifact(run_dir, "planning_unblock_tasks.json")) + required_checks = _collect_required_checks(planning_contracts, contract=contract) + dod_checker = _build_dod_checker( + required_checks=required_checks, + task_result=task_result, + test_report=test_report, + review_report=review_report, + run_dir=run_dir, + status=status, + ) + reply_auditor = _build_reply_auditor( + status=status, + failure_reason=failure_reason, + dod_checker=dod_checker, + unblock_tasks=unblock_tasks, + ) + on_incomplete = _collect_policy_action(planning_contracts, "on_incomplete") + on_blocked = _collect_policy_action(planning_contracts, "on_blocked") + + updated_unblock_tasks: list[dict[str, Any]] | None = None + unblock_task_id = "" + selected_action = "none" + action_source = "none" + overall_verdict: str + continuation_summary: str + + if reply_auditor["status"] == "blocked" and on_blocked: + selected_action = on_blocked + action_source = "continuation_policy.on_blocked" + updated_unblock_tasks, unblock_task_id = _queue_unblock_tasks(unblock_tasks) + if updated_unblock_tasks: + overall_verdict = "queue_unblock_task" + continuation_summary = "The run is blocked. Queue the L0-managed unblock task before continuing." + else: + overall_verdict = "manual_triage" + continuation_summary = "The run is blocked, but no persisted unblock task is available. Manual triage is required." + elif reply_auditor["status"] == "needs_follow_up" and on_incomplete: + selected_action = on_incomplete + action_source = "continuation_policy.on_incomplete" + overall_verdict = "continue_same_session" + continuation_summary = "The run still needs follow-up. Continue the same session under the reply-auditor policy." + elif reply_auditor["status"] == "accepted" and dod_checker["status"] == "passed": + overall_verdict = "complete" + continuation_summary = "All completion governance checks passed. No continuation is required." + else: + overall_verdict = "manual_triage" + continuation_summary = "Completion governance could not select a safe automatic continuation path." + + context_pack_artifact = _build_context_pack_artifact( + contract=contract, + run_dir=run_dir, + failure_reason=failure_reason, + continuation_summary=continuation_summary, + reply_auditor=reply_auditor, + ) + harness_request_artifact, harness_policy_state = _derive_harness_request_artifact( + contract=contract, + task_result=task_result, + run_dir=run_dir, + ) + + report = { + "report_type": "completion_governance_report", + "generated_at": generated_at, + "authority": "completion-governance-runtime", + "source": "finalize_run", + "execution_authority": "task_contract", + "overall_verdict": overall_verdict, + "dod_checker": dod_checker, + "reply_auditor": reply_auditor, + "continuation_decision": { + "status": "selected" if selected_action != "none" else "none", + "selected_action": selected_action, + "action_source": action_source, + "unblock_task_id": unblock_task_id, + "summary": continuation_summary, + }, + "context_pack": { + "status": "generated" if context_pack_artifact else "not_requested", + "summary": ( + f"Generated {context_pack_artifact['pack_id']} for fallback handoff." + if context_pack_artifact + else "No fallback Context Pack was requested for this run." + ), + }, + "harness_request": { + "status": harness_policy_state, + "summary": ( + f"Generated {harness_request_artifact['request_id']} with {harness_policy_state} policy verdict." + if harness_request_artifact + else "No harness evolution request was needed for this run." + ), + }, + } + return report, updated_unblock_tasks, context_pack_artifact, harness_request_artifact diff --git a/apps/orchestrator/src/cortexpilot_orch/scheduler/scheduler_bridge_finalize.py b/apps/orchestrator/src/cortexpilot_orch/scheduler/scheduler_bridge_finalize.py index 255e575..da0c1e6 100644 --- a/apps/orchestrator/src/cortexpilot_orch/scheduler/scheduler_bridge_finalize.py +++ b/apps/orchestrator/src/cortexpilot_orch/scheduler/scheduler_bridge_finalize.py @@ -1,5 +1,7 @@ from __future__ import annotations +import hashlib +import json from collections.abc import Callable from pathlib import Path from typing import Any @@ -7,6 +9,7 @@ from cortexpilot_orch.contract.validator import ContractValidator from cortexpilot_orch.scheduler import ( artifact_refs, + completion_governance, core_helpers, evidence_pipeline, gate_orchestration, @@ -14,6 +17,7 @@ test_pipeline, ) from cortexpilot_orch.scheduler.runtime_utils import schema_root, write_manifest +from cortexpilot_orch.queue.store import QueueStore from cortexpilot_orch.store.run_store import RunStore from cortexpilot_orch.temporal.manager import notify_run_completed from cortexpilot_orch.worktrees import manager as worktree_manager @@ -269,6 +273,282 @@ def finalize_run( path="reports/evidence_report.json", ) + def _sha256_text(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + def _append_input_artifact( + follow_up_contract: dict[str, Any], + *, + name: str, + uri: str, + sha256: str, + ) -> None: + inputs = follow_up_contract.get("inputs") + if not isinstance(inputs, dict): + inputs = {"spec": "", "artifacts": []} + artifacts = inputs.get("artifacts") + if not isinstance(artifacts, list): + artifacts = [] + artifacts.append({"name": name, "uri": uri, "sha256": sha256}) + inputs["artifacts"] = artifacts + follow_up_contract["inputs"] = inputs + + def _build_follow_up_contract( + *, + task_id_override: str, + spec: str, + preserve_thread: bool, + ) -> dict[str, Any]: + follow_up_contract = json.loads(json.dumps(contract, ensure_ascii=False)) + follow_up_contract["task_id"] = task_id_override + follow_up_contract["parent_task_id"] = task_id + inputs = follow_up_contract.get("inputs") + if not isinstance(inputs, dict): + inputs = {"spec": spec, "artifacts": []} + inputs["spec"] = spec + if not isinstance(inputs.get("artifacts"), list): + inputs["artifacts"] = [] + follow_up_contract["inputs"] = inputs + if not preserve_thread: + assigned_agent = follow_up_contract.get("assigned_agent") + if isinstance(assigned_agent, dict): + assigned_agent.pop("codex_thread_id", None) + owner_agent = follow_up_contract.get("owner_agent") + if isinstance(owner_agent, dict): + owner_agent.pop("codex_thread_id", None) + return follow_up_contract + + def _queue_follow_up_contract( + *, + follow_up_contract: dict[str, Any], + artifact_name: str, + task_id_for_queue: str, + reason: str, + ) -> dict[str, Any]: + artifact_path = store.write_artifact( + run_id, + artifact_name, + json.dumps(follow_up_contract, ensure_ascii=False, indent=2), + ) + queue_store = QueueStore(queue_path=run_dir.parent.parent / "queue.jsonl") + owner_agent = contract.get("owner_agent") if isinstance(contract.get("owner_agent"), dict) else {} + assigned_agent = contract.get("assigned_agent") if isinstance(contract.get("assigned_agent"), dict) else {} + workflow_meta = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {} + queue_item = queue_store.enqueue( + artifact_path.resolve(), + task_id_for_queue, + owner=str(owner_agent.get("agent_id") or assigned_agent.get("agent_id") or "").strip(), + metadata={ + "workflow_id": str(workflow_meta.get("workflow_id") or "").strip(), + "source_run_id": run_id, + "priority": 0, + "reason": reason, + }, + ) + return { + "artifact_path": str(artifact_path), + "queue_item": queue_item, + } + + if isinstance(final_task_result, dict): + final_task_result["status"] = "SUCCESS" if status == "SUCCESS" else "FAILED" + final_task_result["failure"] = {"message": failure_reason} if failure_reason else None + if not final_task_result.get("summary") and failure_reason: + final_task_result["summary"] = failure_reason + + ( + completion_governance_report, + updated_unblock_tasks, + context_pack_artifact, + harness_request_artifact, + ) = completion_governance.evaluate_completion_governance( + contract=contract, + run_dir=run_dir, + task_result=final_task_result if isinstance(final_task_result, dict) else task_result, + test_report=test_report, + review_report=review_report, + status=status, + failure_reason=failure_reason, + generated_at=finished_at, + ) + try: + report_validator.validate_report(completion_governance_report, "completion_governance_report.v1.json") + except Exception as exc: # noqa: BLE001 + failure_reason = failure_reason or f"completion_governance_report schema invalid: {exc}" + status = "FAILURE" + append_gate_failed_fn( + store, + run_id, + "schema_validation", + str(exc), + schema="completion_governance_report.v1.json", + path="reports/completion_governance_report.json", + ) + else: + if updated_unblock_tasks is not None: + store.write_artifact( + run_id, + "planning_unblock_tasks.json", + json.dumps(updated_unblock_tasks, ensure_ascii=False, indent=2), + ) + if context_pack_artifact is not None: + context_pack_path = store.write_artifact( + run_id, + "context_pack.json", + json.dumps(context_pack_artifact, ensure_ascii=False, indent=2), + ) + store.append_event( + run_id, + { + "level": "INFO", + "event": "CONTEXT_PACK_GENERATED", + "run_id": run_id, + "meta": { + "pack_id": context_pack_artifact.get("pack_id"), + "trigger_reason": context_pack_artifact.get("trigger_reason"), + }, + }, + ) + if harness_request_artifact is not None: + store.write_artifact( + run_id, + "harness_request.json", + json.dumps(harness_request_artifact, ensure_ascii=False, indent=2), + ) + store.append_event( + run_id, + { + "level": "INFO", + "event": "HARNESS_REQUEST_CREATED", + "run_id": run_id, + "meta": { + "request_id": harness_request_artifact.get("request_id"), + "scope": harness_request_artifact.get("scope"), + "approval_required": harness_request_artifact.get("approval_required"), + }, + }, + ) + if isinstance(final_task_result, dict): + continuation_decision = completion_governance_report.get("continuation_decision", {}) + if isinstance(continuation_decision, dict): + selected_action = str(continuation_decision.get("selected_action") or "").strip() + if selected_action == "reply_auditor_reprompt_and_continue_same_session": + follow_up_contract = _build_follow_up_contract( + task_id_override=f"continue-{task_id}", + spec=( + "Continue the same session after completion governance marked the reply incomplete. " + f"Follow-up reason: {str(continuation_decision.get('summary') or '').strip()}" + ), + preserve_thread=True, + ) + if context_pack_artifact is not None: + context_pack_text = json.dumps(context_pack_artifact, ensure_ascii=False, indent=2) + _append_input_artifact( + follow_up_contract, + name="context_pack.json", + uri=str(context_pack_path), + sha256=_sha256_text(context_pack_text), + ) + follow_up_queue = _queue_follow_up_contract( + follow_up_contract=follow_up_contract, + artifact_name="continuation_task_contract.json", + task_id_for_queue=str(follow_up_contract.get("task_id") or f"continue-{task_id}"), + reason="completion_governance_on_incomplete", + ) + continuation_decision["summary"] = ( + f"{str(continuation_decision.get('summary') or '').strip()} " + f"Queued follow-up contract at {follow_up_queue['artifact_path']}." + ).strip() + continuation_decision["action_source"] = "continuation_policy.on_incomplete" + store.append_event( + run_id, + { + "level": "INFO", + "event": "CONTINUATION_QUEUED", + "run_id": run_id, + "meta": { + "task_id": follow_up_contract.get("task_id"), + "queue_id": follow_up_queue["queue_item"].get("queue_id"), + }, + }, + ) + elif selected_action == "spawn_independent_temporary_unblock_task" and updated_unblock_tasks is not None: + selected_unblock_task = next( + ( + item + for item in updated_unblock_tasks + if str(item.get("unblock_task_id") or "").strip() + == str(continuation_decision.get("unblock_task_id") or "").strip() + ), + {}, + ) + if isinstance(selected_unblock_task, dict) and selected_unblock_task: + unblock_spec = ( + f"{str(selected_unblock_task.get('objective') or '').strip()} " + f"Reason: {str(selected_unblock_task.get('reason') or '').strip()} " + f"Scope: {str(selected_unblock_task.get('scope_hint') or '').strip()}" + ).strip() + unblock_contract = _build_follow_up_contract( + task_id_override=str(selected_unblock_task.get("unblock_task_id") or f"unblock-{task_id}"), + spec=unblock_spec, + preserve_thread=False, + ) + unblock_queue = _queue_follow_up_contract( + follow_up_contract=unblock_contract, + artifact_name="unblock_task_contract.json", + task_id_for_queue=str(unblock_contract.get("task_id") or f"unblock-{task_id}"), + reason="completion_governance_on_blocked", + ) + continuation_decision["summary"] = ( + f"{str(continuation_decision.get('summary') or '').strip()} " + f"Queued unblock contract at {unblock_queue['artifact_path']}." + ).strip() + store.append_event( + run_id, + { + "level": "INFO", + "event": "UNBLOCK_TASK_QUEUED", + "run_id": run_id, + "meta": { + "unblock_task_id": unblock_contract.get("task_id"), + "queue_id": unblock_queue["queue_item"].get("queue_id"), + }, + }, + ) + final_task_result["next_steps"] = { + "suggested_action": str(continuation_decision.get("selected_action") or "none"), + "notes": str(continuation_decision.get("summary") or "n/a"), + } + try: + report_validator.validate_report(final_task_result, "task_result.v1.json") + except Exception as exc: # noqa: BLE001 + failure_reason = failure_reason or f"task_result schema invalid: {exc}" + status = "FAILURE" + append_gate_failed_fn( + store, + run_id, + "schema_validation", + str(exc), + schema="task_result.v1.json", + path="reports/task_result.json", + ) + else: + store.write_report(run_id, "task_result", final_task_result) + store.write_task_result(run_id, task_id, final_task_result) + store.write_report(run_id, "completion_governance_report", completion_governance_report) + store.append_event( + run_id, + { + "level": "INFO", + "event": "COMPLETION_GOVERNANCE_EVALUATED", + "run_id": run_id, + "meta": { + "overall_verdict": completion_governance_report.get("overall_verdict"), + "selected_action": completion_governance_report.get("continuation_decision", {}).get("selected_action"), + }, + }, + ) + if isinstance(manifest.get("repo"), dict): manifest["repo"]["baseline_ref"] = baseline_ref or manifest["repo"].get("baseline_ref") or "UNKNOWN" if head_ref: diff --git a/apps/orchestrator/tests/test_completion_governance_runtime.py b/apps/orchestrator/tests/test_completion_governance_runtime.py new file mode 100644 index 0000000..a5b4622 --- /dev/null +++ b/apps/orchestrator/tests/test_completion_governance_runtime.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from cortexpilot_orch.scheduler.completion_governance import evaluate_completion_governance + + +def _write_artifact(run_dir: Path, filename: str, payload: object) -> None: + artifacts_dir = run_dir / "artifacts" + artifacts_dir.mkdir(parents=True, exist_ok=True) + (artifacts_dir / filename).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + + +def test_evaluate_completion_governance_marks_complete_when_required_checks_pass(tmp_path: Path) -> None: + run_dir = tmp_path / "run-complete" + _write_artifact( + run_dir, + "planning_worker_prompt_contracts.json", + [ + { + "prompt_contract_id": "worker-1", + "done_definition": {"acceptance_checks": ["repo_hygiene", "test_report", "review_report"]}, + "continuation_policy": { + "on_incomplete": "reply_auditor_reprompt_and_continue_same_session", + "on_blocked": "spawn_independent_temporary_unblock_task", + }, + } + ], + ) + task_result = { + "status": "SUCCESS", + "summary": "Completed the scoped worker assignment.", + "gates": { + "diff_gate": {"passed": True}, + "policy_gate": {"passed": True}, + "review_gate": {"passed": True}, + "tests_gate": {"passed": True}, + }, + } + test_report = {"status": "PASS"} + review_report = {"verdict": "PASS"} + + report, updated_unblock_tasks, context_pack_artifact, harness_request_artifact = evaluate_completion_governance( + contract={"acceptance_tests": [{"cmd": ["pytest", "-q"]}]}, + run_dir=run_dir, + task_result=task_result, + test_report=test_report, + review_report=review_report, + status="SUCCESS", + failure_reason="", + generated_at="2026-04-12T21:00:00Z", + ) + + assert report["overall_verdict"] == "complete" + assert report["dod_checker"]["status"] == "passed" + assert report["reply_auditor"]["status"] == "accepted" + assert report["continuation_decision"]["selected_action"] == "none" + assert updated_unblock_tasks is None + assert context_pack_artifact is None + assert harness_request_artifact is None + + +def test_evaluate_completion_governance_queues_unblock_task_for_blocked_run(tmp_path: Path) -> None: + run_dir = tmp_path / "run-blocked" + _write_artifact( + run_dir, + "planning_worker_prompt_contracts.json", + [ + { + "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", + }, + } + ], + ) + _write_artifact( + run_dir, + "planning_unblock_tasks.json", + [ + { + "version": "v1", + "unblock_task_id": "unblock-worker-1", + "source_prompt_contract_id": "worker-1", + "objective": "Unblock the scoped worker assignment", + "scope_hint": "Inspect the external blocker.", + "assigned_agent": {"role": "WORKER", "agent_id": "agent-1"}, + "owner": "L0", + "mode": "independent_temporary_task", + "status": "proposed", + "trigger": "spawn_independent_temporary_unblock_task", + "reason": "an external blocker requires an L0-managed unblock task", + "verification_requirements": ["repo_hygiene"], + } + ], + ) + task_result = { + "status": "FAILED", + "summary": "Blocked on an external dependency.", + "gates": { + "diff_gate": {"passed": True}, + "policy_gate": {"passed": True}, + "review_gate": {"passed": True}, + "tests_gate": {"passed": False}, + }, + } + test_report = {"status": "FAIL"} + review_report = {"verdict": "PASS"} + + report, updated_unblock_tasks, context_pack_artifact, harness_request_artifact = evaluate_completion_governance( + contract={}, + run_dir=run_dir, + task_result=task_result, + test_report=test_report, + review_report=review_report, + status="FAILURE", + failure_reason="external blocker", + generated_at="2026-04-12T21:00:00Z", + ) + + assert report["overall_verdict"] == "queue_unblock_task" + assert report["reply_auditor"]["status"] == "blocked" + assert report["continuation_decision"]["selected_action"] == "spawn_independent_temporary_unblock_task" + assert report["continuation_decision"]["unblock_task_id"] == "unblock-worker-1" + assert updated_unblock_tasks is not None + assert updated_unblock_tasks[0]["status"] == "queued" + assert context_pack_artifact is None + assert harness_request_artifact is None + + +def test_evaluate_completion_governance_generates_context_pack_and_harness_request(tmp_path: Path) -> None: + run_dir = tmp_path / "run-follow-up" + _write_artifact( + run_dir, + "planning_worker_prompt_contracts.json", + [ + { + "prompt_contract_id": "worker-ctx", + "assigned_agent": {"role": "WORKER", "agent_id": "agent-ctx"}, + "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", + }, + } + ], + ) + task_result = { + "status": "FAILED", + "summary": "policy gate blocked the run", + "gates": { + "diff_gate": {"passed": True}, + "policy_gate": {"passed": False, "violations": ["mcp_gate"]}, + "review_gate": {"passed": True}, + "tests_gate": {"passed": False}, + }, + "evidence_refs": {"thread_id": "thread-ctx"}, + } + report, updated_unblock_tasks, context_pack_artifact, harness_request_artifact = evaluate_completion_governance( + contract={"assigned_agent": {"role": "WORKER", "agent_id": "agent-ctx", "codex_thread_id": "thread-ctx"}}, + run_dir=run_dir, + task_result=task_result, + test_report={"status": "FAIL"}, + review_report={"verdict": "PASS"}, + status="FAILURE", + failure_reason="context contamination while mcp_gate blocked the run", + generated_at="2026-04-12T21:30:00Z", + ) + + assert report["context_pack"]["status"] == "generated" + assert context_pack_artifact is not None + assert context_pack_artifact["trigger_reason"] == "contamination" + assert report["harness_request"]["status"] == "approval_required" + assert harness_request_artifact is not None + assert harness_request_artifact["scope"] == "project-local" + assert harness_request_artifact["approval_required"] is True + assert updated_unblock_tasks is None diff --git a/apps/orchestrator/tests/test_scheduler_bridge_finalize_edges.py b/apps/orchestrator/tests/test_scheduler_bridge_finalize_edges.py index a8503d8..c42325c 100644 --- a/apps/orchestrator/tests/test_scheduler_bridge_finalize_edges.py +++ b/apps/orchestrator/tests/test_scheduler_bridge_finalize_edges.py @@ -172,3 +172,260 @@ def test_finalize_execute_task_run_releases_lock_and_worktree(monkeypatch, tmp_p assert calls["remove"] == [(run_id, "task-finalize-execute")] assert calls["finalize_kwargs"]["run_id"] == run_id assert calls["finalize_kwargs"]["task_id"] == "task-finalize-execute" + + +def test_finalize_run_writes_completion_governance_report_and_updates_task_result(tmp_path: Path) -> None: + store = RunStore(runs_root=tmp_path / "runs") + run_id = store.create_run("task-completion-governance") + run_dir = store._runs_root / run_id + artifacts_dir = run_dir / "artifacts" + artifacts_dir.mkdir(parents=True, exist_ok=True) + (artifacts_dir / "planning_worker_prompt_contracts.json").write_text( + json.dumps( + [ + { + "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", + }, + } + ], + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + (artifacts_dir / "planning_unblock_tasks.json").write_text( + json.dumps( + [ + { + "version": "v1", + "unblock_task_id": "unblock-worker-1", + "source_prompt_contract_id": "worker-1", + "objective": "Unblock the scoped worker assignment", + "scope_hint": "Inspect the external blocker.", + "assigned_agent": {"role": "WORKER", "agent_id": "agent-1"}, + "owner": "L0", + "mode": "independent_temporary_task", + "status": "proposed", + "trigger": "spawn_independent_temporary_unblock_task", + "reason": "an external blocker requires an L0-managed unblock task", + "verification_requirements": ["repo_hygiene"], + } + ], + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + + bridge_finalize.finalize_run( + store=store, + run_id=run_id, + task_id="task-completion-governance", + status="FAILURE", + failure_reason="external blocker", + manifest={"run_id": run_id, "task_id": "task-completion-governance", "status": "RUNNING", "repo": {}, "workflow": {}}, + attempt=1, + start_ts="2026-04-12T20:00:00Z", + tests_result={"ok": False}, + test_report={"status": "FAIL"}, + review_report={"verdict": "PASS"}, + policy_gate_result={"ok": True, "passed": True}, + integrated_gate=None, + network_gate=None, + mcp_gate=None, + sampling_gate=None, + tool_gate=None, + human_approval_required=False, + human_approved=None, + contract={"assigned_agent": {"role": "WORKER", "agent_id": "agent-1", "codex_thread_id": ""}}, + runner_summary="blocked on an external dependency", + diff_gate_result={"ok": True, "violations": [], "changed_files": []}, + review_gate_result={"ok": True}, + baseline_ref="base-ref", + head_ref="head-ref", + search_request=None, + tamper_request=None, + task_result={"status": "FAILED", "summary": "blocked", "gates": {"diff_gate": {"passed": True}, "policy_gate": {"passed": True}, "review_gate": {"passed": True}, "tests_gate": {"passed": False}}}, + now_ts_fn=lambda: "2026-04-12T20:05:00Z", + ensure_text_file_fn=lambda path: path.write_text("", encoding="utf-8"), + contract_validator_cls=_AlwaysValidValidator, + schema_root_fn=lambda: tmp_path, + build_test_report_stub_fn=lambda *_args, **_kwargs: {"status": "FAIL"}, + build_review_report_stub_fn=lambda *_args, **_kwargs: {"verdict": "PASS"}, + build_policy_gate_fn=lambda *_args, **_kwargs: {"passed": True}, + build_task_result_fn=lambda *_args, **_kwargs: { + "run_id": run_id, + "task_id": "task-completion-governance", + "attempt": 1, + "producer": {"role": "WORKER", "agent_id": "agent-1"}, + "status": "FAILED", + "started_at": "2026-04-12T20:00:00Z", + "finished_at": "2026-04-12T20:05:00Z", + "summary": "blocked", + "artifacts": [], + "git": {"baseline_ref": "base-ref", "head_ref": "head-ref", "changed_files": {"name": "changed"}, "patch": {"name": "patch"}}, + "gates": { + "diff_gate": {"passed": True, "violations": []}, + "policy_gate": {"passed": True, "violations": []}, + "review_gate": {"passed": True, "violations": []}, + "tests_gate": {"passed": False, "violations": ["tests failed"]}, + }, + "next_steps": {"suggested_action": "investigate", "notes": "external blocker"}, + "failure": {"message": "external blocker"}, + }, + build_work_report_fn=lambda *_args, **_kwargs: {"status": "FAILED"}, + build_evidence_report_fn=lambda _run_dir, _extra=None: {"status": "ok"}, + append_gate_failed_fn=lambda *_args, **_kwargs: None, + write_evidence_bundle_fn=bridge_finalize.write_evidence_bundle, + manifest_task_role_fn=lambda _assigned: "WORKER", + artifact_ref_from_path_fn=lambda name, *_args, **_kwargs: {"name": name}, + collect_evidence_hashes_fn=lambda _run_dir: {}, + artifact_refs_from_hashes_fn=lambda _run_dir, _hashes: [], + write_manifest_fn=lambda store_obj, rid, data: store_obj.write_manifest(rid, data), + notify_run_completed_fn=lambda _rid, _payload: {"ok": True}, + ) + + completion_governance = json.loads((run_dir / "reports" / "completion_governance_report.json").read_text(encoding="utf-8")) + assert completion_governance["overall_verdict"] == "queue_unblock_task" + assert completion_governance["continuation_decision"]["selected_action"] == "spawn_independent_temporary_unblock_task" + assert "Queued unblock contract" in completion_governance["continuation_decision"]["summary"] + + task_result = json.loads((run_dir / "reports" / "task_result.json").read_text(encoding="utf-8")) + assert task_result["next_steps"]["suggested_action"] == "spawn_independent_temporary_unblock_task" + + unblock_tasks = json.loads((artifacts_dir / "planning_unblock_tasks.json").read_text(encoding="utf-8")) + assert unblock_tasks[0]["status"] == "queued" + unblock_contract = json.loads((artifacts_dir / "unblock_task_contract.json").read_text(encoding="utf-8")) + assert unblock_contract["task_id"] == "unblock-worker-1" + assert unblock_contract["parent_task_id"] == "task-completion-governance" + assert unblock_contract["assigned_agent"].get("codex_thread_id") in {"", None} + queue_lines = (tmp_path / "queue.jsonl").read_text(encoding="utf-8").splitlines() + queue_items = [json.loads(line) for line in queue_lines if line.strip()] + assert queue_items[-1]["task_id"] == "unblock-worker-1" + assert queue_items[-1]["source_run_id"] == run_id + + +def test_finalize_run_writes_context_pack_and_harness_request_artifacts(tmp_path: Path) -> None: + store = RunStore(runs_root=tmp_path / "runs") + run_id = store.create_run("task-context-harness") + run_dir = store._runs_root / run_id + artifacts_dir = run_dir / "artifacts" + artifacts_dir.mkdir(parents=True, exist_ok=True) + (artifacts_dir / "planning_worker_prompt_contracts.json").write_text( + json.dumps( + [ + { + "prompt_contract_id": "worker-ctx", + "assigned_agent": {"role": "WORKER", "agent_id": "agent-ctx"}, + "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", + }, + } + ], + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + + bridge_finalize.finalize_run( + store=store, + run_id=run_id, + task_id="task-context-harness", + status="FAILURE", + failure_reason="context contamination while mcp_gate blocked the run", + manifest={"run_id": run_id, "task_id": "task-context-harness", "status": "RUNNING", "repo": {}, "workflow": {}}, + attempt=1, + start_ts="2026-04-12T20:00:00Z", + tests_result={"ok": False}, + test_report={"status": "FAIL"}, + review_report={"verdict": "PASS"}, + policy_gate_result={"passed": False, "violations": ["mcp_gate"]}, + integrated_gate=None, + network_gate=None, + mcp_gate=None, + sampling_gate=None, + tool_gate=None, + human_approval_required=False, + human_approved=None, + contract={"assigned_agent": {"role": "WORKER", "agent_id": "agent-ctx", "codex_thread_id": "thread-ctx"}}, + runner_summary="blocked by capability gap", + diff_gate_result={"ok": True, "violations": [], "changed_files": []}, + review_gate_result={"ok": True}, + baseline_ref="base-ref", + head_ref="head-ref", + search_request=None, + tamper_request=None, + task_result={ + "status": "FAILED", + "summary": "blocked by capability gap", + "gates": { + "diff_gate": {"passed": True}, + "policy_gate": {"passed": False, "violations": ["mcp_gate"]}, + "review_gate": {"passed": True}, + "tests_gate": {"passed": False}, + }, + "evidence_refs": {"thread_id": "thread-ctx"}, + }, + now_ts_fn=lambda: "2026-04-12T20:05:00Z", + ensure_text_file_fn=lambda path: path.write_text("", encoding="utf-8"), + contract_validator_cls=_AlwaysValidValidator, + schema_root_fn=lambda: tmp_path, + build_test_report_stub_fn=lambda *_args, **_kwargs: {"status": "FAIL"}, + build_review_report_stub_fn=lambda *_args, **_kwargs: {"verdict": "PASS"}, + build_policy_gate_fn=lambda *_args, **_kwargs: {"passed": False, "violations": ["mcp_gate"]}, + build_task_result_fn=lambda *_args, **_kwargs: { + "run_id": run_id, + "task_id": "task-context-harness", + "attempt": 1, + "producer": {"role": "WORKER", "agent_id": "agent-ctx"}, + "status": "FAILED", + "started_at": "2026-04-12T20:00:00Z", + "finished_at": "2026-04-12T20:05:00Z", + "summary": "blocked", + "artifacts": [], + "git": {"baseline_ref": "base-ref", "head_ref": "head-ref", "changed_files": {"name": "changed"}, "patch": {"name": "patch"}}, + "gates": { + "diff_gate": {"passed": True, "violations": []}, + "policy_gate": {"passed": False, "violations": ["mcp_gate"]}, + "review_gate": {"passed": True, "violations": []}, + "tests_gate": {"passed": False, "violations": ["tests failed"]}, + }, + "next_steps": {"suggested_action": "investigate", "notes": "capability gap"}, + "failure": {"message": "context contamination while mcp_gate blocked the run"}, + }, + build_work_report_fn=lambda *_args, **_kwargs: {"status": "FAILED"}, + build_evidence_report_fn=lambda _run_dir, _extra=None: {"status": "ok"}, + append_gate_failed_fn=lambda *_args, **_kwargs: None, + write_evidence_bundle_fn=bridge_finalize.write_evidence_bundle, + manifest_task_role_fn=lambda _assigned: "WORKER", + artifact_ref_from_path_fn=lambda name, *_args, **_kwargs: {"name": name}, + collect_evidence_hashes_fn=lambda _run_dir: {}, + artifact_refs_from_hashes_fn=lambda _run_dir, _hashes: [], + write_manifest_fn=lambda store_obj, rid, data: store_obj.write_manifest(rid, data), + notify_run_completed_fn=lambda _rid, _payload: {"ok": True}, + ) + + context_pack = json.loads((artifacts_dir / "context_pack.json").read_text(encoding="utf-8")) + assert context_pack["trigger_reason"] == "contamination" + assert context_pack["source_session_id"] == "thread-ctx" + + harness_request = json.loads((artifacts_dir / "harness_request.json").read_text(encoding="utf-8")) + assert harness_request["scope"] == "project-local" + assert harness_request["approval_required"] is True + assert harness_request["requested_capabilities"]["mcp_servers"] == ["runtime-governance"] + continuation_contract = json.loads((artifacts_dir / "continuation_task_contract.json").read_text(encoding="utf-8")) + assert continuation_contract["parent_task_id"] == "task-context-harness" + assert continuation_contract["assigned_agent"]["codex_thread_id"] == "thread-ctx" + artifact_names = [item["name"] for item in continuation_contract["inputs"]["artifacts"]] + assert "context_pack.json" in artifact_names + queue_lines = (tmp_path / "queue.jsonl").read_text(encoding="utf-8").splitlines() + queue_items = [json.loads(line) for line in queue_lines if line.strip()] + assert queue_items[-1]["task_id"] == continuation_contract["task_id"] + assert queue_items[-1]["source_run_id"] == run_id diff --git a/apps/orchestrator/tests/test_schema_validation.py b/apps/orchestrator/tests/test_schema_validation.py index cb142f5..34949f8 100644 --- a/apps/orchestrator/tests/test_schema_validation.py +++ b/apps/orchestrator/tests/test_schema_validation.py @@ -327,6 +327,48 @@ def test_new_operator_report_and_task_pack_schemas_pass() -> None: } assert validator.validate_report(harness_request, "harness_request.v1.json")["request_id"] == "harness-1" + completion_governance_report = { + "report_type": "completion_governance_report", + "generated_at": "2026-04-12T21:00:00Z", + "authority": "completion-governance-runtime", + "source": "finalize_run", + "execution_authority": "task_contract", + "overall_verdict": "queue_unblock_task", + "dod_checker": { + "status": "failed", + "summary": "Required completion checks are still missing or failed.", + "required_checks": ["repo_hygiene", "test_report"], + "unmet_checks": ["test_report", "run_status"], + }, + "reply_auditor": { + "status": "blocked", + "summary": "The run ended with blocker signals and an unblock path is available.", + "signals": ["run_status_not_success", "dod_unmet"], + }, + "continuation_decision": { + "status": "selected", + "selected_action": "spawn_independent_temporary_unblock_task", + "action_source": "continuation_policy.on_blocked", + "unblock_task_id": "unblock-worker-prompt-1", + "summary": "The run is blocked. Queue the L0-managed unblock task before continuing.", + }, + "context_pack": { + "status": "not_wired", + "summary": "Context Pack remains fallback-only, but no runtime producer/consumer is wired into finalize_run yet.", + }, + "harness_request": { + "status": "not_wired", + "summary": "Harness Request has a schema home, but no request/apply lifecycle is wired into this run finalizer yet.", + }, + } + assert ( + validator.validate_report( + completion_governance_report, + "completion_governance_report.v1.json", + )["overall_verdict"] + == "queue_unblock_task" + ) + control_plane_runtime_policy = { "version": "v1", "product_identity": { diff --git a/docs/README.md b/docs/README.md index 965b282..573e452 100644 --- a/docs/README.md +++ b/docs/README.md @@ -141,6 +141,7 @@ navigation set. - `policies/role_config_registry.json`: repo-owned mutable defaults surface for role configuration preview/apply flows; changes here affect future compiled role defaults, not the execution authority of already-issued task contracts - `configs/env_direct_read_allowlist.json`: machine allowlist for governed backend direct env reads; update this alongside docs when a role/runtime helper legitimately reads env-backed model metadata - `docs/api/openapi.cortexpilot.json`: canonical frontend contract extension that now carries Prompt 8 run/workflow route bindings plus Prompt 9 agents/contracts catalog bindings and generated read-model metadata for `RoleBindingReadModel` / `WorkflowCaseReadModel` +- `schemas/completion_governance_report.v1.json`: runtime-evaluated completion-governance report emitted during run finalize; use it when operator surfaces need the live DoD / reply-audit / continuation verdict instead of only the planning artifact summary - `scripts/generate_frontend_contracts.py`: repo-owned generator that now emits Prompt 8 read-model types plus Prompt 9 agents/contracts catalog routes into `@cortexpilot/frontend-api-contract` - `schemas/role_config_registry.v1.json`: schema-first contract for the repo-owned role configuration overlay used by Prompt 10 role-default preview/apply surfaces - `packages/frontend-api-contract/generated/index.d.ts`: generated TypeScript contract surface for frontend-safe run/workflow routes and read-model types; avoid hand-maintaining parallel overlays when this file changes diff --git a/docs/architecture/runtime-topology.md b/docs/architecture/runtime-topology.md index b3e434c..c236085 100644 --- a/docs/architecture/runtime-topology.md +++ b/docs/architecture/runtime-topology.md @@ -61,6 +61,19 @@ flowchart LR - The same planning preview may now derive `unblock_tasks`, and run bundles may persist `planning_unblock_tasks.json` when worker continuation policy says blocked work should spawn an independent temporary unblock task. +- Run finalize may now emit `reports/completion_governance_report.json`, a + runtime-evaluated read-back that records the current DoD verdict, reply audit, + continuation decision, Context Pack fallback posture, and Harness Request + posture without promoting those summaries above `task_contract`. +- The same finalize step may now persist: + - `artifacts/context_pack.json` + - `artifacts/harness_request.json` + when runtime governance detects a fallback handoff condition or a capability + blocker that should become a tracked Harness Request. +- When continuation policy selects the unblock branch during finalize, the same + runtime may advance `planning_unblock_tasks.json` from `proposed` to `queued` + and emit `UNBLOCK_TASK_QUEUED`, so the unblock path is no longer only a + planning preview artifact. - Queue truth currently lives in `.runtime-cache/cortexpilot/queue.jsonl`; API and workflow surfaces read that queue state and derive `eligible` / `sla_state` instead of storing a second scheduler database. diff --git a/docs/specs/00_SPEC.md b/docs/specs/00_SPEC.md index 6dc61f8..e4b5a52 100644 --- a/docs/specs/00_SPEC.md +++ b/docs/specs/00_SPEC.md @@ -330,6 +330,13 @@ - These planner artifacts are advisory planning surfaces that must stay aligned with the compiled `task_contract`; they do not supersede the execution contract itself. +- When a run finalizes, runtime evaluation may write + `reports/completion_governance_report.json` + (`schemas/completion_governance_report.v1.json`) plus follow-on events such + as `COMPLETION_GOVERNANCE_EVALUATED` and `UNBLOCK_TASK_QUEUED`. +- This runtime-evaluated report may update `task_result.next_steps` and queue an + unblock task artifact, but it still remains a read-back surface beneath + `task_contract`. ### 6.5 Context Pack And Harness Request Contracts @@ -341,6 +348,12 @@ loop. - `harness_request` represents a proposed capability change; applying that change still depends on policy and approval boundaries. +- When run finalize detects a fallback handoff condition or a capability-policy + blocker, the runtime may now persist: + - `artifacts/context_pack.json` + - `artifacts/harness_request.json` +- These runtime-generated artifacts remain read-back coordination objects. They + do not replace `task_contract` as execution authority. ### 6.6 Unblock Task Contract diff --git a/packages/frontend-shared/uiCopy.ts b/packages/frontend-shared/uiCopy.ts index e3b9510..8a226e0 100644 --- a/packages/frontend-shared/uiCopy.ts +++ b/packages/frontend-shared/uiCopy.ts @@ -795,6 +795,32 @@ export type UiCopy = { }; completionGovernance: { title: string; + runtimeTitle: string; + overallVerdict: string; + reportAuthority: string; + reportSource: string; + reportExecutionAuthority: string; + dodChecker: string; + dodSummary: string; + dodRequiredChecks: string; + dodUnmetChecks: string; + replyAuditor: string; + replySummary: string; + continuationDecision: string; + continuationSummary: string; + actionSource: string; + selectedUnblockTask: string; + contextPack: string; + contextPackSummary: string; + contextPackId: string; + contextPackTrigger: string; + harnessRequest: string; + harnessRequestSummary: string; + harnessRequestId: string; + harnessRequestScope: string; + harnessRequestApproval: string; + runtimeNote: string; + planningFallbackTitle: string; workerPromptContracts: string; unblockTasks: string; onIncomplete: string; @@ -2021,6 +2047,33 @@ const UI_COPY: Record = { }, completionGovernance: { title: "Completion governance", + runtimeTitle: "Runtime evaluator verdict", + overallVerdict: "Overall verdict", + reportAuthority: "Report authority", + reportSource: "Report source", + reportExecutionAuthority: "Report execution authority", + dodChecker: "DoD checker", + dodSummary: "DoD summary", + dodRequiredChecks: "Required checks", + dodUnmetChecks: "Unmet checks", + replyAuditor: "Reply auditor", + replySummary: "Reply summary", + continuationDecision: "Continuation decision", + continuationSummary: "Continuation summary", + actionSource: "Action source", + selectedUnblockTask: "Selected unblock task", + contextPack: "Context Pack", + contextPackSummary: "Context Pack summary", + contextPackId: "Context Pack ID", + contextPackTrigger: "Context Pack trigger", + harnessRequest: "Harness Request", + harnessRequestSummary: "Harness Request summary", + harnessRequestId: "Harness Request ID", + harnessRequestScope: "Harness Request scope", + harnessRequestApproval: "Harness approval required", + runtimeNote: + "Runtime-evaluated read-back: this report reflects the live completion evaluator. task_contract still owns execution authority; this report does not replace the contract.", + planningFallbackTitle: "Planning advisory fallback", workerPromptContracts: "Worker prompt contracts", unblockTasks: "Unblock tasks", onIncomplete: "On incomplete", @@ -3279,6 +3332,33 @@ const UI_COPY: Record = { }, completionGovernance: { title: "完成治理摘要", + runtimeTitle: "运行时治理结论", + overallVerdict: "总体结论", + reportAuthority: "报告权威来源", + reportSource: "报告来源", + reportExecutionAuthority: "报告执行权威", + dodChecker: "完成定义检查器", + dodSummary: "完成定义摘要", + dodRequiredChecks: "要求检查项", + dodUnmetChecks: "未满足检查项", + replyAuditor: "回复审计器", + replySummary: "回复摘要", + continuationDecision: "续跑决策", + continuationSummary: "续跑摘要", + actionSource: "决策来源", + selectedUnblockTask: "选中的解阻塞任务", + contextPack: "Context Pack", + contextPackSummary: "Context Pack 摘要", + contextPackId: "Context Pack ID", + contextPackTrigger: "Context Pack 触发器", + harnessRequest: "Harness Request", + harnessRequestSummary: "Harness Request 摘要", + harnessRequestId: "Harness Request ID", + harnessRequestScope: "Harness Request 范围", + harnessRequestApproval: "Harness 审批要求", + runtimeNote: + "运行时只读回读:这份报告反映的是 live completion evaluator 的结论;`task_contract` 仍然掌握执行权威,这份报告不是第二份执行合同。", + planningFallbackTitle: "规划期 advisory 摘要", workerPromptContracts: "工作者提示合约", unblockTasks: "解阻塞任务", onIncomplete: "未完成时", diff --git a/schemas/CHANGELOG.md b/schemas/CHANGELOG.md index b8d419b..ea5d1db 100644 --- a/schemas/CHANGELOG.md +++ b/schemas/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2026-04-12 - Added `control_plane_runtime_policy.v1.json` to formalize L0 command-tower runtime rules. +- Added `completion_governance_report.v1.json` so run finalize can emit a runtime-evaluated verdict for DoD, reply audit, continuation, Context Pack posture, and Harness Request posture. - Added `wave_plan.v1.json` and `worker_prompt_contract.v1.json` for planner preview artifacts. - Added `unblock_task.v1.json` to formalize L0-managed independent temporary unblock assignments. - Added `context_pack.v1.json` and `harness_request.v1.json` to reserve first-class schema homes for explicit handoff and harness-evolution contracts. diff --git a/schemas/README.md b/schemas/README.md index 942d9d0..db1f800 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -7,11 +7,15 @@ Machine-readable schemas for contracts, events, and policy validation. - `orchestrator_event.v1.json` — canonical event schema emitted by orchestrator runtime. - `execution_plan_report.v1.json` — advisory intake-preview report used before execution starts. - `control_plane_runtime_policy.v1.json` — machine-readable command-tower runtime constitution for L0/L1/L2, wake policy, completion governance, and harness boundaries. +- `completion_governance_report.v1.json` — runtime-evaluated completion verdict for DoD, reply audit, continuation, Context Pack fallback readiness, and harness-request lifecycle posture. - `wave_plan.v1.json` — wave-level orchestration preview artifact derived from intake planning. - `worker_prompt_contract.v1.json` — worker-scoped planner artifact for scope, reading list, continuation, and verification rules. - `unblock_task.v1.json` — L0-managed independent temporary unblock assignment derived from worker continuation policy. - `context_pack.v1.json` — explicit fallback handoff contract for context-pressure and role-switch situations. - `harness_request.v1.json` — capability-evolution request contract for session-local/project-local/global harness changes. +- `artifacts/context_pack.json` and `artifacts/harness_request.json` are now the + runtime-generated minimal lifecycle surfaces emitted by completion governance + when those schema homes are actually exercised during run finalize. - `approval_pack.v1.json` / `incident_pack.v1.json` / `run_compare_report.v1.json` — derived operator-readable decision packs for approval, failure triage, and replay compare surfaces. - `proof_pack.v1.json` — derived success-pack for public task slices that completed with reusable proof artifacts. - `task_pack_manifest.v1.json` — source-owned manifest schema for registry-driven task packs under `contracts/packs/`, including `input_fields` and evidence hints. diff --git a/schemas/completion_governance_report.v1.json b/schemas/completion_governance_report.v1.json new file mode 100644 index 0000000..c748074 --- /dev/null +++ b/schemas/completion_governance_report.v1.json @@ -0,0 +1,177 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.local/schemas/completion_governance_report.v1.json", + "title": "CompletionGovernanceReport", + "type": "object", + "additionalProperties": false, + "required": [ + "report_type", + "generated_at", + "authority", + "source", + "execution_authority", + "overall_verdict", + "dod_checker", + "reply_auditor", + "continuation_decision", + "context_pack", + "harness_request" + ], + "$defs": { + "StatusSummary": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "summary" + ], + "properties": { + "status": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string", + "minLength": 1 + } + } + } + }, + "properties": { + "report_type": { + "type": "string", + "const": "completion_governance_report" + }, + "generated_at": { + "type": "string", + "format": "date-time" + }, + "authority": { + "type": "string", + "const": "completion-governance-runtime" + }, + "source": { + "type": "string", + "const": "finalize_run" + }, + "execution_authority": { + "type": "string", + "const": "task_contract" + }, + "overall_verdict": { + "type": "string", + "enum": [ + "complete", + "continue_same_session", + "queue_unblock_task", + "manual_triage" + ] + }, + "dod_checker": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "summary", + "required_checks", + "unmet_checks" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "passed", + "failed" + ] + }, + "summary": { + "type": "string", + "minLength": 1 + }, + "required_checks": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "unmet_checks": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "reply_auditor": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "summary", + "signals" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "accepted", + "needs_follow_up", + "blocked" + ] + }, + "summary": { + "type": "string", + "minLength": 1 + }, + "signals": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "continuation_decision": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "selected_action", + "action_source", + "unblock_task_id", + "summary" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "none", + "selected" + ] + }, + "selected_action": { + "type": "string" + }, + "action_source": { + "type": "string" + }, + "unblock_task_id": { + "type": "string" + }, + "summary": { + "type": "string", + "minLength": 1 + } + } + }, + "context_pack": { + "$ref": "#/$defs/StatusSummary" + }, + "harness_request": { + "$ref": "#/$defs/StatusSummary" + } + } +} diff --git a/schemas/schema_registry.json b/schemas/schema_registry.json index 9c8676f..2008462 100644 --- a/schemas/schema_registry.json +++ b/schemas/schema_registry.json @@ -61,6 +61,10 @@ "sha256": "8396495f1c463834a690e981e8a2d17d19ce8e7534c7668e0fccfa15f2d31759", "bytes": 10326 }, + "completion_governance_report.v1.json": { + "sha256": "686d028be3c4cb7b77a164c49d986b991295f2f78a59ca7e1732069ce5ba854f", + "bytes": 3710 + }, "context_pack.v1.json": { "sha256": "8a45ef1e9f30e56aac57510e1875d6b43f4b320762e288adb2925787b3a1585c", "bytes": 1842