From e51db9096fbb90a3f7c9f2b11b1bb208c5b8faa9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 27 Jun 2026 11:30:38 +0800 Subject: [PATCH] feat(awooop): expose repair candidate automation closure --- .../src/services/repair_candidate_service.py | 92 ++++++++++++++ .../tests/test_repair_candidate_service.py | 14 +++ apps/web/messages/en.json | 16 +++ apps/web/messages/zh-TW.json | 16 +++ .../app/[locale]/awooop/work-items/page.tsx | 114 ++++++++++++++++++ .../src/components/awooop/status-chain.tsx | 29 +++++ docs/LOGBOOK.md | 28 +++++ 7 files changed, 309 insertions(+) diff --git a/apps/api/src/services/repair_candidate_service.py b/apps/api/src/services/repair_candidate_service.py index ec9e2737..605e0e6a 100644 --- a/apps/api/src/services/repair_candidate_service.py +++ b/apps/api/src/services/repair_candidate_service.py @@ -841,6 +841,98 @@ class RepairCandidateService: "assign_km_and_playbook_trust_writeback_owner", "rerun_repair_candidate_gate_after_owner_release", ], + "controlled_automation_closure": { + "schema_version": "repair_candidate_controlled_automation_closure_v1", + "status": "owner_review_ready_no_write_rehearsal", + "runtime_write_gate": "closed_until_owner_release", + "no_write_rehearsal": { + "rehearsal_id": ( + f"repair-rehearsal:{project_id}:{incident_id}:{lane}" + if incident_id + else f"repair-rehearsal:{project_id}:unbound:{lane}" + ), + "status": "ready_for_owner_review", + "route_id": route or "--", + "input_refs": { + "source_work_item_id": work_item_id or None, + "evidence_refs": list(evidence_refs), + "coverage_key": coverage_gap.get("coverage_key"), + }, + "planned_steps": [ + "render_repair_command_without_execution", + "render_rollback_command_without_execution", + "check_route_allowlist_and_target_selector", + "produce_verifier_rehearsal_plan", + ], + "writes_runtime_state": False, + "executes_command": False, + }, + "verifier_writeback_plan": { + "verifier_id": ( + f"verifier-plan:{project_id}:{incident_id}:{lane}" + if incident_id + else f"verifier-plan:{project_id}:unbound:{lane}" + ), + "status": "planned_not_executed", + "checks": list(verifier_plan), + "result_states": ["healthy", "degraded", "failed", "rollback_required"], + "writes_after_execution": [ + "incident_timeline", + "auto_repair_execution_result", + "km_draft", + "playbook_trust_event", + ], + "writes_runtime_state": False, + }, + "automation_asset_ledger": [ + { + "asset_type": "KM", + "asset_id": ( + f"km-draft:{project_id}:{incident_id}:{lane}" + if incident_id + else f"km-draft:{project_id}:unbound:{lane}" + ), + "owner": "Hermes", + "status": "draft_required_after_verifier", + "next_action": "prepare_km_draft_from_incident_evidence", + }, + { + "asset_type": "PlayBook", + "asset_id": ( + f"playbook-draft:{project_id}:{incident_id}:{lane}" + if incident_id + else f"playbook-draft:{project_id}:unbound:{lane}" + ), + "owner": "OpenClaw", + "status": "owner_review_ready", + "next_action": "review_service_specific_repair_steps", + }, + { + "asset_type": "ScriptOrAnsible", + "asset_id": route or "--", + "owner": "Executor lane", + "status": "route_allowlist_review_required", + "next_action": "confirm_check_mode_and_blast_radius", + }, + { + "asset_type": "Verifier", + "asset_id": ( + f"verifier-plan:{project_id}:{incident_id}:{lane}" + if incident_id + else f"verifier-plan:{project_id}:unbound:{lane}" + ), + "owner": "PostExecutionVerifier", + "status": "planned_not_executed", + "next_action": "owner_review_post_apply_verifier", + }, + ], + "operator_next_actions": [ + "review_no_write_rehearsal_inputs", + "approve_or_reject_owner_release", + "fill_blast_radius_maintenance_window_and_rollback_owner", + "promote_to_controlled_apply_only_after_all_gates_pass", + ], + }, } def _promotion_contract_field( diff --git a/apps/api/tests/test_repair_candidate_service.py b/apps/api/tests/test_repair_candidate_service.py index b2b76f1e..47cfbe2e 100644 --- a/apps/api/tests/test_repair_candidate_service.py +++ b/apps/api/tests/test_repair_candidate_service.py @@ -359,6 +359,20 @@ async def test_candidate_blocked_observe_only_prompts_repair_playbook_draft() -> assert "maintenance_window" in promotion_contract["blocked_fields"] assert promotion_contract["runtime_execution_authorized"] is False assert promotion_contract["runtime_write_allowed"] is False + closure = promotion_contract["controlled_automation_closure"] + assert closure["schema_version"] == "repair_candidate_controlled_automation_closure_v1" + assert closure["status"] == "owner_review_ready_no_write_rehearsal" + assert closure["runtime_write_gate"] == "closed_until_owner_release" + assert closure["no_write_rehearsal"]["status"] == "ready_for_owner_review" + assert closure["no_write_rehearsal"]["executes_command"] is False + assert closure["verifier_writeback_plan"]["status"] == "planned_not_executed" + assert "incident_timeline" in closure["verifier_writeback_plan"]["writes_after_execution"] + assert [item["asset_type"] for item in closure["automation_asset_ledger"]] == [ + "KM", + "PlayBook", + "ScriptOrAnsible", + "Verifier", + ] assert result.metadata["repair_candidate_promotion_contract"] == promotion_contract assert "promotion=6/11" in result.metadata["repair_candidate_promotion_summary"] assert "runtime=false" in result.metadata["repair_candidate_promotion_summary"] diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 581f7387..de809cde 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9807,6 +9807,22 @@ "blast_radius": "影響範圍", "km_writeback_owner": "KM owner", "playbook_trust_owner": "PlayBook trust owner" + }, + "closure": { + "title": "受控自動化閉環", + "subtitle": "AI 已把下一步拆成無寫入演練、Verifier 與資產沉澱;這裡只呈現可審查的閉環,不代表已執行命令。", + "rehearsal": { + "title": "無寫入演練", + "meta": "已規劃 {steps} 個演練步驟" + }, + "verifier": { + "title": "Verifier 回寫", + "meta": "已規劃 {checks} 個驗證檢查" + }, + "writeback": { + "title": "資產沉澱", + "meta": "已建立 {assets} 個資產槽位" + } } }, "flow": { diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 581f7387..de809cde 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9807,6 +9807,22 @@ "blast_radius": "影響範圍", "km_writeback_owner": "KM owner", "playbook_trust_owner": "PlayBook trust owner" + }, + "closure": { + "title": "受控自動化閉環", + "subtitle": "AI 已把下一步拆成無寫入演練、Verifier 與資產沉澱;這裡只呈現可審查的閉環,不代表已執行命令。", + "rehearsal": { + "title": "無寫入演練", + "meta": "已規劃 {steps} 個演練步驟" + }, + "verifier": { + "title": "Verifier 回寫", + "meta": "已規劃 {checks} 個驗證檢查" + }, + "writeback": { + "title": "資產沉澱", + "meta": "已建立 {assets} 個資產槽位" + } } }, "flow": { diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 64bebba4..c93f408e 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1121,6 +1121,15 @@ function promotionStatusTone(status: string | null | undefined): WorkStatus { return "blocked"; } +function automationClosureTone(status: string | null | undefined): WorkStatus { + const normalized = String(status ?? "").toLowerCase(); + if (normalized.includes("ready")) return "in_progress"; + if (normalized.includes("planned") || normalized.includes("draft")) return "watching"; + if (normalized.includes("blocked") || normalized.includes("failed")) return "blocked"; + if (normalized.includes("executed") || normalized.includes("healthy")) return "live"; + return "watching"; +} + function closureGateTone(status: string | null | undefined): WorkStatus { const normalized = String(status ?? "").toLowerCase(); if (normalized === "passed" || normalized === "ready") return "live"; @@ -4203,6 +4212,48 @@ function RepairCandidateDraftPanel({ ) .filter((field): field is string => Boolean(field)) .slice(0, 8); + const controlledClosure = promotionContract?.controlled_automation_closure; + const noWriteRehearsal = controlledClosure?.no_write_rehearsal; + const verifierWritebackPlan = controlledClosure?.verifier_writeback_plan; + const controlledClosureCards = [ + { + key: "rehearsal", + icon: Activity, + title: t("promotion.closure.rehearsal.title"), + status: noWriteRehearsal?.status, + primary: noWriteRehearsal?.rehearsal_id, + detail: noWriteRehearsal?.route_id, + meta: t("promotion.closure.rehearsal.meta", { + steps: noWriteRehearsal?.planned_steps?.length ?? 0, + }), + }, + { + key: "verifier", + icon: SearchCheck, + title: t("promotion.closure.verifier.title"), + status: verifierWritebackPlan?.status, + primary: verifierWritebackPlan?.verifier_id, + detail: verifierWritebackPlan?.result_states?.slice(0, 3).join(" / "), + meta: t("promotion.closure.verifier.meta", { + checks: verifierWritebackPlan?.checks?.length ?? 0, + }), + }, + { + key: "writeback", + icon: Database, + title: t("promotion.closure.writeback.title"), + status: controlledClosure?.status, + primary: controlledClosure?.runtime_write_gate, + detail: controlledClosure?.automation_asset_ledger + ?.map((asset) => asset.asset_type) + .filter(Boolean) + .slice(0, 4) + .join(" / "), + meta: t("promotion.closure.writeback.meta", { + assets: controlledClosure?.automation_asset_ledger?.length ?? 0, + }), + }, + ]; const closureReadiness = chain?.automation_handoff?.closure_readiness; const closureAvailable = Boolean(!promotionAvailable && closureReadiness); const closureReady = toCount(closureReadiness?.ready_count); @@ -4382,6 +4433,69 @@ function RepairCandidateDraftPanel({ ) : null} + + {controlledClosure ? ( +
+
+
+

{t("promotion.closure.title")}

+

+ {t("promotion.closure.subtitle")} +

+
+ + {controlledClosure.runtime_write_gate ?? "--"} + +
+
+ {controlledClosureCards.map((card) => { + const tone = automationClosureTone(card.status); + const Icon = card.icon; + const ToneIcon = statusConfig[tone].icon; + return ( +
+
+
+
+
+

+ {card.primary ?? "--"} +

+

+ {card.detail ?? "--"} +

+
+ + +
+

+ {card.meta} +

+
+ ); + })} +
+
+ {(controlledClosure.automation_asset_ledger ?? []).map((asset) => ( +
+

+ {asset.asset_type ?? "--"} +

+

+ {asset.asset_id ?? "--"} +

+

+ {asset.owner ?? "--"} · {asset.status ?? "--"} +

+
+ ))} +
+
+ ) : null} ) : closureAvailable ? (
diff --git a/apps/web/src/components/awooop/status-chain.tsx b/apps/web/src/components/awooop/status-chain.tsx index 5e767a9d..b210ed9d 100644 --- a/apps/web/src/components/awooop/status-chain.tsx +++ b/apps/web/src/components/awooop/status-chain.tsx @@ -376,6 +376,35 @@ export interface AwoooPStatusChain { owner_review_required?: boolean | null; forbidden_until_promoted?: string[]; next_steps?: string[]; + controlled_automation_closure?: { + schema_version?: string | null; + status?: string | null; + runtime_write_gate?: string | null; + no_write_rehearsal?: { + rehearsal_id?: string | null; + status?: string | null; + route_id?: string | null; + planned_steps?: string[]; + writes_runtime_state?: boolean | null; + executes_command?: boolean | null; + } | null; + verifier_writeback_plan?: { + verifier_id?: string | null; + status?: string | null; + checks?: string[]; + result_states?: string[]; + writes_after_execution?: string[]; + writes_runtime_state?: boolean | null; + } | null; + automation_asset_ledger?: Array<{ + asset_type?: string | null; + asset_id?: string | null; + owner?: string | null; + status?: string | null; + next_action?: string | null; + }>; + operator_next_actions?: string[]; + } | null; } | null; } | null; source_refs?: { diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index db192572..ad1f15a9 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,31 @@ +## 2026-06-27|D1J 修復候選升級合約:受控自動化閉環可視化 + +**背景**:Telegram / AwoooP 告警卡已能指出 `repair_candidate_draft_ready_owner_review`,但 Work Items / Approvals 仍主要呈現「需人工」與長文字,操作員看不到 AI 已準備好的無寫入演練、Verifier 與 KM / PlayBook / Script 資產沉澱槽位,容易被誤判為 AI Agent 沒有任何自動化進展。 + +**完成內容**: +- `repair_candidate_promotion_contract_v1` 新增 `controlled_automation_closure`: + - `no_write_rehearsal`:固定 rehearsal id、route、input refs、planned steps,明確 `executes_command=false`、`writes_runtime_state=false`。 + - `verifier_writeback_plan`:固定 verifier id、checks、result states 與執行後應回寫的 timeline / auto-repair / KM / PlayBook trust 欄位。 + - `automation_asset_ledger`:固定 KM、PlayBook、ScriptOrAnsible、Verifier 四類資產槽位,帶 asset id、owner、status、next action。 +- `/zh-TW/awooop/work-items` 的修復候選草案處置板新增「受控自動化閉環」三段卡:無寫入演練、Verifier 回寫、資產沉澱;保留原有 promotion readiness、blocked fields 與 runtime gate=0。 +- `AwoooPStatusChain` 型別同步支援新 closure contract,避免前端只拿到長文字。 + +**本地驗證**: +- `apps/api/venv/bin/python -m pytest apps/api/tests/test_repair_candidate_service.py apps/api/tests/test_awooop_operator_timeline_labels.py -q`:`77 passed`。 +- `apps/api/venv/bin/python -m py_compile apps/api/src/services/repair_candidate_service.py apps/api/src/services/platform_operator_service.py`:通過。 +- `python3 -m json.tool apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`:通過。 +- i18n key mirror:`14284 / 14284`,diff `0`。 +- `pnpm --filter @awoooi/web typecheck`:通過。 +- `python3 scripts/security/source-control-owner-response-guard.py --root .`:`SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `git diff --check`:通過。 + +**完成度 / 邊界**: +- 修復候選升級合約可視化:本地 `100%`。 +- Work Items 受控閉環 UI:本地 `100%`。 +- 真實 executor / Ansible apply / SSH / Telegram 實發 / KM 寫入 / PlayBook trust 寫入:仍 `0 / false`,本段沒有開 runtime gate。 +- 這段把「下一步要自動化什麼」從長文字變成可讀回的 contract 與 UI 卡片;下一段必須接 owner release / no-write rehearsal persistence / verifier result capture,不可再只新增靜態治理卡。 + ## 2026-06-27|00:58 reboot SOP 實際修復:188 MOMO backup core 假紅收斂 **時間與來源**: