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。