From a21f94ced1f8967a6a50c45a9d04b45bdba369bb Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 31 May 2026 17:28:41 +0800 Subject: [PATCH] fix(alerts): clarify execution result verdict --- apps/api/src/services/approval_execution.py | 11 ++ apps/api/src/services/operator_outcome.py | 143 ++++++++++++++++++ apps/api/src/services/telegram_gateway.py | 7 + apps/api/tests/test_operator_outcome.py | 12 ++ apps/web/messages/en.json | 1 + apps/web/messages/zh-TW.json | 1 + .../src/components/awooop/status-chain.tsx | 26 +++- docs/LOGBOOK.md | 60 ++++++++ ...-04-15-MASTER-ai-autonomous-flywheel-v2.md | 6 + 9 files changed, 266 insertions(+), 1 deletion(-) diff --git a/apps/api/src/services/approval_execution.py b/apps/api/src/services/approval_execution.py index 91ae79d34..fb46127ea 100644 --- a/apps/api/src/services/approval_execution.py +++ b/apps/api/src/services/approval_execution.py @@ -1204,6 +1204,11 @@ class ApprovalExecutionService: if isinstance(outcome.get("notification"), dict) else {} ) + execution_result = ( + outcome.get("execution_result") + if isinstance(outcome.get("execution_result"), dict) + else {} + ) channels = notification.get("channels") if isinstance(notification.get("channels"), list) else [] lines = [ f"{icon} 處置結果 | {html.escape(title)}", @@ -1214,6 +1219,12 @@ class ApprovalExecutionService: f"├ 人工: {'yes' if outcome.get('needs_human') else 'no'}", f"├ 通知: {html.escape(','.join(str(item) for item in channels) or 'none')}", f"└ 下一步: {html.escape(str(outcome.get('next_action') or '--'))}", + "🧪 執行判定", + f"├ 完成狀態: {html.escape(str(execution_result.get('completion_status') or 'unknown'))}", + f"├ 指令狀態: {html.escape(str(execution_result.get('command_status') or 'unknown'))}", + f"├ 修復結果: {html.escape(str(execution_result.get('repair_status') or 'unknown'))}", + f"└ 失敗判定: {html.escape(str(execution_result.get('failure_status') or 'unknown'))}", + f"判讀: {html.escape(str(execution_result.get('summary_zh') or '尚未能判定執行是否完成或失敗'))}", f"⚙️ Action: {html.escape((approval.action or '')[:220])}", ] if error: diff --git a/apps/api/src/services/operator_outcome.py b/apps/api/src/services/operator_outcome.py index 54b2262a5..818bb8997 100644 --- a/apps/api/src/services/operator_outcome.py +++ b/apps/api/src/services/operator_outcome.py @@ -48,6 +48,140 @@ def _build_notification( } +def _build_execution_result( + *, + state: str, + verdict: str, + stage: str, + has_repair_execution: bool, + has_nonrepair_operation: bool, + verification: str, +) -> dict[str, Any]: + """Describe execution completion separately from remediation outcome.""" + approval_status = "unknown" + completion_status = "unknown" + command_status = "unknown" + repair_status = "unknown" + failure_status = "unknown" + summary = "尚未能判定執行是否完成或失敗" + terminal = False + + if state == "completed_verified": + approval_status = "completed" + completion_status = "completed_verified" + command_status = "succeeded" + repair_status = "verified_repaired" + failure_status = "no_failure" + summary = "已完成:修復指令成功,且驗證通過" + terminal = True + elif state == "execution_failed_manual_required": + approval_status = "completed" + completion_status = "failed" + command_status = "failed" + repair_status = "failed" + failure_status = "command_failed" + summary = "已失敗:修復指令執行失敗,需人工接手" + terminal = True + elif state == "diagnostic_only_manual_review": + approval_status = "completed" + completion_status = "completed_no_repair" + command_status = "diagnostic_completed" if has_nonrepair_operation else "skipped_no_action" + repair_status = "not_executed" + failure_status = "no_command_failed" + summary = "流程已完成:只完成診斷/觀察,沒有修復指令成功或失敗" + terminal = True + elif state == "verification_degraded_manual_required": + approval_status = "completed" + completion_status = "completed_verification_degraded" + command_status = "succeeded" if has_repair_execution else "diagnostic_completed" + repair_status = "verification_degraded" + failure_status = "no_command_failed" + summary = "已執行,但驗證結果退化;需人工確認是否真的修復" + terminal = False + elif state == "execution_unverified_manual_required": + approval_status = "completed" + completion_status = "completed_unverified" + command_status = "succeeded" + repair_status = "unverified" + failure_status = "no_command_failed" + summary = "已執行成功,但缺少修復驗證結果" + terminal = False + elif state == "no_action_manual_review": + approval_status = "pending_manual_review" + completion_status = "not_started_no_action" + command_status = "not_started" + repair_status = "not_executed" + failure_status = "not_applicable" + summary = "尚未執行:AI 建議不修復,等待人工決定是否接手" + terminal = False + elif state == "approval_rejected_no_execution": + approval_status = "rejected" + completion_status = "closed_no_execution" + command_status = "not_run" + repair_status = "not_executed" + failure_status = "not_applicable" + summary = "已拒絕:審批結案,未執行任何修復指令" + terminal = True + elif state == "approval_expired_manual_review": + approval_status = "expired" + completion_status = "expired_no_execution" + command_status = "not_run" + repair_status = "not_executed" + failure_status = "not_applicable" + summary = "審批逾期:未執行修復,需人工重新審查" + terminal = False + elif state == "approval_required": + approval_status = "pending" + completion_status = "pending_approval" + command_status = "not_started" + repair_status = "not_executed" + failure_status = "not_applicable" + summary = "尚未執行:等待人工批准" + terminal = False + elif state == "read_only_dry_run_manual_gate": + approval_status = "manual_gate" + completion_status = "dry_run_completed" + command_status = "dry_run_succeeded" + repair_status = "not_executed" + failure_status = "not_applicable" + summary = "只讀試跑完成,尚未執行修復" + terminal = False + elif state == "observed_not_executed": + approval_status = "not_required" + completion_status = "observed_not_executed" + command_status = "not_run" + repair_status = "not_executed" + failure_status = "not_applicable" + summary = "只完成收件/觀測,尚未進入執行" + terminal = False + elif has_repair_execution: + approval_status = "completed" + completion_status = "completed_needs_review" + command_status = "succeeded" if verification != "missing" else "completed" + repair_status = "needs_review" + failure_status = "no_command_failed" + summary = "已有執行紀錄,但仍需人工確認最終修復結果" + terminal = False + elif verdict in {"received_only", "observed_not_executed"} or stage == "received": + approval_status = "not_started" + completion_status = "not_started" + command_status = "not_started" + repair_status = "not_executed" + failure_status = "not_applicable" + summary = "尚未執行修復指令" + terminal = False + + return { + "approval_status": approval_status, + "completion_status": completion_status, + "command_status": command_status, + "repair_status": repair_status, + "failure_status": failure_status, + "terminal": terminal, + "summary_zh": summary, + } + + def build_operator_outcome( *, truth_status: dict[str, Any] | None = None, @@ -206,6 +340,14 @@ def build_operator_outcome( summary = "處置結果尚未形成明確結論" reason = first_blocker or f"{verdict}:{stage}/{stage_status}" + execution_result = _build_execution_result( + state=state, + verdict=verdict, + stage=stage, + has_repair_execution=has_repair_execution, + has_nonrepair_operation=has_nonrepair_operation, + verification=verification, + ) mode = "action_required" if needs_human else "result_only" channels = _ACTION_REQUIRED_CHANNELS if needs_human else _RESULT_ONLY_CHANNELS return { @@ -217,6 +359,7 @@ def build_operator_outcome( "human_action_required": needs_human, "human_action_reason": reason, "next_action": next_action, + "execution_result": execution_result, "notification": _build_notification( mode=mode, channels=channels, diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 8f97323cc..ef8f3662a 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -197,10 +197,17 @@ def _format_operator_outcome_lines(outcome: dict[str, object] | None) -> list[st else {} ) channels = notification.get("channels") if isinstance(notification.get("channels"), list) else [] + execution_result = ( + outcome.get("execution_result") + if isinstance(outcome.get("execution_result"), dict) + else {} + ) return [ "👤 處置結論", f"├ 結果:{html.escape(str(outcome.get('summary_zh') or '尚未形成明確結論'))}", f"├ 狀態:{html.escape(str(outcome.get('state') or 'unknown'))}", + f"├ 執行:{html.escape(str(execution_result.get('completion_status') or 'unknown'))} | " + f"修復:{html.escape(str(execution_result.get('repair_status') or 'unknown'))}", f"├ 人工:{'yes' if outcome.get('needs_human') else 'no'} | " f"通知:{html.escape(','.join(str(item) for item in channels) or 'none')}", f"└ 下一步:{html.escape(str(outcome.get('next_action') or '--'))}", diff --git a/apps/api/tests/test_operator_outcome.py b/apps/api/tests/test_operator_outcome.py index 8cf0424ad..7bd17bc4b 100644 --- a/apps/api/tests/test_operator_outcome.py +++ b/apps/api/tests/test_operator_outcome.py @@ -37,6 +37,9 @@ def test_operator_outcome_marks_diagnostic_only_as_manual_action_required() -> N assert outcome["notification"]["mode"] == "action_required" assert "telegram_sre_war_room" in outcome["notification"]["channels"] assert outcome["next_action"] == "manual_review_or_collect_repair_evidence" + assert outcome["execution_result"]["completion_status"] == "completed_no_repair" + assert outcome["execution_result"]["repair_status"] == "not_executed" + assert outcome["execution_result"]["failure_status"] == "no_command_failed" def test_operator_outcome_marks_unverified_execution_as_human_review() -> None: @@ -85,6 +88,8 @@ def test_operator_outcome_marks_verified_repair_as_result_only() -> None: assert outcome["state"] == "completed_verified" assert outcome["needs_human"] is False assert outcome["notification"]["mode"] == "result_only" + assert outcome["execution_result"]["completion_status"] == "completed_verified" + assert outcome["execution_result"]["repair_status"] == "verified_repaired" def test_operator_outcome_marks_rejected_approval_as_closed_no_execution() -> None: @@ -107,6 +112,7 @@ def test_operator_outcome_marks_rejected_approval_as_closed_no_execution() -> No assert outcome["human_action_reason"] == "approval_rejected" assert outcome["notification"]["mode"] == "result_only" assert outcome["next_action"] == "monitor_or_reopen_if_alert_recurs" + assert outcome["execution_result"]["completion_status"] == "closed_no_execution" def test_operator_outcome_marks_expired_approval_as_manual_review() -> None: @@ -128,6 +134,7 @@ def test_operator_outcome_marks_expired_approval_as_manual_review() -> None: assert outcome["needs_human"] is True assert outcome["notification"]["mode"] == "action_required" assert outcome["next_action"] == "reopen_close_or_escalate_expired_approval" + assert outcome["execution_result"]["completion_status"] == "expired_no_execution" def test_execution_result_message_includes_operator_outcome_and_human_channels() -> None: @@ -154,6 +161,9 @@ def test_execution_result_message_includes_operator_outcome_and_human_channels() ) assert "處置結果" in text + assert "執行判定" in text + assert "not_started_no_action" in text + assert "not_executed" in text assert "人工: yes" in text assert "telegram_sre_war_room" in text assert "manual_review_no_action_decision" in text @@ -189,6 +199,8 @@ def test_execution_result_message_does_not_call_diagnostic_success_repair_done() ) assert "已記錄診斷,尚未證明修復" in text + assert "completed_no_repair" in text + assert "no_command_failed" in text assert "執行成功" not in text assert "修復完成" not in text diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 99ec50348..3d23b9cf6 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3662,6 +3662,7 @@ }, "outcome": { "summary": "處置結論", + "execution": "執行判定", "notification": "人工通知通道", "reason": "人工原因" }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 99ec50348..3d23b9cf6 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3662,6 +3662,7 @@ }, "outcome": { "summary": "處置結論", + "execution": "執行判定", "notification": "人工通知通道", "reason": "人工原因" }, diff --git a/apps/web/src/components/awooop/status-chain.tsx b/apps/web/src/components/awooop/status-chain.tsx index d5dc02e81..d8a6ed0f1 100644 --- a/apps/web/src/components/awooop/status-chain.tsx +++ b/apps/web/src/components/awooop/status-chain.tsx @@ -26,6 +26,15 @@ export interface AwoooPStatusChain { human_action_required?: boolean | null; human_action_reason?: string | null; next_action?: string | null; + execution_result?: { + approval_status?: string | null; + completion_status?: string | null; + command_status?: string | null; + repair_status?: string | null; + failure_status?: string | null; + terminal?: boolean | null; + summary_zh?: string | null; + }; notification?: { mode?: string | null; channels?: string[]; @@ -228,6 +237,7 @@ export function AwoooPStatusChainPanel({ const emptyLabel = t("emptyValue"); const evidence = chain?.evidence ?? {}; const outcome = chain?.operator_outcome; + const outcomeExecution = outcome?.execution_result; const blockers = chain?.blockers ?? []; const sourceCorrelation = chain?.source_refs?.correlation; @@ -599,13 +609,27 @@ export function AwoooPStatusChainPanel({ {outcome && ( -
+

{t("outcome.summary")}

{valueOrEmpty(outcome.summary_zh, emptyLabel)}

+
+

{t("outcome.execution")}

+

+ {valueOrEmpty(outcomeExecution?.completion_status, emptyLabel)} +

+

+ {valueOrEmpty(outcomeExecution?.command_status, emptyLabel)} + {" / "} + {valueOrEmpty(outcomeExecution?.repair_status, emptyLabel)} +

+

{t("outcome.notification")}

diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 1d9e78393..fc18f5131 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,63 @@ +## 2026-05-31|Telegram / AwoooP 執行完成與失敗判定明確化 + +**背景**: + +- 使用者指出 `INC-20260531-432726` 在批准 `OBSERVE` 後,Telegram 雖回覆「已記錄觀察,未執行修復」與 `diagnostic_only_manual_review`,但仍無法一眼看出「到底是執行完成、執行失敗,還是沒有修復指令被執行」。 +- 原 `operator_outcome_v1` 已說明人工需求與下一步,但缺少獨立欄位把「審批流程完成」與「修復指令成功 / 失敗 / 未執行」分開。 + +**本次調整**: + +- `operator_outcome_v1` 新增 `execution_result`: + - `approval_status` + - `completion_status` + - `command_status` + - `repair_status` + - `failure_status` + - `terminal` + - `summary_zh` +- Telegram ACTION REQUIRED 主卡的「處置結論」新增 `執行 / 修復` 一行。 +- Telegram execution result 回覆新增「執行判定」段落,明確列出完成狀態、指令狀態、修復結果與失敗判定。 +- AwoooP status-chain 前台新增「執行判定」欄位,顯示同一份 `execution_result`,避免 Telegram 與前台口徑分叉。 + +**OBSERVE / NO_ACTION 新語意**: + +```text +完成狀態: completed_no_repair +指令狀態: skipped_no_action +修復結果: not_executed +失敗判定: no_command_failed +判讀: 流程已完成:只完成診斷/觀察,沒有修復指令成功或失敗 +``` + +**驗證**: + +```text +python3 -m py_compile operator_outcome.py approval_execution.py telegram_gateway.py test_operator_outcome.py + -> pass +cd apps/api && ruff check --select E9,F401,F821,F841 ... + -> pass +cd apps/api && DATABASE_URL=postgresql+asyncpg://test:test@localhost:5432/test pytest \ + tests/test_operator_outcome.py \ + tests/test_awooop_truth_chain_service.py \ + tests/test_awooop_operator_timeline_labels.py \ + tests/test_telegram_message_templates.py -q + -> 164 passed +python3 -m json.tool apps/web/messages/zh-TW.json / en.json + -> pass +pnpm --dir apps/web exec tsc --noEmit + -> pass +NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web run build + -> pass +git diff --check + -> pass +sample format for INC-20260531-432726: + completed_no_repair / skipped_no_action / not_executed / no_command_failed +``` + +**進度邊界**: + +- 本輪只修 operator-facing result 判讀,不改自動修復權限、不重跑 `INC-20260531-432726`,也不把 OBSERVE 視為修復成功。 + ## 2026-05-31|AwoooP 真相鏈 Ansible 證據與 Run 詳情 500 修復 **背景**: diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md index 9a5113ce6..7eb4c193e 100644 --- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md +++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md @@ -2704,6 +2704,12 @@ Phase 6 完成後 - Verification:dry-run candidate `67`(`EXECUTION_SUCCESS=57`, `EXECUTION_FAILED=10`);actual `metadata_updates=67`, `execution_completed_inserted=25`, `telegram_result_inserted=67`, failures=0。Post-verify 7d:`EXECUTION_SUCCESS total=61 missing_result=0 missing_completed=0`,`EXECUTION_FAILED total=16 missing_result=0 missing_completed=0`。Production smoke:`INC-20260531-BE2B25 -> execution_failed_manual_required`;rejected/expired samples continue returning `approval_rejected_no_execution` / `approval_expired_manual_review`。 - 判讀:T154e 是歷史 audit/result 補洞與單封摘要補通知,不重跑任何修復。`APPROVED` / `PENDING` / `EXPIRED` / `REJECTED` 的 missing result 不是執行缺口,需由 approval queue / terminal outcome 分流呈現。 +**T154f Execution result verdict clarity(2026-05-31 台北)**: +- 觸發:`INC-20260531-432726` 的 `OBSERVE` approval result 雖顯示「已記錄觀察,未執行修復」,但使用者仍無法一眼分辨「流程是否完成、修復指令是否成功或失敗」。原 `operator_outcome_v1` 缺少獨立 execution verdict 欄位。 +- 修正:`operator_outcome_v1` 新增 `execution_result`,包含 `approval_status`、`completion_status`、`command_status`、`repair_status`、`failure_status`、`terminal`、`summary_zh`。Telegram ACTION REQUIRED 主卡、Telegram result reply、AwoooP status-chain 前台均顯示同一份 execution result。`OBSERVE` / `NO_ACTION` result 現在明確呈現 `completed_no_repair / skipped_no_action / not_executed / no_command_failed`,判讀為「流程已完成:只完成診斷/觀察,沒有修復指令成功或失敗」。 +- Verification:API `py_compile` pass;targeted `ruff --select E9,F401,F821,F841` pass;`test_operator_outcome.py` + `test_awooop_truth_chain_service.py` + `test_awooop_operator_timeline_labels.py` + `test_telegram_message_templates.py` -> 164 passed;i18n JSON parse pass;web `tsc --noEmit` pass;production API URL build pass;`git diff --check` pass。Sample formatting for `INC-20260531-432726` confirms result card includes `completed_no_repair`, `skipped_no_action`, `not_executed`, `no_command_failed`。 +- 判讀:T154f 不擴權自動修復、不重跑舊 incident;它只把「審批處理完成」與「修復指令成功/失敗/未執行」拆開,避免 OBSERVE 被誤解成 repair success 或 repair failure。 + **T152 Ansible runtime readiness surfaced(2026-05-24 台北)**: - 觸發:T151 已讓首頁看到 execution backend / Ansible attribution,但 operator 仍看不到 runtime 端缺什麼,容易把「Ansible 有候選」誤解成「Ansible 已能自動修復」。 - 修正:API image 複製 `infra/ansible/` 作 read-only catalog;`truth-chain/quality/summary` 新增 `ansible_runtime`,回報 playbook binary、catalog、inventory、playbook_count、can_run_check_mode、blockers。首頁 execution evidence 同步顯示 runtime 狀態;目前 production 顯示 `runtime 未就緒:ansible_playbook_binary_missing`。未安裝 `ansible-core`、未啟用 check-mode / apply。