+ {/* 異常脈衝雷達 (Ping Animation) */}
+ {isAlert && (
+
+
+
+
+ )}
+
+ {/* 標頭資訊 */}
+
+ {id}
+
+ {serviceName}
+
+
+
+ {/* 核心數據與訊息 */}
+
+ {message}
+
+
{timestamp}
+
+ {/* 大腦決策層 (Proposal UI) */}
+ {isAlert && tier && (
+
+
+ {tier === 1 ? '>_ AI 執行中 (Tier 1)' : `>_ 等待統帥親核 (Tier ${tier})`}
+
+ {tier > 1 && (
+
+ )}
+
+ )}
+
+ )
+}
+
+export default DualStateIncidentCard
diff --git a/apps/web/src/components/incident/index.ts b/apps/web/src/components/incident/index.ts
index 49d29d6b..732af21e 100644
--- a/apps/web/src/components/incident/index.ts
+++ b/apps/web/src/components/incident/index.ts
@@ -1,8 +1,12 @@
/**
- * Incident Components - Phase 7
+ * Incident Components - Phase 7 + 6.5a
*/
export { IncidentCard, IncidentCardGrid, IncidentEmptyState } from './incident-card'
+export {
+ DualStateIncidentCard,
+ type DualStateIncidentCardProps,
+} from './dual-state-incident-card'
export {
ThinkingTerminal,
DEMO_DECISION_CHAIN,
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md
index 414802bf..935b0902 100644
--- a/docs/LOGBOOK.md
+++ b/docs/LOGBOOK.md
@@ -27,10 +27,10 @@
| **6.4b** | **lewooogo-data 骨架** | `packages/` | 1h | ✅ 完成 |
| **6.4c** | **Interface 定義 (ABC)** | `packages/` | 2h | ✅ 完成 |
| **6.4d** | **MemoryProvider 實作** | `packages/` | 4h | 🔲 待辦 |
-| **6.4e** | **Engine 搬遷** | `packages/` | 4h | 🔲 待辦 |
-| **6.4f** | **SkillLoader** | `packages/` | 2h | 🔲 待辦 |
-| **6.4g** | **apps/api 引用更新** | `apps/api` | 2h | 🔲 待辦 |
-| **6.4h** | **Decision Proposal API** | .188 API | 4h | 🔲 待辦 |
+| **6.4e** | **Engine 搬遷** | `packages/` | 4h | ✅ 完成 |
+| **6.4f** | **SkillLoader** | `packages/` | 2h | ✅ 完成 |
+| **6.4g** | **API 突觸對接 `/propose`** | `apps/api` | 2h | ✅ 完成 |
+| **6.4h** | **真實 ProposalEngine DI** | .188 API | 4h | 🔲 **下一步** |
| 6.5 | Runner 整合 + 5+1 狀態機 | .188 API | 4h | 🔲 待辦 |
| 6.6 | Sensor Agent (各主機) | .110/.112/.120 | 2d | 🔲 待辦 |
@@ -40,6 +40,8 @@
| 時間 | 事件 | 負責人 |
|------|------|--------|
+| 2026-03-23 11:50 | **🧠 Phase 6.4g API 突觸對接完成**: `/propose` 路由建立 + Guardrails 8/8 測試通過 + lewooogo-brain 積木綁定 | Claude Code |
+| 2026-03-23 11:55 | **🎨 Phase 6.5a 視覺皮層啟動**: DualStateIncidentCard.tsx 雙態戰情室卡片 + Nothing.tech 視覺憲法 | Claude Code |
| 2026-03-23 09:30 | **🔧 NetworkPolicy 修復**: `allow-required-egress` podSelector 改為 `system=awoooi` (原本只允許 API pod) | Claude Code |
| 2026-03-23 09:20 | **🚨 生產修復 #2**: Worker CrashLoopBackOff 92次 + `init_redis` → `init_redis_pool` 函數名修正 + 7h 無告警根因 | Claude Code |
| 2026-03-23 09:15 | **🚨 生產修復 #1**: 簽核卡片閃爍消失 + Polling Race Condition + approval.store.ts 暫停/恢復機制 | Claude Code |
diff --git a/packages/lewooogo-brain/src/lewooogo_brain/engines/__init__.py b/packages/lewooogo-brain/src/lewooogo_brain/engines/__init__.py
index 3c0847cd..c27a7b98 100644
--- a/packages/lewooogo-brain/src/lewooogo_brain/engines/__init__.py
+++ b/packages/lewooogo-brain/src/lewooogo_brain/engines/__init__.py
@@ -1,19 +1,44 @@
"""
-leWOOOgo Brain Engines - 推論引擎
+leWOOOgo Brain Engines - 核心引擎
==================================
-具體實作 IProposalEngine 和 IIncidentProcessor
+Phase 6.4e: 引擎積木化完成
引擎列表:
-- ProposalEngine: 決策提案引擎
-- IncidentEngine: 事件處理引擎
+- IncidentEngine: 事件處理引擎 (告警聚合、爆炸半徑分析)
+- ProposalEngine: 決策提案引擎 (含 Guardrails)
+- GuardrailsValidator: 獨立安全驗證器
"""
-# TODO: Phase 6.4e 搬遷後啟用
-# from lewooogo_brain.engines.proposal_engine import ProposalEngine
-# from lewooogo_brain.engines.incident_engine import IncidentEngine
+from lewooogo_brain.engines.incident_engine import (
+ IncidentEngine,
+ IIncidentMemory,
+ IBlastRadiusAnalyzer,
+ AGGREGATION_WINDOW_MINUTES,
+ WORKING_MEMORY_TTL,
+)
-__all__: list[str] = [
- # "ProposalEngine",
- # "IncidentEngine",
+from lewooogo_brain.engines.proposal_engine import (
+ ProposalEngine,
+ GuardrailsValidator,
+ ILLMProvider,
+ FORBIDDEN_COMMANDS,
+ ALLOWED_NAMESPACES,
+ SYSTEM_NAMESPACES,
+)
+
+__all__ = [
+ # IncidentEngine
+ "IncidentEngine",
+ "IIncidentMemory",
+ "IBlastRadiusAnalyzer",
+ "AGGREGATION_WINDOW_MINUTES",
+ "WORKING_MEMORY_TTL",
+ # ProposalEngine
+ "ProposalEngine",
+ "GuardrailsValidator",
+ "ILLMProvider",
+ "FORBIDDEN_COMMANDS",
+ "ALLOWED_NAMESPACES",
+ "SYSTEM_NAMESPACES",
]
diff --git a/packages/lewooogo-brain/src/lewooogo_brain/engines/incident_engine.py b/packages/lewooogo-brain/src/lewooogo_brain/engines/incident_engine.py
new file mode 100644
index 00000000..d5317f33
--- /dev/null
+++ b/packages/lewooogo-brain/src/lewooogo_brain/engines/incident_engine.py
@@ -0,0 +1,315 @@
+"""
+IncidentEngine - 事件處理引擎 (積木化版本)
+==========================================
+
+Phase 6.4e: 從 apps/api/src/services/incident_engine.py 搬遷
+
+設計原則:
+- 依賴注入: 透過建構子注入 IMemoryProvider
+- 無外部耦合: 禁止直接引用 redis_client 或 db
+- 可測試性: 可注入 Mock Provider 進行單元測試
+
+統帥鐵律:
+- 禁止告警風暴 (相關告警必須聚合)
+- 禁止 O(N) 掃描 (所有查詢必須 O(1))
+- 禁止 Race Condition (所有寫入必須原子操作)
+"""
+
+from datetime import datetime, timezone, timedelta
+from typing import Any, Protocol, Callable
+from uuid import uuid4
+import hashlib
+import json
+
+from lewooogo_brain.interfaces.incident_processor import (
+ IIncidentProcessor,
+ Incident,
+ IncidentStatus,
+ Severity,
+ Signal,
+)
+
+
+# =============================================================================
+# Memory Provider Protocol (依賴注入用)
+# =============================================================================
+
+class IIncidentMemory(Protocol):
+ """Incident 專用記憶體提供者協定"""
+
+ async def load_incident(self, incident_id: str) -> Incident | None:
+ """從 Working Memory 載入 Incident"""
+ ...
+
+ async def save_incident(self, incident: Incident, ttl_seconds: int = 604800) -> bool:
+ """儲存 Incident 到 Working Memory (預設 7 天 TTL)"""
+ ...
+
+ async def persist_incident(self, incident: Incident) -> bool:
+ """持久化到 Episodic Memory (PostgreSQL)"""
+ ...
+
+ async def find_related_incident(
+ self,
+ namespace: str,
+ target: str,
+ window_minutes: int = 30,
+ ) -> Incident | None:
+ """尋找相關的活躍 Incident (用於聚合)"""
+ ...
+
+ async def update_index(
+ self,
+ incident_id: str,
+ namespace: str,
+ target: str,
+ ) -> bool:
+ """更新反向索引 (namespace/target → incident_id)"""
+ ...
+
+
+class IBlastRadiusAnalyzer(Protocol):
+ """爆炸半徑分析器協定"""
+
+ def analyze(self, target: str) -> list[str]:
+ """分析受影響的服務列表"""
+ ...
+
+
+# =============================================================================
+# Constants
+# =============================================================================
+
+AGGREGATION_WINDOW_MINUTES = 30
+WORKING_MEMORY_TTL = 604800 # 7 days
+
+
+# =============================================================================
+# IncidentEngine Implementation
+# =============================================================================
+
+class IncidentEngine(IIncidentProcessor):
+ """
+ 事件處理引擎
+
+ 職責:
+ 1. 聚合相關告警到同一 Incident
+ 2. 分析爆炸半徑
+ 3. 雙層持久化 (Working + Episodic Memory)
+
+ 使用方式:
+ memory = DualIncidentMemory(redis_client, db_session)
+ analyzer = GraphBlastRadiusAnalyzer(topology_graph)
+ engine = IncidentEngine(memory, analyzer)
+
+ incident = await engine.process_signal(signal_data)
+ """
+
+ def __init__(
+ self,
+ memory: IIncidentMemory,
+ blast_analyzer: IBlastRadiusAnalyzer | None = None,
+ logger: Any | None = None,
+ ):
+ """
+ 初始化 IncidentEngine
+
+ Args:
+ memory: 記憶體提供者 (Working + Episodic)
+ blast_analyzer: 爆炸半徑分析器 (可選)
+ logger: 日誌記錄器 (可選)
+ """
+ self._memory = memory
+ self._blast_analyzer = blast_analyzer
+ self._logger = logger
+
+ def _log(self, event: str, **kwargs) -> None:
+ """記錄日誌 (如果有 logger)"""
+ if self._logger:
+ self._logger.info(event, **kwargs)
+
+ async def process_signal(
+ self,
+ signal_data: dict[str, Any],
+ ) -> Incident | None:
+ """
+ 處理告警信號
+
+ 流程:
+ 1. 解析 Signal
+ 2. 計算 Fingerprint (去重用)
+ 3. 查找相關 Incident (聚合)
+ 4. 創建或更新 Incident
+ 5. 分析爆炸半徑
+ 6. 雙層持久化
+ """
+ try:
+ # Step 1: 解析 Signal
+ signal = self._parse_signal(signal_data)
+ namespace = signal_data.get("namespace", "default")
+ target = signal_data.get("target", "unknown")
+
+ # Step 2: 計算 Fingerprint
+ fingerprint = self._compute_fingerprint(signal_data)
+ signal.fingerprint = fingerprint
+
+ # Step 3: 查找相關 Incident
+ existing = await self._memory.find_related_incident(
+ namespace=namespace,
+ target=target,
+ window_minutes=AGGREGATION_WINDOW_MINUTES,
+ )
+
+ if existing:
+ # 聚合到現有 Incident
+ incident = await self._aggregate_signal(existing, signal)
+ else:
+ # 創建新 Incident
+ incident = await self._create_incident(signal, namespace, target)
+
+ # Step 4: 分析爆炸半徑
+ if self._blast_analyzer and target not in incident.affected_services:
+ affected = self._blast_analyzer.analyze(target)
+ incident.affected_services = list(set(incident.affected_services + affected))
+
+ # Step 5: 雙層持久化
+ await self._memory.save_incident(incident, WORKING_MEMORY_TTL)
+ await self._memory.update_index(incident.incident_id, namespace, target)
+ persisted = await self._memory.persist_incident(incident)
+
+ self._log(
+ "signal_processed",
+ incident_id=incident.incident_id,
+ signal_count=len(incident.signals),
+ persisted_to_pg=persisted,
+ )
+
+ return incident
+
+ except Exception as e:
+ self._log("signal_processing_error", error=str(e))
+ return None
+
+ async def get_incident(self, incident_id: str) -> Incident | None:
+ """取得 Incident"""
+ return await self._memory.load_incident(incident_id)
+
+ async def update_status(
+ self,
+ incident_id: str,
+ status: IncidentStatus,
+ ) -> bool:
+ """更新 Incident 狀態"""
+ incident = await self._memory.load_incident(incident_id)
+ if not incident:
+ return False
+
+ incident.status = status
+ incident.updated_at = datetime.now(timezone.utc)
+
+ if status == IncidentStatus.RESOLVED:
+ incident.resolved_at = datetime.now(timezone.utc)
+ elif status == IncidentStatus.CLOSED:
+ incident.closed_at = datetime.now(timezone.utc)
+
+ await self._memory.save_incident(incident, WORKING_MEMORY_TTL)
+ await self._memory.persist_incident(incident)
+
+ return True
+
+ # =========================================================================
+ # Private Methods
+ # =========================================================================
+
+ def _parse_signal(self, data: dict[str, Any]) -> Signal:
+ """解析 Signal 資料"""
+ severity_map = {
+ "critical": Severity.P0,
+ "warning": Severity.P2,
+ "info": Severity.P3,
+ }
+
+ severity_str = data.get("severity", "warning")
+ severity = severity_map.get(severity_str, Severity.P2)
+
+ return Signal(
+ alert_name=data.get("alert_name", "Unknown"),
+ severity=severity,
+ source=data.get("source", "unknown"),
+ fired_at=datetime.now(timezone.utc),
+ labels=data.get("labels", {}) if isinstance(data.get("labels"), dict) else {},
+ annotations=data.get("annotations", {}) if isinstance(data.get("annotations"), dict) else {},
+ )
+
+ def _compute_fingerprint(self, data: dict[str, Any]) -> str:
+ """計算 Signal Fingerprint (用於去重)"""
+ key_parts = [
+ data.get("source", ""),
+ data.get("alert_name", ""),
+ data.get("namespace", ""),
+ data.get("target", ""),
+ ]
+ key_str = ":".join(key_parts)
+ return hashlib.sha256(key_str.encode()).hexdigest()[:16]
+
+ async def _create_incident(
+ self,
+ signal: Signal,
+ namespace: str,
+ target: str,
+ ) -> Incident:
+ """創建新 Incident"""
+ incident_id = f"INC-{datetime.now(timezone.utc).strftime('%Y%m%d')}-{uuid4().hex[:6].upper()}"
+
+ incident = Incident(
+ incident_id=incident_id,
+ status=IncidentStatus.INVESTIGATING,
+ severity=signal.severity,
+ signals=[signal],
+ affected_services=[target] if target != "unknown" else [],
+ created_at=datetime.now(timezone.utc),
+ updated_at=datetime.now(timezone.utc),
+ )
+
+ self._log(
+ "incident_created",
+ incident_id=incident_id,
+ severity=signal.severity.value,
+ namespace=namespace,
+ target=target,
+ )
+
+ return incident
+
+ async def _aggregate_signal(
+ self,
+ incident: Incident,
+ signal: Signal,
+ ) -> Incident:
+ """聚合 Signal 到現有 Incident"""
+ # 檢查重複 (Fingerprint)
+ existing_fingerprints = {s.fingerprint for s in incident.signals if s.fingerprint}
+ if signal.fingerprint and signal.fingerprint in existing_fingerprints:
+ self._log(
+ "signal_deduplicated",
+ incident_id=incident.incident_id,
+ fingerprint=signal.fingerprint,
+ )
+ return incident
+
+ # 聚合
+ incident.signals.append(signal)
+ incident.updated_at = datetime.now(timezone.utc)
+
+ # 嚴重度升級 (取最高)
+ if signal.severity.value < incident.severity.value:
+ incident.severity = signal.severity
+
+ self._log(
+ "signal_aggregated",
+ incident_id=incident.incident_id,
+ signal_count=len(incident.signals),
+ severity=incident.severity.value,
+ )
+
+ return incident
diff --git a/packages/lewooogo-brain/src/lewooogo_brain/engines/proposal_engine.py b/packages/lewooogo-brain/src/lewooogo_brain/engines/proposal_engine.py
new file mode 100644
index 00000000..8dd9ba0c
--- /dev/null
+++ b/packages/lewooogo-brain/src/lewooogo_brain/engines/proposal_engine.py
@@ -0,0 +1,516 @@
+"""
+ProposalEngine - 決策提案引擎 (積木化版本)
+==========================================
+
+Phase 6.4e: 從 apps/api/src/services/proposal_service.py 搬遷
+
+設計原則:
+- 依賴注入: 透過建構子注入 IMemoryProvider 與 ILLMProvider
+- 無外部耦合: 禁止直接引用 redis_client 或 db
+- Guardrails 強制: 所有提案必須通過安全檢查
+
+統帥鐵律 + 首席架構師鐵律:
+- 禁止毀滅性指令 (rm -rf, DROP DATABASE, kubectl delete ns)
+- K8s 操作必須綁定 Namespace
+- 所有提案必須 require_dry_run: true
+"""
+
+from datetime import datetime, timezone
+from typing import Any, Protocol, Callable
+from uuid import uuid4
+import re
+
+from lewooogo_brain.interfaces.proposal_engine import (
+ IProposalEngine,
+ Proposal,
+ Guardrails,
+)
+from lewooogo_brain.interfaces.incident_processor import (
+ Incident,
+ IncidentStatus,
+)
+
+
+# =============================================================================
+# Provider Protocols (依賴注入用)
+# =============================================================================
+
+class IIncidentMemory(Protocol):
+ """Incident 記憶體提供者協定"""
+
+ async def load_incident(self, incident_id: str) -> Incident | None:
+ """載入 Incident"""
+ ...
+
+ async def update_incident(
+ self,
+ incident_id: str,
+ updates: dict[str, Any],
+ ) -> bool:
+ """更新 Incident"""
+ ...
+
+
+class ILLMProvider(Protocol):
+ """LLM 提供者協定"""
+
+ async def generate(
+ self,
+ prompt: str,
+ context: str | None = None,
+ max_tokens: int = 2048,
+ ) -> str:
+ """生成 LLM 回應"""
+ ...
+
+
+class ISkillLoader(Protocol):
+ """Skill 載入器協定"""
+
+ def load(self, skill_id: str) -> str | None:
+ """載入 Skill 內容"""
+ ...
+
+
+# =============================================================================
+# Constants - Guardrails 黑名單
+# =============================================================================
+
+FORBIDDEN_COMMANDS = [
+ "rm -rf /",
+ "rm -rf /*",
+ "rm -rf .",
+ "drop database",
+ "drop table",
+ "truncate",
+ "delete from",
+ "kubectl delete namespace",
+ "kubectl delete ns",
+ "kubectl delete -A",
+ "> /dev/sda",
+ "mkfs",
+ ":(){:|:&};:", # Fork bomb
+ "--no-preserve-root",
+ "dd if=/dev/zero",
+]
+
+ALLOWED_NAMESPACES = ["awoooi-prod", "awoooi-dev"]
+
+SYSTEM_NAMESPACES = ["kube-system", "kube-public", "kube-node-lease", "default"]
+
+
+# =============================================================================
+# ProposalEngine Implementation
+# =============================================================================
+
+class ProposalEngine(IProposalEngine):
+ """
+ 決策提案引擎
+
+ 職責:
+ 1. 分析 Incident 生成修復建議
+ 2. 評估風險等級
+ 3. 強制 Guardrails 檢查
+ 4. 更新 Incident 狀態
+
+ 使用方式:
+ memory = IncidentMemoryAdapter(redis_client, db_session)
+ llm = OllamaProvider(base_url="http://192.168.0.188:11434")
+ skill_loader = SkillLoader(skills_dir=".agents/skills")
+
+ engine = ProposalEngine(memory, llm, skill_loader)
+ proposal, message = await engine.generate(incident_id)
+ """
+
+ def __init__(
+ self,
+ memory: IIncidentMemory,
+ llm: ILLMProvider | None = None,
+ skill_loader: ISkillLoader | None = None,
+ logger: Any | None = None,
+ ):
+ """
+ 初始化 ProposalEngine
+
+ Args:
+ memory: Incident 記憶體提供者
+ llm: LLM 提供者 (用於生成提案)
+ skill_loader: Skill 載入器 (可選)
+ logger: 日誌記錄器 (可選)
+ """
+ self._memory = memory
+ self._llm = llm
+ self._skill_loader = skill_loader
+ self._logger = logger
+
+ def _log(self, event: str, **kwargs) -> None:
+ """記錄日誌"""
+ if self._logger:
+ self._logger.info(event, **kwargs)
+
+ def get_default_guardrails(self) -> Guardrails:
+ """取得預設安全護欄配置"""
+ return Guardrails(
+ require_dry_run=True,
+ allowed_namespace=ALLOWED_NAMESPACES.copy(),
+ forbidden_commands=FORBIDDEN_COMMANDS.copy(),
+ max_retries=1,
+ timeout_sec=60,
+ audit_log="mandatory",
+ rollback_window_sec=300,
+ )
+
+ async def generate(
+ self,
+ incident_id: str,
+ ) -> tuple[Proposal | None, str]:
+ """
+ 生成決策提案
+
+ Args:
+ incident_id: 事件 ID
+
+ Returns:
+ (Proposal, message) 或 (None, error_message)
+ """
+ return await self._generate_proposal(incident_id, skill_id=None)
+
+ async def generate_with_skill(
+ self,
+ incident_id: str,
+ skill_id: str,
+ ) -> tuple[Proposal | None, str]:
+ """
+ 使用指定 Skill 生成決策提案
+
+ Args:
+ incident_id: 事件 ID
+ skill_id: Skill 識別碼 (e.g., "04-awoooi-devops-commander")
+
+ Returns:
+ (Proposal, message) 或 (None, error_message)
+ """
+ return await self._generate_proposal(incident_id, skill_id=skill_id)
+
+ async def _generate_proposal(
+ self,
+ incident_id: str,
+ skill_id: str | None,
+ ) -> tuple[Proposal | None, str]:
+ """內部提案生成邏輯"""
+ try:
+ # Step 1: 載入 Incident
+ incident = await self._memory.load_incident(incident_id)
+ if not incident:
+ return None, f"Incident {incident_id} not found"
+
+ # Step 2: 載入 Skill (如果指定)
+ skill_context = None
+ if skill_id and self._skill_loader:
+ skill_context = self._skill_loader.load(skill_id)
+ if not skill_context:
+ self._log("skill_not_found", skill_id=skill_id)
+
+ # Step 3: 構建提案
+ if self._llm:
+ proposal = await self._generate_with_llm(incident, skill_context)
+ else:
+ proposal = self._generate_fallback(incident)
+
+ # Step 4: Guardrails 檢查
+ is_safe, violation = self._validate_guardrails(proposal)
+ if not is_safe:
+ self._log(
+ "guardrails_violation",
+ incident_id=incident_id,
+ violation=violation,
+ )
+ return None, f"Guardrails violation: {violation}"
+
+ # Step 5: 更新 Incident
+ await self._memory.update_incident(
+ incident_id,
+ {
+ "status": IncidentStatus.MITIGATING.value,
+ "proposal_ids": incident.proposal_ids + [proposal.proposal_id],
+ "updated_at": datetime.now(timezone.utc).isoformat(),
+ },
+ )
+
+ self._log(
+ "proposal_generated",
+ incident_id=incident_id,
+ proposal_id=proposal.proposal_id,
+ risk_level=proposal.risk_level,
+ )
+
+ return proposal, "Proposal generated successfully"
+
+ except Exception as e:
+ self._log("proposal_generation_error", error=str(e))
+ return None, f"Error generating proposal: {str(e)}"
+
+ async def _generate_with_llm(
+ self,
+ incident: Incident,
+ skill_context: str | None,
+ ) -> Proposal:
+ """使用 LLM 生成提案"""
+ # 構建 prompt
+ prompt = self._build_prompt(incident, skill_context)
+
+ # 調用 LLM
+ response = await self._llm.generate(prompt, context=skill_context)
+
+ # 解析 LLM 回應 (簡化版,實際應使用結構化輸出)
+ action = self._extract_action(response)
+ description = self._extract_description(response)
+ risk_level = self._assess_risk(incident, action)
+
+ return Proposal(
+ proposal_id=str(uuid4()),
+ incident_id=incident.incident_id,
+ action=action,
+ description=description,
+ risk_level=risk_level,
+ guardrails=self.get_default_guardrails().model_dump(),
+ metadata={
+ "generated_by": "llm",
+ "skill_used": skill_context is not None,
+ "signal_count": len(incident.signals),
+ },
+ )
+
+ def _generate_fallback(self, incident: Incident) -> Proposal:
+ """備援提案生成 (無 LLM 時使用)"""
+ # 根據嚴重度和服務決定動作
+ if incident.severity.value in ["P0", "P1"]:
+ action = "kubectl rollout restart deployment/