
Orchestrate
- 1 installs
- 4 repo stars
- Updated May 12, 2026
- miles990/multi-agent-workflow
End-to-end workflow orchestrator that chains research, plan, tasks, implement, review, and verify stages with file-based handoffs and parallel decisions.
About
An orchestrator skill that drives a full multi-agent development workflow from requirement to verification using file-based handoffs. A developer uses it to run the entire research-to-verify pipeline in one command.
- File-based handoff across all workflow stages
- Checks for and prompts /setup-workflow hook configuration
Orchestrate by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,983 of 2,715 Automation & Workflows 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 orchestrateAdd 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
End-to-end workflow orchestrator that chains research, plan, tasks, implement, review, and verify stages with file-based handoffs and parallel decisions.
Files
Multi-Agent Orchestrate v3.4.0
路徑解析(必讀)
此 Skill 使用共用工具。執行時必須先解析資源根目錄,再使用完整路徑。
從 command-message header 取得 Base directory,例如:
Base directory for this skill: /path/to/.../skills/orchestrate支援兩種安裝形態:
- Plugin install:repo-level
shared/位於 plugin root。 npx skills add:generated_shared/、_scripts/位於本 skill 目錄內。
if [ -d "$BASE_DIR/_shared" ]; then
SHARED_DIR="$BASE_DIR/_shared"
SCRIPTS_DIR="$BASE_DIR/_scripts"
else
PLUGIN_ROOT="${BASE_DIR%/skills/orchestrate}"
SHARED_DIR="$PLUGIN_ROOT/shared"
SCRIPTS_DIR="$PLUGIN_ROOT/scripts"
fi
"$SHARED_DIR/tools/workflow-init.sh" init "$WORKFLOW_ID" orchestrate "需求摘要"重要:不要使用 ./shared/tools/...,因為工作目錄是用戶專案,不是 skill/plugin 目錄。
---
前置檢查(重要)
在開始工作流之前,檢查專案是否已配置規範執行機制:
# 檢查 settings.local.json 是否包含必要的 hooks
cat .claude/settings.local.json 2>/dev/null | grep -q "PostToolUse" && echo "✓ Hooks configured" || echo "✗ Hooks not configured"如果 Hooks 未配置,顯示以下提示:
⚠️ 專案尚未配置 workflow 規範執行機制。
建議執行 `/setup-workflow` 啟用以下功能:
• Task 完成後自動 commit(保存進度)
• 自動運行測試驗證
• Memory 變更追蹤
執行 `/setup-workflow` 一鍵完成配置。如果用戶選擇繼續:可以繼續執行,但不會有自動 commit 和驗證功能。
---
需求輸入 → 6 階段串聯 → 品質閘門 → 智慧回退 → 完成交付
使用方式
/orchestrate [需求描述]
/orchestrate 建立用戶認證系統,支援 JWT 和 OAuth2Flags:
--profile <mode>- 執行模式:default(預設)|express(快速)|quality(最高品質)--start-from STAGE- 從指定階段開始--skip STAGE- 跳過指定階段--quick- 快速模式(等同--profile express)--deep- 深度模式(等同--profile quality)
工作流階段
RESEARCH → PLAN → TASKS → IMPLEMENT → REVIEW → VERIFY
↑__________↓
智慧回退機制| 階段 | 輸入 | 輸出 | 閘門分數 |
|---|---|---|---|
| RESEARCH | 需求 | synthesis.md | ≥70 |
| PLAN | 研究報告 | implementation-plan.md | ≥75 |
| TASKS | 實作計劃 | tasks.yaml | ≥80 |
| IMPLEMENT | 任務清單 | 程式碼 | ≥80 |
| REVIEW | 程式碼 | review-summary.md | ≥75 |
| VERIFY | 審查報告 | 驗證結果 | ≥85 |
執行流程
Phase 0: 初始化工作流
├── 生成 workflow-id
├── **【必要】執行 workflow-init.sh 初始化通訊環境**
│ └── Bash: "$SHARED_DIR/tools/workflow-init.sh" init <workflow-id> orchestrate "<需求摘要>"
│ └── 這會創建 .claude/workflow/current.json(Hooks 依賴此檔案)
├── 載入執行模式配置
│ └── 讀取 $SHARED_DIR/config/execution-profiles.yaml
│ └── 套用視角數和模型配置
├── 建立報告目錄
└── 記錄開始時間
↓
For each stage in [RESEARCH, PLAN, TASKS, IMPLEMENT, REVIEW, VERIFY]:
↓
Phase 1: 執行階段
├── 呼叫對應 skill
└── 等待完成
↓
Phase 2: 早期終止檢查
├── 滿足條件?→ 可跳過後續步驟
└── 不滿足 → 繼續
↓
Phase 3: 品質閘門
├── 通過 → 下一階段
└── 失敗 → 智慧回退
↓
End for
↓
Phase 4: 完成
├── 生成報告
├── 更新 Memory
└── 驗證變更已 commit(見下方 Fallback)Phase 4 Fallback:手動 Commit
如果自動 checkpoint commit 沒有觸發(例如 hooks 未配置),手動執行:
# 檢查是否有未 commit 的變更
git status
# 如果有變更,手動 commit
git add -A
git commit -m "chore(workflow): complete {workflow-id}"自動 Commit 觸發時機:
- 寫入
synthesis.md→ RESEARCH 完成 - 寫入
implementation-plan.md→ PLAN 完成 - 寫入
tasks.yaml→ TASKS 完成 - 寫入
summary.md→ IMPLEMENT 完成 - 寫入
review-summary.md→ REVIEW 完成 - 寫入
verify-summary.md→ VERIFY 完成
智慧回退機制
根據迭代次數決定回退目標:
| 迭代 | 回退目標 | 原因 |
|---|---|---|
| 1-2 | IMPLEMENT | 可能是實作問題 |
| 3 | TASKS | 可能是任務分解問題 |
| 4 | PLAN | 可能是設計問題 |
| 5+ | HUMAN | 超過自動修復能力 |
循環偵測:
- 相同錯誤兩次 → 升級回退層級
- 階段間振盪 → 暫停分析根因
- 總迭代 > 10 → 強制停止
→ 配置:shared/quality/rollback-strategy.yaml
部分完成處理(Partial Completion)
當工作流中斷或某階段失敗時,自動保存進度以便恢復。
自動保存進度
每個階段完成後立即保存進度到 .claude/workflow/{id}/recovery/progress.yaml:
workflow_id: "orchestrate_20260206_123456_abc123"
status: "interrupted" # completed | interrupted | failed
current_stage: "IMPLEMENT"
completed_stages:
- stage: RESEARCH
output: ".claude/memory/research/{topic-id}/synthesis.md"
score: 82
timestamp: "2026-02-06T12:35:00Z"
- stage: PLAN
output: ".claude/memory/plans/{feature-id}/implementation-plan.md"
score: 78
timestamp: "2026-02-06T12:40:00Z"
- stage: TASKS
output: ".claude/memory/tasks/{plan-id}/tasks.yaml"
score: 85
timestamp: "2026-02-06T12:45:00Z"
failed_stage:
stage: IMPLEMENT
error: "Tests failed after 3 retries"
last_checkpoint: "task-3-of-8"
timestamp: "2026-02-06T13:00:00Z"恢復執行
/orchestrate --resume {workflow-id}
# 或
/orchestrate --resume # 自動找到最近中斷的工作流恢復流程: 1. 讀取 progress.yaml 確認中斷點 2. 驗證已完成階段的輸出仍然有效(檔案存在 + git status 乾淨) 3. 從中斷的階段/任務繼續執行 4. 如果已完成階段的輸出被破壞,回退到該階段重新執行
部分完成的通知
中斷時輸出:
⚠️ 工作流部分完成 (3/6 階段)
✅ RESEARCH → PLAN → TASKS
❌ IMPLEMENT(失敗:Tests failed after 3 retries)
⏸️ REVIEW → VERIFY(未執行)
進度已保存至:.claude/workflow/{id}/recovery/progress.yaml
恢復命令:/orchestrate --resume {id}與智慧回退的區別
| 情境 | 機制 | 說明 |
|---|---|---|
| 品質閘門失敗 | 智慧回退 | 自動回退到適當階段重試(最多 5 次) |
| 回退次數超限 | 部分完成 | 保存進度,等待人工介入後 --resume |
| Context Limit | 部分完成 | 保存進度,新 session 中 --resume |
| 外部錯誤(網路/權限) | 部分完成 | 保存進度,修復問題後 --resume |
| Session 崩潰 | 部分完成 | 依賴 git checkpoint,--resume 從最後 commit 恢復 |
早期終止
| 階段 | 條件 | 動作 |
|---|---|---|
| RESEARCH | consensus ≥ 0.9 | 跳過衝突解決 |
| PLAN | risk < 0.2 | 快速模式 |
| REVIEW | 無 BLOCKER/HIGH | 直接通過 |
| VERIFY | pass_rate ≥ 0.98 | 可發布 |
→ 配置:shared/config/early-termination.yaml
報告生成
完成後自動生成:
dashboard.md- 總覽timeline.md- 時間線quality-report.md- 品質報告decisions.md- 決策記錄
→ 工具:shared/tools/generate-report.sh
輸出結構
.claude/memory/workflows/[workflow-id]/
├── dashboard.md # 總覽報告
├── timeline.md # 時間線
├── decisions.md # 決策記錄
├── quality-report.md # 品質報告
├── stages/ # 各階段報告
├── agents/ # Agent 記錄
└── exports/ # 匯出格式共用模組
| 模組 | 用途 |
|---|---|
| quality/gates.yaml | 品質閘門 |
| quality/rollback-strategy.yaml | 智慧回退 |
| config/early-termination.yaml | 早期終止 |
| config/execution-profiles.yaml | 執行模式 |
| config/context-freshness.yaml | 上下文新鮮 |
| tools/generate-report.sh | 報告生成 |
| tools/workflow-init.sh | 工作流初始化 |
【重要】初始化步驟
在執行任何階段之前,必須先初始化工作流環境:
# 0. 從 command-message header 取得 Base directory,計算 Plugin Root
# 例如:Base directory: /path/.../2.4.2/skills/orchestrate
# SHARED_DIR 是 repo-level shared 或本 skill 內 generated _shared
# 1. 生成 workflow ID(格式:orchestrate_YYYYMMDD_HHMMSS_xxxx)
WORKFLOW_ID="orchestrate_$(date +%Y%m%d_%H%M%S)_$(openssl rand -hex 4)"
# 2. 執行初始化(創建 current.json,讓 Hooks 能記錄活動)
# 使用完整路徑,不要用 ./shared/...
"$SHARED_DIR/tools/workflow-init.sh" init "$WORKFLOW_ID" orchestrate "需求摘要"為什麼這很重要?
- Hooks(log-tool-pre.sh、log-tool-post.sh、log-agent-lifecycle.sh)依賴
.claude/workflow/current.json - 如果沒有這個檔案,所有 Agent 活動都不會被記錄
- 這會導致
/status和 statusline 無法顯示正確的工作流狀態
File-Based Handoff Protocol(v3.3 核心機制)
解決 Context Limit 的根本方案:使用檔案系統作為 Agent 間的「外部記憶體」。
→ 完整說明:shared/coordination/file-based-handoff.md
為什麼會 Context Limit?
傳統方式:
Agent A 完成 → 15K tokens 回傳 Orchestrator
Agent B 完成 → 15K tokens 回傳
Agent C 完成 → 15K tokens 回傳
Agent D 完成 → 15K tokens 回傳
─────────────────────────────────
總計:60K+ tokens → 爆炸 💥新方式
File-Based Handoff:
Agent A 完成 → 寫入檔案 → 回傳 "完成,見 path/a.md" (~100 tokens)
Agent B 完成 → 寫入檔案 → 回傳 "完成,見 path/b.md" (~100 tokens)
...
─────────────────────────────────
Orchestrator 只累積:~400 tokens ✅
完整結果在:檔案系統 + Git執行規則
1. 大型任務使用背景執行:
Task({
description: "複雜任務",
prompt: "...結果寫入 {path},只回覆確認",
run_in_background: true // 關鍵!
})2. Agent 輸出到檔案:
- 完整報告 →
.claude/memory/{type}/{id}/result.md - 回傳給 Orchestrator → 只說「完成,結果在 {path}」
3. Git Checkpoint:
- 每批任務完成 →
git commit - 即使 session 崩潰,結果保留
4. 下一階段讀取檔案:
- Orchestrator 告訴下一個 Agent 檔案路徑
- Agent 自己用 Read 讀取
Context 使用量對比
| 方式 | 4 個大型 Agent | Orchestrator Context |
|---|---|---|
| 傳統 | 全部回傳 | ~60K tokens(危險) |
| File-Based | 只回傳路徑 | ~10K tokens(安全) |
智能並行決策(v3.2 新增)
當用戶要求同時執行多個任務時,Orchestrator 會智能決定執行策略。
→ 配置:shared/config/parallel-execution.yaml
並行度控制
| 任務複雜度 | 最大並行數 | 判斷依據 |
|---|---|---|
| 簡單 | 4 | 單一模組、< 3 檔案、測試已存在 |
| 中等 | 2 | 跨模組、3-10 檔案、需新測試 |
| 複雜 | 1 | 架構變更、> 10 檔案、多階段 |
決策流程
用戶請求多任務
↓
┌─────────────────────────────────────┐
│ 1. 分析每個任務的複雜度 │
│ 2. 偵測任務間的依賴關係 │
│ 3. 估算 context 消耗 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 決策: │
│ - 無依賴 + 簡單 → 全部並行 │
│ - 有依賴 → 拓撲排序後分批 │
│ - 複雜任務 → 順序執行 │
│ - context 緊張 → 降低並行度 │
└─────────────────────────────────────┘
↓
執行並監控依賴偵測
自動偵測以下依賴:
- 檔案重疊:修改相同檔案的任務不能並行
- 模組依賴:A 模組 import B 模組,B 要先完成
- 測試依賴:測試依賴實作,實作要先完成
範例
輸入:同時執行 4 個 Phase 1 任務
分析:
- 複雜度:全部是複雜任務
- 依賴:無直接依賴
- 估算 context:高
決策:
批次 1: [智能待辦 P1, 動態 Skill P1] → 並行
↓ 完成後 /compact
批次 2: [記憶演化 P1, 分布式記憶 P1] → 並行原因:每批 2 個任務,避免 context 爆炸。
Context Limit 處理(v3.2 新增)
→ 指南:shared/coordination/context-limit-handler.md
預防機制
1. 監控閾值:
- < 50%:正常並行
- 50-70%:減少到 2 並行
- 70-85%:順序執行
- \> 85%:暫停並壓縮
2. 即時壓縮:
- Agent 完成後只保留摘要在 context
- 完整輸出保存到檔案
3. 背景執行:
- 大型任務使用
run_in_background: true - 不佔用 orchestrator 的 context
發生時的處理
當看到 "Context limit reached":
┌─────────────────────────────────────┐
│ 1. 記錄哪些 Agent 已完成 │
│ 2. 記錄哪些 Agent 還在運行 │
│ 3. 開新 session │
│ 4. 執行完成狀態檢查: │
│ - git status │
│ - pnpm typecheck │
│ - pnpm test <paths> │
│ 5. 從中斷點繼續,降低並行度 │
└─────────────────────────────────────┘進度保存
自動保存到 .claude/workflow/{id}/recovery/progress.yaml:
tasks:
- id: "task-1"
status: "completed"
commit: "abc123"
- id: "task-2"
status: "in_progress"
last_checkpoint: "80%"恢復指令模板
我需要繼續之前因 context limit 中斷的工作。
**已完成**:
- [x] 任務 A(已 commit)
- [x] 任務 B(未 commit 但完整)
**未完成**:
- [ ] 任務 C(進度 80%)
請:
1. 先 commit 任務 B
2. 驗證並繼續任務 C
3. **一個一個執行,不要並行**新增 Flags(v3.2)
--max-parallel <n>- 限制最大並行數(預設:自動決定)--sequential- 強制順序執行所有任務--save-progress- 每個任務完成後保存進度檔--resume <progress-file>- 從進度檔恢復執行
"""
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 {}
"""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",
]
"""
IMPLEMENT 階段 Prompt 模板
"""
from typing import Any, Dict
from ..config.models import PerspectiveConfig
from .base import (
build_system_context,
build_perspective_header,
build_json_output_format,
build_previous_outputs,
PERSPECTIVE_REPORT_SCHEMA_DESC,
)
def get_implement_perspective_prompt(
perspective: PerspectiveConfig,
context: Dict[str, Any],
) -> str:
"""生成實作階段的視角 Prompt"""
topic = context.get("topic", "")
parts = []
parts.append(build_system_context(context))
parts.append(build_perspective_header(perspective))
parts.append(build_previous_outputs(context))
parts.append(f"""
## Task
Implement the planned tasks for:
**{topic}**
As a **{perspective.name}**, guide the implementation from your perspective.
### Implementation Guidelines
1. **TDD First**: Write tests before implementation
2. **Small Commits**: Make atomic, focused changes
3. **Quality Focus**: Follow coding standards and best practices
4. **Continuous Review**: Self-review before moving forward
5. **Documentation**: Update docs as you go
### Focus On
{chr(10).join(f"- {area}" for area in perspective.focus_areas)}
""")
parts.append(build_json_output_format(PERSPECTIVE_REPORT_SCHEMA_DESC))
return "\n".join(parts)
def get_implement_synthesis_prompt(
perspective_contents: Dict[str, Dict],
context: Dict[str, Any],
) -> str:
"""生成實作階段的綜合報告 Prompt"""
from .base import SYNTHESIS_REPORT_SCHEMA_DESC
topic = context.get("topic", "")
parts = []
parts.append(build_system_context(context))
parts.append(f"""
# Task: Summarize Implementation Progress
Synthesize implementation feedback for:
**{topic}**
## Implementation Perspectives
""")
for pid, content in perspective_contents.items():
name = content.get("perspective_name", pid)
summary = content.get("summary", "")
parts.append(f"- **{name}**: {summary}")
parts.append("""
## Summary Requirements
1. **Task Completion**: Report on completed vs remaining tasks
2. **Test Coverage**: Summarize test results and coverage
3. **Issues Found**: List any issues or blockers
4. **Quality Assessment**: Overall code quality assessment
5. **Next Steps**: What needs to happen next
""")
parts.append(build_json_output_format(SYNTHESIS_REPORT_SCHEMA_DESC))
return "\n".join(parts)