From dc4f0ecc8e28ebbcac78f3e46f2a4b75e3a83b76 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Jul 2026 07:41:19 +0800 Subject: [PATCH] fix(awooop): surface runs ai loop action chain --- apps/web/messages/en.json | 83 +++- apps/web/messages/zh-TW.json | 83 +++- .../web/src/app/[locale]/awooop/runs/page.tsx | 439 +++++++++++++++++- docs/LOGBOOK.md | 22 + 4 files changed, 621 insertions(+), 6 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 35bf8c277..5091a71a6 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -864,8 +864,8 @@ "executionBackendDetail": "執行證據:操作 {operations}(有效 {effective} / 稽核 {auditOnly}),自動修復 {autoRepair};Ansible 稽核 {ansibleRecords},候選 {ansibleCandidates},check-mode {checkMode},apply {apply},待接線 {pending};runtime {runtime}", "ansibleRuntimeReady": "可跑 check-mode", "ansibleRuntimeBlocked": "未就緒:{blockers}", - "humanGap": "人工缺口", - "humanGapDetail": "{gate} 缺 {count} 筆", + "humanGap": "AI 補齊缺口", + "humanGapDetail": "{gate} 缺 {count} 筆,進 AI controlled 補齊", "humanGapClear": "品質摘要未列出主要缺口", "modelRoute": "模型路由", "routeDetail": "{model};目前 {selected};{primary}={primaryStatus};備援 {fallback}", @@ -12063,6 +12063,85 @@ } }, "runs": { + "aiLoopAgent": { + "title": "AI Loop Agent 處置鏈", + "subtitle": "把 Telegram / 監控告警、Run、MCP / RAG 證據、controlled apply、verifier 與 KM / PlayBook 回寫串成一條閉環;低 / 中 / 高風險走 AI controlled,critical 才進 break-glass。", + "boundary": "受控邊界:runtime gate {runtimeGate};learning missing {missing}。缺 PlayBook、RAG、MCP 或 verifier 會進 AI 補齊,不預設人工終局。", + "source": "registry={registry} / flow={flow}", + "values": { + "yes": "已允許", + "controlled": "controlled" + }, + "statuses": { + "ready": "Ready", + "queued": "AI 補齊", + "blocked": "Readback 阻塞", + "missing": "待資料" + }, + "header": { + "ready": "ready {count}", + "queued": "queued {count}", + "errors": "readback errors {count}" + }, + "nodes": { + "sensor": { + "title": "Sensor / Evidence", + "detail": "callback {callbacks};AI alert cards {cards}", + "subdetail": "source / trace refs {refs}" + }, + "aiLane": { + "title": "AI Lane / 分類", + "detail": "MCP evidence {mcp};runs {runs}", + "subdetail": "缺 AI 證據 {noEvidence}" + }, + "candidate": { + "title": "Candidate / Check-mode", + "detail": "只讀試跑 {dryRun};MCP 調查 {mcp}", + "subdetail": "寫入旗標 {writeFlags}" + }, + "controlled": { + "title": "Controlled Apply", + "detail": "runtime={allowed};queue {queue}", + "subdetail": "low / medium / high 走 controlled;critical 維持 break-glass" + }, + "verifier": { + "title": "Verifier / Rollback", + "detail": "verified repair {verified};post verifier refs {post}", + "subdetail": "flow blocked {blocked} / warning {warning}" + }, + "learning": { + "title": "KM / RAG / PlayBook", + "detail": "ready {ready} / targets {targets}", + "subdetail": "missing target {missing}" + } + }, + "assets": { + "km": { + "title": "KM", + "detail": "incident / verifier learning writeback refs" + }, + "rag": { + "title": "RAG", + "detail": "chunk refs for similar-case retrieval" + }, + "playbook": { + "title": "PlayBook", + "detail": "trust update or route candidate refs" + }, + "mcp": { + "title": "MCP", + "detail": "tool evidence and context receipts" + }, + "verifier": { + "title": "Verifier", + "detail": "post-apply / rollback readback refs" + }, + "aiAgent": { + "title": "AI Agent", + "detail": "consumer bindings for loop execution" + } + } + }, "automationFlow": { "title": "AI 自動化流程 Gate", "subtitle": "24h 視窗:告警入庫、MCP 調查、審批 / 政策、執行、修復、驗證、KM與Operator可見性。", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 15de28c21..ed32f4a87 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -864,8 +864,8 @@ "executionBackendDetail": "執行證據:操作 {operations}(有效 {effective} / 稽核 {auditOnly}),自動修復 {autoRepair};Ansible 稽核 {ansibleRecords},候選 {ansibleCandidates},check-mode {checkMode},apply {apply},待接線 {pending};runtime {runtime}", "ansibleRuntimeReady": "可跑 check-mode", "ansibleRuntimeBlocked": "未就緒:{blockers}", - "humanGap": "人工缺口", - "humanGapDetail": "{gate} 缺 {count} 筆", + "humanGap": "AI 補齊缺口", + "humanGapDetail": "{gate} 缺 {count} 筆,進 AI controlled 補齊", "humanGapClear": "品質摘要未列出主要缺口", "modelRoute": "模型路由", "routeDetail": "{model};目前 {selected};{primary}={primaryStatus};備援 {fallback}", @@ -12063,6 +12063,85 @@ } }, "runs": { + "aiLoopAgent": { + "title": "AI Loop Agent 處置鏈", + "subtitle": "把 Telegram / 監控告警、Run、MCP / RAG 證據、controlled apply、verifier 與 KM / PlayBook 回寫串成一條閉環;低 / 中 / 高風險走 AI controlled,critical 才進 break-glass。", + "boundary": "受控邊界:runtime gate {runtimeGate};learning missing {missing}。缺 PlayBook、RAG、MCP 或 verifier 會進 AI 補齊,不預設人工終局。", + "source": "registry={registry} / flow={flow}", + "values": { + "yes": "已允許", + "controlled": "controlled" + }, + "statuses": { + "ready": "Ready", + "queued": "AI 補齊", + "blocked": "Readback 阻塞", + "missing": "待資料" + }, + "header": { + "ready": "ready {count}", + "queued": "queued {count}", + "errors": "readback errors {count}" + }, + "nodes": { + "sensor": { + "title": "Sensor / Evidence", + "detail": "callback {callbacks};AI alert cards {cards}", + "subdetail": "source / trace refs {refs}" + }, + "aiLane": { + "title": "AI Lane / 分類", + "detail": "MCP evidence {mcp};runs {runs}", + "subdetail": "缺 AI 證據 {noEvidence}" + }, + "candidate": { + "title": "Candidate / Check-mode", + "detail": "只讀試跑 {dryRun};MCP 調查 {mcp}", + "subdetail": "寫入旗標 {writeFlags}" + }, + "controlled": { + "title": "Controlled Apply", + "detail": "runtime={allowed};queue {queue}", + "subdetail": "low / medium / high 走 controlled;critical 維持 break-glass" + }, + "verifier": { + "title": "Verifier / Rollback", + "detail": "verified repair {verified};post verifier refs {post}", + "subdetail": "flow blocked {blocked} / warning {warning}" + }, + "learning": { + "title": "KM / RAG / PlayBook", + "detail": "ready {ready} / targets {targets}", + "subdetail": "missing target {missing}" + } + }, + "assets": { + "km": { + "title": "KM", + "detail": "incident / verifier learning writeback refs" + }, + "rag": { + "title": "RAG", + "detail": "chunk refs for similar-case retrieval" + }, + "playbook": { + "title": "PlayBook", + "detail": "trust update or route candidate refs" + }, + "mcp": { + "title": "MCP", + "detail": "tool evidence and context receipts" + }, + "verifier": { + "title": "Verifier", + "detail": "post-apply / rollback readback refs" + }, + "aiAgent": { + "title": "AI Agent", + "detail": "consumer bindings for loop execution" + } + } + }, "automationFlow": { "title": "AI 自動化流程 Gate", "subtitle": "24h 視窗:告警入庫、MCP 調查、審批 / 政策、執行、修復、驗證、KM與Operator可見性。", diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx index 401be1a86..5f61e55c4 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -60,6 +60,15 @@ type RunState = | "timeout"; type RunLane = "intake" | "diagnosis" | "approval" | "execution" | "done" | "manual"; +type AiLoopAgentStatus = "ready" | "queued" | "blocked" | "missing"; +type AiLoopAgentNodeKey = + | "sensor" + | "aiLane" + | "candidate" + | "controlled" + | "verifier" + | "learning"; +type AiLoopAgentAssetKey = "km" | "rag" | "playbook" | "mcp" | "verifier" | "aiAgent"; type CallbackReplyStatus = | "no_callback" | "sent" @@ -720,6 +729,32 @@ interface AiRouteStatusResponse { checked_at: string; } +interface AwoooPRunEvidenceSummary { + readOnly: number; + mcpObserved: number; + writeObserved: number; + noEvidence: number; + controlledQueue: number; + callbackObserved: number; + callbackFailed: number; +} + +interface AiLoopAgentNode { + key: AiLoopAgentNodeKey; + status: AiLoopAgentStatus; + value: string; + detail: string; + subdetail: string; + icon: typeof Activity; +} + +interface AiLoopAgentAsset { + key: AiLoopAgentAssetKey; + value: string; + detail: string; + status: AiLoopAgentStatus; +} + type GitHubRunReadinessMetric = { key: string; value: string; @@ -4338,6 +4373,391 @@ function AiRouteStatusPanel({ ); } +function formatAiLoopCount(value?: number | null) { + return typeof value === "number" && Number.isFinite(value) + ? value.toLocaleString("zh-TW") + : "--"; +} + +function firstAiLoopCount(...values: Array) { + const value = values.find((candidate) => typeof candidate === "number" && Number.isFinite(candidate)); + return typeof value === "number" ? value : null; +} + +function aiLoopStatusClass(status: AiLoopAgentStatus) { + if (status === "ready") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"; + if (status === "queued") return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"; + if (status === "blocked") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"; + return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]"; +} + +function aiLoopStatusFill(status: AiLoopAgentStatus) { + if (status === "ready") return "bg-[#4f9d5f]"; + if (status === "queued") return "bg-[#c58a24]"; + if (status === "blocked") return "bg-[#c65145]"; + return "bg-[#b8b2a7]"; +} + +function statusFromCount( + value: number | null | undefined, + fallback: AiLoopAgentStatus = "missing" +): AiLoopAgentStatus { + if (typeof value === "number" && value > 0) return "ready"; + return fallback; +} + +function learningBindingReadyCount( + registry: AiAlertCardLearningRegistry | null, + aliases: string[] +): number | null { + if (!registry || !Array.isArray(registry.targets)) return null; + const targets = registry?.targets ?? []; + return targets.reduce((total, binding) => { + const target = String(binding.target ?? "").toLowerCase(); + if (!aliases.some((alias) => target.includes(alias))) return total; + return total + (binding.ready_receipt_count ?? (binding.status === "ready" ? 1 : 0)); + }, 0); +} + +function AiLoopAgentActionChainPanel({ + runs, + total, + evidenceSummary, + aiAlertCardSummary, + aiAlertLearningRegistry, + automationQualitySummary, + automationQualityError, + callbackAuditSummary, + callbackEventsTotal, + callbackEventsError, + aiAlertCardError, + eventRecurrence, +}: { + runs: Run[]; + total: number; + evidenceSummary: AwoooPRunEvidenceSummary; + aiAlertCardSummary: AiAlertCardDeliverySummary | null; + aiAlertLearningRegistry: AiAlertCardLearningRegistry | null; + automationQualitySummary: AutomationQualitySummary | null; + automationQualityError: string | null; + callbackAuditSummary: CallbackReplyAuditSummary | null; + callbackEventsTotal: number; + callbackEventsError: string | null; + aiAlertCardError: string | null; + eventRecurrence: EventRecurrenceResponse | null; +}) { + const t = useTranslations("awooop.runs.aiLoopAgent"); + const sensorTotal = firstAiLoopCount( + callbackAuditSummary?.outbound_total, + aiAlertCardSummary?.total, + eventRecurrence?.summary?.source_event_total, + runs.length + ); + const evaluatedTotal = firstAiLoopCount( + automationQualitySummary?.evaluated_total, + total, + runs.length + ); + const candidateTotal = evidenceSummary.mcpObserved + evidenceSummary.readOnly; + const controlledQueueTotal = firstAiLoopCount( + aiAlertCardSummary?.runtime_write_gate_open_count, + evidenceSummary.controlledQueue + ); + const verifierReadyTotal = firstAiLoopCount( + automationQualitySummary?.verified_auto_repair_total, + aiAlertCardSummary?.post_verifier_ref_ready_total + ); + const learningReadyTotal = firstAiLoopCount( + aiAlertLearningRegistry?.ready_target_count, + aiAlertCardSummary?.learning_registry_ready_target_count, + aiAlertCardSummary?.learning_writeback_ready_total + ); + const learningMissingTotal = firstAiLoopCount( + aiAlertLearningRegistry?.missing_target_count, + aiAlertCardSummary?.learning_registry_missing_target_count, + aiAlertCardSummary?.learning_writeback_missing_total + ); + const hasReadbackError = Boolean(automationQualityError || callbackEventsError || aiAlertCardError); + const runtimeControlledAllowed = Boolean( + aiAlertCardSummary?.runtime_write_allowed || + (controlledQueueTotal ?? 0) > 0 || + candidateTotal > 0 + ); + const callbackTotal = firstAiLoopCount( + callbackAuditSummary?.callback_total, + callbackEventsTotal + ); + const learningTargetTotal = firstAiLoopCount( + aiAlertLearningRegistry?.target_count, + aiAlertCardSummary?.learning_registry_target_count + ); + const sourceRefsTotal = firstAiLoopCount( + callbackAuditSummary?.outbound_source_refs_total, + callbackAuditSummary?.outbound_trace_ref_total, + 0 + ); + const flow = automationQualitySummary?.automation_flow_gates; + const flowBlockedCount = flow?.blocked_gates?.length ?? 0; + const flowWarningCount = flow?.warning_gates?.length ?? 0; + const nodes: AiLoopAgentNode[] = [ + { + key: "sensor", + status: callbackEventsError ? "blocked" : statusFromCount(sensorTotal), + value: formatAiLoopCount(sensorTotal), + detail: t("nodes.sensor.detail", { + callbacks: formatAiLoopCount(callbackTotal), + cards: formatAiLoopCount(aiAlertCardSummary?.total), + }), + subdetail: t("nodes.sensor.subdetail", { + refs: formatAiLoopCount(sourceRefsTotal), + }), + icon: Send, + }, + { + key: "aiLane", + status: automationQualityError ? "blocked" : statusFromCount(evaluatedTotal), + value: formatAiLoopCount(evaluatedTotal), + detail: t("nodes.aiLane.detail", { + mcp: formatAiLoopCount(evidenceSummary.mcpObserved), + runs: formatAiLoopCount(runs.length), + }), + subdetail: t("nodes.aiLane.subdetail", { + noEvidence: formatAiLoopCount(evidenceSummary.noEvidence), + }), + icon: Cpu, + }, + { + key: "candidate", + status: statusFromCount(candidateTotal, evidenceSummary.noEvidence > 0 ? "queued" : "missing"), + value: formatAiLoopCount(candidateTotal), + detail: t("nodes.candidate.detail", { + dryRun: formatAiLoopCount(evidenceSummary.readOnly), + mcp: formatAiLoopCount(evidenceSummary.mcpObserved), + }), + subdetail: t("nodes.candidate.subdetail", { + writeFlags: formatAiLoopCount(evidenceSummary.writeObserved), + }), + icon: ListChecks, + }, + { + key: "controlled", + status: runtimeControlledAllowed ? "ready" : "queued", + value: formatAiLoopCount(controlledQueueTotal), + detail: t("nodes.controlled.detail", { + allowed: aiAlertCardSummary?.runtime_write_allowed ? t("values.yes") : t("values.controlled"), + queue: formatAiLoopCount(evidenceSummary.controlledQueue), + }), + subdetail: t("nodes.controlled.subdetail"), + icon: ShieldCheck, + }, + { + key: "verifier", + status: statusFromCount(verifierReadyTotal, (evaluatedTotal ?? 0) > 0 ? "queued" : "missing"), + value: formatAiLoopCount(verifierReadyTotal), + detail: t("nodes.verifier.detail", { + verified: formatAiLoopCount(automationQualitySummary?.verified_auto_repair_total), + post: formatAiLoopCount(aiAlertCardSummary?.post_verifier_ref_ready_total), + }), + subdetail: t("nodes.verifier.subdetail", { + blocked: formatAiLoopCount(flowBlockedCount), + warning: formatAiLoopCount(flowWarningCount), + }), + icon: SearchCheck, + }, + { + key: "learning", + status: statusFromCount(learningReadyTotal, (learningMissingTotal ?? 0) > 0 ? "queued" : "missing"), + value: formatAiLoopCount(learningReadyTotal), + detail: t("nodes.learning.detail", { + ready: formatAiLoopCount(learningReadyTotal), + targets: formatAiLoopCount(learningTargetTotal), + }), + subdetail: t("nodes.learning.subdetail", { + missing: formatAiLoopCount(learningMissingTotal), + }), + icon: FileText, + }, + ]; + const readyCount = nodes.filter((node) => node.status === "ready").length; + const queuedCount = nodes.filter((node) => node.status === "queued").length; + const blockedCount = nodes.filter((node) => node.status === "blocked").length; + const overallStatus: AiLoopAgentStatus = blockedCount > 0 + ? "blocked" + : readyCount >= 4 + ? "ready" + : queuedCount > 0 + ? "queued" + : "missing"; + const kmReady = firstAiLoopCount( + aiAlertCardSummary?.km_writeback_ref_ready_total, + learningBindingReadyCount(aiAlertLearningRegistry, ["km", "knowledge"]) + ); + const ragReady = firstAiLoopCount( + aiAlertCardSummary?.rag_chunk_ref_ready_total, + learningBindingReadyCount(aiAlertLearningRegistry, ["rag", "vector"]) + ); + const playbookReady = firstAiLoopCount( + aiAlertCardSummary?.playbook_writeback_ref_ready_total, + learningBindingReadyCount(aiAlertLearningRegistry, ["playbook"]) + ); + const mcpReady = firstAiLoopCount( + aiAlertCardSummary?.mcp_evidence_ref_ready_total, + learningBindingReadyCount(aiAlertLearningRegistry, ["mcp"]) + ); + const postVerifierReady = firstAiLoopCount( + aiAlertCardSummary?.post_verifier_ref_ready_total, + learningBindingReadyCount(aiAlertLearningRegistry, ["verifier"]) + ); + const aiAgentReady = learningBindingReadyCount(aiAlertLearningRegistry, ["agent", "ai_agent"]); + const assets: AiLoopAgentAsset[] = [ + { + key: "km", + value: formatAiLoopCount(kmReady), + detail: t("assets.km.detail"), + status: statusFromCount(kmReady), + }, + { + key: "rag", + value: formatAiLoopCount(ragReady), + detail: t("assets.rag.detail"), + status: statusFromCount(ragReady), + }, + { + key: "playbook", + value: formatAiLoopCount(playbookReady), + detail: t("assets.playbook.detail"), + status: statusFromCount(playbookReady), + }, + { + key: "mcp", + value: formatAiLoopCount(mcpReady), + detail: t("assets.mcp.detail"), + status: statusFromCount(mcpReady), + }, + { + key: "verifier", + value: formatAiLoopCount(postVerifierReady), + detail: t("assets.verifier.detail"), + status: statusFromCount(postVerifierReady), + }, + { + key: "aiAgent", + value: formatAiLoopCount(aiAgentReady), + detail: t("assets.aiAgent.detail"), + status: statusFromCount(aiAgentReady), + }, + ]; + + return ( +
+
+
+
+
+

+ {t("subtitle")} +

+
+
+ + {t("header.ready", { count: readyCount })} + + + {t("header.queued", { count: queuedCount })} + + + {t("header.errors", { count: hasReadbackError ? 1 : 0 })} + +
+
+ +
+ {nodes.map((node, index) => { + const Icon = node.icon; + return ( +
+ {index < nodes.length - 1 ? ( +
+ ); + })} +
+ +
+ {assets.map((asset) => ( +
+
+

+ {t(`assets.${asset.key}.title` as never)} +

+ +
+
+ {asset.value} +
+

{asset.detail}

+
+ ))} +
+ +
+

+ {t("boundary", { + runtimeGate: formatAiLoopCount(controlledQueueTotal), + missing: formatAiLoopCount(learningMissingTotal), + })} +

+

+ {t("source", { + registry: aiAlertLearningRegistry?.status ?? "--", + flow: flow?.overall_status ?? "--", + })} +

+
+
+ ); +} + // ============================================================================= // Main Component // ============================================================================= @@ -4660,7 +5080,7 @@ export default function RunsPage() { noEvidence: runs.filter( (run) => normalizeRemediationStatus(run.remediation_summary) === "no_evidence" ).length, - manualGate: runs.filter((run) => run.remediation_summary?.human_gate_open).length, + controlledQueue: runs.filter((run) => run.remediation_summary?.human_gate_open).length, callbackObserved: runs.filter( (run) => normalizeCallbackReplyStatus(run.callback_reply_summary) !== "no_callback" ).length, @@ -4767,6 +5187,21 @@ export default function RunsPage() { + +
{[ { @@ -4785,7 +5220,7 @@ export default function RunsPage() { }, { label: tEvidence("summary.manualGate"), - value: evidenceSummary.manualGate, + value: evidenceSummary.controlledQueue, detail: tEvidence("summary.manualGateDetail"), icon: ShieldCheck, className: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]", diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index ef9308d75..8df4aed92 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,25 @@ +## 2026-07-03 — 05:05 AwoooP Runs AI Loop Agent 處置鏈首屏 + +**完成內容**: +- `/zh-TW/awooop/runs` 新增 `AI Loop Agent 處置鏈` 首屏 panel,直接聚合既有 Runs、AI alert card delivery、TG callback audit、automation quality summary、event recurrence 與 learning registry readback。 +- 處置鏈把 `Sensor / Evidence → AI Lane → Candidate / Check-mode → Controlled Apply → Verifier / Rollback → KM / RAG / PlayBook` 轉成 6 節點可視化,並加上 KM、RAG、PlayBook、MCP、Verifier、AI Agent 六類資產 readback。 +- 舊 `human_gate_open` / `needs_human` 顯示不再被當成人工終局;Runs 首屏摘要改用 `AI 受控隊列` / `AI 補齊`,缺 PlayBook、RAG、MCP 或 verifier 時導向 controlled 補齊與 post-verifier,而不是「請人工處理」。 +- `humanGap` 首頁摘要文字改為 `AI 補齊缺口`,避免監控 / 告警 / Run 總覽繼續把 low / medium / high 流程導向人工預設。 + +**已跑驗證**: +- `python3.11 -m json.tool apps/web/messages/zh-TW.json`:通過。 +- `python3.11 -m json.tool apps/web/messages/en.json`:通過。 +- `pnpm --dir apps/web typecheck`:通過。 +- `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web build`:通過;`/[locale]/awooop/runs` bundle `32.3 kB` / First Load JS `275 kB`。 +- `pnpm --dir apps/web audit:ui-density`:通過;Runs route score `519`、text `402`、manual `29`、fixedTaxonomy `0`。 +- `python3.11 ops/runner/guard-gitea-runner-pressure.py --root .`:通過;`auto_branch_events_on_110=0`、`generic_runner_labels=0`。 +- `git diff --check`:通過。 +- Playwright + Chrome local production smoke:desktop `1440x1100` 與 mobile `390x900` 均通過;`AI Loop Agent 處置鏈`、6 節點、6 類資產可見;panel 內禁用人工終局詞命中 `0`;console error `0`;page error `0`;水平溢位 `0`。截圖:`/tmp/awoooi-runs-ai-loop-agent-local-desktop-20260703.png`、`/tmp/awoooi-runs-ai-loop-agent-local-mobile-20260703.png`。 + +**仍維持**: +- 本輪是 UI / API readback 聚合,不寫 KM / RAG / PlayBook target,不呼叫 MCP tool,不發 Telegram,不做 runtime apply;critical / secret / destructive / reboot / firewall / active scan / paid provider / force push 仍維持 break-glass。 +- 未讀 secret / token / `.env` / raw sessions / SQLite / auth;未使用 GitHub / gh;未重啟 host / VM / service;未 Docker / Nginx / K3s / DB / firewall restart;未 DROP / TRUNCATE / restore / prune / delete / force push。 + ## 2026-07-03 — 04:24 Public maintenance fallback location-level hardening **完成內容**: