From c578bcdcfad7a6675b0d7d6c8a04f5f017c1ccd0 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 11:29:10 +0800 Subject: [PATCH 1/4] feat(work-items): surface AI runbook loop coverage --- .../awoooi_priority_work_order_readback.py | 311 ++++++++++++++++++ ...awoooi_priority_work_order_readback_api.py | 66 ++++ apps/web/messages/en.json | 26 ++ apps/web/messages/zh-TW.json | 26 ++ .../app/[locale]/awooop/work-items/page.tsx | 246 ++++++++++++++ 5 files changed, 675 insertions(+) diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 3eb95607c..51aac1e49 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -84,6 +84,16 @@ _AI_AUTOMATION_NODE_RECEIPT_REQUIRED_FIELDS = [ "km_writeback_ref", "playbook_trust_writeback_ref", ] +_AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS = [ + "target_selector", + "source_truth_diff", + "candidate_action", + "check_mode_or_dry_run", + "controlled_apply_boundary", + "rollback_or_no_write", + "post_verifier", + "km_playbook_trust_writeback", +] _COMMANDER_INSERTED_REQUIREMENT_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "CIR-P0-001", @@ -6688,6 +6698,307 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: summary["ai_automation_node_receipt_metadata_only"] = True summary["ai_automation_node_receipt_lanes"] = lanes summary["ai_automation_node_receipt_work_items"] = work_items + _apply_ai_controlled_repair_loop_runbook_matrix(payload) + + +def _contains_any(value: str, needles: tuple[str, ...]) -> bool: + normalized = value.lower() + return any(needle.lower() in normalized for needle in needles) + + +def _apply_ai_controlled_repair_loop_runbook_matrix( + payload: dict[str, Any], +) -> None: + items = [ + _dict(item) + for item in payload.get("commander_inserted_requirement_work_items") or [] + if str(_dict(item).get("priority") or "") == "P0" + ] + receipts = [_dict(item) for item in payload.get("ai_automation_node_receipts") or []] + ready_receipt_work_items = { + str(item.get("work_item_id") or "") + for item in receipts + if str(item.get("node_status") or "") == "ready" + } + learning_receipt_ready = any( + str(item.get("node_id") or "") == "learning_writeback" + and str(item.get("node_status") or "") == "ready" + for item in receipts + ) + post_verifier_receipt_ready = any( + str(item.get("node_id") or "") == "post_verifier" + and str(item.get("node_status") or "") == "ready" + for item in receipts + ) + + rows: list[dict[str, Any]] = [] + for item in sorted(items, key=lambda raw: _int(raw.get("order"))): + item_id = str(item.get("id") or "") + lane = str(item.get("lane") or "") + status = str(item.get("status") or "") + text = " ".join( + str(item.get(key) or "") + for key in ( + "id", + "lane", + "request", + "normalized_work_item", + "current_state", + "acceptance", + "next_action", + "mapped_workplan_id", + "visible_surface", + ) + ) + break_glass_required = lane == "secret_safety" or _contains_any( + text, + ( + "secret plaintext", + "plaintext secret", + "private key", + "cookie", + "authorization header", + "明文", + "密碼", + "敏感值", + ), + ) + fields = { + "target_selector": bool( + item.get("mapped_workplan_id") + or item.get("visible_surface") + or item_id + ), + "source_truth_diff": _contains_any( + text, + ( + "gitea", + "source", + "repo", + "inventory", + "matrix", + "snapshot", + "evidence", + "readback", + "production", + "證據", + "讀回", + "正式環境", + ), + ), + "candidate_action": bool(item.get("next_action")) + or item_id in ready_receipt_work_items, + "check_mode_or_dry_run": _contains_any( + text, + ( + "check-mode", + "check mode", + "dry-run", + "dry run", + "verifier", + "hidden prompt", + "metadata verifier", + "non-secret", + "只跑", + "乾跑", + "驗證", + ), + ), + "controlled_apply_boundary": bool( + break_glass_required + or _contains_any( + text, + ( + "controlled", + "ai", + "fail-closed", + "no-write", + "no_secret", + "non-secret", + "不讀 secret", + "受控", + "不恢復 generic", + ), + ) + ), + "rollback_or_no_write": bool( + break_glass_required + or _contains_any( + text, + ( + "rollback", + "no-write", + "no write", + "no runtime write", + "不入檔", + "不重印", + "不保存", + "不建立", + "不恢復", + "fail-closed", + ), + ) + ), + "post_verifier": bool( + post_verifier_receipt_ready + or _contains_any( + text, + ( + "verifier", + "readback", + "smoke", + "api", + "production", + "deploy marker", + "metadata verifier", + "驗證", + "讀回", + "正式環境", + ), + ) + ), + "km_playbook_trust_writeback": bool( + learning_receipt_ready + or _contains_any( + text, + ( + "km", + "rag", + "mcp", + "playbook", + "trust", + "writeback", + "learning", + "學習", + "回寫", + ), + ) + ), + } + ready_field_count = sum(1 for ready in fields.values() if ready) + missing_fields = [ + field + for field in _AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS + if not fields[field] + ] + controlled_apply_allowed = not break_glass_required and status not in { + "deferred", + } + if break_glass_required: + coverage_state = "break_glass" + elif not missing_fields: + coverage_state = "ready" + elif ready_field_count > 0: + coverage_state = "partial" + else: + coverage_state = "queued" + + rows.append( + { + "work_item_id": item_id, + "priority": "P0", + "order": _int(item.get("order")), + "status": status, + "lane": lane, + "mapped_workplan_id": str(item.get("mapped_workplan_id") or ""), + "coverage_state": coverage_state, + "ready_field_count": ready_field_count, + "required_field_count": len(_AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS), + "missing_fields": missing_fields, + "target_selector_ready": fields["target_selector"], + "source_truth_diff_ready": fields["source_truth_diff"], + "candidate_action_ready": fields["candidate_action"], + "check_mode_or_dry_run_ready": fields["check_mode_or_dry_run"], + "controlled_apply_boundary_ready": fields[ + "controlled_apply_boundary" + ], + "controlled_apply_allowed": controlled_apply_allowed, + "break_glass_required": break_glass_required, + "rollback_or_no_write_ready": fields["rollback_or_no_write"], + "post_verifier_ready": fields["post_verifier"], + "km_playbook_trust_writeback_ready": fields[ + "km_playbook_trust_writeback" + ], + "manual_terminal_allowed": False, + "next_action": str(item.get("next_action") or ""), + } + ) + + required_total = len(rows) * len(_AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS) + ready_total = sum(_int(row.get("ready_field_count")) for row in rows) + state_counts = { + state: sum(1 for row in rows if row["coverage_state"] == state) + for state in ("ready", "partial", "queued", "break_glass") + } + next_row = next( + ( + row + for row in rows + if row["coverage_state"] not in {"ready", "break_glass"} + and row["status"] not in {"done", "deferred"} + ), + next( + ( + row + for row in rows + if row["coverage_state"] not in {"ready", "break_glass"} + ), + rows[0] if rows else {}, + ), + ) + + matrix = { + "schema_version": "ai_controlled_repair_loop_runbook_matrix_v1", + "status": "ai_controlled_repair_loop_runbook_matrix_ready", + "metadata_only": True, + "source": _COMMANDER_INSERTED_REQUIREMENT_SOURCE, + "required_fields": _AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS, + "manual_as_default_terminal_allowed": False, + "hard_blockers_preserved": [ + "secret_plaintext", + "destructive_data_change", + "reboot_or_node_drain", + "force_push_or_repo_ref_delete", + "paid_provider_route_change", + ], + "rows": rows, + "summary": { + "p0_runbook_count": len(rows), + "ready_runbook_count": state_counts["ready"], + "partial_runbook_count": state_counts["partial"], + "queued_runbook_count": state_counts["queued"], + "break_glass_runbook_count": state_counts["break_glass"], + "controlled_apply_allowed_count": sum( + 1 for row in rows if row["controlled_apply_allowed"] is True + ), + "manual_terminal_count": sum( + 1 for row in rows if row["manual_terminal_allowed"] is True + ), + "required_field_total": required_total, + "ready_field_total": ready_total, + "completion_percent": round( + (ready_total / required_total) * 100 + ) + if required_total + else 0, + "next_work_item_id": str(next_row.get("work_item_id") or ""), + "next_missing_fields": next_row.get("missing_fields") or [], + }, + } + + payload["ai_controlled_repair_loop_runbook_matrix"] = matrix + + summary = _dict(payload.setdefault("summary", {})) + rollups = _dict(payload.setdefault("rollups", {})) + for key, value in matrix["summary"].items(): + summary[f"ai_controlled_repair_loop_{key}"] = value + rollups[f"ai_controlled_repair_loop_{key}"] = value + summary["ai_controlled_repair_loop_schema_version"] = matrix["schema_version"] + summary["ai_controlled_repair_loop_metadata_only"] = True + summary["ai_controlled_repair_loop_manual_as_default_terminal_allowed"] = False + rollups["ai_controlled_repair_loop_schema_version"] = matrix["schema_version"] + rollups["ai_controlled_repair_loop_metadata_only"] = True + rollups["ai_controlled_repair_loop_manual_as_default_terminal_allowed"] = False def _mark_runtime_generated_at(payload: dict[str, Any]) -> None: diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index 3eae7357c..285a490b0 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -589,6 +589,53 @@ def test_awoooi_priority_work_order_readback_surfaces_gitea_bundle_truth(): assert "CIR-P0-AILOOP-002" in payload["summary"][ "ai_automation_node_receipt_work_items" ] + runbook_matrix = payload["ai_controlled_repair_loop_runbook_matrix"] + assert runbook_matrix["schema_version"] == ( + "ai_controlled_repair_loop_runbook_matrix_v1" + ) + assert runbook_matrix["metadata_only"] is True + assert runbook_matrix["manual_as_default_terminal_allowed"] is False + assert runbook_matrix["required_fields"] == [ + "target_selector", + "source_truth_diff", + "candidate_action", + "check_mode_or_dry_run", + "controlled_apply_boundary", + "rollback_or_no_write", + "post_verifier", + "km_playbook_trust_writeback", + ] + runbook_rows = runbook_matrix["rows"] + runbook_by_id = {item["work_item_id"]: item for item in runbook_rows} + runbook_summary = runbook_matrix["summary"] + assert runbook_summary["p0_runbook_count"] == sum( + 1 for item in inserted_items if item["priority"] == "P0" + ) + assert runbook_summary["manual_terminal_count"] == 0 + assert runbook_summary["controlled_apply_allowed_count"] == sum( + 1 for item in runbook_rows if item["controlled_apply_allowed"] is True + ) + assert runbook_summary["break_glass_runbook_count"] == sum( + 1 for item in runbook_rows if item["coverage_state"] == "break_glass" + ) + assert runbook_summary["next_work_item_id"] == "CIR-P0-007" + assert runbook_by_id["CIR-P0-006"]["break_glass_required"] is True + assert runbook_by_id["CIR-P0-006"]["controlled_apply_allowed"] is False + assert runbook_by_id["CIR-P0-007"]["controlled_apply_allowed"] is True + assert runbook_by_id["CIR-P0-007"]["coverage_state"] == "partial" + assert runbook_by_id["CIR-P0-007"]["manual_terminal_allowed"] is False + assert "km_playbook_trust_writeback" in runbook_by_id["CIR-P0-007"][ + "missing_fields" + ] + assert payload["summary"]["ai_controlled_repair_loop_p0_runbook_count"] == ( + runbook_summary["p0_runbook_count"] + ) + assert payload["summary"]["ai_controlled_repair_loop_next_work_item_id"] == ( + "CIR-P0-007" + ) + assert payload["summary"][ + "ai_controlled_repair_loop_manual_as_default_terminal_allowed" + ] is False def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_and_windows99( @@ -1452,6 +1499,25 @@ def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot( assert endpoint_receipts["learning_writeback"]["km_writeback_ref"] == ( "context_receipt:km=1" ) + endpoint_matrix = data["ai_controlled_repair_loop_runbook_matrix"] + endpoint_rows = endpoint_matrix["rows"] + endpoint_row_by_id = {item["work_item_id"]: item for item in endpoint_rows} + assert endpoint_matrix["status"] == ( + "ai_controlled_repair_loop_runbook_matrix_ready" + ) + assert endpoint_matrix["summary"]["p0_runbook_count"] == sum( + 1 for item in inserted_items if item["priority"] == "P0" + ) + assert endpoint_matrix["summary"]["manual_terminal_count"] == 0 + assert endpoint_matrix["summary"]["next_work_item_id"] == "CIR-P0-007" + assert endpoint_row_by_id["CIR-P0-006"]["coverage_state"] == "break_glass" + assert endpoint_row_by_id["CIR-P0-006"]["controlled_apply_allowed"] is False + assert endpoint_row_by_id["CIR-P0-007"]["coverage_state"] == "partial" + assert endpoint_row_by_id["CIR-P0-007"]["controlled_apply_allowed"] is True + assert data["summary"]["ai_controlled_repair_loop_next_work_item_id"] == ( + "CIR-P0-007" + ) + assert data["summary"]["ai_controlled_repair_loop_manual_terminal_count"] == 0 def test_controlled_cd_lane_live_metric_readback_falls_back_to_prometheus( diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index a5815d15e..389ed5cb9 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9573,6 +9573,32 @@ "backup": "backup evidence" } }, + "aiControlledRunbookCoverage": { + "eyebrow": "AI Loop Coverage", + "title": "P0 Runbook AI Controlled Coverage", + "subtitle": "Turns each P0 runbook's target selector, source truth, candidate, check-mode, controlled apply, rollback, post verifier, and KM / PlayBook trust writeback into one matrix.", + "next": "Next gap", + "manualTerminal": "Manual terminal", + "allowed": "allowed", + "disabled": "disabled", + "source": "source", + "fieldProgress": "ready {ready}/{total}", + "metrics": { + "completion": "Field coverage", + "controlled": "controlled apply", + "controlledDetail": "Executable after break-glass exclusions", + "breakGlass": "break-glass", + "breakGlassDetail": "Incident-grade blockers such as secret, destructive, or reboot work", + "manual": "manual terminal", + "manualDetail": "Manual is not the default terminal state for low/medium/high work" + }, + "states": { + "ready": "ready", + "partial": "partial", + "queued": "queued", + "break_glass": "break-glass" + } + }, "commanderInsertedRequirements": { "eyebrow": "Mainline priority", "title": "Commander Inserted Requirement Work Items", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 7316e2266..f26602def 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9573,6 +9573,32 @@ "backup": "backup evidence" } }, + "aiControlledRunbookCoverage": { + "eyebrow": "AI Loop Coverage", + "title": "P0 Runbook AI Controlled Coverage", + "subtitle": "把每個 P0 runbook 的 target selector、source truth、candidate、check-mode、controlled apply、rollback、post verifier 與 KM / PlayBook trust 回寫狀態收成矩陣。", + "next": "下一個缺口", + "manualTerminal": "人工終局", + "allowed": "allowed", + "disabled": "disabled", + "source": "source", + "fieldProgress": "ready {ready}/{total}", + "metrics": { + "completion": "欄位覆蓋", + "controlled": "controlled apply", + "controlledDetail": "排除 break-glass 後可受控推進", + "breakGlass": "break-glass", + "breakGlassDetail": "secret / destructive / reboot 等事故級硬阻擋", + "manual": "manual terminal", + "manualDetail": "人工不得是 low/medium/high 預設終局" + }, + "states": { + "ready": "ready", + "partial": "partial", + "queued": "queued", + "break_glass": "break-glass" + } + }, "commanderInsertedRequirements": { "eyebrow": "主線優先序", "title": "統帥插入需求工作項", 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 9ab9902e2..59fc75596 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1067,6 +1067,56 @@ type CommanderInsertedRequirementWorkItem = { source?: string | null; }; +type AiControlledRepairLoopRunbookRow = { + work_item_id?: string | null; + priority?: string | null; + order?: number | null; + status?: string | null; + lane?: string | null; + mapped_workplan_id?: string | null; + coverage_state?: "ready" | "partial" | "queued" | "break_glass" | string | null; + ready_field_count?: number | null; + required_field_count?: number | null; + missing_fields?: string[] | null; + target_selector_ready?: boolean | null; + source_truth_diff_ready?: boolean | null; + candidate_action_ready?: boolean | null; + check_mode_or_dry_run_ready?: boolean | null; + controlled_apply_boundary_ready?: boolean | null; + controlled_apply_allowed?: boolean | null; + break_glass_required?: boolean | null; + rollback_or_no_write_ready?: boolean | null; + post_verifier_ready?: boolean | null; + km_playbook_trust_writeback_ready?: boolean | null; + manual_terminal_allowed?: boolean | null; + next_action?: string | null; +}; + +type AiControlledRepairLoopRunbookMatrix = { + schema_version?: string | null; + status?: string | null; + metadata_only?: boolean | null; + source?: string | null; + required_fields?: string[] | null; + manual_as_default_terminal_allowed?: boolean | null; + hard_blockers_preserved?: string[] | null; + rows?: AiControlledRepairLoopRunbookRow[] | null; + summary?: { + p0_runbook_count?: number | null; + ready_runbook_count?: number | null; + partial_runbook_count?: number | null; + queued_runbook_count?: number | null; + break_glass_runbook_count?: number | null; + controlled_apply_allowed_count?: number | null; + manual_terminal_count?: number | null; + required_field_total?: number | null; + ready_field_total?: number | null; + completion_percent?: number | null; + next_work_item_id?: string | null; + next_missing_fields?: string[] | null; + } | null; +}; + type PriorityWorkOrderResponse = { current_head?: { gitea_main_sha?: string | null; @@ -1434,6 +1484,7 @@ type PriorityWorkOrderResponse = { metadata_only?: boolean | null; hard_blockers_preserved?: string[] | null; } | null; + ai_controlled_repair_loop_runbook_matrix?: AiControlledRepairLoopRunbookMatrix | null; }; type SecurityCockpitRuntimeReadback = { @@ -8523,6 +8574,196 @@ function aiAutomationNodeStatusTone(status: string) { } } +function aiControlledRunbookCoverageTone(status: string) { + switch (status) { + case "ready": + return "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]"; + case "break_glass": + return "border-[#d4c1ef] bg-[#f8f4ff] text-[#5b3a8a]"; + case "queued": + return "border-[#ead5bd] bg-[#fff8ef] text-[#7a3d00]"; + default: + return "border-[#c9d8ea] bg-[#eef5ff] text-[#1f5b9b]"; + } +} + +function AiControlledRunbookCoveragePanel({ + priority, + loading, +}: { + priority: PriorityWorkOrderResponse | null; + loading: boolean; +}) { + const t = useTranslations("awooop.workItems.aiControlledRunbookCoverage"); + const matrix = priority?.ai_controlled_repair_loop_runbook_matrix; + const summary = matrix?.summary; + const rows = matrix?.rows ?? []; + const nextId = summary?.next_work_item_id ?? "--"; + const spotlightIds = new Set([ + nextId, + "CIR-P0-006", + "CIR-P0-007", + "CIR-P0-LOG-001", + "CIR-P0-TG-001", + ]); + const spotlightRows = rows + .filter((row) => row.work_item_id && spotlightIds.has(row.work_item_id)) + .slice(0, 6); + const metricCards = [ + { + key: "completion", + label: t("metrics.completion"), + value: loading ? "--" : `${summary?.completion_percent ?? 0}%`, + detail: loading + ? "--" + : `${summary?.ready_field_total ?? 0}/${summary?.required_field_total ?? 0}`, + icon: Gauge, + tone: "border-[#c9d8ea] bg-[#eef5ff] text-[#1f5b9b]", + }, + { + key: "controlled", + label: t("metrics.controlled"), + value: loading + ? "--" + : `${summary?.controlled_apply_allowed_count ?? 0}/${summary?.p0_runbook_count ?? 0}`, + detail: t("metrics.controlledDetail"), + icon: ShieldCheck, + tone: "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]", + }, + { + key: "breakGlass", + label: t("metrics.breakGlass"), + value: loading ? "--" : summary?.break_glass_runbook_count ?? 0, + detail: t("metrics.breakGlassDetail"), + icon: Lock, + tone: "border-[#d4c1ef] bg-[#f8f4ff] text-[#5b3a8a]", + }, + { + key: "manual", + label: t("metrics.manual"), + value: loading ? "--" : summary?.manual_terminal_count ?? 0, + detail: t("metrics.manualDetail"), + icon: CheckCircle2, + tone: "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]", + }, + ]; + + return ( +
+
+
+
+
+

+ {t("title")} +

+
+
+
+ {t("next")} +
+
+ {loading ? "--" : nextId} +
+
+
+
+ {t("manualTerminal")} +
+
+ {loading + ? "--" + : matrix?.manual_as_default_terminal_allowed + ? t("allowed") + : t("disabled")} +
+
+
+

+ {t("subtitle")} +

+
+ +
+
+ {metricCards.map((metric) => { + const Icon = metric.icon; + return ( +
+
+ {metric.label} +
+
+ {metric.value} +
+
+ {metric.detail} +
+
+ ); + })} +
+ +
+ {spotlightRows.map((row) => { + const state = String(row.coverage_state ?? "partial"); + return ( +
+
+
+
+ {row.work_item_id} +
+
+ {row.lane} +
+
+ + {t(`states.${state}` as never)} + +
+
+ {t("fieldProgress", { + ready: row.ready_field_count ?? 0, + total: row.required_field_count ?? 0, + })} +
+
+ {(row.missing_fields ?? []).slice(0, 4).map((field) => ( + + {field} + + ))} +
+
+ ); + })} +
+ +
+ {t("source")}{" "} + + {loading ? "--" : matrix?.source ?? "--"} + +
+
+
+
+ ); +} + function AiLoopLogSourceTagsPanel({ priority, loading, @@ -12325,6 +12566,11 @@ export default function AwoooPWorkItemsPage() { > + + Date: Fri, 10 Jul 2026 11:33:08 +0800 Subject: [PATCH 2/4] feat(work-items): surface current p0 execution focus --- .../awoooi_priority_work_order_readback.py | 165 +++++++++++++++++ ...awoooi_priority_work_order_readback_api.py | 38 ++++ apps/web/messages/en.json | 6 + apps/web/messages/zh-TW.json | 6 + .../app/[locale]/awooop/work-items/page.tsx | 172 ++++++++++++++++-- 5 files changed, 374 insertions(+), 13 deletions(-) diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 51aac1e49..ad327bdc8 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -5006,6 +5006,149 @@ def _stockplatform_ai_recommendations_wait( ) +def _active_p0_blocker_group_counts(active_blockers: list[str]) -> dict[str, int]: + return { + "host_boot_detection": sum( + 1 for blocker in active_blockers if blocker in _REBOOT_HOST_BOOT_DETECTION_BLOCKERS + ), + "windows99_vmware": sum( + 1 for blocker in active_blockers if blocker.startswith("windows99_") + ), + "service_data_backup": sum( + 1 + for blocker in active_blockers + if blocker + in { + "backup_core_green_not_1", + "host_188_service_green_not_1", + "post_start_blocked_not_zero", + "product_data_green_not_1", + "service_green_not_1", + "wazuh_dashboard_degraded", + } + ), + "other": sum( + 1 + for blocker in active_blockers + if blocker not in _REBOOT_HOST_BOOT_DETECTION_BLOCKERS + and not blocker.startswith("windows99_") + and blocker + not in { + "backup_core_green_not_1", + "host_188_service_green_not_1", + "post_start_blocked_not_zero", + "product_data_green_not_1", + "service_green_not_1", + "wazuh_dashboard_degraded", + } + ), + } + + +def _build_current_p0_execution_focus(state: dict[str, Any]) -> dict[str, Any]: + active_blockers = _strings(state.get("active_p0_live_active_blockers")) + group_counts = _active_p0_blocker_group_counts(active_blockers) + service_blocker_count = _int( + state.get("controlled_service_data_backup_blocker_count") + ) + service_backup_closed = ( + state.get("controlled_service_data_backup_can_clear_blockers") is True + or service_blocker_count == 0 + ) + locator_dry_run_ready = bool( + state.get("windows99_vmx_source_locator_controlled_relink_dry_run_ready") + is True + or state.get("windows99_vmx_source_locator_candidate_relink_dry_run_ready") + is True + or str(state.get("windows99_vmx_source_locator_status") or "") + == "collector_readback_scoped_relink_dry_run_ready" + ) + autostart_check_ready = bool( + state.get("windows99_vmware_autostart_check_mode_package_ready") is True + and state.get("windows99_vmware_autostart_check_mode_allowed") is True + ) + + ready_packages: list[dict[str, Any]] = [] + if locator_dry_run_ready: + ready_packages.append( + { + "id": "windows99_vmx_source_scoped_relink_dry_run", + "status": str( + state.get("windows99_vmx_source_locator_status") or "ready" + ), + "target_aliases": _strings( + state.get("windows99_vmx_source_locator_target_aliases") + ), + "next_executable_step": str( + state.get("windows99_vmx_source_locator_next_executable_step") + or "rerun_no_secret_collector_and_scorecard" + ), + } + ) + if autostart_check_ready: + ready_packages.append( + { + "id": "windows99_vmware_autostart_check_mode", + "status": str( + state.get("windows99_vmware_autostart_check_mode_package_status") + or "ready" + ), + "target_aliases": _strings( + state.get("windows99_vmware_autostart_check_mode_target_vm_aliases") + ), + "next_executable_step": str( + state.get("windows99_vmware_autostart_check_mode_next_executable_step") + or "" + ), + } + ) + + if service_blocker_count and not service_backup_closed: + stage = "service_data_backup_readback_not_green" + lane = "service_data_backup_readback" + next_executable_step = "collect_service_data_backup_readback_no_restart_no_db_write" + elif ready_packages: + stage = "windows99_no_secret_verifier_rerun_ready" + lane = "windows99_vmware_verify_collection" + next_executable_step = "rerun_no_secret_collector_and_scorecard_no_vm_power_change" + elif group_counts["host_boot_detection"]: + stage = "host_probe_verifier_only_required" + lane = "reboot_event_detector_and_host_probe" + next_executable_step = "rerun_reboot_event_detector_and_host_probe_verify_only_no_reboot" + else: + stage = "p0_readback_review_required" + lane = "p0_mainline_readback" + next_executable_step = str(state.get("active_p0_next_safe_action") or "") + + focus = { + "schema_version": "awoooi_current_p0_execution_focus_v1", + "workplan_id": str(state.get("active_p0_workplan_id") or ""), + "state": str(state.get("active_p0_state") or ""), + "stage": stage, + "lane": lane, + "primary_blocker": str(state.get("active_p0_primary_blocker") or ""), + "next_executable_step": next_executable_step, + "readiness_percent": _int(state.get("active_p0_readiness_percent")), + "active_blocker_count": len(active_blockers), + "blocker_group_counts": group_counts, + "ready_package_count": len(ready_packages), + "ready_packages": ready_packages, + "no_secret_verifier_ready": bool(ready_packages), + "scoped_relink_dry_run_ready": locator_dry_run_ready, + "autostart_check_mode_ready": autostart_check_ready, + "service_data_backup_closed": service_backup_closed, + "operation_boundaries": { + "secret_value_read": False, + "host_reboot": False, + "vm_power_change": False, + "remote_write": False, + "database_write": False, + "github_api": False, + }, + } + return focus + + def _apply_parallel_mainline_execution_overlay(payload: dict[str, Any]) -> None: state = _dict(payload.setdefault("mainline_execution_state", {})) items = _list(payload.get("commander_inserted_requirement_work_items")) @@ -5116,6 +5259,8 @@ def _set_rollups_and_summary( ) active_blockers = _strings(state.get("active_p0_live_active_blockers")) active_gap_count = _int(state.get("active_p0_immediate_apply_gap_count")) + current_p0_focus = _build_current_p0_execution_focus(state) + payload["current_p0_execution_focus"] = current_p0_focus payload["rollups"] = { "completed_p0_count": len(completed), @@ -5533,9 +5678,29 @@ def _set_rollups_and_summary( "stale_current_head_metadata_normalized": runtime_source_truth_available, "stale_cd_runs_reopen_closed_work": False, "break_glass_reboot_drill_required": True, + "current_p0_execution_focus_stage": current_p0_focus["stage"], + "current_p0_execution_focus_lane": current_p0_focus["lane"], + "current_p0_execution_focus_ready_package_count": current_p0_focus[ + "ready_package_count" + ], + "current_p0_execution_focus_no_secret_verifier_ready": current_p0_focus[ + "no_secret_verifier_ready" + ], } payload["summary"] = { "status": str(payload.get("status") or ""), + "current_p0_execution_focus": current_p0_focus, + "current_p0_execution_focus_stage": current_p0_focus["stage"], + "current_p0_execution_focus_lane": current_p0_focus["lane"], + "current_p0_execution_focus_next_executable_step": current_p0_focus[ + "next_executable_step" + ], + "current_p0_execution_focus_ready_package_count": current_p0_focus[ + "ready_package_count" + ], + "current_p0_execution_focus_no_secret_verifier_ready": current_p0_focus[ + "no_secret_verifier_ready" + ], "active_p0_workplan_id": str(state.get("active_p0_workplan_id") or ""), "active_p0_state": str(state.get("active_p0_state") or ""), "active_p0_readiness_percent": _int( diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index 285a490b0..1ddea8512 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -759,6 +759,29 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a assert state["active_p0_next_safe_action"] == ( "rerun_reboot_event_detector_and_host_probe_verify_only_no_reboot" ) + focus = payload["current_p0_execution_focus"] + assert focus["schema_version"] == "awoooi_current_p0_execution_focus_v1" + assert focus["workplan_id"] == "P0-006" + assert focus["stage"] == "windows99_no_secret_verifier_rerun_ready" + assert focus["lane"] == "windows99_vmware_verify_collection" + assert focus["next_executable_step"] == ( + "rerun_no_secret_collector_and_scorecard_no_vm_power_change" + ) + assert focus["blocker_group_counts"]["host_boot_detection"] > 0 + assert focus["blocker_group_counts"]["windows99_vmware"] == 2 + assert focus["ready_package_count"] == 2 + assert focus["no_secret_verifier_ready"] is True + assert focus["scoped_relink_dry_run_ready"] is True + assert focus["autostart_check_mode_ready"] is True + assert focus["service_data_backup_closed"] is True + assert focus["operation_boundaries"] == { + "secret_value_read": False, + "host_reboot": False, + "vm_power_change": False, + "remote_write": False, + "database_write": False, + "github_api": False, + } assert ( state["active_p0_projection_source"] == "reboot_auto_recovery_slo_scorecard" @@ -1008,6 +1031,21 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a assert payload["summary"]["active_p0_next_safe_action"] == ( "rerun_reboot_event_detector_and_host_probe_verify_only_no_reboot" ) + assert payload["summary"]["current_p0_execution_focus_stage"] == ( + "windows99_no_secret_verifier_rerun_ready" + ) + assert payload["summary"]["current_p0_execution_focus_lane"] == ( + "windows99_vmware_verify_collection" + ) + assert payload["summary"]["current_p0_execution_focus_ready_package_count"] == 2 + assert ( + payload["summary"]["current_p0_execution_focus_no_secret_verifier_ready"] + is True + ) + assert payload["rollups"]["current_p0_execution_focus_stage"] == ( + "windows99_no_secret_verifier_rerun_ready" + ) + assert payload["rollups"]["current_p0_execution_focus_ready_package_count"] == 2 assert ( payload["summary"]["active_p0_projection_source"] == "reboot_auto_recovery_slo_scorecard" diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 389ed5cb9..43e382e6e 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9539,9 +9539,15 @@ "loading": "Reading production work-order", "completion": "Overall completion", "currentP0": "Current P0", + "focusStage": "Current stage", "primaryBlocker": "Primary blocker", "blockerCount": "{count} blockers", + "readyPackages": "Executable packages", "nextAction": "Next ordered action", + "verifierReady": "no-secret verifier ready", + "verifierWaiting": "verifier waiting", + "boundary": "no secret / no host change / no DB write", + "rawSignal": "Raw signal", "statuses": { "done": "Completed", "in_progress": "In progress", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index f26602def..8ed5bdf57 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9539,9 +9539,15 @@ "loading": "讀取正式 work-order", "completion": "整體完成度", "currentP0": "目前 P0", + "focusStage": "目前階段", "primaryBlocker": "主要阻擋", "blockerCount": "{count} 個 blocker", + "readyPackages": "可執行套件", "nextAction": "下一個 ordered action", + "verifierReady": "no-secret verifier ready", + "verifierWaiting": "verifier waiting", + "boundary": "不讀 secret / 不動主機 / 不寫 DB", + "rawSignal": "原始訊號", "statuses": { "done": "已完成", "in_progress": "進行中", 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 59fc75596..86e48f991 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1117,6 +1117,38 @@ type AiControlledRepairLoopRunbookMatrix = { } | null; }; +type CurrentP0ExecutionFocus = { + schema_version?: string | null; + workplan_id?: string | null; + state?: string | null; + stage?: string | null; + lane?: string | null; + primary_blocker?: string | null; + next_executable_step?: string | null; + readiness_percent?: number | null; + active_blocker_count?: number | null; + ready_package_count?: number | null; + no_secret_verifier_ready?: boolean | null; + scoped_relink_dry_run_ready?: boolean | null; + autostart_check_mode_ready?: boolean | null; + service_data_backup_closed?: boolean | null; + operation_boundaries?: { + secret_value_read?: boolean | null; + host_reboot?: boolean | null; + vm_power_change?: boolean | null; + remote_write?: boolean | null; + database_write?: boolean | null; + github_api?: boolean | null; + } | null; + ready_packages?: Array<{ + id?: string | null; + status?: string | null; + target_aliases?: string[] | null; + next_executable_step?: string | null; + }> | null; + blocker_group_counts?: Record | null; +}; + type PriorityWorkOrderResponse = { current_head?: { gitea_main_sha?: string | null; @@ -1137,7 +1169,14 @@ type PriorityWorkOrderResponse = { next_action?: string | null; } | null; } | null; + current_p0_execution_focus?: CurrentP0ExecutionFocus | null; summary?: { + current_p0_execution_focus?: CurrentP0ExecutionFocus | null; + current_p0_execution_focus_stage?: string | null; + current_p0_execution_focus_lane?: string | null; + current_p0_execution_focus_next_executable_step?: string | null; + current_p0_execution_focus_ready_package_count?: number | null; + current_p0_execution_focus_no_secret_verifier_ready?: boolean | null; active_p0_state?: string | null; active_p0_workplan_id?: string | null; active_p0_readiness_percent?: number | null; @@ -9616,7 +9655,12 @@ function securityOperatorLabel(value: string | null | undefined, isZh: boolean) "候選身分待確認", host_unreachable_after_reboot: "重開機後主機未連線", reboot_event_required_host_unreachable: "重開機目標不可達", + host_probe_verifier_only_required: "Host probe 只驗證", + p0_readback_review_required: "P0 讀回待判讀", + service_data_backup_readback_not_green: "Service/Data/Backup 未綠", waiting_readback: "等待正式讀回", + windows99_no_secret_verifier_rerun_ready: "Windows99 verifier 可重跑", + windows99_vmware_verify_collection: "Windows99 VMware 驗證收件", windows99_remote_execution_channel_unavailable: "Windows99 通道未連線", windows99_vmware_autostart_readback_missing: "VMware 自啟讀回缺口", windows99_vmware_vmx_missing: "VMX 來源缺口", @@ -9627,7 +9671,12 @@ function securityOperatorLabel(value: string | null | undefined, isZh: boolean) "Candidate identity pending", host_unreachable_after_reboot: "Host unreachable after reboot", reboot_event_required_host_unreachable: "Reboot target unreachable", + host_probe_verifier_only_required: "Host probe verify-only", + p0_readback_review_required: "P0 readback review", + service_data_backup_readback_not_green: "Service/Data/Backup not green", waiting_readback: "Waiting readback", + windows99_no_secret_verifier_rerun_ready: "Windows99 verifier rerun ready", + windows99_vmware_verify_collection: "Windows99 VMware verification", windows99_remote_execution_channel_unavailable: "Windows99 channel offline", windows99_vmware_autostart_readback_missing: "VMware autostart readback gap", windows99_vmware_vmx_missing: "VMX source gap", @@ -11411,12 +11460,19 @@ function pickMainlineVisibleWorkItems( function MainlineWorkProgressStrip({ priority, loading, + locale, }: { priority: PriorityWorkOrderResponse | null; loading: boolean; + locale: string; }) { const t = useTranslations("awooop.workItems.mainlineProgress"); + const isZh = locale === "zh-TW"; const summary = priority?.summary; + const focus = + priority?.current_p0_execution_focus ?? + summary?.current_p0_execution_focus ?? + null; const items = (priority?.commander_inserted_requirement_work_items ?? []) .slice() .sort((left, right) => (left.order ?? 0) - (right.order ?? 0)); @@ -11446,8 +11502,40 @@ function MainlineWorkProgressStrip({ const currentState = summary?.active_p0_state ?? "--"; const activeBlockers = summary?.active_p0_live_active_blockers ?? []; const primaryBlocker = - summary?.active_p0_primary_blocker ?? activeBlockers[0] ?? "--"; - const visibleItems = pickMainlineVisibleWorkItems(items, nextId); + focus?.primary_blocker ?? + summary?.active_p0_primary_blocker ?? + activeBlockers[0] ?? + "--"; + const focusStage = + focus?.stage ?? + summary?.current_p0_execution_focus_stage ?? + currentState; + const focusLane = + focus?.lane ?? + summary?.current_p0_execution_focus_lane ?? + "--"; + const focusNext = + focus?.next_executable_step ?? + summary?.current_p0_execution_focus_next_executable_step ?? + nextAction; + const focusReadyPackages = toCount( + focus?.ready_package_count ?? + summary?.current_p0_execution_focus_ready_package_count + ); + const focusNoSecretReady = + focus?.no_secret_verifier_ready ?? + summary?.current_p0_execution_focus_no_secret_verifier_ready ?? + false; + const boundary = focus?.operation_boundaries; + const boundaryClosed = + boundary && + boundary.secret_value_read === false && + boundary.host_reboot === false && + boundary.vm_power_change === false && + boundary.remote_write === false && + boundary.database_write === false && + boundary.github_api === false; + const visibleItems = pickMainlineVisibleWorkItems(items, nextId, 6); const statusMetrics = [ { key: "done", @@ -11563,8 +11651,11 @@ function MainlineWorkProgressStrip({
{item.id}
-
- {item.normalized_work_item ?? item.request} +
+ {item.lane ?? + item.mapped_workplan_id ?? + item.normalized_work_item ?? + item.request}
); @@ -11587,19 +11678,43 @@ function MainlineWorkProgressStrip({ {loading ? "--" : `${readiness}%`}
- {currentState} + {loading ? "--" : securityOperatorLabel(currentState, isZh)} +
+ + +
+
+
+ {t("focusStage")} +
+
+ {loading ? "--" : securityOperatorLabel(focusStage, isZh)} +
+
+
+
+ {t("readyPackages")} +
+
+ {loading ? "--" : focusReadyPackages} +
-
- {t("primaryBlocker")} +
+
+ {t("primaryBlocker")} +
+ + {t("blockerCount", { count: activeBlockers.length })} +
-
- {loading ? "--" : primaryBlocker} +
+ {loading ? "--" : securityOperatorLabel(primaryBlocker, isZh)}
-
- {t("blockerCount", { count: activeBlockers.length })} +
+ {loading ? "--" : focusLane}
@@ -11607,9 +11722,39 @@ function MainlineWorkProgressStrip({
{t("nextAction")}
-
- {loading ? "--" : nextAction} +
+ {loading ? "--" : securityNextActionLabel(focusNext, isZh)}
+
+ + {focusNoSecretReady ? t("verifierReady") : t("verifierWaiting")} + + + {t("boundary")} + +
+
+ + {t("rawSignal")} + +
+ {focusNext} +
+
@@ -12513,6 +12658,7 @@ export default function AwoooPWorkItemsPage() { Date: Fri, 10 Jul 2026 11:40:25 +0800 Subject: [PATCH 3/4] fix(work-items): prioritize spotlight items --- .../src/app/[locale]/awooop/work-items/page.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 86e48f991..2cf4c2b92 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -11412,7 +11412,7 @@ function commanderStatusTone(status: string) { } } -const MAINLINE_WORK_PROGRESS_SPOTLIGHT_IDS = new Set([ +const MAINLINE_WORK_PROGRESS_SPOTLIGHT_IDS = [ "CIR-P1-AI-002", "CIR-P0-AILOOP-001", "CIR-P0-LOG-001", @@ -11421,7 +11421,11 @@ const MAINLINE_WORK_PROGRESS_SPOTLIGHT_IDS = new Set([ "CIR-P1-UI-001", "CIR-P1-OPENCLAW-001", "CIR-P2-UX-002", -]); +]; + +const MAINLINE_WORK_PROGRESS_SPOTLIGHT_ID_SET = new Set( + MAINLINE_WORK_PROGRESS_SPOTLIGHT_IDS +); function pickMainlineVisibleWorkItems( items: CommanderInsertedRequirementWorkItem[], @@ -11439,13 +11443,17 @@ function pickMainlineVisibleWorkItems( seen.add(id); }; + const itemsById = new Map(items.map((item) => [item.id, item])); addItem(items.find((item) => item.id === nextId)); + for (const id of MAINLINE_WORK_PROGRESS_SPOTLIGHT_IDS) { + addItem(itemsById.get(id)); + } for (const item of items) { const status = item.status ?? "pending"; if ( status === "in_progress" || status === "blocked" || - MAINLINE_WORK_PROGRESS_SPOTLIGHT_IDS.has(item.id ?? "") + MAINLINE_WORK_PROGRESS_SPOTLIGHT_ID_SET.has(item.id ?? "") ) { addItem(item); } From f22fa23ac1017ab6ce320f4908bd0ffee25f39d3 Mon Sep 17 00:00:00 2001 From: AWOOOI CD Date: Fri, 10 Jul 2026 11:46:35 +0800 Subject: [PATCH 4/4] chore(cd): deploy 585d930 [skip ci] --- k8s/awoooi-prod/06-deployment-api.yaml | 4 ++-- k8s/awoooi-prod/kustomization.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/k8s/awoooi-prod/06-deployment-api.yaml b/k8s/awoooi-prod/06-deployment-api.yaml index 532513f02..241c9eb68 100644 --- a/k8s/awoooi-prod/06-deployment-api.yaml +++ b/k8s/awoooi-prod/06-deployment-api.yaml @@ -84,12 +84,12 @@ spec: - name: AWOOOI_BUILD_COMMIT_SHA # 2026-06-29 Codex: CD rewrites this to the deployed image tag so # production deploy readback does not rely on a stale static snapshot. - value: "4026a23dcf2bedc0203907ec05d75c4d7f4a72a2" + value: "585d93029995912fcfc67048070ff505c680d067" - name: AWOOOI_DESIRED_API_IMAGE_TAG # 2026-06-30 Codex: CD rewrites this alongside AWOOOI_BUILD_COMMIT_SHA. # Production readback compares runtime image truth against this # GitOps desired tag instead of doing a slow Gitea raw fetch. - value: "4026a23dcf2bedc0203907ec05d75c4d7f4a72a2" + value: "585d93029995912fcfc67048070ff505c680d067" - name: DATABASE_POOL_SIZE # 2026-07-01 Codex: production role `awoooi` currently has a low # connection limit. Keep API pool conservative until DB role diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml index c0a9b6574..cbde77461 100644 --- a/k8s/awoooi-prod/kustomization.yaml +++ b/k8s/awoooi-prod/kustomization.yaml @@ -41,7 +41,7 @@ resources: images: - name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/api - newTag: 4026a23dcf2bedc0203907ec05d75c4d7f4a72a2 + newTag: 585d93029995912fcfc67048070ff505c680d067 - name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/web - newTag: 4026a23dcf2bedc0203907ec05d75c4d7f4a72a2 + newTag: 585d93029995912fcfc67048070ff505c680d067