feat(awooop): expose apply gate closure readiness
All checks were successful
Code Review / ai-code-review (push) Successful in 16s
CD Pipeline / tests (push) Successful in 1m45s
CD Pipeline / build-and-deploy (push) Successful in 6m22s
CD Pipeline / post-deploy-checks (push) Successful in 2m7s

This commit is contained in:
Your Name
2026-06-26 06:40:21 +08:00
parent bae6423d72
commit d798d09edb
5 changed files with 348 additions and 0 deletions

View File

@@ -143,6 +143,30 @@ export interface AwoooPStatusChain {
apply_candidate?: string | null;
verifier?: string | null;
};
closure_readiness?: {
schema_version?: string;
status?: string | null;
completion_percent?: number | null;
ready_count?: number | null;
total_count?: number | null;
blocked_count?: number | null;
runtime_execution_authorized?: boolean | null;
writes_runtime_state?: boolean | null;
next_action?: string | null;
blocked_reason?: string | null;
gates?: Array<{
key?: string | null;
status?: string | null;
detail?: string | null;
asset_id?: string | null;
}>;
required_owner_fields?: string[];
readback_assets?: Array<{
key?: string | null;
asset_id?: string | null;
status?: string | null;
}>;
};
candidate?: {
catalog_id?: string | null;
check_mode_playbook_path?: string | null;
@@ -272,6 +296,7 @@ export function AwoooPStatusChainPanel({
const outcome = chain?.operator_outcome;
const outcomeExecution = outcome?.execution_result;
const automationHandoff = chain?.automation_handoff;
const closureReadiness = automationHandoff?.closure_readiness;
const blockers = chain?.blockers ?? [];
const sourceCorrelation = chain?.source_refs?.correlation;
const nextActionLabel = (value: unknown) => {
@@ -281,6 +306,7 @@ export function AwoooPStatusChainPanel({
owner_review_apply_gate_or_create_verifier_plan: t("nextActions.openApplyGateWorkItem"),
manual_review_no_action_decision: t("nextActions.manualReviewNoActionDecision"),
owner_review_repair_candidate_draft: t("nextActions.ownerReviewRepairCandidateDraft"),
review_owner_release_packet_before_apply: t("nextActions.reviewOwnerReleasePacket"),
manual_review_or_collect_repair_evidence: t("nextActions.collectRepairEvidence"),
manual_investigation: t("nextActions.manualInvestigation"),
run_or_review_post_execution_verification: t("nextActions.reviewVerifier"),
@@ -437,6 +463,9 @@ export function AwoooPStatusChainPanel({
const executionTotal = execution.operation_total ?? 0;
const handoffCandidate = automationHandoff?.candidate;
const handoffAssets = automationHandoff?.asset_ids;
const closureGates = closureReadiness?.gates ?? [];
const closureReadbackAssets = closureReadiness?.readback_assets ?? [];
const closureOwnerFields = closureReadiness?.required_owner_fields ?? [];
const ownerReviewChecklist = automationHandoff?.owner_review_checklist ?? [];
const forbiddenActions = automationHandoff?.forbidden_actions ?? [];
const sourceToolchainTone: SourceFlowTone = sourceCorrelation
@@ -548,6 +577,35 @@ export function AwoooPStatusChainPanel({
{ key: "applyAsset", label: t("applyGate.applyAsset"), value: handoffAssets?.apply_candidate },
{ key: "verifierAsset", label: t("applyGate.verifierAsset"), value: handoffAssets?.verifier },
];
const closureGateLabel = (key: string | null | undefined) => {
const labels: Record<string, string> = {
mcp_evidence: t("applyGate.closureGates.mcpEvidence"),
dry_run: t("applyGate.closureGates.dryRun"),
apply_candidate: t("applyGate.closureGates.applyCandidate"),
owner_release: t("applyGate.closureGates.ownerRelease"),
controlled_execution: t("applyGate.closureGates.controlledExecution"),
post_apply_verifier: t("applyGate.closureGates.postApplyVerifier"),
km_writeback: t("applyGate.closureGates.kmWriteback"),
playbook_trust: t("applyGate.closureGates.playbookTrust"),
};
return labels[String(key ?? "")] ?? valueOrEmpty(key, emptyLabel);
};
const closureAssetLabel = (key: string | null | undefined) => {
const labels: Record<string, string> = {
owner_execution_rehearsal: t("applyGate.closureAssets.ownerExecutionRehearsal"),
final_candidate_readback: t("applyGate.closureAssets.finalCandidateReadback"),
release_verifier_preflight: t("applyGate.closureAssets.releaseVerifierPreflight"),
};
return labels[String(key ?? "")] ?? valueOrEmpty(key, emptyLabel);
};
const closureStatusLabel = (status: string | null | undefined) => {
const labels: Record<string, string> = {
blocked_before_owner_release: t("applyGate.closureStatuses.blockedBeforeOwnerRelease"),
no_write_rehearsal: t("applyGate.closureStatuses.noWriteRehearsal"),
read_only: t("applyGate.closureStatuses.readOnly"),
};
return labels[String(status ?? "")] ?? handoffStatusLabel(status);
};
const handoffWorkItemHref = automationHandoff?.work_item_id
? `/awooop/work-items?project_id=awoooi&work_item_id=${encodeURIComponent(automationHandoff.work_item_id)}${automationHandoff.source_id ? `&incident_id=${encodeURIComponent(automationHandoff.source_id)}` : ""}`
: null;
@@ -807,6 +865,100 @@ export function AwoooPStatusChainPanel({
</div>
))}
</div>
{closureReadiness && (
<div>
<div className="border-t border-[#e0ddd4] bg-[#f5f8f2] px-4 py-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="min-w-0">
<p className="text-xs font-semibold text-[#141413]">{t("applyGate.closureTitle")}</p>
<p className="mt-1 truncate text-xs text-[#5f5b52]" title={valueOrEmpty(closureReadiness.blocked_reason, emptyLabel)}>
{t("applyGate.closureBlockedReason", {
reason: valueOrEmpty(closureReadiness.blocked_reason, emptyLabel),
})}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="border border-[#9fcbab] bg-[#f1fbf2] px-2 py-0.5 font-mono text-[11px] font-semibold text-[#17602a]">
{t("applyGate.closureProgress", {
ready: closureReadiness.ready_count ?? 0,
total: closureReadiness.total_count ?? closureGates.length,
percent: closureReadiness.completion_percent ?? 0,
})}
</span>
<span className="border border-[#e2a29b] bg-[#fff0ef] px-2 py-0.5 font-mono text-[11px] font-semibold text-[#9f2f25]">
{closureStatusLabel(closureReadiness.status)}
</span>
</div>
</div>
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-4">
{closureGates.map((gate) => (
<div key={`${gate.key}-${gate.status}`} className="min-w-0 bg-white px-4 py-3">
<div className="flex min-w-0 items-start gap-3">
<span className={cn(
"flex h-8 w-8 shrink-0 items-center justify-center border",
sourceFlowToneClass(handoffTone(gate.status))
)}>
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className="text-xs font-semibold text-[#77736a]">{closureGateLabel(gate.key)}</p>
<p className="mt-1 truncate font-mono text-sm font-semibold text-[#141413]" title={closureStatusLabel(gate.status)}>
{closureStatusLabel(gate.status)}
</p>
</div>
</div>
<p className="mt-2 truncate font-mono text-xs text-[#5f5b52]" title={valueOrEmpty(gate.detail, emptyLabel)}>
{valueOrEmpty(gate.detail, emptyLabel)}
</p>
<p className="mt-1 truncate font-mono text-[11px] text-[#77736a]" title={valueOrEmpty(gate.asset_id, emptyLabel)}>
{valueOrEmpty(gate.asset_id, emptyLabel)}
</p>
</div>
))}
</div>
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-2">
<div className="min-w-0 bg-white px-4 py-3">
<div className="flex items-center justify-between gap-3">
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.closureOwnerFields")}</p>
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-[11px] text-[#5f5b52]">
{closureOwnerFields.length}
</span>
</div>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
{(closureOwnerFields.length ? closureOwnerFields : [emptyLabel]).map((item, index) => (
<span key={`${item}-${index}`} className="min-w-0 border border-[#ece8dd] bg-[#faf9f3] px-2 py-1 font-mono text-[11px] text-[#141413]">
{item}
</span>
))}
</div>
</div>
<div className="min-w-0 bg-white px-4 py-3">
<div className="flex items-center justify-between gap-3">
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.closureReadbackAssets")}</p>
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-[11px] text-[#5f5b52]">
{closureReadbackAssets.length}
</span>
</div>
<div className="mt-3 space-y-2">
{(closureReadbackAssets.length ? closureReadbackAssets : [{ key: emptyLabel, asset_id: emptyLabel, status: emptyLabel }]).map((item, index) => (
<div key={`${item.key}-${index}`} className="min-w-0 border border-[#ece8dd] bg-[#faf9f3] px-2 py-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-xs font-semibold text-[#141413]">{closureAssetLabel(item.key)}</p>
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 font-mono text-[11px] text-[#5f5b52]">
{closureStatusLabel(item.status)}
</span>
</div>
<p className="mt-1 break-all font-mono text-[11px] text-[#5f5b52]" title={valueOrEmpty(item.asset_id, emptyLabel)}>
{valueOrEmpty(item.asset_id, emptyLabel)}
</p>
</div>
))}
</div>
</div>
</div>
</div>
)}
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3">
<div className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.workItem")}</p>