
Audit
- 1 installs
- 4 repo stars
- Updated May 12, 2026
- miles990/multi-agent-workflow
Runs a parallel multi-perspective architecture audit of a codebase to find dependency violations, naming inconsistencies, DRY issues, and test gaps.
About
A skill that dispatches four parallel auditor agents to review architecture health across dependencies, patterns, test coverage, and doc sync. A developer uses it to surface technical debt and inconsistency across a codebase.
- Four parallel perspectives: dependency, pattern, coverage, doc-sync
- Flags circular deps, direction violations, and missing cross-cutting concerns
Audit by the numbers
- 1 all-time installs (skills.sh)
- Ranked #984 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/miles990/multi-agent-workflow --skill auditAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 4 |
| Last updated | May 12, 2026 |
| Repository | miles990/multi-agent-workflow ↗ |
What it does
Runs a parallel multi-perspective architecture audit of a codebase to find dependency violations, naming inconsistencies, DRY issues, and test gaps.
Files
Audit v1.0.0
跨子系統架構一致性審查 -- 多視角並行分析架構健康度,識別不一致和技術債務
用途
對 codebase 進行全面的架構健康度審查,識別:
- 依賴方向違反(下層依賴上層)
- 命名不一致
- 錯誤處理模式不統一
- 橫切關注點缺失(logging、metrics、error handling)
- 代碼重複與 DRY 違反
- 測試覆蓋率缺口
使用方式
/audit [目標路徑或模組]
/audit src/api/ # 審查 API 模組
/audit src/ # 審查 src 整體
/audit --full # 全面審查(整個 codebase)Flags: --full | --focus deps|patterns|tests|docs | --quick | --deep
預設 4 視角
| ID | 名稱 | 模型 | 聚焦 |
|---|---|---|---|
dependency-auditor | 依賴分析師 | sonnet | import/require 掃描、循環依賴、方向違反 |
pattern-checker | 模式一致性檢查員 | haiku | 錯誤處理、命名慣例、API 介面風格 |
coverage-auditor | 測試覆蓋分析師 | haiku | 缺少測試的模組、測試品質、mock 濫用 |
doc-sync-checker | 文檔同步檢查員 | haiku | 代碼與文檔比對、過時文檔、API 文檔完整性 |
-> 模型路由配置:shared/config/model-routing.yaml
執行流程
Phase 0: 收集目標 -> 確定審查範圍、掃描目標路徑
|
Phase 1: MAP(並行審查)
+-------------+----------------+----------------+----------------+
| 依賴分析師 | 模式一致性 | 測試覆蓋 | 文檔同步 |
| (sonnet) | 檢查員 (haiku) | 分析師 (haiku) | 檢查員 (haiku) |
+-------------+----------------+----------------+----------------+
** 並行執行關鍵 **:
在單一訊息中發送 4 個 Task 工具呼叫:
- Task({description: "依賴方向審查", ...})
- Task({description: "模式一致性檢查", ...})
- Task({description: "測試覆蓋分析", ...})
- Task({description: "文檔同步檢查", ...})
** 強制 **:每個 Agent 完成前必須執行:
1. mkdir -p .claude/memory/audit/{audit-id}/perspectives/
2. Write -> .claude/memory/audit/{audit-id}/perspectives/{perspective_id}.md
未執行 Write = 任務失敗
|
Phase 2: REDUCE(問題匯總)
+-- 問題分類(CRITICAL/HIGH/MEDIUM/LOW)
+-- 按嚴重度排序
+-- 修復建議
|
Phase 3: 產出審查報告問題分類
| 級別 | 定義 | 處理 |
|---|---|---|
| CRITICAL | 架構缺陷,可能導致系統故障 | 立即修復 |
| HIGH | 一致性違反,影響可維護性 | 限時修復 |
| MEDIUM | 建議改善,提升品質 | 記錄追蹤 |
| LOW | 風格建議或小改進 | 可選 |
輸出結構
.claude/memory/audit/{audit-id}/
+-- meta.yaml # 元數據(審查範圍、時間、配置)
+-- perspectives/ # 完整視角報告(MAP 產出)
| +-- dependency-auditor.md
| +-- pattern-checker.md
| +-- coverage-auditor.md
| +-- doc-sync-checker.md
+-- issues.yaml # 問題清單(按嚴重度排序)
+-- audit-summary.md # 審查摘要 + 建議(主輸出)perspectives/ 保存各視角的完整分析報告,issues.yaml 匯總所有問題。
Agent 能力限制
審查 Agent 不應該開啟 Task:
| 允許的操作 | 說明 |
|---|---|
| Read | 讀取程式碼和配置 |
| Glob/Grep | 搜尋檔案和內容模式 |
| Bash | 執行分析命令(如 dependency graph) |
| Write | 寫入審查報告 |
| ~~Task~~ | 不允許開子 Agent |
視角 Prompt 指引
視角 1: 依賴分析師 (dependency-auditor)
目標:掃描 import/require 語句,繪製依賴關係,識別問題。
檢查項目: 1. 掃描所有 import/require/from...import 語句 2. 識別模組間依賴方向(上層不應依賴下層的具體實現) 3. 檢測循環依賴 4. 識別過度耦合的模組(依賴數超過閾值) 5. 檢查是否有跨層依賴違反
輸出格式:
- 依賴圖摘要(文字描述)
- 問題列表(含檔案路徑、行號、嚴重度)
- 建議的重構方向
視角 2: 模式一致性檢查員 (pattern-checker)
目標:檢查 codebase 中的模式一致性。
檢查項目: 1. 錯誤處理模式(統一 try-catch 風格、自定義錯誤類型使用) 2. 命名慣例(變數、函數、類別、檔案命名) 3. API 介面風格(REST 路由命名、請求/回應格式) 4. 日誌記錄模式(logger 使用一致性) 5. 配置存取模式(環境變數 vs 配置檔案)
輸出格式:
- 每種模式的一致性評分
- 不一致的具體案例(含檔案路徑、行號)
- 建議的統一標準
視角 3: 測試覆蓋分析師 (coverage-auditor)
目標:找出測試覆蓋缺口,評估測試品質。
檢查項目: 1. 找出沒有對應測試的模組 2. 評估現有測試的品質(是否只測試 happy path) 3. 識別 mock/stub 濫用(過度 mock 導致測試失去意義) 4. 檢查邊界條件測試覆蓋 5. 評估整合測試 vs 單元測試比例
輸出格式:
- 未覆蓋模組列表
- 測試品質評估
- 建議新增的測試案例
視角 4: 文檔同步檢查員 (doc-sync-checker)
目標:確保文檔與代碼同步。
檢查項目: 1. 比對 README/CLAUDE.md 中描述的功能與實際代碼 2. 檢查 API 文檔與實際 endpoint 是否一致 3. 識別過時的文檔(提到已刪除的模組/函數) 4. 檢查配置文檔完整性(所有環境變數是否有文檔) 5. 確認命令文檔與實際可用命令一致
輸出格式:
- 文檔 vs 代碼差異清單
- 過時文檔列表
- 缺失文檔建議
CP4: Task Commit
審查完成後執行 Task Commit:
Phase 3: 審查報告產出
|
CP4: Task Commit
+-- git add .claude/memory/audit/{audit-id}/
+-- git commit -m "docs(audit): complete architecture audit for {target}"-> 協議:shared/git/commit-protocol.md
共用模組
| 模組 | 用途 |
|---|---|
| coordination/map-phase.md | 並行協調 |
| coordination/reduce-phase.md | 匯總整合 |
| perspectives/catalog.yaml | 視角定義 |
工作流位置
RESEARCH -> PLAN -> TASKS -> IMPLEMENT -> REVIEW -> VERIFY
|
AUDIT <---+
(獨立工具,隨時可用)- 輸入:目標路徑或模組名稱
- 輸出:架構審查報告,可供 PLAN 或 IMPLEMENT 參考
- 定位:獨立的架構健康檢查工具,不屬於固定工作流階段,隨時可執行
"""
Multi-Agent Workflow CLI - 混合架構編排器
CLI 是控制中心,Agent 是思考引擎。
- 確定性操作(CLI 做):目錄建立、檔案寫入、日誌記錄、驗證、狀態追蹤
- 不確定性操作(Agent 做):分析、思考、產出報告內容
"""
__version__ = "3.1.0"
"""
支援 python -m cli 執行
"""
from cli.dependencies import CLI_DEPENDENCIES, ensure_python_dependencies
ensure_python_dependencies(CLI_DEPENDENCIES)
from cli.main import app
if __name__ == "__main__":
app()
"""
Config 模組 - 配置與定義
包含:
- schema.py: JSON Schema 定義
- actions.py: Action 描述映射
- stages.py: 階段描述
- perspectives.py: 視角描述
- models.py: Pydantic 資料模型
"""
"""
Action 描述映射 - 定義所有 Action 的描述與格式
"""
from typing import Dict, List, TypedDict
class ActionInfo(TypedDict):
"""Action 資訊"""
name: str
description: str
level: str # info, warning, error
format: str # 格式化字串模板
# ─────────────────────────────────────────────────────────────────────────────
# Action 定義
# ─────────────────────────────────────────────────────────────────────────────
ACTIONS: Dict[str, ActionInfo] = {
"workflow_init": ActionInfo(
name="工作流初始化",
description="開始新的工作流",
level="info",
format="🚀 工作流開始: {topic}",
),
"stage_start": ActionInfo(
name="階段開始",
description="開始執行新階段",
level="info",
format="📋 階段開始: {stage_name} ({stage_id})",
),
"stage_complete": ActionInfo(
name="階段完成",
description="階段執行完成",
level="info",
format="✅ 階段完成: {stage_id}",
),
"agent_start": ActionInfo(
name="Agent 開始",
description="Agent 開始執行任務",
level="info",
format="🤖 Agent 開始: {agent_name} ({agent_id})",
),
"agent_complete": ActionInfo(
name="Agent 完成",
description="Agent 執行完成",
level="info",
format="✅ Agent 完成: {agent_id}",
),
"agent_call_error": ActionInfo(
name="Agent 錯誤",
description="Agent 調用失敗",
level="error",
format="❌ Agent 錯誤: {agent_id} - {reason}",
),
"file_write": ActionInfo(
name="檔案寫入",
description="寫入檔案",
level="info",
format="📝 寫入: {path} ({size_bytes} bytes)",
),
"gate_check": ActionInfo(
name="品質閘門檢查",
description="執行品質閘門檢查",
level="info",
format="🔍 閘門檢查: {stage} - {score}/{threshold}",
),
"gate_failed": ActionInfo(
name="閘門失敗",
description="品質閘門檢查失敗",
level="error",
format="❌ 閘門失敗: {stage} - {failed_criteria}",
),
"rollback_triggered": ActionInfo(
name="回退觸發",
description="觸發智慧回退",
level="warning",
format="🔙 回退: {from_stage} → {to_stage} (第 {iteration} 次)",
),
"workflow_complete": ActionInfo(
name="工作流完成",
description="工作流執行完成",
level="info",
format="🎉 工作流完成: {final_status} ({duration_seconds:.1f}s)",
),
"workflow_error": ActionInfo(
name="工作流錯誤",
description="工作流執行錯誤",
level="error",
format="❌ 工作流錯誤: {error}",
),
"human_intervention": ActionInfo(
name="人工介入",
description="需要人工介入",
level="warning",
format="👤 需要人工介入: {reason}",
),
}
# ─────────────────────────────────────────────────────────────────────────────
# 便捷函數
# ─────────────────────────────────────────────────────────────────────────────
def get_action_info(action: str) -> ActionInfo | None:
"""取得 Action 資訊"""
return ACTIONS.get(action)
def format_action(action: str, details: Dict) -> str:
"""格式化 Action 訊息"""
info = ACTIONS.get(action)
if not info:
return f"{action}: {details}"
try:
return info["format"].format(**details)
except KeyError:
return f"{info['name']}: {details}"
def get_action_level(action: str) -> str:
"""取得 Action 等級"""
info = ACTIONS.get(action)
return info["level"] if info else "info"
def list_actions() -> List[str]:
"""列出所有 Action"""
return list(ACTIONS.keys())
def list_error_actions() -> List[str]:
"""列出所有錯誤類型的 Action"""
return [action for action, info in ACTIONS.items() if info["level"] == "error"]
def list_warning_actions() -> List[str]:
"""列出所有警告類型的 Action"""
return [action for action, info in ACTIONS.items() if info["level"] == "warning"]
"""
Pydantic 資料模型 - 定義所有資料結構
包含:
- Agent 相關模型
- 階段相關模型
- 工作流相關模型
- 品質閘門相關模型
"""
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
# ─────────────────────────────────────────────────────────────────────────────
# 列舉類型
# ─────────────────────────────────────────────────────────────────────────────
class StageID(str, Enum):
"""階段 ID"""
RESEARCH = "RESEARCH"
PLAN = "PLAN"
TASKS = "TASKS"
IMPLEMENT = "IMPLEMENT"
REVIEW = "REVIEW"
VERIFY = "VERIFY"
class AgentStatus(str, Enum):
"""Agent 狀態"""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class WorkflowStatus(str, Enum):
"""工作流狀態"""
INITIALIZED = "initialized"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
ROLLBACK = "rollback"
HUMAN_INTERVENTION = "human_intervention"
class WorkflowMode(str, Enum):
"""工作流模式"""
QUICK = "quick" # 快速模式:減少視角數量
NORMAL = "normal" # 正常模式
DEEP = "deep" # 深度模式:更多視角、更嚴格閘門
# ─────────────────────────────────────────────────────────────────────────────
# Agent 模型
# ─────────────────────────────────────────────────────────────────────────────
class AgentConfig(BaseModel):
"""Agent 配置"""
id: str = Field(..., description="Agent ID")
name: str = Field(..., description="Agent 名稱")
description: Optional[str] = Field(None, description="Agent 描述")
model: str = Field("sonnet", description="使用的模型")
perspective: Optional[str] = Field(None, description="視角 ID")
class AgentResponse(BaseModel):
"""Agent 回應"""
success: bool = Field(..., description="是否成功")
content: Optional[Dict[str, Any]] = Field(None, description="JSON 回應內容")
raw_output: Optional[str] = Field(None, description="原始輸出")
error: Optional[str] = Field(None, description="錯誤訊息")
tokens_used: Optional[int] = Field(None, description="使用的 token 數")
duration_seconds: Optional[float] = Field(None, description="執行時間(秒)")
class AgentState(BaseModel):
"""Agent 執行狀態"""
id: str
name: str
description: Optional[str] = None
model: str = "sonnet"
status: AgentStatus = AgentStatus.PENDING
task: Optional[str] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
# ─────────────────────────────────────────────────────────────────────────────
# 階段模型
# ─────────────────────────────────────────────────────────────────────────────
class StageConfig(BaseModel):
"""階段配置"""
id: StageID
name: str
description: str
perspectives: List[str] = Field(default_factory=list)
gate_threshold: float = Field(75.0, description="品質閘門閾值")
required_outputs: List[str] = Field(default_factory=list)
class StageState(BaseModel):
"""階段狀態"""
id: StageID
name: str
description: str
index: int = Field(..., description="階段索引 (1-based)")
total: int = Field(..., description="總階段數")
status: AgentStatus = AgentStatus.PENDING
class StageResult(BaseModel):
"""階段執行結果"""
stage_id: StageID
success: bool
outputs: Dict[str, str] = Field(default_factory=dict, description="輸出檔案路徑")
quality_score: Optional[float] = None
errors: List[str] = Field(default_factory=list)
duration_seconds: Optional[float] = None
# ─────────────────────────────────────────────────────────────────────────────
# 品質閘門模型
# ─────────────────────────────────────────────────────────────────────────────
class GateCheckResult(BaseModel):
"""品質閘門檢查結果"""
stage: StageID
passed: bool
score: float
threshold: float
criteria: Dict[str, bool] = Field(default_factory=dict)
failed_criteria: List[str] = Field(default_factory=list)
details: Optional[Dict[str, Any]] = None
class RollbackDecision(BaseModel):
"""回退決策"""
should_rollback: bool
from_stage: StageID
to_stage: StageID
iteration: int
reason: str
require_human: bool = False
# ─────────────────────────────────────────────────────────────────────────────
# 工作流模型
# ─────────────────────────────────────────────────────────────────────────────
class WorkflowConfig(BaseModel):
"""工作流配置"""
topic: str = Field(..., description="工作流主題/需求描述")
mode: WorkflowMode = Field(WorkflowMode.NORMAL, description="執行模式")
start_from: Optional[StageID] = Field(None, description="從指定階段開始")
skip_stages: List[StageID] = Field(default_factory=list, description="跳過的階段")
max_iterations: int = Field(10, description="最大迭代次數")
class WorkflowState(BaseModel):
"""工作流狀態"""
id: str
topic: str
status: WorkflowStatus = WorkflowStatus.INITIALIZED
current_stage: Optional[StageID] = None
iteration: int = 0
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
class WorkflowResult(BaseModel):
"""工作流執行結果"""
workflow_id: str
success: bool
final_status: WorkflowStatus
quality_score: Optional[float] = None
stage_results: Dict[str, StageResult] = Field(default_factory=dict)
total_iterations: int = 0
duration_seconds: Optional[float] = None
errors: List[str] = Field(default_factory=list)
# ─────────────────────────────────────────────────────────────────────────────
# 視角模型
# ─────────────────────────────────────────────────────────────────────────────
class PerspectiveConfig(BaseModel):
"""視角配置"""
id: str = Field(..., description="視角 ID")
name: str = Field(..., description="視角名稱")
description: str = Field(..., description="視角描述")
focus_areas: List[str] = Field(default_factory=list, description="關注領域")
model: str = Field("sonnet", description="使用的模型")
class PerspectiveReport(BaseModel):
"""視角報告"""
perspective_id: str
perspective_name: str
content: str
key_findings: List[str] = Field(default_factory=list)
recommendations: List[str] = Field(default_factory=list)
concerns: List[str] = Field(default_factory=list)
# ─────────────────────────────────────────────────────────────────────────────
# 輸出格式
# ─────────────────────────────────────────────────────────────────────────────
class CurrentState(BaseModel):
"""即時狀態(current.json 格式)"""
updated_at: datetime = Field(default_factory=datetime.now)
workflow: Dict[str, Any]
stage: Optional[StageState] = None
agents: List[AgentState] = Field(default_factory=list)
progress: Dict[str, int] = Field(
default_factory=lambda: {"agents_completed": 0, "agents_total": 0}
)
"""
視角配置 - 定義各階段的視角
每個視角代表一個專業角色,負責從特定角度分析問題。
"""
from typing import Dict, List
from .models import PerspectiveConfig, StageID
# ─────────────────────────────────────────────────────────────────────────────
# 視角定義
# ─────────────────────────────────────────────────────────────────────────────
PERSPECTIVES: Dict[str, PerspectiveConfig] = {
# ───────────────────────────────────────────────────────────────────────
# RESEARCH 階段視角
# ───────────────────────────────────────────────────────────────────────
"architecture": PerspectiveConfig(
id="architecture",
name="架構分析師",
description="分析系統結構、設計模式、技術選型",
focus_areas=[
"系統架構",
"設計模式",
"技術棧選擇",
"可擴展性",
"模組化",
],
model="sonnet",
),
"cognitive": PerspectiveConfig(
id="cognitive",
name="認知研究員",
description="研究使用者心智模型、認知負荷、學習曲線",
focus_areas=[
"使用者心智模型",
"認知負荷",
"學習曲線",
"錯誤預防",
"直覺性",
],
model="sonnet",
),
"workflow": PerspectiveConfig(
id="workflow",
name="工作流設計師",
description="設計操作流程、狀態轉換、錯誤處理",
focus_areas=[
"操作流程",
"狀態管理",
"錯誤處理",
"邊界情況",
"資料流",
],
model="sonnet",
),
"industry": PerspectiveConfig(
id="industry",
name="業界實踐專家",
description="研究業界最佳實踐、競品分析、趨勢",
focus_areas=[
"業界標準",
"最佳實踐",
"競品分析",
"技術趨勢",
"案例研究",
],
model="sonnet",
),
# ───────────────────────────────────────────────────────────────────────
# PLAN 階段視角
# ───────────────────────────────────────────────────────────────────────
"system_architect": PerspectiveConfig(
id="system_architect",
name="系統架構師",
description="設計系統架構、元件關係、介面定義",
focus_areas=[
"系統設計",
"元件劃分",
"介面定義",
"依賴管理",
"部署架構",
],
model="sonnet",
),
"ux_designer": PerspectiveConfig(
id="ux_designer",
name="UX 設計師",
description="設計使用者體驗、互動流程、介面規格",
focus_areas=[
"使用者體驗",
"互動設計",
"資訊架構",
"可用性",
"無障礙",
],
model="sonnet",
),
"security_analyst": PerspectiveConfig(
id="security_analyst",
name="安全分析師",
description="分析安全風險、設計防護措施",
focus_areas=[
"威脅模型",
"安全控制",
"資料保護",
"認證授權",
"合規要求",
],
model="sonnet",
),
"quality_engineer": PerspectiveConfig(
id="quality_engineer",
name="品質工程師",
description="設計測試策略、品質指標、驗收標準",
focus_areas=[
"測試策略",
"品質指標",
"驗收標準",
"效能要求",
"可靠性",
],
model="sonnet",
),
# ───────────────────────────────────────────────────────────────────────
# TASKS 階段視角
# ───────────────────────────────────────────────────────────────────────
"task_decomposer": PerspectiveConfig(
id="task_decomposer",
name="任務分解專家",
description="將計劃分解為可執行的原子任務",
focus_areas=[
"任務粒度",
"完成定義",
"驗收條件",
"可測試性",
"獨立性",
],
model="sonnet",
),
"dependency_analyst": PerspectiveConfig(
id="dependency_analyst",
name="依賴分析師",
description="分析任務依賴、設計執行順序",
focus_areas=[
"依賴關係",
"執行順序",
"並行機會",
"瓶頸識別",
"關鍵路徑",
],
model="sonnet",
),
"test_planner": PerspectiveConfig(
id="test_planner",
name="測試規劃師",
description="為每個任務設計對應的測試",
focus_areas=[
"TDD 映射",
"測試用例",
"邊界條件",
"錯誤路徑",
"整合測試",
],
model="sonnet",
),
"risk_preventor": PerspectiveConfig(
id="risk_preventor",
name="風險預防師",
description="識別任務風險、設計預防措施",
focus_areas=[
"風險識別",
"影響評估",
"預防措施",
"回退計劃",
"監控指標",
],
model="sonnet",
),
# ───────────────────────────────────────────────────────────────────────
# IMPLEMENT 階段視角
# ───────────────────────────────────────────────────────────────────────
"developer": PerspectiveConfig(
id="developer",
name="開發者",
description="實作功能、撰寫程式碼",
focus_areas=[
"功能實作",
"程式碼品質",
"效能優化",
"錯誤處理",
"文件註解",
],
model="sonnet",
),
"tdd_coach": PerspectiveConfig(
id="tdd_coach",
name="TDD 教練",
description="指導測試驅動開發、確保測試覆蓋",
focus_areas=[
"測試先行",
"紅綠重構",
"測試覆蓋",
"測試品質",
"邊界測試",
],
model="sonnet",
),
"reviewer": PerspectiveConfig(
id="reviewer",
name="即時審查者",
description="即時審查程式碼、提供改進建議",
focus_areas=[
"程式碼審查",
"最佳實踐",
"一致性",
"可讀性",
"改進建議",
],
model="sonnet",
),
# ───────────────────────────────────────────────────────────────────────
# REVIEW 階段視角
# ───────────────────────────────────────────────────────────────────────
"code_quality": PerspectiveConfig(
id="code_quality",
name="程式碼品質審查員",
description="審查程式碼品質、一致性、可讀性",
focus_areas=[
"程式碼風格",
"命名規範",
"結構清晰",
"重複程式碼",
"複雜度",
],
model="sonnet",
),
"security": PerspectiveConfig(
id="security",
name="安全審查員",
description="審查安全漏洞、敏感資料處理",
focus_areas=[
"注入攻擊",
"認證授權",
"資料暴露",
"加密處理",
"日誌安全",
],
model="sonnet",
),
"performance": PerspectiveConfig(
id="performance",
name="效能審查員",
description="審查效能問題、資源使用",
focus_areas=[
"時間複雜度",
"空間複雜度",
"資源洩漏",
"並發處理",
"快取策略",
],
model="sonnet",
),
"maintainability": PerspectiveConfig(
id="maintainability",
name="可維護性審查員",
description="審查可維護性、擴展性、文件",
focus_areas=[
"模組化",
"解耦合",
"文件完整",
"測試覆蓋",
"技術債務",
],
model="sonnet",
),
# ───────────────────────────────────────────────────────────────────────
# VERIFY 階段視角
# ───────────────────────────────────────────────────────────────────────
"functional_tester": PerspectiveConfig(
id="functional_tester",
name="功能測試員",
description="驗證功能正確性、邊界情況",
focus_areas=[
"功能驗證",
"邊界測試",
"錯誤處理",
"使用者流程",
"異常情況",
],
model="sonnet",
),
"regression_tester": PerspectiveConfig(
id="regression_tester",
name="回歸測試員",
description="驗證既有功能未受影響",
focus_areas=[
"回歸測試",
"整合測試",
"相容性",
"向後相容",
"副作用",
],
model="sonnet",
),
"acceptance_validator": PerspectiveConfig(
id="acceptance_validator",
name="驗收驗證員",
description="驗證是否符合驗收標準",
focus_areas=[
"驗收標準",
"需求符合",
"品質達標",
"文件完整",
"發布準備",
],
model="sonnet",
),
}
# ─────────────────────────────────────────────────────────────────────────────
# 階段視角映射
# ─────────────────────────────────────────────────────────────────────────────
STAGE_PERSPECTIVES: Dict[StageID, List[str]] = {
StageID.RESEARCH: ["architecture", "cognitive", "workflow", "industry"],
StageID.PLAN: ["system_architect", "ux_designer", "security_analyst", "quality_engineer"],
StageID.TASKS: ["task_decomposer", "dependency_analyst", "test_planner", "risk_preventor"],
StageID.IMPLEMENT: ["developer", "tdd_coach", "reviewer"],
StageID.REVIEW: ["code_quality", "security", "performance", "maintainability"],
StageID.VERIFY: ["functional_tester", "regression_tester", "acceptance_validator"],
}
# 快速模式視角(減少數量)
QUICK_MODE_PERSPECTIVES: Dict[StageID, List[str]] = {
StageID.RESEARCH: ["architecture", "workflow"],
StageID.PLAN: ["system_architect", "quality_engineer"],
StageID.TASKS: ["task_decomposer", "dependency_analyst"],
StageID.IMPLEMENT: ["developer", "reviewer"],
StageID.REVIEW: ["code_quality", "security"],
StageID.VERIFY: ["functional_tester", "acceptance_validator"],
}
# ─────────────────────────────────────────────────────────────────────────────
# 便捷函數
# ─────────────────────────────────────────────────────────────────────────────
def get_perspective(perspective_id: str) -> PerspectiveConfig | None:
"""取得視角配置"""
return PERSPECTIVES.get(perspective_id)
def get_stage_perspectives(
stage_id: StageID,
quick_mode: bool = False,
) -> List[PerspectiveConfig]:
"""取得階段的視角列表"""
if quick_mode:
perspective_ids = QUICK_MODE_PERSPECTIVES.get(stage_id, [])
else:
perspective_ids = STAGE_PERSPECTIVES.get(stage_id, [])
return [PERSPECTIVES[pid] for pid in perspective_ids if pid in PERSPECTIVES]
def list_all_perspectives() -> List[PerspectiveConfig]:
"""列出所有視角"""
return list(PERSPECTIVES.values())
"""
JSON Schema 定義 - 定義各種輸出格式的 Schema
用於驗證 Agent 返回的 JSON 格式。
"""
from typing import Any, Dict
# ─────────────────────────────────────────────────────────────────────────────
# 視角報告 Schema
# ─────────────────────────────────────────────────────────────────────────────
PERSPECTIVE_REPORT_SCHEMA: Dict[str, Any] = {
"type": "object",
"required": ["perspective_id", "perspective_name", "findings", "recommendations"],
"properties": {
"perspective_id": {
"type": "string",
"description": "視角 ID",
},
"perspective_name": {
"type": "string",
"description": "視角名稱",
},
"summary": {
"type": "string",
"description": "摘要(1-2 句話)",
},
"findings": {
"type": "array",
"description": "主要發現",
"items": {
"type": "object",
"required": ["title", "description"],
"properties": {
"title": {"type": "string"},
"description": {"type": "string"},
"importance": {
"type": "string",
"enum": ["high", "medium", "low"],
},
},
},
},
"recommendations": {
"type": "array",
"description": "建議",
"items": {
"type": "object",
"required": ["title", "description"],
"properties": {
"title": {"type": "string"},
"description": {"type": "string"},
"priority": {
"type": "string",
"enum": ["must", "should", "could"],
},
},
},
},
"concerns": {
"type": "array",
"description": "擔憂或風險",
"items": {
"type": "object",
"required": ["title", "description"],
"properties": {
"title": {"type": "string"},
"description": {"type": "string"},
"severity": {
"type": "string",
"enum": ["critical", "high", "medium", "low"],
},
},
},
},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# 階段綜合報告 Schema
# ─────────────────────────────────────────────────────────────────────────────
SYNTHESIS_REPORT_SCHEMA: Dict[str, Any] = {
"type": "object",
"required": ["stage_id", "consensus", "key_insights", "action_items"],
"properties": {
"stage_id": {
"type": "string",
"description": "階段 ID",
},
"consensus": {
"type": "object",
"description": "共識點",
"properties": {
"score": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "共識分數 (0-1)",
},
"points": {
"type": "array",
"items": {"type": "string"},
"description": "共識要點",
},
},
},
"key_insights": {
"type": "array",
"description": "關鍵洞察",
"items": {
"type": "object",
"required": ["insight", "source_perspectives"],
"properties": {
"insight": {"type": "string"},
"source_perspectives": {
"type": "array",
"items": {"type": "string"},
},
"confidence": {
"type": "string",
"enum": ["high", "medium", "low"],
},
},
},
},
"conflicts": {
"type": "array",
"description": "衝突點",
"items": {
"type": "object",
"required": ["topic", "perspectives"],
"properties": {
"topic": {"type": "string"},
"perspectives": {
"type": "array",
"items": {
"type": "object",
"properties": {
"perspective_id": {"type": "string"},
"position": {"type": "string"},
},
},
},
"resolution": {"type": "string"},
},
},
},
"action_items": {
"type": "array",
"description": "行動項目",
"items": {
"type": "object",
"required": ["action", "priority"],
"properties": {
"action": {"type": "string"},
"priority": {
"type": "string",
"enum": ["critical", "high", "medium", "low"],
},
"owner": {"type": "string"},
},
},
},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# 任務清單 Schema
# ─────────────────────────────────────────────────────────────────────────────
TASKS_SCHEMA: Dict[str, Any] = {
"type": "object",
"required": ["tasks"],
"properties": {
"metadata": {
"type": "object",
"properties": {
"total_tasks": {"type": "integer"},
"total_waves": {"type": "integer"},
"estimated_effort": {"type": "string"},
},
},
"tasks": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "title", "type"],
"properties": {
"id": {
"type": "string",
"pattern": "^(T-[A-Z]+-[0-9]+|TEST-[0-9]+|SETUP-[0-9]+)$",
"description": "任務 ID (如 T-F-01, TEST-01)",
},
"title": {"type": "string"},
"description": {"type": "string"},
"type": {
"type": "string",
"enum": ["feature", "test", "setup", "config", "docs"],
},
"wave": {
"type": "integer",
"minimum": 1,
"description": "執行波次",
},
"depends_on": {
"type": "array",
"items": {"type": "string"},
"description": "依賴的任務 ID",
},
"acceptance_criteria": {
"type": "array",
"items": {"type": "string"},
},
"test_id": {
"type": "string",
"description": "對應的測試任務 ID (TDD)",
},
},
},
},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# 審查報告 Schema
# ─────────────────────────────────────────────────────────────────────────────
REVIEW_REPORT_SCHEMA: Dict[str, Any] = {
"type": "object",
"required": ["perspective_id", "issues"],
"properties": {
"perspective_id": {"type": "string"},
"perspective_name": {"type": "string"},
"summary": {"type": "string"},
"issues": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "title", "severity", "category"],
"properties": {
"id": {"type": "string"},
"title": {"type": "string"},
"description": {"type": "string"},
"severity": {
"type": "string",
"enum": ["BLOCKER", "HIGH", "MEDIUM", "LOW", "INFO"],
},
"category": {
"type": "string",
"enum": [
"bug",
"security",
"performance",
"style",
"maintainability",
"documentation",
],
},
"file": {"type": "string"},
"line": {"type": "integer"},
"suggestion": {"type": "string"},
},
},
},
"approval": {
"type": "string",
"enum": ["approved", "approved_with_comments", "changes_requested"],
},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# 驗證結果 Schema
# ─────────────────────────────────────────────────────────────────────────────
VERIFICATION_REPORT_SCHEMA: Dict[str, Any] = {
"type": "object",
"required": ["perspective_id", "test_results"],
"properties": {
"perspective_id": {"type": "string"},
"perspective_name": {"type": "string"},
"summary": {"type": "string"},
"test_results": {
"type": "array",
"items": {
"type": "object",
"required": ["test_id", "status"],
"properties": {
"test_id": {"type": "string"},
"test_name": {"type": "string"},
"status": {
"type": "string",
"enum": ["passed", "failed", "skipped", "error"],
},
"details": {"type": "string"},
"duration_ms": {"type": "number"},
},
},
},
"coverage": {
"type": "object",
"properties": {
"line_coverage": {"type": "number"},
"branch_coverage": {"type": "number"},
"function_coverage": {"type": "number"},
},
},
"verdict": {
"type": "string",
"enum": ["pass", "fail", "conditional"],
},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# Schema 映射
# ─────────────────────────────────────────────────────────────────────────────
SCHEMAS: Dict[str, Dict[str, Any]] = {
"perspective_report": PERSPECTIVE_REPORT_SCHEMA,
"synthesis_report": SYNTHESIS_REPORT_SCHEMA,
"tasks": TASKS_SCHEMA,
"review_report": REVIEW_REPORT_SCHEMA,
"verification_report": VERIFICATION_REPORT_SCHEMA,
}
def get_schema(schema_name: str) -> Dict[str, Any] | None:
"""取得 Schema"""
return SCHEMAS.get(schema_name)
"""
階段配置 - 定義各階段的描述與配置
6 個階段:RESEARCH → PLAN → TASKS → IMPLEMENT → REVIEW → VERIFY
"""
from typing import Dict, List
from .models import StageConfig, StageID
# ─────────────────────────────────────────────────────────────────────────────
# 階段定義
# ─────────────────────────────────────────────────────────────────────────────
STAGES: Dict[StageID, StageConfig] = {
StageID.RESEARCH: StageConfig(
id=StageID.RESEARCH,
name="研究階段",
description="多視角並行研究,收集資訊與洞察",
perspectives=[
"architecture",
"cognitive",
"workflow",
"industry",
],
gate_threshold=70.0,
required_outputs=["synthesis.md"],
),
StageID.PLAN: StageConfig(
id=StageID.PLAN,
name="規劃階段",
description="多視角設計,產出實作計劃",
perspectives=[
"system_architect",
"ux_designer",
"security_analyst",
"quality_engineer",
],
gate_threshold=75.0,
required_outputs=["implementation-plan.md"],
),
StageID.TASKS: StageConfig(
id=StageID.TASKS,
name="任務分解階段",
description="將計劃分解為可執行的任務 DAG",
perspectives=[
"task_decomposer",
"dependency_analyst",
"test_planner",
"risk_preventor",
],
gate_threshold=80.0,
required_outputs=["tasks.yaml"],
),
StageID.IMPLEMENT: StageConfig(
id=StageID.IMPLEMENT,
name="實作階段",
description="TDD 驅動、即時審查、品質守護",
perspectives=[
"developer",
"tdd_coach",
"reviewer",
],
gate_threshold=80.0,
required_outputs=["implementation.md"],
),
StageID.REVIEW: StageConfig(
id=StageID.REVIEW,
name="審查階段",
description="多視角程式碼審查,問題分類與優先排序",
perspectives=[
"code_quality",
"security",
"performance",
"maintainability",
],
gate_threshold=75.0,
required_outputs=["review-summary.md"],
),
StageID.VERIFY: StageConfig(
id=StageID.VERIFY,
name="驗證階段",
description="多視角測試驗證,驗收標準確認",
perspectives=[
"functional_tester",
"regression_tester",
"acceptance_validator",
],
gate_threshold=85.0,
required_outputs=["verification.md"],
),
}
# 階段順序
STAGE_ORDER: List[StageID] = [
StageID.RESEARCH,
StageID.PLAN,
StageID.TASKS,
StageID.IMPLEMENT,
StageID.REVIEW,
StageID.VERIFY,
]
# 階段權重(用於進度計算)
STAGE_WEIGHTS: Dict[StageID, float] = {
StageID.RESEARCH: 0.15,
StageID.PLAN: 0.15,
StageID.TASKS: 0.10,
StageID.IMPLEMENT: 0.35,
StageID.REVIEW: 0.15,
StageID.VERIFY: 0.10,
}
# ─────────────────────────────────────────────────────────────────────────────
# 便捷函數
# ─────────────────────────────────────────────────────────────────────────────
def get_stage(stage_id: StageID) -> StageConfig:
"""取得階段配置"""
return STAGES[stage_id]
def get_stage_index(stage_id: StageID) -> int:
"""取得階段索引 (1-based)"""
return STAGE_ORDER.index(stage_id) + 1
def get_next_stage(stage_id: StageID) -> StageID | None:
"""取得下一個階段"""
idx = STAGE_ORDER.index(stage_id)
if idx < len(STAGE_ORDER) - 1:
return STAGE_ORDER[idx + 1]
return None
def get_prev_stage(stage_id: StageID) -> StageID | None:
"""取得上一個階段"""
idx = STAGE_ORDER.index(stage_id)
if idx > 0:
return STAGE_ORDER[idx - 1]
return None
def is_final_stage(stage_id: StageID) -> bool:
"""是否為最後階段"""
return stage_id == STAGE_ORDER[-1]
def get_stage_by_name(name: str) -> StageID | None:
"""根據名稱取得階段 ID"""
name_upper = name.upper()
for stage_id in StageID:
if stage_id.value == name_upper:
return stage_id
return None
"""Best-effort runtime dependency bootstrap for portable CLI copies."""
from __future__ import annotations
import importlib
import os
import site
import subprocess
import sys
import sysconfig
import venv
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
@dataclass(frozen=True)
class PythonDependency:
import_name: str
package_name: str
CLI_DEPENDENCIES = [
PythonDependency('typer', 'typer[all]>=0.9.0'),
PythonDependency('rich', 'rich>=13.0.0'),
PythonDependency('pydantic', 'pydantic>=2.0.0'),
PythonDependency('yaml', 'PyYAML>=6.0'),
]
def ensure_python_dependencies(dependencies: Iterable[PythonDependency]) -> None:
missing = [dep for dep in dependencies if not _can_import(dep.import_name)]
if not missing:
return
if os.environ.get('MAW_DISABLE_AUTO_INSTALL') == '1':
names = ', '.join(dep.package_name for dep in missing)
raise RuntimeError(f'Missing Python dependencies: {names}')
packages = [dep.package_name for dep in missing]
install_commands = [
[sys.executable, '-m', 'pip', 'install', '--quiet', *packages],
[sys.executable, '-m', 'pip', 'install', '--quiet', '--user', *packages],
]
installed = False
for command in install_commands:
try:
subprocess.run(
command,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=180,
)
installed = True
break
except Exception:
continue
if not installed:
installed = _install_into_managed_venv(packages)
still_missing = [dep.package_name for dep in missing if not _can_import(dep.import_name)]
if still_missing:
joined = ', '.join(still_missing)
if installed:
raise RuntimeError(f'Installed dependencies, but imports still failed: {joined}')
raise RuntimeError(f'Unable to install Python dependencies: {joined}')
def _can_import(import_name: str) -> bool:
try:
importlib.import_module(import_name)
return True
except ImportError:
return False
def _install_into_managed_venv(packages: list[str]) -> bool:
"""Install dependencies into a managed user venv for PEP 668 systems."""
venv_dir = Path(
os.environ.get(
'MAW_PYTHON_ENV',
str(Path.home() / '.cache' / 'multi-agent-workflow' / 'python-env'),
)
)
try:
if not venv_dir.exists():
venv.EnvBuilder(with_pip=True, clear=False).create(venv_dir)
python = venv_dir / ('Scripts/python.exe' if os.name == 'nt' else 'bin/python')
subprocess.run(
[str(python), '-m', 'pip', 'install', '--quiet', *packages],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=240,
)
purelib = subprocess.check_output(
[
str(python),
'-c',
'import sysconfig; print(sysconfig.get_paths()["purelib"])',
],
text=True,
).strip()
platlib = subprocess.check_output(
[
str(python),
'-c',
'import sysconfig; print(sysconfig.get_paths()["platlib"])',
],
text=True,
).strip()
for path in [purelib, platlib, *site.getsitepackages(), sysconfig.get_paths().get('purelib', '')]:
if path and path not in sys.path:
sys.path.insert(0, path)
return True
except Exception:
return False
"""
I/O 模組 - 讀寫層
包含:
- memory.py: Memory 讀寫
- logging.py: Action Log
- state.py: 即時狀態追蹤
- report.py: 報告生成
"""
"""
Action Log 模組 - 記錄所有操作
所有操作都以 JSONL 格式記錄到 logs/actions.jsonl
Actions:
- workflow_init: 工作流開始
- stage_start: 階段開始
- agent_start: Agent 開始
- agent_complete: Agent 完成
- agent_call_error: Agent 失敗
- file_write: 寫入檔案
- gate_check: 品質閘門
- gate_failed: 閘門失敗
- rollback_triggered: 觸發回退
- workflow_complete: 完成
"""
import json
import os
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional
from .memory import get_memory
# Action 類型
ActionType = Literal[
"workflow_init",
"stage_start",
"stage_complete",
"agent_start",
"agent_complete",
"agent_call_error",
"file_write",
"gate_check",
"gate_failed",
"rollback_triggered",
"workflow_complete",
"workflow_error",
"human_intervention",
]
class ActionLogger:
"""Action 日誌記錄器"""
def __init__(self, workflow_id: str, base_path: Optional[str] = None):
"""
初始化 Action Logger
Args:
workflow_id: 工作流 ID
base_path: Memory 根目錄
"""
self.workflow_id = workflow_id
self.memory = get_memory(base_path)
workflow_dir = self.memory.get_workflow_dir(workflow_id)
if workflow_dir:
self.log_file = workflow_dir / "logs" / "actions.jsonl"
else:
# 工作流尚未建立,使用臨時路徑
self.log_file = self.memory.base_path / "workflows" / workflow_id / "logs" / "actions.jsonl"
self.log_file.parent.mkdir(parents=True, exist_ok=True)
def log(
self,
action: ActionType,
details: Optional[Dict[str, Any]] = None,
level: str = "info",
) -> Dict:
"""
記錄一個 Action
Args:
action: Action 類型
details: 詳細資訊
level: 日誌等級 (info, warning, error)
Returns:
記錄的完整內容
"""
record = {
"timestamp": datetime.now().isoformat(),
"workflow_id": self.workflow_id,
"action": action,
"level": level,
"details": details or {},
}
self.memory.append_jsonl(self.log_file, record)
return record
# ─────────────────────────────────────────────────────────────────────────
# 便捷方法
# ─────────────────────────────────────────────────────────────────────────
def workflow_init(self, topic: str, config: Optional[Dict] = None) -> Dict:
"""記錄工作流初始化"""
return self.log(
"workflow_init",
{"topic": topic, "config": config or {}},
)
def stage_start(
self,
stage_id: str,
stage_name: str,
perspectives: Optional[List[str]] = None,
) -> Dict:
"""記錄階段開始"""
return self.log(
"stage_start",
{
"stage_id": stage_id,
"stage_name": stage_name,
"perspectives": perspectives or [],
},
)
def stage_complete(
self,
stage_id: str,
success: bool,
duration_seconds: Optional[float] = None,
) -> Dict:
"""記錄階段完成"""
return self.log(
"stage_complete",
{
"stage_id": stage_id,
"success": success,
"duration_seconds": duration_seconds,
},
level="info" if success else "warning",
)
def agent_start(
self,
agent_id: str,
agent_name: str,
model: str,
task: str,
) -> Dict:
"""記錄 Agent 開始"""
return self.log(
"agent_start",
{
"agent_id": agent_id,
"agent_name": agent_name,
"model": model,
"task": task,
},
)
def agent_complete(
self,
agent_id: str,
success: bool,
response_tokens: Optional[int] = None,
duration_seconds: Optional[float] = None,
) -> Dict:
"""記錄 Agent 完成"""
return self.log(
"agent_complete",
{
"agent_id": agent_id,
"success": success,
"response_tokens": response_tokens,
"duration_seconds": duration_seconds,
},
level="info" if success else "warning",
)
def agent_call_error(
self,
agent_id: str,
reason: str,
attempt: int,
retryable: bool,
) -> Dict:
"""記錄 Agent 調用錯誤"""
return self.log(
"agent_call_error",
{
"agent_id": agent_id,
"reason": reason,
"attempt": attempt,
"retryable": retryable,
},
level="error",
)
def file_write(self, path: str, size_bytes: int) -> Dict:
"""記錄檔案寫入"""
return self.log(
"file_write",
{"path": path, "size_bytes": size_bytes},
)
def gate_check(
self,
stage: str,
passed: bool,
score: float,
threshold: float,
) -> Dict:
"""記錄品質閘門檢查"""
return self.log(
"gate_check",
{
"stage": stage,
"passed": passed,
"score": score,
"threshold": threshold,
},
level="info" if passed else "warning",
)
def gate_failed(self, stage: str, failed_criteria: List[str]) -> Dict:
"""記錄閘門失敗"""
return self.log(
"gate_failed",
{"stage": stage, "failed_criteria": failed_criteria},
level="error",
)
def rollback_triggered(
self,
from_stage: str,
to_stage: str,
iteration: int,
reason: str,
) -> Dict:
"""記錄回退觸發"""
return self.log(
"rollback_triggered",
{
"from_stage": from_stage,
"to_stage": to_stage,
"iteration": iteration,
"reason": reason,
},
level="warning",
)
def workflow_complete(
self,
duration_seconds: float,
final_status: str,
quality_score: Optional[float] = None,
) -> Dict:
"""記錄工作流完成"""
return self.log(
"workflow_complete",
{
"duration_seconds": duration_seconds,
"final_status": final_status,
"quality_score": quality_score,
},
)
def workflow_error(self, error: str, stage: Optional[str] = None) -> Dict:
"""記錄工作流錯誤"""
return self.log(
"workflow_error",
{"error": error, "stage": stage},
level="error",
)
def human_intervention(self, reason: str, context: Optional[Dict] = None) -> Dict:
"""記錄需要人工介入"""
return self.log(
"human_intervention",
{"reason": reason, "context": context or {}},
level="warning",
)
# ─────────────────────────────────────────────────────────────────────────
# 查詢方法
# ─────────────────────────────────────────────────────────────────────────
def get_logs(
self,
action_filter: Optional[List[ActionType]] = None,
level_filter: Optional[List[str]] = None,
limit: Optional[int] = None,
) -> List[Dict]:
"""
取得日誌記錄
Args:
action_filter: 只包含指定的 action 類型
level_filter: 只包含指定的 level
limit: 限制返回數量(最新的)
Returns:
日誌記錄列表
"""
records = self.memory.read_jsonl(self.log_file)
if action_filter:
records = [r for r in records if r.get("action") in action_filter]
if level_filter:
records = [r for r in records if r.get("level") in level_filter]
if limit:
records = records[-limit:]
return records
def get_errors(self) -> List[Dict]:
"""取得所有錯誤記錄"""
return self.get_logs(level_filter=["error"])
def get_stage_logs(self, stage: str) -> List[Dict]:
"""取得指定階段的日誌"""
records = self.memory.read_jsonl(self.log_file)
return [
r
for r in records
if r.get("details", {}).get("stage_id", "").upper() == stage.upper()
or r.get("details", {}).get("stage", "").upper() == stage.upper()
]
# 全域 Logger 實例快取
_loggers: Dict[str, ActionLogger] = {}
def get_logger(workflow_id: str, base_path: Optional[str] = None) -> ActionLogger:
"""取得 Action Logger 實例"""
if workflow_id not in _loggers:
_loggers[workflow_id] = ActionLogger(workflow_id, base_path)
return _loggers[workflow_id]
"""
Memory 讀寫模組 - 管理 .claude/memory/ 目錄結構
負責:
- 工作流目錄建立與管理
- YAML/JSON 檔案讀寫
- 路徑解析與驗證
"""
import json
import os
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from ..dependencies import ensure_python_dependencies, PythonDependency
ensure_python_dependencies([PythonDependency('yaml', 'PyYAML>=6.0')])
import yaml # type: ignore
class MemoryManager:
"""Memory 目錄管理器"""
def __init__(self, base_path: Optional[str] = None):
"""
初始化 Memory 管理器
Args:
base_path: Memory 根目錄,預設為 .claude/memory/
"""
if base_path:
self.base_path = Path(base_path)
else:
self.base_path = Path(".claude/memory")
self._ensure_base_dirs()
def _ensure_base_dirs(self) -> None:
"""確保基礎目錄存在"""
dirs = [
self.base_path,
self.base_path / "workflows",
self.base_path / "research",
self.base_path / "plans",
self.base_path / "tasks",
self.base_path / "implement",
self.base_path / "review",
self.base_path / "verify",
]
for d in dirs:
d.mkdir(parents=True, exist_ok=True)
# ─────────────────────────────────────────────────────────────────────────
# 工作流管理
# ─────────────────────────────────────────────────────────────────────────
def create_workflow_dir(
self,
workflow_id: str,
topic: str,
config: Optional[Dict] = None,
) -> Path:
"""
建立工作流目錄結構
Args:
workflow_id: 工作流 ID
topic: 工作流主題
config: 額外配置
Returns:
工作流目錄路徑
"""
workflow_dir = self.base_path / "workflows" / workflow_id
workflow_dir.mkdir(parents=True, exist_ok=True)
# 建立子目錄
subdirs = ["stages", "agents", "logs", "exports"]
for subdir in subdirs:
(workflow_dir / subdir).mkdir(exist_ok=True)
# 建立 meta.yaml
meta = {
"id": workflow_id,
"topic": topic,
"status": "initialized",
"created_at": datetime.now().isoformat(),
"date": datetime.now().strftime("%Y-%m-%d"),
"config": config or {},
"current_stage": None,
"stages": {},
}
self.write_yaml(workflow_dir / "meta.yaml", meta)
return workflow_dir
def get_workflow_dir(self, workflow_id: str) -> Optional[Path]:
"""取得工作流目錄"""
workflow_dir = self.base_path / "workflows" / workflow_id
if workflow_dir.exists():
return workflow_dir
return None
def list_workflows(self, limit: int = 10) -> List[Dict]:
"""列出所有工作流"""
workflows_dir = self.base_path / "workflows"
if not workflows_dir.exists():
return []
workflows = []
for wf_dir in workflows_dir.iterdir():
if wf_dir.is_dir():
meta_file = wf_dir / "meta.yaml"
if meta_file.exists():
meta = self.read_yaml(meta_file)
if meta:
workflows.append(meta)
# 按日期排序
workflows.sort(key=lambda x: x.get("created_at", ""), reverse=True)
return workflows[:limit]
def get_active_workflow(self) -> Optional[Dict]:
"""取得當前活動的工作流"""
workflows = self.list_workflows()
for wf in workflows:
if wf.get("status") in ["running", "in_progress", "initialized"]:
return wf
return workflows[0] if workflows else None
# ─────────────────────────────────────────────────────────────────────────
# 階段目錄管理
# ─────────────────────────────────────────────────────────────────────────
def create_stage_dir(
self,
workflow_id: str,
stage: str,
) -> Path:
"""
建立階段目錄
Args:
workflow_id: 工作流 ID
stage: 階段名稱
Returns:
階段目錄路徑
"""
workflow_dir = self.get_workflow_dir(workflow_id)
if not workflow_dir:
raise FileNotFoundError(f"Workflow not found: {workflow_id}")
stage_dir = workflow_dir / "stages" / stage.lower()
stage_dir.mkdir(parents=True, exist_ok=True)
# 建立子目錄
(stage_dir / "perspectives").mkdir(exist_ok=True)
(stage_dir / "summaries").mkdir(exist_ok=True)
return stage_dir
def get_stage_dir(self, workflow_id: str, stage: str) -> Optional[Path]:
"""取得階段目錄"""
workflow_dir = self.get_workflow_dir(workflow_id)
if not workflow_dir:
return None
stage_dir = workflow_dir / "stages" / stage.lower()
if stage_dir.exists():
return stage_dir
return None
# ─────────────────────────────────────────────────────────────────────────
# 檔案讀寫
# ─────────────────────────────────────────────────────────────────────────
def read_yaml(self, path: Union[str, Path]) -> Optional[Dict]:
"""讀取 YAML 檔案"""
path = Path(path)
if not path.exists():
return None
try:
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except Exception:
return None
def write_yaml(self, path: Union[str, Path], data: Dict) -> bool:
"""寫入 YAML 檔案"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
try:
with open(path, "w", encoding="utf-8") as f:
yaml.dump(
data,
f,
allow_unicode=True,
default_flow_style=False,
sort_keys=False,
)
return True
except Exception:
return False
def read_json(self, path: Union[str, Path]) -> Optional[Dict]:
"""讀取 JSON 檔案"""
path = Path(path)
if not path.exists():
return None
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def write_json(self, path: Union[str, Path], data: Dict) -> bool:
"""寫入 JSON 檔案"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return True
except Exception:
return False
def read_text(self, path: Union[str, Path]) -> Optional[str]:
"""讀取文字檔案"""
path = Path(path)
if not path.exists():
return None
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return None
def write_text(self, path: Union[str, Path], content: str) -> bool:
"""寫入文字檔案"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
try:
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return True
except Exception:
return False
def append_jsonl(self, path: Union[str, Path], data: Dict) -> bool:
"""追加 JSONL 記錄"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
try:
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps(data, ensure_ascii=False) + "\n")
return True
except Exception:
return False
def read_jsonl(self, path: Union[str, Path]) -> List[Dict]:
"""讀取 JSONL 檔案"""
path = Path(path)
if not path.exists():
return []
records = []
try:
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
records.append(json.loads(line))
except Exception:
pass
return records
# ─────────────────────────────────────────────────────────────────────────
# 工作流狀態更新
# ─────────────────────────────────────────────────────────────────────────
def update_workflow_meta(
self,
workflow_id: str,
updates: Dict[str, Any],
) -> bool:
"""更新工作流 meta.yaml"""
workflow_dir = self.get_workflow_dir(workflow_id)
if not workflow_dir:
return False
meta_file = workflow_dir / "meta.yaml"
meta = self.read_yaml(meta_file) or {}
meta.update(updates)
meta["updated_at"] = datetime.now().isoformat()
return self.write_yaml(meta_file, meta)
def update_stage_status(
self,
workflow_id: str,
stage: str,
status: str,
details: Optional[Dict] = None,
) -> bool:
"""更新階段狀態"""
workflow_dir = self.get_workflow_dir(workflow_id)
if not workflow_dir:
return False
meta_file = workflow_dir / "meta.yaml"
meta = self.read_yaml(meta_file) or {}
if "stages" not in meta:
meta["stages"] = {}
meta["stages"][stage.lower()] = {
"status": status,
"updated_at": datetime.now().isoformat(),
**(details or {}),
}
if status == "running":
meta["current_stage"] = stage.upper()
meta["status"] = "running"
return self.write_yaml(meta_file, meta)
# 全域實例
_memory: Optional[MemoryManager] = None
def get_memory(base_path: Optional[str] = None) -> MemoryManager:
"""取得全域 Memory 管理器實例"""
global _memory
if _memory is None or base_path:
_memory = MemoryManager(base_path)
return _memory
"""
即時狀態追蹤模組 - 管理 current.json
支援並行 Agent 的即時狀態追蹤:
- 工作流資訊
- 當前階段
- 多個 Agent 的狀態
- 進度統計
"""
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional
from .memory import get_memory
AgentStatus = Literal["pending", "running", "completed", "failed"]
StageStatus = Literal["pending", "running", "completed", "failed", "skipped"]
class StateTracker:
"""即時狀態追蹤器"""
def __init__(self, workflow_id: str, base_path: Optional[str] = None):
"""
初始化狀態追蹤器
Args:
workflow_id: 工作流 ID
base_path: Memory 根目錄
"""
self.workflow_id = workflow_id
self.memory = get_memory(base_path)
workflow_dir = self.memory.get_workflow_dir(workflow_id)
if workflow_dir:
self.state_file = workflow_dir / "current.json"
else:
self.state_file = (
self.memory.base_path / "workflows" / workflow_id / "current.json"
)
self.state_file.parent.mkdir(parents=True, exist_ok=True)
# 初始化狀態
if not self.state_file.exists():
self._init_state()
def _init_state(self) -> None:
"""初始化狀態檔案"""
state = {
"updated_at": datetime.now().isoformat(),
"workflow": {
"id": self.workflow_id,
"topic": None,
},
"stage": None,
"agents": [],
"progress": {
"agents_completed": 0,
"agents_total": 0,
},
}
self._save_state(state)
def _load_state(self) -> Dict:
"""載入當前狀態"""
return self.memory.read_json(self.state_file) or self._get_default_state()
def _save_state(self, state: Dict) -> bool:
"""儲存狀態"""
state["updated_at"] = datetime.now().isoformat()
return self.memory.write_json(self.state_file, state)
def _get_default_state(self) -> Dict:
"""取得預設狀態"""
return {
"updated_at": datetime.now().isoformat(),
"workflow": {"id": self.workflow_id, "topic": None},
"stage": None,
"agents": [],
"progress": {"agents_completed": 0, "agents_total": 0},
}
# ─────────────────────────────────────────────────────────────────────────
# 工作流狀態
# ─────────────────────────────────────────────────────────────────────────
def set_workflow(self, topic: str) -> None:
"""設定工作流資訊"""
state = self._load_state()
state["workflow"]["topic"] = topic
self._save_state(state)
# ─────────────────────────────────────────────────────────────────────────
# 階段狀態
# ─────────────────────────────────────────────────────────────────────────
def set_stage(
self,
stage_id: str,
stage_name: str,
description: str,
index: int,
total: int,
) -> None:
"""
設定當前階段
Args:
stage_id: 階段 ID (如 RESEARCH, PLAN)
stage_name: 階段名稱 (如 研究階段)
description: 階段描述
index: 當前階段索引 (1-based)
total: 總階段數
"""
state = self._load_state()
state["stage"] = {
"id": stage_id,
"name": stage_name,
"description": description,
"index": index,
"total": total,
}
# 重置 agents
state["agents"] = []
state["progress"] = {"agents_completed": 0, "agents_total": 0}
self._save_state(state)
def clear_stage(self) -> None:
"""清除階段狀態"""
state = self._load_state()
state["stage"] = None
state["agents"] = []
state["progress"] = {"agents_completed": 0, "agents_total": 0}
self._save_state(state)
# ─────────────────────────────────────────────────────────────────────────
# Agent 狀態
# ─────────────────────────────────────────────────────────────────────────
def add_agent(
self,
agent_id: str,
agent_name: str,
description: Optional[str] = None,
model: str = "sonnet",
task: Optional[str] = None,
) -> None:
"""
新增 Agent
Args:
agent_id: Agent ID
agent_name: Agent 名稱
description: Agent 描述
model: 使用的模型
task: 分配的任務
"""
state = self._load_state()
# 檢查是否已存在
existing = next(
(a for a in state["agents"] if a["id"] == agent_id),
None,
)
if existing:
return
agent = {
"id": agent_id,
"name": agent_name,
"description": description,
"model": model,
"status": "pending",
"task": task,
}
state["agents"].append(agent)
state["progress"]["agents_total"] = len(state["agents"])
self._save_state(state)
def update_agent_status(
self,
agent_id: str,
status: AgentStatus,
task: Optional[str] = None,
) -> None:
"""
更新 Agent 狀態
Args:
agent_id: Agent ID
status: 新狀態
task: 更新任務描述(可選)
"""
state = self._load_state()
for agent in state["agents"]:
if agent["id"] == agent_id:
agent["status"] = status
if task:
agent["task"] = task
break
# 更新進度
completed = sum(
1 for a in state["agents"] if a["status"] in ["completed", "failed"]
)
state["progress"]["agents_completed"] = completed
self._save_state(state)
def set_agents(self, agents: List[Dict]) -> None:
"""
批次設定 Agents
Args:
agents: Agent 列表,每個包含 id, name, description, model, status, task
"""
state = self._load_state()
state["agents"] = agents
state["progress"]["agents_total"] = len(agents)
state["progress"]["agents_completed"] = sum(
1 for a in agents if a.get("status") in ["completed", "failed"]
)
self._save_state(state)
# ─────────────────────────────────────────────────────────────────────────
# 查詢方法
# ─────────────────────────────────────────────────────────────────────────
def get_state(self) -> Dict:
"""取得完整狀態"""
return self._load_state()
def get_stage(self) -> Optional[Dict]:
"""取得當前階段"""
state = self._load_state()
return state.get("stage")
def get_agents(self) -> List[Dict]:
"""取得所有 Agents"""
state = self._load_state()
return state.get("agents", [])
def get_progress(self) -> Dict:
"""取得進度統計"""
state = self._load_state()
return state.get("progress", {"agents_completed": 0, "agents_total": 0})
def is_all_agents_done(self) -> bool:
"""檢查是否所有 Agents 都完成"""
progress = self.get_progress()
return (
progress["agents_total"] > 0
and progress["agents_completed"] >= progress["agents_total"]
)
# 全域狀態追蹤器快取
_trackers: Dict[str, StateTracker] = {}
def get_tracker(workflow_id: str, base_path: Optional[str] = None) -> StateTracker:
"""取得狀態追蹤器實例"""
if workflow_id not in _trackers:
_trackers[workflow_id] = StateTracker(workflow_id, base_path)
return _trackers[workflow_id]
def read_current_state(workflow_id: str, base_path: Optional[str] = None) -> Dict:
"""快速讀取當前狀態"""
tracker = get_tracker(workflow_id, base_path)
return tracker.get_state()
"""
Multi-Agent Workflow CLI 入口
使用 Typer 建立命令行介面
命令:
- maw run "需求" 執行完整工作流
- maw run "需求" --start-from PLAN 從指定階段開始
- maw run "需求" --mode quick 快速模式
- maw current 查看當前執行狀態
- maw status [workflow_id] 查看工作流狀態
- maw logs <workflow_id> 查看日誌
- maw list 列出工作流
- maw validate <workflow_id> 驗證工作流
"""
from typing import List, Optional
import typer
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table
from . import __version__
from .config.models import StageID, WorkflowMode
from .config.stages import STAGE_ORDER
from .io.memory import get_memory
from .io.state import read_current_state
app = typer.Typer(
name="maw",
help="Multi-Agent Workflow CLI - 混合架構編排器",
add_completion=False,
)
console = Console()
# ─────────────────────────────────────────────────────────────────────────────
# 版本
# ─────────────────────────────────────────────────────────────────────────────
def version_callback(value: bool):
if value:
console.print(f"maw version {__version__}")
raise typer.Exit()
@app.callback()
def main(
version: bool = typer.Option(
None,
"--version",
"-v",
callback=version_callback,
is_eager=True,
help="顯示版本",
),
):
"""Multi-Agent Workflow CLI"""
pass
# ─────────────────────────────────────────────────────────────────────────────
# run 命令
# ─────────────────────────────────────────────────────────────────────────────
@app.command()
def run(
topic: str = typer.Argument(..., help="工作流主題/需求描述"),
start_from: Optional[str] = typer.Option(
None,
"--start-from",
"-s",
help="從指定階段開始 (RESEARCH/PLAN/TASKS/IMPLEMENT/REVIEW/VERIFY)",
),
skip: Optional[List[str]] = typer.Option(
None,
"--skip",
help="跳過的階段",
),
mode: str = typer.Option(
"normal",
"--mode",
"-m",
help="執行模式 (quick/normal/deep)",
),
dry_run: bool = typer.Option(
False,
"--dry-run",
help="只顯示計劃,不執行",
),
):
"""
執行完整工作流
Example:
maw run "建立用戶認證系統"
maw run "優化效能" --start-from IMPLEMENT
maw run "新增功能" --mode quick
"""
from .orchestrator.workflow import create_workflow
console.print(Panel(f"[bold blue]Multi-Agent Workflow[/bold blue]\n{topic}"))
# 驗證參數
if start_from:
try:
StageID(start_from.upper())
except ValueError:
console.print(f"[red]無效的階段: {start_from}[/red]")
console.print(f"有效階段: {', '.join(s.value for s in StageID)}")
raise typer.Exit(1)
if mode not in ["quick", "normal", "deep"]:
console.print(f"[red]無效的模式: {mode}[/red]")
console.print("有效模式: quick, normal, deep")
raise typer.Exit(1)
if dry_run:
_show_plan(topic, start_from, skip, mode)
return
# 建立並執行工作流
workflow = create_workflow(
topic=topic,
mode=mode,
start_from=start_from,
skip_stages=skip,
)
console.print(f"[dim]Workflow ID: {workflow.workflow_id}[/dim]")
console.print()
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("執行工作流...", total=None)
result = workflow.run()
progress.update(task, completed=True)
# 顯示結果
if result.success:
console.print(Panel(
f"[green]工作流完成[/green]\n"
f"品質分數: {result.quality_score:.1f}\n"
f"執行時間: {result.duration_seconds:.1f}s\n"
f"迭代次數: {result.total_iterations}",
title="Result",
))
else:
console.print(Panel(
f"[red]工作流失敗[/red]\n"
f"狀態: {result.final_status.value}\n"
f"錯誤: {', '.join(result.errors)}",
title="Result",
))
raise typer.Exit(1)
def _show_plan(
topic: str,
start_from: Optional[str],
skip: Optional[List[str]],
mode: str,
):
"""顯示執行計劃"""
console.print("\n[bold]執行計劃[/bold]\n")
table = Table(show_header=True)
table.add_column("階段", style="cyan")
table.add_column("狀態", style="green")
table.add_column("視角數")
from .config.perspectives import STAGE_PERSPECTIVES, QUICK_MODE_PERSPECTIVES
start_idx = 0
if start_from:
start_idx = [s.value for s in STAGE_ORDER].index(start_from.upper())
skip_set = set(s.upper() for s in (skip or []))
perspectives_map = QUICK_MODE_PERSPECTIVES if mode == "quick" else STAGE_PERSPECTIVES
for i, stage in enumerate(STAGE_ORDER):
if i < start_idx:
status = "跳過 (start-from)"
elif stage.value in skip_set:
status = "跳過"
else:
status = "執行"
perspectives = perspectives_map.get(stage, [])
table.add_row(stage.value, status, str(len(perspectives)))
console.print(table)
console.print("\n[dim]使用 --dry-run=False 執行工作流[/dim]")
# ─────────────────────────────────────────────────────────────────────────────
# current 命令
# ─────────────────────────────────────────────────────────────────────────────
@app.command()
def current():
"""查看當前執行狀態"""
memory = get_memory()
workflow = memory.get_active_workflow()
if not workflow:
console.print("[yellow]沒有活動的工作流[/yellow]")
return
workflow_id = workflow.get("id", "unknown")
try:
state = read_current_state(workflow_id)
except Exception:
console.print("[yellow]無法讀取狀態[/yellow]")
return
# 顯示狀態
console.print(Panel(
f"[bold]{state['workflow'].get('topic', 'Unknown')}[/bold]",
title=f"Workflow: {workflow_id}",
))
# 階段資訊
if stage := state.get("stage"):
console.print(f"\n[cyan]Stage {stage['index']}/{stage['total']}:[/cyan] {stage['name']}")
console.print(f"[dim]{stage['description']}[/dim]")
# Agent 狀態
if agents := state.get("agents"):
console.print("\n[bold]Agents:[/bold]")
table = Table(show_header=True, show_lines=False)
table.add_column("ID", style="cyan")
table.add_column("Name")
table.add_column("Status")
table.add_column("Task", max_width=40)
status_icons = {
"pending": "⏳",
"running": "🔄",
"completed": "✅",
"failed": "❌",
}
for agent in agents:
icon = status_icons.get(agent.get("status", "pending"), "?")
table.add_row(
agent.get("id", ""),
agent.get("name", ""),
f"{icon} {agent.get('status', '')}",
agent.get("task", "")[:40] if agent.get("task") else "",
)
console.print(table)
# 進度
progress = state.get("progress", {})
completed = progress.get("agents_completed", 0)
total = progress.get("agents_total", 0)
if total > 0:
console.print(f"\n[bold]Progress:[/bold] {completed}/{total} agents completed")
# ─────────────────────────────────────────────────────────────────────────────
# status 命令
# ─────────────────────────────────────────────────────────────────────────────
@app.command()
def status(
workflow_id: Optional[str] = typer.Argument(None, help="工作流 ID"),
):
"""查看工作流狀態"""
memory = get_memory()
if workflow_id:
# 查找特定工作流
workflows = memory.list_workflows(limit=100)
workflow = next(
(w for w in workflows if workflow_id in w.get("id", "")),
None,
)
if not workflow:
console.print(f"[red]找不到工作流: {workflow_id}[/red]")
raise typer.Exit(1)
else:
workflow = memory.get_active_workflow()
if not workflow:
console.print("[yellow]沒有活動的工作流[/yellow]")
return
# 顯示詳細狀態
_show_workflow_status(workflow)
def _show_workflow_status(workflow: dict):
"""顯示工作流詳細狀態"""
console.print(Panel(
f"[bold]{workflow.get('topic', 'Unknown')}[/bold]\n"
f"狀態: {workflow.get('status', 'unknown')}\n"
f"階段: {workflow.get('current_stage', 'N/A')}\n"
f"品質: {workflow.get('quality_score', 'N/A')}",
title=f"Workflow: {workflow.get('id', 'unknown')}",
))
# 階段狀態
if stages := workflow.get("stages"):
console.print("\n[bold]Stages:[/bold]")
status_icons = {
"pending": "⏳",
"running": "🔄",
"completed": "✅",
"failed": "❌",
}
for stage_id in [s.value for s in STAGE_ORDER]:
stage_info = stages.get(stage_id.lower(), {})
status = stage_info.get("status", "pending")
icon = status_icons.get(status, "?")
console.print(f" {icon} {stage_id}")
# ─────────────────────────────────────────────────────────────────────────────
# list 命令
# ─────────────────────────────────────────────────────────────────────────────
@app.command(name="list")
def list_workflows(
limit: int = typer.Option(10, "--limit", "-n", help="顯示數量"),
):
"""列出工作流"""
memory = get_memory()
workflows = memory.list_workflows(limit=limit)
if not workflows:
console.print("[yellow]沒有找到工作流[/yellow]")
return
table = Table(show_header=True)
table.add_column("ID", style="cyan")
table.add_column("Topic")
table.add_column("Status")
table.add_column("Quality")
table.add_column("Date")
status_icons = {
"initialized": "⏳",
"running": "🔄",
"completed": "✅",
"failed": "❌",
"human_intervention": "👤",
}
for wf in workflows:
status = wf.get("status", "unknown")
icon = status_icons.get(status, "?")
quality = wf.get("quality_score")
quality_str = f"{quality:.1f}" if quality else "-"
table.add_row(
wf.get("id", "")[:25],
(wf.get("topic", "") or "")[:30],
f"{icon} {status}",
quality_str,
str(wf.get("date", ""))[:10],
)
console.print(table)
# ─────────────────────────────────────────────────────────────────────────────
# logs 命令
# ─────────────────────────────────────────────────────────────────────────────
@app.command()
def logs(
workflow_id: str = typer.Argument(..., help="工作流 ID"),
action: Optional[str] = typer.Option(None, "--action", "-a", help="篩選 action 類型"),
level: Optional[str] = typer.Option(None, "--level", "-l", help="篩選 level"),
limit: int = typer.Option(50, "--limit", "-n", help="顯示數量"),
):
"""查看工作流日誌"""
from .io.logging import get_logger
logger = get_logger(workflow_id)
records = logger.get_logs(limit=limit)
if not records:
console.print("[yellow]沒有找到日誌[/yellow]")
return
# 篩選
if action:
records = [r for r in records if r.get("action") == action]
if level:
records = [r for r in records if r.get("level") == level]
# 顯示
level_colors = {
"info": "white",
"warning": "yellow",
"error": "red",
}
for record in records[-limit:]:
timestamp = record.get("timestamp", "")[:19]
lvl = record.get("level", "info")
act = record.get("action", "")
details = record.get("details", {})
color = level_colors.get(lvl, "white")
# 格式化 details
details_str = ""
if details:
key_values = [f"{k}={v}" for k, v in list(details.items())[:3]]
details_str = " | " + ", ".join(key_values)
console.print(f"[dim]{timestamp}[/dim] [{color}]{lvl:7}[/{color}] {act}{details_str}")
# ─────────────────────────────────────────────────────────────────────────────
# validate 命令
# ─────────────────────────────────────────────────────────────────────────────
@app.command()
def validate(
workflow_id: str = typer.Argument(..., help="工作流 ID"),
stage: Optional[str] = typer.Option(None, "--stage", "-s", help="驗證特定階段"),
):
"""驗證工作流"""
memory = get_memory()
workflow_dir = memory.get_workflow_dir(workflow_id)
if not workflow_dir:
console.print(f"[red]找不到工作流: {workflow_id}[/red]")
raise typer.Exit(1)
console.print(f"[bold]驗證工作流: {workflow_id}[/bold]\n")
# 檢查目錄結構
console.print("[cyan]目錄結構:[/cyan]")
required_dirs = ["stages", "agents", "logs"]
for d in required_dirs:
exists = (workflow_dir / d).exists()
icon = "✅" if exists else "❌"
console.print(f" {icon} {d}/")
# 檢查必要檔案
console.print("\n[cyan]必要檔案:[/cyan]")
required_files = ["meta.yaml", "current.json", "logs/actions.jsonl"]
for f in required_files:
exists = (workflow_dir / f).exists()
icon = "✅" if exists else "❌"
console.print(f" {icon} {f}")
# 驗證 meta.yaml
meta = memory.read_yaml(workflow_dir / "meta.yaml")
if meta:
console.print("\n[cyan]Meta 驗證:[/cyan]")
required_meta = ["id", "topic", "status"]
for field in required_meta:
exists = field in meta
icon = "✅" if exists else "❌"
console.print(f" {icon} {field}")
console.print("\n[green]驗證完成[/green]")
# ─────────────────────────────────────────────────────────────────────────────
# resume 命令
# ─────────────────────────────────────────────────────────────────────────────
@app.command()
def resume(
workflow_id: str = typer.Argument(..., help="工作流 ID"),
from_stage: Optional[str] = typer.Option(
None,
"--from",
"-f",
help="從指定階段恢復",
),
):
"""恢復中斷的工作流"""
console.print(f"[bold]恢復工作流: {workflow_id}[/bold]")
# TODO: 實作恢復邏輯
console.print("[yellow]功能開發中...[/yellow]")
if __name__ == "__main__":
app()
"""
Orchestrator 模組 - 工作流編排核心
包含:
- workflow.py: 工作流狀態機
- stage_runner.py: 階段執行器
- agent_caller.py: Agent 調用
- rollback.py: 智慧回退
- errors.py: 錯誤定義
"""
from .errors import (
MAWError,
WorkflowError,
StageError,
AgentError,
ValidationError,
RollbackError,
)
__all__ = [
"MAWError",
"WorkflowError",
"StageError",
"AgentError",
"ValidationError",
"RollbackError",
]
"""
Agent 調用模組 - 限制工具、強制 JSON 返回
核心原則:
- Agent 只負責思考和分析
- 限制工具:只允許 Read/Glob/Grep/Bash/WebFetch
- 禁止:Write/Task(確定性操作由 CLI 執行)
- 強制 JSON 輸出
"""
import json
import re
import subprocess
import time
from typing import Any, Dict, List, Optional
from ..config.models import AgentResponse
from .errors import AgentError
# 允許的工具列表
ALLOWED_TOOLS = [
"Read",
"Glob",
"Grep",
"Bash",
"WebFetch",
"WebSearch",
]
# 禁止的工具
FORBIDDEN_TOOLS = [
"Write",
"Edit",
"Task",
"NotebookEdit",
]
class AgentCaller:
"""Agent 調用器"""
def __init__(
self,
default_model: str = "sonnet",
timeout: int = 300,
max_retries: int = 3,
):
"""
初始化 Agent 調用器
Args:
default_model: 預設模型
timeout: 超時時間(秒)
max_retries: 最大重試次數
"""
self.default_model = default_model
self.timeout = timeout
self.max_retries = max_retries
def call(
self,
prompt: str,
model: Optional[str] = None,
output_format: str = "json",
context: Optional[Dict] = None,
) -> AgentResponse:
"""
調用 Agent 並獲取 JSON 回應
Args:
prompt: Prompt 內容
model: 使用的模型(預設使用 default_model)
output_format: 輸出格式(json/text)
context: 額外上下文
Returns:
AgentResponse 物件
"""
model = model or self.default_model
start_time = time.time()
# 構建完整 prompt
full_prompt = self._build_prompt(prompt, output_format, context)
# 重試邏輯
last_error = None
for attempt in range(1, self.max_retries + 1):
try:
result = self._execute_claude(full_prompt, model)
duration = time.time() - start_time
# 解析回應
if output_format == "json":
content = self._parse_json_response(result)
return AgentResponse(
success=True,
content=content,
raw_output=result,
duration_seconds=duration,
)
else:
return AgentResponse(
success=True,
content={"text": result},
raw_output=result,
duration_seconds=duration,
)
except AgentError as e:
last_error = e
if not e.retryable or attempt >= self.max_retries:
break
time.sleep(2 ** attempt) # 指數退避
except Exception as e:
last_error = AgentError(
f"Unexpected error: {str(e)}",
retryable=False,
)
break
# 所有重試失敗
duration = time.time() - start_time
return AgentResponse(
success=False,
error=str(last_error) if last_error else "Unknown error",
duration_seconds=duration,
)
def _build_prompt(
self,
prompt: str,
output_format: str,
context: Optional[Dict],
) -> str:
"""構建完整 prompt"""
parts = []
# 系統指令
parts.append(self._get_system_instructions(output_format))
# 上下文
if context:
parts.append(f"\n## Context\n```json\n{json.dumps(context, ensure_ascii=False, indent=2)}\n```")
# 主要任務
parts.append(f"\n## Task\n{prompt}")
# 輸出格式要求
if output_format == "json":
parts.append(self._get_json_output_instructions())
return "\n".join(parts)
def _get_system_instructions(self, output_format: str) -> str:
"""取得系統指令"""
return """# Agent Instructions
You are an analysis agent. Your role is to think, analyze, and provide insights.
## Important Rules
1. You are NOT allowed to write files or create tasks
2. You can only use Read, Glob, Grep, Bash, WebFetch, WebSearch tools
3. Focus on analysis and return structured results
4. Be thorough but concise"""
def _get_json_output_instructions(self) -> str:
"""取得 JSON 輸出指令"""
return """
## Output Format
You MUST return your response as valid JSON.
- Start your final answer with ```json
- End with ```
- Ensure the JSON is valid and parseable
- Do not include any text outside the JSON block in your final response"""
def _execute_claude(self, prompt: str, model: str) -> str:
"""
執行 Claude CLI
Args:
prompt: 完整 prompt
model: 模型名稱
Returns:
CLI 輸出
"""
# 構建命令
cmd = [
"claude",
"--print",
"--model", model,
"--allowedTools", ",".join(ALLOWED_TOOLS),
]
try:
result = subprocess.run(
cmd,
input=prompt,
capture_output=True,
text=True,
timeout=self.timeout,
)
if result.returncode != 0:
error_msg = result.stderr or "Unknown error"
raise AgentError(
f"Claude CLI failed: {error_msg}",
retryable=True,
)
return result.stdout
except subprocess.TimeoutExpired:
raise AgentError(
f"Agent timed out after {self.timeout}s",
retryable=True,
)
except FileNotFoundError:
raise AgentError(
"Claude CLI not found. Please install claude-code.",
retryable=False,
)
def _parse_json_response(self, output: str) -> Dict[str, Any]:
"""
解析 JSON 回應
Args:
output: CLI 輸出
Returns:
解析後的 JSON 物件
"""
# 嘗試找到 JSON 區塊
json_patterns = [
r"```json\s*([\s\S]*?)\s*```", # ```json ... ```
r"```\s*([\s\S]*?)\s*```", # ``` ... ```
r"\{[\s\S]*\}", # 直接 JSON
]
for pattern in json_patterns:
matches = re.findall(pattern, output, re.MULTILINE)
if matches:
# 取最後一個匹配(通常是最終答案)
json_str = matches[-1] if isinstance(matches[-1], str) else matches[-1]
try:
return json.loads(json_str)
except json.JSONDecodeError:
continue
# 無法解析 JSON
raise AgentError(
"Failed to parse JSON from agent response",
retryable=False,
details={"raw_output": output[:500]},
)
class ParallelAgentCaller:
"""並行 Agent 調用器"""
def __init__(self, caller: Optional[AgentCaller] = None):
"""
初始化並行調用器
Args:
caller: 基礎調用器(可選)
"""
self.caller = caller or AgentCaller()
def call_parallel(
self,
agents: List[Dict],
context: Optional[Dict] = None,
) -> Dict[str, AgentResponse]:
"""
並行調用多個 Agent
Args:
agents: Agent 配置列表,每個包含 id, prompt, model (可選)
context: 共享上下文
Returns:
以 agent_id 為 key 的結果字典
"""
import concurrent.futures
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=len(agents)) as executor:
# 提交所有任務
future_to_agent = {}
for agent in agents:
future = executor.submit(
self.caller.call,
prompt=agent["prompt"],
model=agent.get("model"),
context=context,
)
future_to_agent[future] = agent["id"]
# 收集結果
for future in concurrent.futures.as_completed(future_to_agent):
agent_id = future_to_agent[future]
try:
results[agent_id] = future.result()
except Exception as e:
results[agent_id] = AgentResponse(
success=False,
error=str(e),
)
return results
# 全域實例
_caller: Optional[AgentCaller] = None
_parallel_caller: Optional[ParallelAgentCaller] = None
def get_caller(
model: str = "sonnet",
timeout: int = 300,
) -> AgentCaller:
"""取得 Agent 調用器"""
global _caller
if _caller is None:
_caller = AgentCaller(default_model=model, timeout=timeout)
return _caller
def get_parallel_caller() -> ParallelAgentCaller:
"""取得並行調用器"""
global _parallel_caller
if _parallel_caller is None:
_parallel_caller = ParallelAgentCaller(get_caller())
return _parallel_caller
"""
錯誤定義模組 - 所有自定義例外
"""
from typing import Optional
class MAWError(Exception):
"""Multi-Agent Workflow 基礎錯誤"""
def __init__(self, message: str, details: Optional[dict] = None):
super().__init__(message)
self.message = message
self.details = details or {}
def __str__(self) -> str:
if self.details:
return f"{self.message} | {self.details}"
return self.message
class WorkflowError(MAWError):
"""工作流層級錯誤"""
pass
class StageError(MAWError):
"""階段執行錯誤"""
def __init__(
self,
message: str,
stage: str,
details: Optional[dict] = None,
):
super().__init__(message, details)
self.stage = stage
class AgentError(MAWError):
"""Agent 調用錯誤"""
def __init__(
self,
message: str,
agent_id: Optional[str] = None,
retryable: bool = True,
details: Optional[dict] = None,
):
super().__init__(message, details)
self.agent_id = agent_id
self.retryable = retryable
class ValidationError(MAWError):
"""驗證錯誤"""
def __init__(
self,
message: str,
validator: str,
errors: Optional[list] = None,
details: Optional[dict] = None,
):
super().__init__(message, details)
self.validator = validator
self.errors = errors or []
class RollbackError(MAWError):
"""回退錯誤"""
def __init__(
self,
message: str,
from_stage: str,
to_stage: str,
iteration: int,
details: Optional[dict] = None,
):
super().__init__(message, details)
self.from_stage = from_stage
self.to_stage = to_stage
self.iteration = iteration
class GateFailedError(MAWError):
"""品質閘門失敗"""
def __init__(
self,
message: str,
stage: str,
score: float,
threshold: float,
failed_criteria: Optional[list] = None,
details: Optional[dict] = None,
):
super().__init__(message, details)
self.stage = stage
self.score = score
self.threshold = threshold
self.failed_criteria = failed_criteria or []
class HumanInterventionRequired(MAWError):
"""需要人工介入"""
def __init__(
self,
message: str,
reason: str,
context: Optional[dict] = None,
):
super().__init__(message, context)
self.reason = reason
self.context = context or {}
"""
智慧回退模組 - 根據迭代次數和錯誤類型決定回退目標
回退策略:
| 迭代 | 回退目標 | 原因 |
|------|----------|------|
| 1-2 | IMPLEMENT | 可能是實作問題 |
| 3 | TASKS | 可能是任務分解問題 |
| 4 | PLAN | 可能是設計問題 |
| 5+ | HUMAN | 超過自動修復能力 |
循環偵測:
- 相同錯誤兩次 → 升級回退層級
- 階段間振盪 → 暫停分析根因
- 總迭代 > 10 → 強制停止
"""
from collections import defaultdict
from typing import Dict, List, Optional, Tuple
from ..config.models import GateCheckResult, RollbackDecision, StageID
# 回退目標映射
ROLLBACK_TARGETS: Dict[int, StageID] = {
1: StageID.IMPLEMENT,
2: StageID.IMPLEMENT,
3: StageID.TASKS,
4: StageID.PLAN,
}
# 最大迭代次數
MAX_ITERATIONS = 10
# 相同錯誤閾值
SAME_ERROR_THRESHOLD = 2
class RollbackManager:
"""智慧回退管理器"""
def __init__(self):
"""初始化回退管理器"""
# 錯誤歷史:記錄每個階段的失敗原因
self.error_history: Dict[str, List[str]] = defaultdict(list)
# 階段轉換歷史:用於偵測振盪
self.stage_transitions: List[Tuple[StageID, StageID]] = []
def decide(
self,
current_stage: StageID,
gate_result: GateCheckResult,
iteration: int,
) -> RollbackDecision:
"""
決定回退策略
Args:
current_stage: 當前階段
gate_result: 品質閘門檢查結果
iteration: 當前迭代次數
Returns:
RollbackDecision 物件
"""
# 記錄錯誤
error_key = self._create_error_key(current_stage, gate_result)
self.error_history[current_stage.value].append(error_key)
# 檢查是否超過最大迭代次數
if iteration >= MAX_ITERATIONS:
return RollbackDecision(
should_rollback=False,
from_stage=current_stage,
to_stage=current_stage,
iteration=iteration,
reason=f"超過最大迭代次數 ({MAX_ITERATIONS})",
require_human=True,
)
# 檢查循環偵測
cycle_detected, cycle_reason = self._detect_cycle(
current_stage,
error_key,
iteration,
)
if cycle_detected:
return RollbackDecision(
should_rollback=False,
from_stage=current_stage,
to_stage=current_stage,
iteration=iteration,
reason=cycle_reason,
require_human=True,
)
# 根據迭代次數決定回退目標
if iteration >= 5:
return RollbackDecision(
should_rollback=False,
from_stage=current_stage,
to_stage=current_stage,
iteration=iteration,
reason="迭代次數過多,需要人工介入",
require_human=True,
)
# 取得回退目標
target_stage = ROLLBACK_TARGETS.get(iteration, StageID.PLAN)
# 確保不會回退到當前階段之後
from ..config.stages import STAGE_ORDER
current_idx = STAGE_ORDER.index(current_stage)
target_idx = STAGE_ORDER.index(target_stage)
if target_idx >= current_idx:
# 回退到前一個階段
if current_idx > 0:
target_stage = STAGE_ORDER[current_idx - 1]
else:
# 已經是第一階段,需要人工介入
return RollbackDecision(
should_rollback=False,
from_stage=current_stage,
to_stage=current_stage,
iteration=iteration,
reason="已經是第一階段,無法回退",
require_human=True,
)
# 記錄階段轉換
self.stage_transitions.append((current_stage, target_stage))
return RollbackDecision(
should_rollback=True,
from_stage=current_stage,
to_stage=target_stage,
iteration=iteration,
reason=self._get_rollback_reason(iteration, gate_result),
require_human=False,
)
def _create_error_key(
self,
stage: StageID,
gate_result: GateCheckResult,
) -> str:
"""建立錯誤識別鍵"""
failed = sorted(gate_result.failed_criteria)
return f"{stage.value}:{','.join(failed)}"
def _detect_cycle(
self,
current_stage: StageID,
error_key: str,
iteration: int,
) -> Tuple[bool, str]:
"""
偵測循環
Returns:
(是否偵測到循環, 原因)
"""
# 檢查相同錯誤
stage_errors = self.error_history[current_stage.value]
same_error_count = sum(1 for e in stage_errors if e == error_key)
if same_error_count >= SAME_ERROR_THRESHOLD:
return True, f"相同錯誤重複 {same_error_count} 次"
# 檢查階段振盪 (A → B → A)
if len(self.stage_transitions) >= 2:
recent = self.stage_transitions[-2:]
if len(recent) == 2:
(from1, to1), (from2, to2) = recent
if to1 == from2 and to2 == from1:
return True, f"階段振盪: {from1.value} ↔ {to1.value}"
return False, ""
def _get_rollback_reason(
self,
iteration: int,
gate_result: GateCheckResult,
) -> str:
"""取得回退原因描述"""
reasons = {
1: "首次失敗,回退到實作階段重試",
2: "第二次失敗,回退到實作階段進行修復",
3: "多次實作失敗,回退到任務分解階段重新規劃",
4: "任務分解可能有問題,回退到設計階段",
}
base_reason = reasons.get(iteration, "回退重試")
if gate_result.failed_criteria:
failed_str = ", ".join(gate_result.failed_criteria)
return f"{base_reason} (失敗項目: {failed_str})"
return base_reason
def reset(self) -> None:
"""重置回退管理器"""
self.error_history.clear()
self.stage_transitions.clear()
def get_history(self) -> Dict:
"""取得回退歷史"""
return {
"error_history": dict(self.error_history),
"stage_transitions": [
(f.value, t.value) for f, t in self.stage_transitions
],
}
"""Plugin management CLI module.
Provides commands for plugin development, testing, versioning, and release.
"""
from .cache import CacheManager
from .version import VersionManager
from .dev import DevCommands
from .release import ReleaseCommands
__all__ = [
"CacheManager",
"VersionManager",
"DevCommands",
"ReleaseCommands",
]
"""Plugin management exceptions."""
from pathlib import Path
from typing import Optional
class PluginError(Exception):
"""Base exception for plugin operations."""
def __init__(self, message: str, suggestion: Optional[str] = None):
self.message = message
self.suggestion = suggestion
super().__init__(message)
def __str__(self) -> str:
if self.suggestion:
return f"{self.message}\n 💡 Suggestion: {self.suggestion}"
return self.message
class CacheError(PluginError):
"""Cache-related errors."""
pass
class CacheNotFoundError(CacheError):
"""Cache directory not found."""
def __init__(self, cache_path: Path):
super().__init__(
f"Cache directory not found: {cache_path}",
"Run 'plugin dev sync' to create the cache, or install the plugin first."
)
self.cache_path = cache_path
class CacheCorruptedError(CacheError):
"""Cache is corrupted or invalid."""
def __init__(self, cache_path: Path, reason: str):
super().__init__(
f"Cache corrupted at {cache_path}: {reason}",
"Run 'plugin cache clean' and then 'plugin dev sync' to rebuild."
)
self.cache_path = cache_path
self.reason = reason
class SyncError(PluginError):
"""Synchronization errors."""
pass
class SyncFailedError(SyncError):
"""Sync operation failed."""
def __init__(self, source: Path, dest: Path, reason: str):
super().__init__(
f"Sync failed from {source} to {dest}: {reason}",
"Check file permissions and disk space."
)
self.source = source
self.dest = dest
self.reason = reason
class VersionError(PluginError):
"""Version-related errors."""
pass
class InvalidVersionError(VersionError):
"""Invalid version format."""
def __init__(self, version: str):
super().__init__(
f"Invalid version format: {version}",
"Use semantic versioning format: MAJOR.MINOR.PATCH (e.g., 1.2.3)"
)
self.version = version
class VersionConflictError(VersionError):
"""Version conflict detected."""
def __init__(self, files: list[str], versions: list[str]):
super().__init__(
f"Version mismatch across files: {dict(zip(files, versions))}",
"Run 'plugin version sync' to align versions."
)
self.files = files
self.versions = versions
class ReleaseError(PluginError):
"""Release-related errors."""
pass
class DirtyWorkspaceError(ReleaseError):
"""Workspace has uncommitted changes."""
def __init__(self, changed_files: list[str]):
super().__init__(
f"Cannot release with uncommitted changes: {len(changed_files)} files modified",
"Commit or stash your changes first."
)
self.changed_files = changed_files
class ValidationError(ReleaseError):
"""Pre-release validation failed."""
def __init__(self, failures: list[str]):
super().__init__(
f"Validation failed: {', '.join(failures)}",
"Fix the issues and try again."
)
self.failures = failures
"""
Validators 模組 - 驗證器
包含:
- perspective.py: 視角報告驗證
- quality_gate.py: 品質閘門
- dag.py: DAG 驗證
"""
from .dag import DAGValidator, DAGValidationResult, validate_dag, is_dag_valid
from .perspective import PerspectiveValidator, validate_perspective_report
from .quality_gate import QualityGate, check_quality_gate
__all__ = [
"DAGValidator",
"DAGValidationResult",
"validate_dag",
"is_dag_valid",
"PerspectiveValidator",
"validate_perspective_report",
"QualityGate",
"check_quality_gate",
]
"""
Claude Code Hooks for Multi-Agent Workflow
自動處理:
- Action logging (所有工具調用)
- State tracking (Agent 狀態更新)
- Memory commits (CP4 自動 commit)
"""
from .log_action import log_action
from .update_state import update_state
__all__ = ["log_action", "update_state"]
CT Failure Miner
Failure mining converts CT violations and weak conclusions into reusable research material.
Inputs
- Perspective reports
- Cross validation output
- CT compliance report
- Quality gate results
- Experiment plan
Failure Mode Format
failure_mode:
id: FM001
category: evidence | drift | conflict | experiment | output
description: string
trigger: string
observed_in: string
severity: low | medium | high | blocker
prevention: string
regression_case: stringRequired Output
Write failure-modes.md with:
- Recurrent CT violations
- Root causes
- Regression prompts or evaluation cases
- Prevention rules to add to future CT stacks