Files
awoooi/scripts/ai_code_reviewer.py
OG T 7478dc0254 feat(phase6-9): Complete modular architecture and Agent Teams
Phase 6.4 - Modular Architecture:
- Add lewooogo-brain adapters for LLM providers
- Add lewooogo-data dual memory (Redis + PostgreSQL)
- Implement consensus engine for multi-agent decisions
- Add incident memory service for historical context

Phase 9 - Agent Teams (Claude Agent SDK):
- Add base agent class with Claude Sonnet 4 integration
- Implement action planner, blast radius, and security agents
- Add agent API endpoints and proposal workflow
- Integrate ADR-009 OpenClaw Agent Teams architecture

DevOps & CI/CD:
- Add GitHub Actions CI/CD workflows (ci.yaml, cd.yaml)
- Add pre-commit hooks and secrets baseline
- Add docker-compose for local development
- Update Kubernetes network policies

Frontend Improvements:
- Add auto-healing error boundary component
- Update i18n messages for agent features
- Enhance dual-state incident card with execution feedback

Documentation:
- Add 7 ADRs covering MCP, design system, architecture decisions
- Update ARCHITECTURE_MEMORY.md with modular design
- Add GLOBAL_RULES.md and SOUL.md for project identity

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-23 18:40:36 +08:00

261 lines
7.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
AI Code Reviewer (AI-on-AI Review)
===================================
Phase 5: 全自動防禦網 - AI 督戰隊
功能:
1. 讀取 git diff (staged changes)
2. 讀取 .awoooi-agent-rules.md 規則
3. 呼叫 Ollama API 進行架構審查
4. 回傳 PASS/FAIL 結果
使用方式:
python scripts/ai_code_reviewer.py
Exit Codes:
0 = PASS (允許 commit)
1 = FAIL (阻止 commit)
"""
import json
import subprocess
import sys
from pathlib import Path
import httpx
# =============================================================================
# Configuration
# =============================================================================
OLLAMA_URL = "http://192.168.0.188:11434/api/generate"
MODEL = "llama3.2:8b"
PROJECT_ROOT = Path(__file__).parent.parent
RULES_FILE = PROJECT_ROOT / ".awoooi-agent-rules.md"
TIMEOUT = 120 # seconds
# =============================================================================
# Git Operations
# =============================================================================
def get_staged_diff() -> str:
"""取得 staged changes 的 diff"""
try:
result = subprocess.run(
["git", "diff", "--cached", "--no-color"],
capture_output=True,
text=True,
cwd=PROJECT_ROOT,
)
return result.stdout
except Exception as e:
print(f"[AI-REVIEWER] Error getting git diff: {e}")
return ""
def get_staged_files() -> list[str]:
"""取得 staged files 清單"""
try:
result = subprocess.run(
["git", "diff", "--cached", "--name-only"],
capture_output=True,
text=True,
cwd=PROJECT_ROOT,
)
return [f.strip() for f in result.stdout.splitlines() if f.strip()]
except Exception as e:
print(f"[AI-REVIEWER] Error getting staged files: {e}")
return []
# =============================================================================
# Rule Extraction
# =============================================================================
def load_rules() -> str:
"""讀取 AWOOOI 規則檔案 (精簡版)"""
if not RULES_FILE.exists():
return "No rules file found."
content = RULES_FILE.read_text()
# 萃取關鍵規則 (避免 prompt 過長)
key_sections = []
# 萃取禁止事項
if "## 🚫 絕對禁止事項" in content:
start = content.find("## 🚫 絕對禁止事項")
end = content.find("## ✅ 開發準則", start)
if end > start:
key_sections.append(content[start:end])
# 萃取六大鐵律
if "## 🚨 六大鐵律" in content:
start = content.find("## 🚨 六大鐵律")
end = content.find("---", start + 10)
if end > start:
key_sections.append(content[start:end])
# 萃取 i18n 鐵律
if "## 🌐 國際化 (i18n) 鐵律" in content:
start = content.find("## 🌐 國際化 (i18n) 鐵律")
end = content.find("## 📜 API 契約驅動開發", start)
if end > start:
key_sections.append(content[start:end])
return "\n\n".join(key_sections) if key_sections else content[:5000]
# =============================================================================
# Ollama Review
# =============================================================================
def call_ollama_review(diff: str, rules: str, files: list[str]) -> dict:
"""呼叫 Ollama 進行代碼審查"""
prompt = f"""You are AWOOOI's AI Code Reviewer. Analyze the following git diff and check for violations.
## RULES TO ENFORCE:
{rules}
## FILES CHANGED:
{', '.join(files[:20])}
## GIT DIFF:
```diff
{diff[:8000]}
```
## YOUR TASK:
1. Check for hardcoded secrets (API keys, passwords, tokens)
2. Check for hardcoded Chinese/English strings in UI components (should use next-intl)
3. Check for imports from forbidden paths (../../../wooo-aiops/)
4. Check for architecture violations (mixing layers, etc.)
5. Check for potential security issues
## RESPONSE FORMAT (JSON only):
{{
"verdict": "PASS" or "FAIL",
"issues": [
{{"severity": "critical|warning", "file": "path", "line": "N/A", "message": "description"}}
],
"summary": "One sentence summary"
}}
Respond with ONLY the JSON, no markdown, no explanation.
"""
try:
response = httpx.post(
OLLAMA_URL,
json={
"model": MODEL,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.1,
"num_predict": 1000,
},
},
timeout=TIMEOUT,
)
response.raise_for_status()
result = response.json()
response_text = result.get("response", "")
# 嘗試解析 JSON
# 清理可能的 markdown 包裹
cleaned = response_text.strip()
if cleaned.startswith("```json"):
cleaned = cleaned[7:]
if cleaned.startswith("```"):
cleaned = cleaned[3:]
if cleaned.endswith("```"):
cleaned = cleaned[:-3]
cleaned = cleaned.strip()
return json.loads(cleaned)
except httpx.TimeoutException:
print("[AI-REVIEWER] Ollama timeout - allowing commit (fail-open)")
return {"verdict": "PASS", "issues": [], "summary": "Review skipped (timeout)"}
except httpx.ConnectError:
print("[AI-REVIEWER] Cannot connect to Ollama - allowing commit (fail-open)")
return {"verdict": "PASS", "issues": [], "summary": "Review skipped (no connection)"}
except json.JSONDecodeError as e:
print(f"[AI-REVIEWER] JSON parse error: {e}")
print(f"[AI-REVIEWER] Raw response: {response_text[:500]}")
return {"verdict": "PASS", "issues": [], "summary": "Review skipped (parse error)"}
except Exception as e:
print(f"[AI-REVIEWER] Error: {e}")
return {"verdict": "PASS", "issues": [], "summary": f"Review skipped ({type(e).__name__})"}
# =============================================================================
# Main
# =============================================================================
def main() -> int:
"""主程式"""
print("\n" + "=" * 60)
print("🤖 AWOOOI AI Code Reviewer (AI-on-AI)")
print("=" * 60)
# 取得 staged changes
files = get_staged_files()
if not files:
print("[AI-REVIEWER] No staged changes. Skipping review.")
return 0
print(f"[AI-REVIEWER] Reviewing {len(files)} staged file(s)...")
for f in files[:10]:
print(f" - {f}")
if len(files) > 10:
print(f" ... and {len(files) - 10} more")
diff = get_staged_diff()
if not diff:
print("[AI-REVIEWER] Empty diff. Skipping review.")
return 0
# 載入規則
rules = load_rules()
# 呼叫 Ollama
print(f"[AI-REVIEWER] Calling Ollama ({MODEL})...")
result = call_ollama_review(diff, rules, files)
# 輸出結果
verdict = result.get("verdict", "PASS")
issues = result.get("issues", [])
summary = result.get("summary", "")
print("\n" + "-" * 60)
print(f"Summary: {summary}")
print("-" * 60)
if issues:
print("\n📋 Issues Found:")
for issue in issues:
severity = issue.get("severity", "warning")
file = issue.get("file", "N/A")
msg = issue.get("message", "")
icon = "🔴" if severity == "critical" else "🟡"
print(f" {icon} [{severity.upper()}] {file}: {msg}")
print("\n" + "=" * 60)
if verdict == "PASS":
print("✅ VERDICT: PASS - Commit allowed")
print("=" * 60 + "\n")
return 0
else:
print("❌ VERDICT: FAIL - Commit blocked")
print("=" * 60 + "\n")
print("Fix the issues above and try again.")
return 1
if __name__ == "__main__":
sys.exit(main())