# 三国团队工具链与开发流程设计

> **状态**: v3.1 — P3 端到端验证通过 + 调研结论写入 + Review API 枚举值修正
> **作者**: 庞统（副军师）🐦
> **评审**: 司马懿（仲达）🗡️
> **日期**: 2026-06-06
> **v0.4 变更**: 主公 6 条反馈（需求review前置、E2E用户触发、测试分工、审查默认司马懿、项目维度组织、分支保护说明）
> **v0.5 变更**: E2E 改为自动跑（CI隔离环境不污染生产）、三环境架构、schema变更检查确认、分支保护确认开启
> **v0.6 变更**: Agent spawn 走生产 openclaw（不隔离）、程序逻辑走 CI 隔离环境、路径硬编码修复清单
> **定位**: 普适工具链和开发流程，适用于三国团队所有项目，与 moziplus 2.0 解耦

---

## §1. 设计原则

| # | 原则 | 说明 |
|---|------|------|
| 1 | **声明式 + 代码执行分离** | 规则声明在配置中，执行由框架代码保证，不依赖 Agent 自觉（来源: moziplus-quality-governance） |
| 2 | **能自动就自动** | CI/CD/部署能自动触发就不手动，人只做决策和审查 |
| 3 | **Skill 固化流程** | 所有流程通过 Skill（提示词）固化，不靠口头约定 |
| 4 | **分级治理** | 按风险级别选择流程严格度，不搞一刀切（来源: quality-gate-patterns） |
| 5 | **验证才算完** | Agent 说"完成"不算，CI 通过 + 产出物存在才算（来源: 幻觉门控） |
| 6 | **每个项目自带部署** | 部署脚本归项目管，工具链负责触发和验证，不替项目做部署决策 |
| 7 | **需求/问题必须经过 Review** | 所有需求和问题提交前必须经过 Review，不直接进入开发 |
| 8 | **项目维度组织** | 所有内容（Issue、CI、部署、Skill 配置）以项目为单位组织，支持多项目并行 |

---

## §2. 基础设施层

### 2.1 代码托管：Gitea（唯一）

| 项 | 配置 |
|----|------|
| 地址 | `http://192.168.2.154:3000` |
| 版本 | v1.23.4 |
| 认证 | HTTP + token（待配置） |
| 权限 | cfdaily 用户；姜维持有 admin 权限（启用 Actions、分支保护等） |

### 2.2 CI/CD：Gitea Actions

| 项 | 配置 |
|----|------|
| Runner | Mac mini 裸机，act-runner（Go 二进制） |
| 配置文件 | `.gitea/workflows/*.yml`，每个项目自管 |
| 语法 | 兼容 GitHub Actions（v1.23.4 已验证支持） |
| 触发 | push / PR / tag |

### 2.3 部署目标

| 环境 | 位置 | 说明 |
|------|------|------|
| Mac mini 本机 | `~/.sanguo_projects/<project>/` | 主力开发和运行环境 |
| NAS Docker | `192.168.2.154` | 部分服务（Gitea、回测等） |

---

## §3. 分支策略

### 3.1 采用 Trunk-Based Development（简化版）

选 Trunk-Based 而非 Git Flow 的理由：
- **团队小**（<10 人），不需要复杂的 release 分支
- **AI Agent 参与编码**，Agent 在短生命周期 feature 分支上工作，完成后快速合并
- **CI 是安全网**，main 分支始终可部署，feature 分支通过 CI 才能合并

前提条件：**Agent 必须知道当前所在分支**。git-workflow Skill 需注入 `git branch --show-current` 到 Agent 上下文（仲达 Q1 补充）。

### 3.2 分支规则

| 分支类型 | 命名 | 生命周期 | 合并方式 | 适用场景 |
|---------|------|---------|---------|---------|
| `main` | `main` | 永久 | — | 始终可部署 |
| feature | `feat/<issue-N>-<简述>` | <3天 | PR → CI → Review → merge | 新功能 |
| bugfix | `fix/<issue-N>-<简述>` | <1天 | PR → CI → merge | Bug 修复 |
| hotfix | `hotfix/<简述>` | <2小时 | 直接 push main + CI + 自动回滚 | 紧急修复 |

### 3.3 分支保护规则

- `main` 分支：
  - 不允许直接 push（hotfix 除外）
  - PR 必须通过 CI（至少 unit + integration tests）
  - PR 必须至少 1 人 Review（人或 Agent）
  - **hotfix 直接 push main 后必须触发 CI**（至少 lint + unit），CI 失败则自动 revert（M1 修订）
- feature/bugfix 分支：
  - 无保护，自由 push
  - 合并前必须 CI 通过

### 3.4 Commit 规范

```
<type>(<scope>): <subject>

<body>
```

| type | 说明 |
|------|------|
| `feat` | 新功能 |
| `fix` | Bug 修复 |
| `refactor` | 重构（不改行为） |
| `test` | 测试相关 |
| `docs` | 文档 |
| `chore` | 构建/工具/配置 |

---

## §4. 问题管理

### 4.1 问题来源

| 来源 | 入口 | 处理方式 |
|------|------|---------|
| 主公口头 | 庞统记录，**经 Review 后** | 创建 Gitea Issue |
| 司马懿 Review | Review 意见 | 创建 Gitea Issue，关联 PR |
| 自动化测试失败 | CI 报告 | 自动评论到已有 Issue，或手动创建 |
| 用户测试反馈 | 人工报告 | 创建 Gitea Issue |
| Agent 运行发现 | Agent 日志，**经 Review 后** | 创建 Gitea Issue |

### 4.2 Issue 标签体系

| 标签 | 颜色 | 说明 |
|------|------|------|
| `bug` | 红 | 功能异常 |
| `feature` | 蓝 | 新功能需求 |
| `improvement` | 绿 | 改进优化 |
| `security` | 橙 | 安全相关 |
| `risk:high/standard/low` | 分级色 | 风险级别（见 §6.1 判定规则） |
| `priority:high/medium/low` | 黄/灰 | 优先级 |
| `blocked` | 紫 | 阻塞中 |

### 4.3 需求/问题 Review 前置

所有需求和问题在进入开发之前必须经过 Review：

| 类型 | Review 者 | 目的 |
|------|-----------|------|
| 新需求 | 庞统（方案合理性）+ 司马懿（技术可行性） | 确认需求清晰、可行、值得做 |
| Bug/问题 | 司马懿（确认根因）或庞统（确认优先级） | 确认是真 bug、不是误报，根因明确 |
| Agent 发现的问题 | 庞统（确认有效性） | 过滤误报 |

未经过 Review 的 Issue 不进入开发流程。

### 4.4 Issue 状态流转

```
Open → In Progress → Review → Closed
  ↑                                  ↓
  └──── Reopened ←───────────────────┘
```

---

## §5. CI/CD 管道设计

### 5.1 通用 CI 流程（所有项目共享骨架）

每个项目在 `.gitea/workflows/ci.yml` 自定义具体步骤，但遵循统一骨架。

> **注**：Gitea Actions v1.23.4 不支持 `paths` 过滤触发条件。通过路径判断放在 job 级别的 `if` 条件中，使用确定支持的语法。（M4 修订）

```yaml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  # Job 1: 后端测试
  backend:
    # 只在有 Python 代码变更时跑
    runs-on: [self-hosted, mac-mini]
    steps:
      - uses: actions/checkout@v4
      - name: Setup Python
        run: |
          python3 -m venv .venv
          .venv/bin/pip install -e ".[dev]"
      - name: Lint
        run: .venv/bin/ruff check src/
      - name: Unit Tests
        run: .venv/bin/pytest tests/unit/ -m "not e2e" --tb=short
      - name: Integration Tests
        run: .venv/bin/pytest tests/integration/ --tb=short
      - name: Coverage Report
        run: .venv/bin/pytest --cov=src --cov-report=term-missing

  # Job 2: 前端构建
  frontend:
    # 只在有前端目录时跑
    runs-on: [self-hosted, mac-mini]
    steps:
      - uses: actions/checkout@v4
      - name: Install & Build
        run: |
          cd src/frontend
          npm ci
          npm run build
      - name: Type Check
        run: cd src/frontend && npx tsc --noEmit
```

**实际使用时**：每个项目根据实际情况删减 job。没有前端的删 frontend job，没有 Python 的删 backend job。

### 5.2 CI 门控级别

| 级别 | 触发条件 | 跑什么 | 阻断行为 |
|------|---------|--------|---------|
| **快速** | feature 分支 push | lint + unit tests | **软阻断**：失败标红，创建 PR 时如果最近一次 CI 失败则不允许进入 Review（S2 修订） |
| **标准** | PR 到 main | lint + unit + integration + coverage | 阻断 merge |
| **完整** | tag / main push | 标准 + build（E2E 仅 tag 触发） | 阻断部署 |
| **手动** | 手动触发 | 完整 + 特定检查 | 按需 |

### 5.3 覆盖率渐进策略（仲达 Q3 补充）

当前多数项目从未跑过覆盖率，直接设阈值不现实。分三阶段：

| 阶段 | 时间 | 覆盖率策略 | 说明 |
|------|------|-----------|------|
| **P1** | 启用后 2 周 | 只报告不阻断 | 先拿基线数据 |
| **P2** | 启用后 1 月 | 40% 阈值，低于警告不阻断 | 逐步提升 |
| **P3** | 启用后 2 月+ | 60% 阈值，低于阻断 | 正式生效 |

### 5.4 CI 结果反馈

- ✅ 通过 → PR 可合并
- ❌ 失败 → PR 阻断，自动评论失败信息
- 覆盖率低于阈值 → P1 只报告，P2 警告，P3 阻断

---

## §6. 代码审查流程

### 6.1 风险级别判定规则（M2 修订）

风险级别不由改动者自评，按以下规则自动判定 + 非改动者确认：

**自动判定规则**（在 CI 或 PR 模板中检查）：

| 规则 | 级别 | 触发条件 |
|------|------|---------|
| 涉及核心模块 | **高** | 改动文件匹配 `**/spawner*`、`**/ticker*`、`**/dispatcher*`、`**/router*`、`**/guardrails*`、`**/strategy*`、`**/risk*` |
| 涉及资金/安全 | **高** | 改动包含交易逻辑、风控规则、密钥管理、资金操作 |
| 数据库 schema 变更 | **高** | 改动匹配 `**/migrations/**`、`**/schema*`、`**/models.py` 中的表定义 |
| 常规功能开发 | **标准** | 不匹配高风险规则的 src/ 下改动 |
| 文档/测试/配置 | **低** | 改动仅在 `docs/`、`tests/`、`*.md`、`*.yaml`（非 guardrails） |

**确认机制**：
1. PR 创建时自动标注风险级别（基于 git diff 文件路径）
2. 审查者（或庞统）确认或升级——**只能升级不能降级**
3. 改动者可以申请升级（但不能申请降级）

### 6.2 审查方式（按风险分级）

| 风险级别 | 审查方式 | 审查者 |
|---------|---------|--------|
| **高** | 固定 Review（必须） | 司马懿（专职） |
| **标准** | 固定 Review（必须） | 司马懿（默认审查者） |
| **低** | 简化走查 | 司马懿（快速确认）+ CI 通过 |

**例外**：司马懿自己的改动 → 庞统交叉审查。

> 所有级别的代码审查默认由司马懿执行。只有司马懿作为改动者时，才由庞统进行交叉审查。不再按改动领域分配审查者——简化流程，减少协调成本。

### 6.3 审查者分配规则

| 改动者 | 审查者 |
|--------|--------|
| 张飞 / 姜维 / 关羽 / 赵云 / 庞统 | 司马懿（默认） |
| 司马懿 | 庞统（交叉审查） |

### 6.4 审查清单（Skill 固化）

审查清单通过 Skill 提示词固化，不依赖审查者自觉。**基础清单 + 项目类型变体**：

**基础清单（所有项目通用）**：

```
1. 正确性
   - [ ] 代码逻辑正确，无死循环/空指针/边界遗漏
   - [ ] 异常处理完整（网络超时、文件不存在、参数非法）
   - [ ] 改动范围和 Issue/需求描述一致（不多不少）

2. 一致性
   - [ ] 命名规范统一（变量/函数/文件）
   - [ ] 和已有代码风格一致

3. 安全性
   - [ ] 无硬编码密钥/密码/Token
   - [ ] 用户输入有校验
   - [ ] 文件操作有路径校验

4. 可维护性
   - [ ] 复杂逻辑有注释（为什么，不是做什么）
   - [ ] 无重复代码（DRY）
   - [ ] 关键参数可配置，无魔法数字

5. 测试覆盖
   - [ ] 新功能有对应测试
   - [ ] Bug 修复有回归测试
   - [ ] CI 通过

结论：通过 / 不通过（附具体行号和修改方向）
```

**量化策略项目变体**（追加项）：
```
6. 量化策略专项
   - [ ] 止损逻辑完整（触发条件、执行路径、异常兜底）
   - [ ] 仓位计算正确（滑点、手续费已计入）
   - [ ] 回测参数和实盘参数分离
```

### 6.5 审查时效

| 改动大小 | 期望审查时间 |
|---------|-------------|
| < 100 行 | 2 小时内 |
| 100-500 行 | 半天内 |
| > 500 行 | 1 天内 |

---

## §7. 部署流程

### 7.1 部署触发

| 触发方式 | 适用场景 | 流程 |
|---------|---------|------|
| **自动**（tag 触发） | 正式发布 | 打 tag → CI 完整跑（含 E2E）→ 自动执行 deploy 脚本 → 健康检查 |
| **自动**（main push） | 日常迭代 | push main → CI 标准跑 → 自动执行 deploy 脚本 → 健康检查 |
| **手动** | 紧急/调试 | 手动执行项目 deploy 脚本 |

### 7.2 项目部署脚本规范

每个项目必须提供 `scripts/deploy.sh`，接口统一：

```bash
#!/usr/bin/env bash
# deploy.sh — 项目部署脚本
#
# 必须支持的参数：
#   --source=DIR      源码目录（CI 自动传入：源码 checkout 路径）
#   --target=DIR      安装目标目录（CI 自动传入：安装路径）
#   --skip-build      跳过构建步骤
#   --health-check    部署后执行健康检查
#   --version         输出当前部署版本（tag + commit hash + 部署时间）
#   --rollback        回滚到上一个已记录的部署版本
#
# 必须维护的文件：
#   data/deploy-history.jsonl — 最近 10 个部署版本记录
#     每行: {"tag":"v1.2.3","commit":"abc123","timestamp":"2026-06-06T12:00:00+08:00","status":"success"}
#
# 必须返回：
#   0 = 成功
#   非0 = 失败（stderr 输出原因）
```

### 7.3 自动部署 Workflow

```yaml
# .gitea/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  deploy:
    needs: [ci]  # 依赖 CI 通过
    runs-on: [self-hosted, mac-mini]
    steps:
      - uses: actions/checkout@v4
      - name: Record current version
        run: bash scripts/deploy.sh --version
      - name: Deploy
        run: bash scripts/deploy.sh --source="$GITHUB_WORKSPACE" --health-check
      - name: Rollback on failure
        if: failure()
        run: bash scripts/deploy.sh --rollback
```

### 7.4 健康检查

每个项目的 deploy 脚本必须包含健康检查：
- HTTP 服务：`curl -sf http://localhost:<port>/api/health`
- 数据服务：数据连通性检查
- 定时任务：PM2 状态检查

### 7.5 回滚策略（M3 修订）

**版本记录**：
- 每次 deploy 成功后，记录 tag + commit + 时间戳到 `data/deploy-history.jsonl`（保留最近 10 条）
- `--version` 读取最后一行输出当前版本
- `--rollback` 读取倒数第二行，checkout 对应 commit，重新 deploy

**回滚流程**：
1. `deploy.sh --rollback` 读取上一个成功版本
2. `git checkout <previous-commit>`
3. 重新执行 deploy（不含 rollback）
4. 健康检查

**已知限制**：
- revert 可能产生合并冲突 → 部署失败时人工介入
- 数据库变更回滚需人工介入 → schema 变更必须向前兼容（只加字段不删/不改），违反此规范由 CI 检查拦截（或人工 Review 拦截）

---

## §8. 验证流程集成

### 8.1 验证来源和层级

| 验证来源 | 层级 | 触发时机 | 自动化程度 |
|---------|------|---------|-----------|
| **Lint** | 机械 | 每次 push | 100% 自动 |
| **单元测试** | 机械 | 每次 push | 100% 自动 |
| **集成测试** | 语义 | PR 到 main | 100% 自动 |
| **覆盖率** | 语义 | PR 到 main | 渐进自动（见 §5.3） |
| **Code Review** | 共识 | PR 到 main | 人工（Skill 辅助） |
| **E2E 测试** | 共识 | PR 到 main 时自动跑（CI 隔离环境）+ 手动触发 | 自动+手动 |
| **用户测试** | 共识 | 部署后 | 手动 |

### 8.2 三阶段门控（来源: quality-gate-patterns）

```
机械门控 (CI 快速) → 语义门控 (CI 标准 + Review) → 共识门控 (E2E + 用户验证)
    零成本              中等成本                    高成本
  语法/lint/编译     API 契约/业务逻辑            端到端/用户体验
```

- 快速门控是**开发期辅助反馈**，不替代标准门控
- **PR 合入必须经过标准门控（机械 + 语义）**，这个顺序不可跳过（S3 修订）
- 机械失败 → 立即阻断，不看 Review
- 语义失败 → 可以 Review 但不能合并
- E2E 在 CI 隔离环境中自动跑，不污染生产环境（见 §8.5）。也可手动触发。

### 8.3 测试失败 → Issue 关联

- CI 失败时，如果已有 Issue → 自动评论失败信息
- CI 失败时，如果无 Issue → 不自动创建（避免 Issue 爆炸），由改动者手动创建

### 8.4 机械门控前提条件（仲达 Q6 补充）

P1 必须先配好以下工具，否则机械门控跑不起来：

| 工具 | 用途 | 安装 |
|------|------|------|
| `ruff` | Python lint + format | `pip install ruff` |
| `pytest-cov` | 覆盖率 | `pip install pytest-cov` |
| `act-runner` | CI 执行器 | 下载 Go 二进制 |

### 8.5 CI 隔离测试环境（E2E 安全运行）

E2E 自动跑的核心前提：**不污染生产环境**。通过 CI 隔离环境实现：

```
CI 临时测试环境（每次 CI 自动创建）
├─ 独立 venv（每次 CI 新建）
├─ 临时 SQLite（tmpdir，跑完销毁）
├─ 临时端口（8084，不和生产 8083 冲突）
├─ 启动 FastAPI 服务 → 跑 E2E → 关闭
└─ 跑完整个 tmpdir 删除，不留痕迹

生产环境（~/.sanguo_projects/）完全不受影响
```

**效果**：E2E 可以在每次 PR 到 main 时自动跑，无需担心污染生产数据或影响生产服务。

#### 隔离边界：程序逻辑 vs Agent 交互

| 层面 | 隔离方式 | 说明 |
|------|---------|------|
| **程序逻辑**（FastAPI + Daemon + SQLite） | ✅ 完全隔离 | 独立 venv + 临时数据库 + 临时端口 | — |
| **Agent spawn**（openclaw agent） | ❌ 走生产 openclaw | openclaw 是全局单例，无法隔离 | 测试 case 用 UUID 前缀标识 + Agent 回写地址指向 8084 临时端口 |

Agent spawn 走生产 openclaw 的决策理由：
1. openclaw 是全局单例（`~/.openclaw/` 只有一份），Agent 配置和 workspace 全局共享
2. 测试 case 会标识测试上下文，Agent 记忆混淆风险低
3. 完全隔离需要在 Docker 中跑独立 openclaw，维护成本不值得
4. Agent 的 API 回写地址由 spawn message 传入，指向 CI 临时服务的端口 8084

#### 路径硬编码修复清单

当前代码有 6 处硬编码绝对路径，需改为环境变量可配置（P1 必须完成）：

| 位置 | 硬编码路径 | 改法 | 现状 | 行数 |
|------|-----------|------|------|------|
| `utils.py` | 数据根目录 | `BLACKBOARD_ROOT` 环境变量 | ✅ 已支持 | 1 |
| `bootstrap.py:42` | Skill 加载路径 | `MOZI_SKILL_PATH` 环境变量 | ✅ 已支持 | 1 |
| `registry.py:264` | 项目扫描目录 `~/.openclaw/sanguo_projects` | 加 `SANGUO_PROJECTS_DIR` | ❌ 硬编码 | 1 |
| `spawner.py:1177,1261` | `~/.openclaw/agents/<id>/sessions.json` | 加 `OPENCLAW_HOME` | ❌ 硬编码 | 2 |
| `blackboard_routes.py:161` | `~/.openclaw/openclaw.json` | 从 `OPENCLAW_HOME` 读 | ❌ 硬编码 | 1 |

共计 6 处（2 已支持、4 待修）。

#### 环境总结

| 环境 | 位置 | 用途 | 生命周期 |
|------|------|------|---------|
| 开发 | `~/.openclaw/sanguo_projects/` | 改代码、本地调试 | 常驻 |
| CI 测试 | CI runner 临时目录 | lint/unit/integration/E2E | CI 跑完即销毁 |
| 生产 | `~/.sanguo_projects/` | 正式运行服务 | 常驻 |

**schema 变更检查**（可选）：CI 中加一步对比数据库 schema 变更，检测破坏性操作（删字段/改类型）。检测到则提 Issue 人工确认。可通过配置开关强制绕过。

---

## §9. 完整开发流程（端到端）

### 9.0 全局工作流串联图

以下是把所有关键环节串起来的完整链路。三个场景（新功能/Bug修复/Hotfix）共享大部分节点，差异在分支。

```
┌─────────────────────────────────────────────────────────────────────────┐
│                        问题/需求入口                                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐               │
│  │ 主公口头  │  │ CI 失败   │  │ 司马懿   │  │ 运行时   │               │
│  │ /需求文档 │  │ 自动报告  │  │ Review   │  │ Agent发现 │               │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘               │
│       └──────────────┴──────────────┴──────────────┘                    │
│                              ↓                                         │
│                   ┌─────────────────────┐                              │
│                   │ 需求/问题 Review     │  ← §4.3                     │
│                   │                     │                              │
│                   │ 需求: 庞统(方案)     │                              │
│                   │       + 司马懿(可行) │                              │
│                   │ Bug: 司马懿(确认根因)│                              │
│                   │ Agent发现: 庞统(过滤)│                              │
│                   └──────────┬──────────┘                              │
│                              ↓ 通过                                    │
│  ┌───────────────────────────────────────────────────────────┐         │
│  │              创建 Gitea Issue #N                          │         │
│  │  标签: bug/feature/improvement + risk:high/standard/low   │         │
│  │  关联: PR / 复现步骤 / 根因分析                            │         │
│  └──────────────────────────┬────────────────────────────────┘         │
│                             ↓                                          │
│              ┌──── 场景分流 ────┐                                      │
│              ↓                  ↓                                      │
│     ┌─── Hotfix ───┐   ┌── 标准流程 ──┐                               │
│     │              │   │              │                               │
│     │ 直接 main    │   │ 分支开发     │                               │
│     │              │   │              │                               │
│     └──────┬───────┘   └──────┬───────┘                               │
│            ↓                   ↓                                       │
└─────────────────────────────────────────────────────────────────────────┘

=== 标准流程（新功能 / Bug修复）===

  ┌─────────────────────────────────────────────┐
  │ Step 1: 分支创建                             │
  │   git checkout -b feat/issue-N-xxx           │
  │   或 git checkout -b fix/issue-N-xxx         │
  │   角色Agent: 开发者（张飞/姜维/关羽/赵云）    │
  │   Skill: git-workflow                        │
  │   输入: Issue #N                             │
  │   输出: feature/bugfix 分支                  │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step 2: 编码 + 自测                          │
  │   编码 → 本地 pytest → commit                │
  │   角色Agent: 开发者                          │
  │   Skill: 按任务类型（无专门编码skill）        │
  │   输入: 分支 + Issue 需求                    │
  │   输出: 代码改动 + UT                        │
  │   约束: Bug修复必须加回归测试                 │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step 3: push → CI 快速门控（每次push）       │
  │   Lint + Unit Tests                          │
  │   工具: Gitea Actions                        │
  │   输入: feature 分支 push                    │
  │   输出: ✅通过 / ❌失败标红                   │
  │   阻断: 软阻断（失败不阻止开发，但阻止PR进入Review）│
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step 4: 创建 PR                              │
  │   git push origin → Gitea 创建 PR            │
  │   自动: 风险级别标注（按改动文件路径）         │
  │   工具: Gitea PR                              │
  │   输入: feature 分支 + CI 快速门控结果        │
  │   输出: PR + 风险标签                         │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step 5: CI 标准门控（PR到main时）            │
  │   Lint + Unit + Integration + Coverage       │
  │   环境: CI 隔离临时环境                       │
  │   工具: Gitea Actions + pytest-cov + ruff    │
  │   输入: PR diff                              │
  │   输出: ✅通过 / ❌阻断merge                 │
  │   阻断: 硬阻断（CI不过不许merge）             │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step 6: 代码审查                             │
  │   审查者确认风险级别（只升不降）              │
  │   默认审查者: 司马懿（所有级别）              │
  │   例外: 司马懿的改动 → 庞统交叉审查          │
  │   Skill: code-review                         │
  │   输入: PR + CI 结果 + 风险级别               │
  │   输出: 通过 / 不通过（附行号+修改方向）      │
  │   不通过 → 回到 Step 2                       │
  └──────────────────┬──────────────────────────┘
                     ↓ 通过
  ┌─────────────────────────────────────────────┐
  │ Step 7: Merge → CI 完整门控                   │
  │   merge到main → 自动触发                     │
  │   标准 CI + deploy workflow                   │
  │   触发: Gitea branch protection（CI+Review通过才许merge）│
  │   输入: main 分支                            │
  │   输出: merge commit + CI 结果                │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step 8: 自动部署                              │
  │   deploy.sh --source=... --health-check      │
  │   记录版本到 deploy-history.jsonl             │
  │   角色Agent: 姜维（deploy.sh由项目维护）      │
  │   Skill: ci-cd-ops                           │
  │   输入: main 分支代码                         │
  │   输出: 部署结果 + 版本记录                   │
  │   失败: 自动回滚（deploy.sh --rollback）     │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step 9: E2E 验证（CI 隔离环境自动跑）        │
  │   临时venv + 临时SQLite + 临时端口8084        │
  │   Agent spawn 走生产 openclaw（不隔离）      │
  │   角色: 司马懿写E2E用例，CI自动跑             │
  │   Skill: testing-workflow                    │
  │   输入: 部署后的代码                          │
  │   输出: E2E 结果                              │
  │   阻断: 初期不阻断，成熟后可阻断              │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step 10: Issue 关闭                          │
  │   部署成功 + 验证通过 → 关闭 Issue #N        │
  │   发现新问题 → 新开 Issue → 回到入口 Review  │
  └─────────────────────────────────────────────┘

=== Hotfix 分支流程 ===

  ┌─────────────────────────────────────────────┐
  │ Step H1: 确认 hotfix                         │
  │   庞统或主公确认（线上故障、核心功能受影响）  │
  │   Skill: hotfix-workflow                     │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step H2: 直接 push main                      │
  │   修改 → commit → push main                  │
  │   CI 必须触发（lint + unit），不可跳过       │
  │   CI 失败 → 自动 revert → 人工介入           │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step H3: 自动部署（同标准 Step 8）           │
  │   E2E 验证（同标准 Step 9）                  │
  └──────────────────┬──────────────────────────┘
                     ↓
  ┌─────────────────────────────────────────────┐
  │ Step H4: 24h 内复盘（必须）                  │
  │   补 Gitea Issue（根因 + hotfix 内容）       │
  │   司马懿 Review（事后审查代码质量）          │
  │   复盘记录（为什么需要 hotfix、如何避免）    │
  │   超时 → 下次 hotfix 权限暂停               │
  └─────────────────────────────────────────────┘
```

#### 衔接机制：每一步怎么触发下一步

上图只画了"谁做什么"，这里补清楚**步骤之间靠什么机制衔接**。

| 衔接点 | 触发方式 | 通知方式 | 技术实现 |
|--------|---------|---------|----------|
| 问题入口 → 问题Review | 庞统收到问题后主动发起 | Mail 通知司马懿（需求类）/ 司马懿自己提交（bug类） | Mail API POST `/api/mail` |
| 问题Review → Issue创建 | Review结论达成 | 庞统或司马懿创建 | Gitea API `POST /repos/{owner}/{repo}/issues` |
| Issue创建 → 分支创建 | 庞统指派任务给开发者 | Mail 通知被指派者，含 Issue #N + 分支名 | Mail API + Agent 读取 Issue |
| 分支创建 → 编码 | 开发者看到 Mail 后开始 | 无额外通知 | Agent 启动时 Skill 注入分支信息 |
| 编码 → CI 快速门控 | **git push 自动触发** | CI 结果显示在 commit 状态（Gitea Status Check 内置） | Gitea Actions `on: push` |
| CI 快速门控 → PR创建 | 开发者主动（认为代码写完） | 无自动通知 | 开发者在 Gitea Web UI 或 git push + 浏览器创建 PR |
| PR创建 → CI 标准门控 | **PR 创建/更新自动触发** | CI 结果自动评论到 PR | Gitea Actions `on: pull_request` |
| CI 标准门控 → 代码审查 | **CI 通过后 daemon Webhook 转发 Mail 给审查者** | Mail 通知司马懿 Review | Gitea Webhook `pull_request` → daemon Webhook 模块 → Mail API |
| 代码审查 → 修改（不通过） | 审查者提交 Review 意见 | daemon Webhook 转发 Mail 通知改动者（附 Review 摘要） | Gitea Webhook `pull_request_review` → daemon Webhook 模块 → Mail API |
| 代码审查 → Merge（通过） | **审查者点 Approve** | daemon Webhook 转发 Mail 通知改动者 merge | Gitea Webhook `pull_request_review` → daemon Webhook 模块 → Mail API |
| Merge → 部署 | **merge 到 main 自动触发** | 无需通知（自动化） | Gitea Actions `on: push: branches: [main]` |
| 部署 → E2E | **部署 job 成功后触发 E2E job** | E2E 结果评论到 merge commit | Gitea Actions `needs: [deploy]` |
| E2E/部署 → Issue关闭 | 庞统或改动者手动确认后关闭 | Issue 关闭通知关注者 | Gitea API `PATCH /repos/{owner}/{repo}/issues/{id}` state=closed |
| CI失败 → Issue评论 | **CI 失败自动评论** → daemon Webhook 转发 Mail 通知改动者 | 评论到关联 Issue + Mail 推送 Agent | Gitea Actions `if: failure()` 写 PR评论 → daemon Webhook 监听 `issue_comment` → Mail |
| 部署失败 → 自动回滚 | **deploy.sh 失败自动执行** | 无需通知（自动化） | Gitea Actions `if: failure()` step 调 `deploy.sh --rollback` |

#### 关键衔接机制详解

**1. Gitea Actions 自动触发（核心衔接）**

整个 CI/CD 链路的自动化靠 Gitea Actions 的触发机制：

```
git push feature分支
  → Gitea Actions on: push 匹配
  → 跑 lint + unit（快速门控）

创建 PR
  → Gitea Actions on: pull_request 匹配
  → 跑 lint + unit + integration + coverage（标准门控）
  → CI 结果自动出现在 PR 页面

Merge 到 main
  → Gitea Actions on: push: branches: [main] 匹配
  → 跑 deploy workflow（含 deploy.sh + health check + E2E）
  → 全自动，无需人干预
```

触发链：push → CI → PR → CI → Review → merge → deploy+E2E，中间三个自动环节靠 Gitea Actions 的 `on` 触发器串联。

**2. Mail 通知（Agent 间协调）**

CI/CD 之外的衔接靠 Mail。Agent 不会自己去看 Gitea PR 评论，所以所有需要 Agent 行动的情况都通过 Mail 推送：

- 需求Review完成 → Mail 通知开发者开始
- Bug确认 → Mail 通知改动者（含根因）
- Hotfix确认 → Mail 通知执行者
- CI 失败 → CI workflow 写 PR 评论 → daemon Webhook 模块转发 Mail 通知改动者 Agent
- 审查不通过 → Mail 通知改动者（附 Review 摘要）
- 部署失败回滚 → Mail 通知庞统/姜维（人工介入）

**3. Gitea branch protection（硬门控）**

确保流程不可跳过：
- `main` 分支设置：CI 必须通过 + 至少 1 人 Review Approve
- 效果：没有 CI 绿勾 + Review Approve，merge 按钮灰色不可点
- 配置位置：Gitea → 仓库 Settings → Branches → Branch Protection

**4. Agent 间通知机制总结**

| 场景 | 通知方式 | 为什么不用另一种 |
|------|---------|----------------|
| 人→Agent任务指派 | Mail | Gitea Issue 不会主动通知 Agent，需要 Mail 推送 |
| CI 结果 | Gitea Status Check（PR页面）+ daemon Webhook 转发 Mail（Agent通知） | Status Check 给人看，Mail 给 Agent 推送 |
| Review 请求 | daemon Webhook 转发 Mail | Gitea Webhook 触发 → daemon 翻译成 Mail 给司马懿 |
| Review 意见（不通过/通过） | daemon Webhook 转发 Mail | Gitea Webhook 触发 → daemon 翻译成 Mail 给改动者 |
| CI 失败 | CI workflow 写 PR评论 → daemon Webhook 转发 Mail | CI 写评论是给所有人看，Webhook → Mail 给 Agent 推送 |
| 跨Agent协作请求 | Mail | Gitea 不支持跨 Agent 私信 |

### 9.0.1 通知模板（工具链通用版）

以下为 §9.0 衔接机制中用到的通用通知模板。§15.5 中还定义了更详细的流程强约束 Mail 模板（含完整流程步骤和 Gitea API 调用指令）。

所有自动化通知统一格式，确保 Agent 收到后能直接行动。

**模板 1：任务指派（庞统 → 开发者）**
```
From: pangtong-fujunshi
To: {被指派者}
Title: 任务指派: Issue #{N} - {Issue 标题}
Text:
  {Issue 标题}
  Issue: http://192.168.2.154:3000/{repo}/issues/{N}
  分支: feat/issue-{N}-{简述}
  风险级别: {high/standard/low}
  要点:
  - {需求要点1}
  - {需求要点2}
  验收标准:
  - {验收标准1}
  - {验收标准2}
```

**模板 2：CI 失败通知（CI workflow → 改动者）**
```
From: daemon-webhook
To: {PR 作者}
Title: CI 失败: PR #{N} - {PR 标题}
Text:
  PR #{N} CI 未通过。
  PR: http://192.168.2.154:3000/{repo}/pulls/{N}
  失败测试:
  - {test_path}::{test_name} FAILED
  - {test_path}::{test_name} FAILED
  失败 step: {step_name}
  错误摘要: {error_output_tail_20_lines}
  请修复后重新 push。
```

**模板 3：Review 不通过通知（审查者 → 改动者）**
```
From: {审查者}
To: {改动者}
Title: Review 不通过: PR #{N} - {PR 标题}
Text:
  PR #{N} 审查未通过。
  PR: http://192.168.2.154:3000/{repo}/pulls/{N}
  风险级别: {confirmed_level}
  审查意见:
  1. [{文件}:{行号}] {问题描述} → {修改方向}
  2. [{文件}:{行号}] {问题描述} → {修改方向}
  必须全部修改后重新 push。
```

**模板 4：部署失败回滚通知（CI workflow → 庞统/姜维）**
```
From: daemon-webhook
To: pangtong-fujunshi, jiangwei-infra
Title: 部署失败已回滚: {repo} {commit_hash}
Text:
  {repo} 部署失败，已自动回滚到上一版本。
  触发 commit: {commit_hash}
  部署脚本: scripts/deploy.sh
  失败原因: {deploy_stderr_tail}
  当前版本: {previous_version}
  请人工介入排查。
```

**模板 5：Bug 确认通知（司马懿 → 改动者）**
```
From: simayi-challenger
To: {改动者}
Title: Bug 确认: Issue #{N} - {Issue 标题}
Text:
  根因: {根因描述}
  复现步骤: {steps}
  影响范围: {scope}
  风险级别: {level}
  Issue: http://192.168.2.154:3000/{repo}/issues/{N}
  分支: fix/issue-{N}-{简述}
  请修复并添加回归测试。
```

### 9.1 场景 A：新功能开发

```
1. 问题入口
   └─ 主公口头 / 需求文档
   └─ 庞统梳理需求方案 → 司马懿确认技术可行性
   └─ Review 通过后 → 庞统创建 Gitea Issue #N

2. 分支创建
   └─ git checkout -b feat/issue-N-xxx
   └─ Agent 或人工编码（git-workflow Skill 注入分支信息到 Agent 上下文）

3. 开发过程中
   └─ 每个 commit → push → CI 快速门控（lint + unit）
   └─ CI 失败 → 标红但不阻断开发 → 修好再 push

4. 开发完成
   └─ git push origin feat/issue-N-xxx
   └─ 创建 PR → 自动标注风险级别（基于改动文件路径）
   └─ CI 标准门控（lint + unit + integration + coverage）

5. 审查
   └─ CI 通过后进入审查
   └─ 审查者确认风险级别（只升不降）
   └─ 司马懿审查（所有级别默认）
   └─ 如果改动者是司马懿 → 庞统交叉审查
   └─ 审查不通过 → 修改 → push → CI → 再审
   └─ 审查通过 → merge

6. 部署
   └─ merge 到 main → 自动触发 CI + deploy workflow
   └─ deploy.sh 执行 → 记录版本 → 健康检查
   └─ 部署成功 → Issue #N 关闭
   └─ 部署失败 → 自动回滚 → 人工介入

7. 验证
   └─ CI 隔离环境中自动跑 E2E（不污染生产）
   └─ 用户测试（手动）
   └─ 发现问题 → 新开 Issue（经 Review 后再开发）
```

### 9.2 场景 B：Bug 修复

```
1. 问题入口
   └─ CI 失败 / 司马懿 Review / 运行时发现
   └─ 司马懿确认根因（是真 bug 不是误报）
   └─ 创建 Gitea Issue #N，标注 bug + 风险级别

2. 根因定位（bugfix-workflow Skill）
   └─ 调查根因（不猜，看日志/复现）
   └─ 在 Issue 中记录根因

3. 修复
   └─ git checkout -b fix/issue-N-xxx
   └─ 修改代码 + 添加回归测试（必须）
   └─ push → PR → CI → Review

4. 合并+部署
   └─ merge → auto deploy → 健康检查
   └─ Issue #N 关闭

5. 验证
   └─ 确认原 bug 不再复现
```

### 9.3 场景 C：紧急修复（hotfix）（M1/S5 修订）

```
1. 判断是否需要 hotfix
   └─ 标准：线上故障影响核心功能，等 PR 流程代价过高
   └─ 庞统或主公确认

2. 直接在 main 上修
   └─ git commit → push main
   └─ CI 必须触发（lint + unit），不可跳过
   └─ auto deploy → 健康检查
   └─ CI 失败 → 自动 revert → 人工介入

3. 事后复盘（hotfix-workflow Skill）
   └─ 24 小时内必须完成：
       ├─ 补 Gitea Issue（记录根因 + hotfix 内容）
       ├─ 司马懿 Review（事后审查代码质量）
       └─ 复盘记录（为什么需要 hotfix、如何避免）
   └─ 超时未复盘 → 下次 hotfix 权限暂停

4. E2E 验证
   └─ CI 隔离环境中自动跑 E2E 确认 hotfix 有效
```

---

## §10. Skill 清单（流程固化）（S4 修订）

以下流程通过 Skill 固化到对应 Agent：

| Skill 名 | 固化什么 | 适用 Agent |
|----------|---------|-----------|
| `git-workflow` | 分支创建/命名/合并规范、commit 规范、PR 创建流程、**当前分支注入上下文** | 所有编码 Agent |
| `code-review` | 风险级别判定规则、审查清单（基础+项目变体）、审查标准、结论格式 | 司马懿（默认审查者）、庞统（交叉审查） |
| `ci-cd-ops` | CI 配置规范、deploy 脚本规范、健康检查标准、版本记录规范 | 姜维（平台）、所有项目维护者 |
| `bugfix-workflow` | 根因定位流程、回归测试要求、Issue 关联 | 所有编码 Agent |
| `hotfix-workflow` | hotfix 判断标准、直接 push main 流程、CI 必跑、24h 复盘要求 | 庞统（协调）、张飞、姜维 |
| `testing-workflow` | 测试分工规范、什么时候跑什么测试、测试数据隔离 | 所有 Agent |
| `release-workflow` | tag 规范、changelog、发布流程、数据库 schema 变更向前兼容规范 | 庞统（协调） |

### 测试分工规范（testing-workflow Skill 内）

| 测试类型 | 谁写 | 谁跑 |
|---------|------|------|
| **单元测试（UT）** | 开发者自己（张飞/姜维/关羽/赵云等） | CI 自动 + 开发者本地 |
| **功能测试** | 司马懿（专职） | CI 自动 |
| **集成测试** | 司马懿（专职） | CI 自动 |
| **E2E 测试** | 司马懿（专职） | CI 自动（隔离环境）+ 手动触发 |
| **测试脚本/工具** | 司马懿 + 赵云（数据相关） | 按需 |

> 所有人都能写代码和写 UT。但功能测试、集成测试、E2E 由司马懿专职编写和维护——确保测试视角独立于开发视角。

---

## §11. 实施路线

| 阶段 | 内容 | 前置条件 | 耗时 |
|------|------|---------|------|
| **P0: 基础启用** | Gitea admin 开启 Actions + 部署 act-runner（Mac mini 裸机） | 主公操作 NAS | 30 分钟 |
| **P1: CI 骨架** | 模板 CI workflow + pytest-cov + ruff + 覆盖率基线测量 | P0 | 1 天 |
| **P2: 流程 Skill** | 7 个 Skill 编写（见 §10） | 无 | 2-3 天 |
| **P3: 自动部署** | 部署 workflow + 项目 deploy.sh 规范化 + 版本记录 | P1 | 1-2 天 |
| **P4: 渐进收紧** | 覆盖率阈值 P1→P2→P3、E2E 集成 | P1-P3 | 按需 |

**P1 必须先就位的工具**（否则机械门控跑不起来）：
- `ruff`（lint）
- `pytest-cov`（覆盖率）
- `act-runner`（CI 执行器）

---

## §12. 评审记录

### v0.1 → v0.2 修订清单

| 编号 | 类型 | 问题 | 修订内容 |
|------|------|------|---------|
| M1 | 必须修 | hotfix 安全网不足 | §3.3 明确 hotfix push main 后 CI 必跑，失败自动 revert；§9.3 统一流程 |
| M2 | 必须修 | 风险级别谁来定 | §6.1 新增自动判定规则（按文件路径）+ 非改动者确认机制（只升不降） |
| M3 | 必须修 | 回滚策略过于简单 | §7.2 新增 `--version` 参数和 deploy-history.jsonl；§7.5 重写回滚策略 |
| M4 | 必须修 | CI 语法问题 | §5.1 删除 `hasPython()`/`hasFrontend()` 伪函数，改为每个项目按需删减 job |
| S1 | 建议改 | 轮查配对不合理 | §6.3 改为按改动领域分配，不再固定轮转 |
| S2 | 建议改 | 快速门控"不阻断"是噪音 | §5.2 改为"软阻断"，PR 进入 Review 前检查最近 CI 状态 |
| S3 | 建议改 | 三阶段门控说明不清 | §8.2 补充：快速门控是开发辅助，PR 合入必须标准门控 |
| S4 | 建议改 | Skill 遗漏 | §10 增加 testing-workflow、hotfix-workflow、release-workflow（含 schema 变更） |
| S5 | 建议改 | hotfix 复盘无时限 | §9.3 加 24 小时复盘约束，超时暂停 hotfix 权限 |
| Q1 | 评审回答 | Trunk-Based 够用吗 | §3.1 补充前提：Agent 必须知道当前分支，git-workflow Skill 注入 |
| Q3 | 评审回答 | 覆盖率基线缺失 | §5.3 新增渐进策略（P1 只报告 → P2 40% → P3 60%） |
| Q6 | 评审回答 | 机械门控前提未就绪 | §8.4 新增工具清单，§11 标注 P1 必须先就位 |

### v0.3 → v0.4 修订清单（主公审阅反馈）

| 编号 | 反馈 | 修订内容 |
|------|------|---------|
| F1 | 需求/问题必须经 Review | §1 新增原则 7；§4.3 新增需求 Review 前置规则；§9.1 场景 A 加入 Review 步骤 |
| F2 | E2E 仅用户触发 | §8.1 E2E 改为“仅用户手动触发”；§8.2 门控说明移除 E2E 自动跑；§9.1/9.3 验证步骤更新 |
| F3 | 编码/测试人人能做，功能/集成/E2E 司马懿专职 | §10 新增测试分工规范表 |
| F4 | 审查默认司马懿 | §6.2 简化为司马懿统一审查，仅司马懿自身改动由庞统交叉审查 |
| F5 | 以项目为单位组织 | §1 新增原则 8；Issue 标签体系含项目维度 |
| F6 | 姜维持有 Gitea admin | §2.1 权限更新 |
| F7 | 分支保护含义说明 | §3.3 补充分支保护规则说明 |
| F8 | schema 变更能自动化就自动化 | 保留在 release-workflow，CI check + Issue 兜底 |

### v0.4 → v0.5 修订清单（主公二次审阅）

| 编号 | 反馈 | 修订内容 |
|------|------|----------|
| G1 | E2E 不自动跑是因为怕污染生产，不是不需要 | §8.5 新增 CI 隔离测试环境，E2E 改为自动跑（临时 venv + 临时 SQLite + 临时端口） |
| G2 | 三套环境是否有必要 | §8.5 环境总结表：开发/CI临时/生产，不需要常驻测试环境 |
| G3 | schema 变更检查可以，可选+开关 | §8.5 补充 schema 检查说明：可选 + 配置开关强制绕过 |
| G4 | 分支保护确认开启 | §14 更新为已确认 |
| G5 | E2E 触发方式 | §8.1 更新：CI 自动 + 手动触发 |

### v0.5 → v0.6 修订清单（主公三次审阅）

| 编号 | 反馈 | 修订内容 |
|------|------|----------|
| H1 | Agent spawn 走生产 openclaw，不隔离 | §8.5 新增隔离边界表：程序逻辑隔离 vs Agent 走生产 |
| H2 | 路径硬编码问题 | §8.5 新增路径硬编码修复清单（6 处，2 处已支持、4 处待修） |

### v0.6 → v1.0 定稿（司马懿终审通过）

- 隔离边界表增加"缓解措施"列（仲达建议）
- 路径硬编码清单增加"行数"列（仲达建议）
- 状态标记为 v1.0 定稿

### v1.0 → v1.1（主公要求：补全局工作流串联图）

- §9.0 新增全局工作流串联图：从问题入口到部署上线的完整链路
- 每个节点标注：谁做、用什么工具、什么Skill、输入输出
- 标准流程 10 步 + Hotfix 分支 4 步
- 新增工具链衔接总览表
- 新增衔接机制详解：每一步怎么触发下一步、靠什么技术、怎么通知
- §9.0.1 新增 5 个通知模板（任务指派/CI失败/Review不通过/部署回滚/Bug确认）
- 修正：Agent 不看 Gitea PR 评论，所有需 Agent 行动的场景都走 Mail 推送

### v1.2 → v2.0（串联架构重构）

- §15 新增完整串联架构：Gitea 自动化 + Daemon Webhook 模块 + Mail 执行层
- 核心变更：ci-notifier 取消，Webhook 转发集成到 daemon
- 新增：超时检测（三级处置）+ 失败处置（走 Issue 流程）
- 新增：Mail 模板（流程强约束固化）
- 新增：Skill vs Mail 边界明确
- 新增：Agent ID ↔ Git 用户名映射
- §9.0 衔接机制表更新：所有 ci-notifier 引用替换为 daemon Webhook 模块
- §9.0.1 通知模板 From 字段更新：ci-notifier → daemon-webhook

### v2.0 司马懿终审修订

| 编号 | 类型 | 问题 | 修订内容 |
|------|------|------|----------|
| M1 | 必须修 | 超时阈值统一 4h 不合理 | §15.4 改为从 Mail metadata 读取个性化 deadline，三级处置按超时程度而非绝对时间 |
| S1 | 建议 | Webhook 代码缺少映射步骤 | §15.3 代码示例加 to_agent_id() 映射函数，所有 send_mail 调用都经映射 |
| S2 | 建议 | Webhook 缺签名验证 | §15.3 代码示例加 X-Gitea-Signature HMAC 验证 |
| S3 | 建议 | 缺 Bug 确认模板 | §15.5 新增模板 6（Bug 确认通知） |
- §14 待讨论更新：已确认事项标记，新增 Webhook 模块待讨论项

### v2.0 实施记录（2026-06-06）

| # | 实施项 | 状态 | 备注 |
|---|--------|------|------|
| 1 | Gitea Actions 能力调研 | ✅ | docs/research/gitea-actions-research.md |
| 2 | Mail API 回复能力调研 | ✅ | docs/research/mail-reply-research.md |
| 3 | 路径硬编码修复 | ✅ | 4处改为环境变量，411 tests passed |
| 4 | CI workflow 编写 | ✅ | ci.yml + deploy.yml + e2e.yml |
| 5 | Skill 文件编写 | ✅ | 7个共通技能 |
| 6 | Gitea 仓库创建 | ✅ | sanguo/moziplus-v2（姜维） |
| 7 | act-runner 部署 | ✅ | arm64 裸机 + LaunchAgent（姜维） |
| 8 | 分支保护配置 | ✅ | main: CI通过+1人Review+禁止force push（姜维） |
| 9 | Webhook 配置 | ✅ | 已配未启用（等 daemon 集成）（姜维） |
| 10 | CI Secret 配置 | ✅ | CI_TOKEN（注意：Gitea 保留 GITEA_* 前缀） |
| 11 | Workflow 变量前缀验证 | ✅ | gitea.* 正确，无需修改 |
| 12 | Mail 模板验证 | ✅ | 5项能力全验证通过 |
| 13 | PR #1 创建 | ✅ | feat/initial-setup → main |
| 14 | 开发→安装目录同步 | ✅ | diff 确认一致 |
| - | Webhook daemon 集成 | ⏳ | 需改 daemon 代码，暂不做 |
| - | Mail 模板自动发送（Webhook 触发） | ⏳ | 依赖 Webhook daemon 集成 |
| - | act-runner LaunchAgent 网络就绪优化 | ⏳ | 偶尔 gRPC 连接失败 |
| - | Gitea 升级到 v1.24+ | 💡 | 建议，非必须 |

---

## §13. 项目维度组织

### 13.1 多项目并行

所有内容以项目为单位组织：

| 维度 | 按项目组织 |
|------|------------|
| **代码仓库** | Gitea 上每个项目一个 repo |
| **Issue** | 每个项目的 Issue 独立管理 |
| **CI/CD** | 每个项目自管 `.gitea/workflows/` |
| **部署脚本** | 每个项目自管 `scripts/deploy.sh` |
| **测试** | 每个项目自管 `tests/` 目录 |
| **Skill 配置** | 通用 Skill 共享，项目特定 Skill 放项目仓库 |

特殊：`_general` 项目用于跨项目的通用任务（如工具链基础设施维护）。

---

## §14. 待讨论

1. **Gitea admin 权限**：姜维持有 admin 权限，可负责启用 Actions、配置分支保护等
2. **分支保护**：已确认开启——PR 必须 CI 通过 + 至少 1 人 Review 才能合并到 main
3. **数据库 schema 变更检查**：CI 自动检测破坏性变更（可选，有开关可强制绕过），检测不到的提 Issue 人工干预 — **已确认**
4. **环境架构**：三环境（开发/CI临时/生产），CI 临时环境支持 E2E 自动跑不污染生产 — **已确认**
5. **Webhook 转发模块集成到 daemon**：已确认方向，具体实现待设计（见 §15）

---

## §15. 串联架构

> §15 是 v2.0 的核心新增章节，定义工具链三层串联架构：Gitea 自动化 + Daemon Webhook 模块 + Mail 执行层。
> 此架构取消了之前的 ci-notifier 虚拟角色概念，所有事件流转通过三层清晰分工完成。

### §15.1 串联架构总览

三层架构图：

```
Layer 1: Gitea 自动化（流程引擎）
  - Actions: push → CI, PR → CI, merge → deploy
  - Status Check: CI 结果自动显示在 PR
  - Branch Protection: CI通过+Review通过才许merge
  - CI workflow 内可调 Gitea API 写 PR 评论

Layer 2: Daemon 流程编排（转发+监控）
  - Webhook 接收模块：监听 /webhook 端点
  - 简单事件：Webhook → 解析 → 直接发 Mail
  - 复杂事件：Webhook → 解析 → spawn 庞统作为任务节点
  - 超时检测：在 ticker 中扫描未回复 Mail
  - 失败处置：轻度重发提醒，中度 spawn 庞统，严重创建 Issue

Layer 3: Mail 执行层（Agent 接口）
  - Mail = 流程指令 + 回复确认 + 时限
  - Agent 收到 Mail 后行动，完成后回复确认
  - Mail 线程 = Issue 生命周期可追溯
  - 流程强约束固化在 Mail 模板中
```

核心原则：

1. **Gitea 是流程引擎**——所有状态流转靠 Gitea 自己（Actions + Status Check + Branch Protection）
2. **Webhook → Mail 转发集成在 daemon 里**——状态共享，可直接触发下一步
3. **Mail 是 Agent 接口**——Agent 不看 Gitea UI，所有需 Agent 行动的事件通过 Mail 推送
4. **简单事件 daemon 直接处理，复杂事件（多 Agent 协作）spawn 庞统作为协调节点**
5. **没有 ci-notifier**——不需要虚拟角色，Gitea 是流程引擎，daemon 是翻译器+监控器

### §15.2 Gitea Webhook 能力清单

| 事件类型 | 触发时机 | Payload 关键字段 | 工具链用途 |
|---------|---------|----------------|-----------|
| `push` | 代码推送 | commit hash, 分支, 作者 | 不需要转发（Actions 自动处理） |
| `pull_request` (opened) | PR 创建 | PR ID, 标题, 分支, 作者 | → Mail 通知司马懿 Review |
| `pull_request_review` (submitted) | Review 提交 | PR ID, 审查者, 结论(APPROVE/REQUEST_CHANGES), 评论 | → Mail 通知张飞 Review 结果 |
| `pull_request` (closed/merged) | PR 合并 | PR ID, 合并 commit | 不需要转发（Actions 自动触发 deploy） |
| `issue_comment` | PR/Issue 评论 | 评论者, 内容 | CI workflow 写的失败评论 → 转发 Mail |
| `issues` (opened+assigned) | Issue 创建/指派 | Issue ID, 标题, 被指派人 | → Mail 通知开发者 |
| `release` | Release 创建 | tag, 名称 | 触发完整 CI+部署 |

> **说明**：Webhook 是单向 POST，Gitea 发完不管。需要在 daemon 里实现 `/webhook` 端点接收。

### §15.3 Daemon Webhook 模块设计

在 daemon 中新增一个 Webhook 接收模块（FastAPI 路由）：

```python
# src/api/webhook_routes.py
@router.post("/webhook/gitea")
async def handle_gitea_webhook(event: dict, x_gitea_event: str = Header(...), x_gitea_signature: str = Header(None)):
    """接收 Gitea Webhook，翻译成 Mail 或 spawn 庞统"""
    
    # 签名验证（防止伪造）
    if x_gitea_signature and WEBHOOK_SECRET:
        expected = hmac.new(WEBHOOK_SECRET, json.dumps(event).encode(), sha256).hexdigest()
        if not hmac.compare_digest(expected, x_gitea_signature):
            raise HTTPException(403, "Invalid webhook signature")
    
    # Git 用户名 → Agent ID 映射
    def to_agent_id(git_username: str) -> str:
        return GIT_TO_AGENT.get(git_username, git_username)
    
    if x_gitea_event == "pull_request":
        action = event["action"]
        if action == "opened":
            pr_author = to_agent_id(event["pull_request"]["user"]["login"])
            await send_mail(to="simayi-challenger", title=f"Review 请求: PR #{event['number']}", ...)
        elif action == "closed" and event["pull_request"]["merged"]:
            # merge 不需要通知，Actions 自动处理
            pass
            
    elif x_gitea_event == "pull_request_review":
        state = event["review"]["state"]
        if state == "APPROVED":
            pr_author = to_agent_id(event["pull_request"]["user"]["login"])
            await send_mail(to=pr_author, 
                          title=f"Review 通过: PR #{event['number']}，请 merge", ...)
        elif state == "REQUEST_CHANGES":
            pr_author = to_agent_id(event["pull_request"]["user"]["login"])
            await send_mail(to=pr_author,
                          title=f"Review 不通过: PR #{event['number']}", ...)
            
    elif x_gitea_event == "issue_comment":
        # CI 自动写的失败评论 → 转发给相关 Agent
        if "CI 失败" in event["comment"]["body"]:
            commenter = to_agent_id(event["sender"]["login"])
            await send_mail(to=commenter,
                          title=f"CI 失败: PR #{event['number']}", ...)
    
    elif x_gitea_event == "issues":
        if event["action"] == "assigned":
            assignee = to_agent_id(event["assignee"]["login"])
            await send_mail(to=assignee, 
                          title=f"任务指派: Issue #{event['number']}", ...)
```

关键设计：
- 模块部署在 daemon 的 FastAPI 里（已有服务，只加路由）
- 和 Mail API 共享 SQLite（直接调内部函数，不经过 HTTP）
- 简单事件（CI失败通知、Review通知、任务指派）直接发 Mail
- 复杂事件（部署失败、架构级 Review 问题）spawn 庞统

### §15.4 回复驱动与超时处置

> daemon ticker 中新增两个定时扫描任务，**回复驱动是主路径，超时检测是兜底**。

**主路径：回复驱动（Agent 回复 Mail 后，daemon 根据结果触发下一步）**

```python
# daemon ticker 每小时跑一次
async def process_flow():
    # 路径1：扫描新回复，驱动下一步（主路径）
    await check_replies()
    # 路径2：扫描超时未回复，兜底处置
    await check_stale_flows()

async def check_replies():
    """扫描 Mail 回复，根据结果触发下一步"""
    # 查所有有新回复的 Mail（回复在上一轮扫描之后）
    replied_mails = queries.find_recently_replied(since=last_scan)
    
    for mail in replied_mails:
        reply = get_reply(mail.id)
        
        if is_success_reply(reply):
            # 成功 → 记录闭环，不触发动作
            # 原因：Gitea 自动化（Actions + Webhook）会自动推动下一步
            # 例：张飞回复"已创建PR" → Gitea Webhook 自动通知司马懿 Review
            # 例：张飞回复"已merge" → Gitea Actions 自动触发 deploy
            log.info(f"Mail {mail.id} 已成功闭环: {reply.text[:100]}")
        
        elif is_failure_reply(reply):
            # 失败 → 创建 Issue 走正式流程
            await create_issue(
                title=f"执行失败: {mail.title}",
                labels=["bug"],
                body=f"原指令: {mail.text}\n\n失败原因: {reply.text}"
            )
            # 通知庞统
            await send_mail(to="pangtong-fujunshi",
                          title=f"执行失败已创建 Issue: {mail.title}", ...)
        
        elif is_help_reply(reply):
            # 需协助 → spawn 庞统协调
            await spawn_agent("pangtong-fujunshi",
                f"Agent {reply.from} 执行 '{mail.title}' 遇到困难，请求协助。详情: {reply.text}")
```

**兜底路径：超时检测（Agent 没回复时的处置）**

```python
async def check_stale_flows():
    """扫描超时未回复的 Mail，按个性化 deadline 判断"""
    pending_mails = queries.find_pending_requests()
    
    for mail in pending_mails:
        deadline_hours = mail.meta.get("deadline_hours", 8)  # 默认 8 小时
        hours_elapsed = (now() - mail.created_at).total_seconds() / 3600
        
        if hours_elapsed < deadline_hours:
            continue  # 未超时，跳过
        
        hours_overdue = hours_elapsed - deadline_hours
        
        if hours_overdue < 4:
            # 轻度：重发 Mail 提醒
            await send_mail(to=mail.to, title=f"提醒: {mail.title}", 
                          text=f"此任务已超时 {hours_overdue:.0f} 小时，请尽快处理。")
        
        elif hours_overdue < 16:
            # 中度：spawn 庞统介入
            await spawn_agent("pangtong-fujunshi", 
                            f"流程卡住: Mail {mail.id} '{mail.title}' 超时 {hours_overdue:.0f} 小时未回复。请介入协调。")
        
        else:
            # 严重：创建 Issue + 通知庞统
            await create_issue(title=f"流程严重超时: {mail.title}", labels=["blocked", "priority:high"])
            await send_mail(to="pangtong-fujunshi", 
                          title=f"严重超时: {mail.title}，已创建 Issue", ...)
```

**失败处置原则**：
- 所有失败 → Issue 走正式流程，不私下通知 Agent 悄悄解决
- CI 失败 → CI workflow 写 PR 评论 → Webhook → Mail 通知 → Agent 修 → 走标准 PR 流程
- 部署失败回滚 → CI workflow 创建 Issue（priority:high） → 走标准 Bug 修复流程
- 超时未响应 → 三级处置（提醒→庞统介入→Issue+通知主公）

**断点续传**：
- 每个环节的状态都在 Gitea 上（PR 状态、CI 结果、Review 状态）
- Mail 线程提供完整的可追溯记录
- 回复驱动确保正常流程自动推进，超时检测确保异常被捕获
- 流程卡住时，庞统可以根据 Mail 线程 + Gitea 状态快速定位断点并继续

### §15.5 Mail 模板（流程强约束固化）

> **注意**：Mail 模板里固化的是**流程强约束**——这个工作链特有的一步步怎么做。
> 共通技能（git 操作、审查清单等）放在 Skill 里（见 §10）。

**模板 1：任务指派（庞统/daemon → 开发者）**

```
Title: 任务指派: Issue #{N} - {标题}
Text:
  Issue: {url}
  分支: {branch_name}
  风险级别: {level}
  验收标准: {criteria}
  
  流程：
  1. 创建分支 {branch_name}
  2. 编码 + 写 UT
  3. push → 等 CI（CI 会自动跑）
  4. CI 通过后创建 PR（Gitea API: POST /repos/{owner}/{repo}/pulls）
  5. 等 Review（司马懿会自动收到通知）
  6. Review 通过后 merge PR
  
  如果 CI 失败：修复后重新 push，回到步骤 3
  如果 Review 不通过：按意见修改后重新 push，回到步骤 3
  
  完成后回复此 Mail 确认。
  时限：{deadline}
```

**模板 2：Review 请求（Webhook → 司马懿）**

```
Title: Review 请求: PR #{N}
Text:
  PR: {url}
  标题: {title}
  风险级别: {level}
  改动文件: {file_list}
  
  流程：
  1. 读取 PR diff（Gitea API: GET /repos/{owner}/{repo}/pulls/{N}.diff）
  2. 按审查清单审查（参考 code-review Skill）
  3. 提交 Review（Gitea API: POST /repos/{owner}/{repo}/pulls/{N}/reviews）
     - APPROVE 或 REQUEST_CHANGES
  4. 提交后张飞会自动收到通知
  
  完成后回复此 Mail 确认。
  时限：{deadline}
```

**模板 3：Review 结果通知（Webhook → 张飞）**

```
Title: Review {result}: PR #{N}
Text:
  PR: {url}
  结果: {APPROVED / REQUEST_CHANGES}
  审查意见: {review_body}
  
  {如果通过}:
  流程：merge PR（Gitea API: POST /repos/{owner}/{repo}/pulls/{N}/merge）
  
  {如果不通过}:
  流程：
  1. 按审查意见修改代码
  2. push → CI 自动跑
  3. CI 通过后等重新 Review
  
  完成后回复此 Mail 确认。
```

**模板 4：CI 失败通知（Webhook from issue_comment → 张飞）**

```
Title: CI 失败: PR #{N}
Text:
  PR: {url}
  失败测试: {failed_tests}
  错误摘要: {error_summary}
  
  流程：
  1. 查看完整 CI 日志（PR 页面）
  2. 修复失败的测试
  3. push → CI 自动重跑
  
  完成后回复此 Mail 确认。
```

**模板 5：部署失败通知（→ 庞统，复杂事件 spawn）**

```
Title: 部署失败已回滚: {repo}
Text:
  仓库: {repo}
  触发 commit: {sha}
  失败原因: {error}
  已自动回滚到: {previous_version}
  
  已自动创建 Issue #{M}。
  请协调排查。
```

**模板 6：Bug 确认通知（司马懿 → 改动者）**

```
Title: Bug 确认: Issue #{N} - {标题}
Text:
  Issue: {url}
  根因: {根因描述}
  复现步骤: {steps}
  影响范围: {scope}
  风险级别: {level}
  分支: fix/issue-{N}-{简述}
  
  流程：
  1. 创建分支 fix/issue-{N}-{简述}
  2. 修复 bug + 写回归测试
  3. push → CI 自动跑
  4. CI 通过后创建 PR
  5. 等 Review
  
  完成后回复此 Mail 确认。
  时限：{deadline}
```

### §15.6 Skill vs Mail 边界

| 维度 | Mail 模板 | Skill |
|------|----------|-------|
| 作用域 | 工具链流程特有 | 全局，所有项目通用 |
| 强制度 | 强约束，Agent 必须按流程走 | 按需加载，提升质量 |
| 内容 | 流程步骤+Gitea API 调用指令 | 共通技能（审查清单、git规范等） |
| 示例 | "CI失败→修→push→等CI→等Review" | "怎么写好 UT"、"审查清单" |
| 没有 Skill 能否运转 | ✅ 能（流程不会断） | 只是质量没那么高 |

**原则：没有 Skill，流程还能运转。有了 Skill，流程运转得更好。**

Skill 只放共通的、所有项目都能用的技能：
- `git-workflow`：分支命名、commit 规范（所有项目通用）
- `code-review`：审查清单（所有项目通用）
- `testing-workflow`：测试规范（所有项目通用）
- `bugfix-workflow`：根因定位方法（所有项目通用）

工具链特有的流程（"CI 失败后怎么做"、"Review 通过后怎么做"）全部固化在 Mail 模板里。

### §15.7 Agent ID = Git 用户名（已确认一致）

姜维确认（2026-06-07）：Gitea 注册用户名就是完整 Agent ID，短名用户已删除。

| Agent ID（= Git 用户名） |
|-------------------------|
| pangtong-fujunshi |
| simayi-challenger |
| zhangfei-dev |
| guanyu-dev |
| zhaoyun-data |
| jiangwei-infra |

不需要映射表，`to_agent_id()` 直用。


---

## §16. 事件中枢详细设计

> §16 是对 §15 串联架构的落地设计补充，定义事件中枢的具体实现方案。
> 版本: v1.0-draft | 日期: 2026-06-07 | 状态: 待评审

### §16.0. 讨论纪要

本节记录设计过程中的关键讨论，供未来回溯。

### 两条线的定位（主公决策）

| 线 | 方向 | 机制 | 定位 |
|----|------|------|------|
| **出线** | Agent → Gitea | Agent 直接操作 Gitea，完成后发 Mail 通知下一个 Agent | Gitea = 黑板，Mail = 触发器 |
| **入线** | 外部 → Agent | 外部事件（CI/部署/Webhook）→ 事件中枢 → Mail → Agent | 事件中枢 = 入口 |

**原则 1**：Agent 之间的协作不走事件中枢。中枢只处理"外部 → Agent"。
**原则 2**：所有工具链留痕在 Gitea，Mail 是辅助推送。即使 Mail 失败，Agent 可主动查 Gitea 恢复上下文。
**原则 3**：工具链反馈 Agent 的通知统一走事件中枢，避免散乱。

### 关键决策记录

| # | 决策 | 理由 |
|---|------|------|
| D1 | 不做第三种 task 类型 | 中枢产出就是 Mail（from=system, type=inform），模板填充 description，投递复用 spawner。区别只在内容（模板化）和元数据（结构化），不需要新的数据结构 |
| D2 | 提示词独立模板文件 | 模板和代码分离，改文案不改代码，可审查可扩展 |
| D3 | 中枢是 daemon 内模块 | 要访问 Blackboard 创建 Task，共享进程。用独立路由模块（`toolchain_routes.py`）保持职责清晰 |
| D4 | 同步处理 | 事件处理很轻（解析+模板填充+创建Task），远在 Gitea 5秒超时内。复杂事件未来用 asyncio.create_task |
| D5 | Agent ID = Gitea 用户名，直用 | 姜维确认：Gitea 注册用户名就是完整 Agent ID（zhangfei-dev / simayi-challenger 等），短名已删除。`to_agent_id()` 直用，不需要映射表 |
| D6 | CI/部署通知也走事件中枢 | 统一入口，不走 workflow 直接调 Mail API。CI workflow 写 PR comment → Gitea 触发 issue_comment Webhook → 中枢处理 |

---

### §16.1. 定位与边界

### 1.1 是什么

事件中枢是 daemon 内的一个路由模块，负责将**外部工具链事件**翻译成 Mail 通知推送给 Agent。

### 1.2 不是什么

- 不是消息队列
- 不是 Agent 间通信通道（Agent 间协作走 Mail，不经过中枢）
- 不是编排引擎（任务编排由 dispatcher + spawner 负责）
- 不负责 Agent 回复 Mail 后的流程驱动（由 daemon ticker 的回复驱动机制负责，见 §15.4）

### 1.3 两条线

```
出线（Agent → 工具链）：
  Agent 直接操作 Gitea（创建 Issue/PR/Review 等）
  → 完成后发 Mail 通知下一个 Agent
  → 下一个 Agent 去读 Gitea 继续干活

入线（工具链 → Agent）：
  外部事件（CI 结果/部署结果/Webhook 事件）
  → 事件中枢
  → 模板化 Mail
  → Agent 收到通知，去 Gitea 查看详情并行动
```

### 1.4 数据流

```
外部事件源                    事件中枢                        Agent
─────────                    ──────                        ──────

Gitea Webhook ─────┐
                   │
CI workflow ───────┤    toolchain_routes.py          Mail Task
  (PR comment) ────┤    ┌─────────────────┐         (from: system)
                   ├──→│ 1. 接收事件      │              │
Deploy workflow ──┤    │ 2. 验签/过滤     │              ↓
  (PR comment) ────┤    │ 3. 幂等检查      │         dispatcher
                   │    │ 4. 选模板+填充   │              │
daemon 内部 ───────┘    │ 5. 创建 Mail    │              ↓
  (预留接口)           └─────────────────┘         spawner 投递
```

---

### §16.2. 事件分类与处理

### 2.1 事件来源

| 来源 | 触发方式 | 事件类型 |
|------|---------|---------|
| Gitea Webhook | Gitea 主动 POST | PR opened, Review submitted, Issue assigned, issue_comment |
| CI workflow | CI 写 PR comment → 触发 issue_comment Webhook | CI 失败 |
| Deploy workflow | Deploy 创建 Issue → 触发 issues Webhook | 部署失败 |
| daemon 内部 | 内部函数调用 | 预留，当前不走中枢 |

### 2.2 事件处理矩阵

| 事件 | 来源 | 通知谁 | 模板 | 关键信息 |
|------|------|--------|------|---------|
| `pull_request` opened | Gitea Webhook | 司马懿（Review） | `review_request.md` | PR号、标题、作者、分支、文件列表、风险级别 |
| `pull_request_review` submitted | Gitea Webhook | PR 作者 | `review_result.md` | PR号、审查结论、评论、审查者 |
| `issues` assigned | Gitea Webhook | 被指派人 | `issue_assigned.md` | Issue号、标题、标签、描述、分支名建议 |
| `issue_comment` (CI失败) | CI workflow → Webhook | PR 作者 | `ci_failure.md` | PR号、分支、失败步骤、错误摘要 |
| ~~部署成功~~ | ~~不需要通知~~ | ~~流程自动闭环~~ | ~~—~~ | ~~Gitea Issue 自动关闭~~ |
| `issues` created (标题匹配 🔴 部署失败) | Deploy workflow → Webhook | 姜维 + 庞统 | `deploy_failure.md` | 仓库、commit、失败原因 |

### 2.3 事件过滤规则

不是所有事件都需要处理：

| 规则 | 说明 |
|------|------|
| 只处理白名单内的事件类型 | 未知的忽略 + 日志 |
| issue_comment 需判断来源 | 只处理 CI workflow 写的评论（按特定前缀匹配：`❌ **CI 失败**` 或统一后的 `[CI]` 前缀） |
| PR 作者/审查者必须是已知 Agent | 未知的忽略 + 日志 |
| 幂等：同一事件不重复创建 Mail | 双重去重：① delivery UUID（`{event}-{delivery}`）标准幂等；② review 事件 payload 内容去重（`{event}:{pr_num}:{sender}:{sha256(body_or_content)[:16]}`），防御同一 review 被不同来源重复提交（2026-06-09 新增） |

---

### §16.3. 模板系统

### 3.1 模板文件组织

```
templates/toolchain/
├── review_request.md      # PR opened → 司马懿 Review
├── review_result.md       # Review submitted → PR 作者
├── issue_assigned.md      # Issue assigned → 开发者
├── ci_failure.md          # CI 失败 → PR 作者
└── deploy_failure.md      # 部署失败 → 庞统+姜维
```

### 3.2 模板格式

模板使用 `{variable}` 占位符，中枢填充后生成 Mail description。

**示例：`review_request.md`**

```markdown
PR Review 请求

PR: http://192.168.2.154:3000/{repo}/pulls/{pr_number}
标题: {pr_title}
作者: {pr_author}
分支: {branch}
风险级别: {risk_level}
改动文件:
{file_list}

流程：
1. 读取 PR diff（Gitea API: GET /repos/{repo}/pulls/{pr_number}.diff）
2. 按审查清单审查（参考 code-review Skill）
3. 提交 Review（Gitea API: POST /repos/{repo}/pulls/{pr_number}/reviews）
4. 提交后改动者会自动收到通知

完成后回复此 Mail 确认。
```

### 3.3 模板变量提取

| 变量 | 来源 | 提取方式 |
|------|------|---------|
| `pr_number` | Webhook payload | `event["pull_request"]["number"]` |
| `pr_title` | Webhook payload | `event["pull_request"]["title"]` |
| `pr_author` | Webhook payload | `to_agent_id(event["pull_request"]["user"]["login"])` |
| `branch` | Webhook payload | `event["pull_request"]["head"]["ref"]` |
| `file_list` | Webhook payload | `event["pull_request"]["changed_files"]` 或需要额外 API 调用 |
| `risk_level` | daemon 计算 | 按文件路径规则匹配（见 §3.4） |
| `repo` | Webhook payload | 从 `event["repository"]["full_name"]` 提取 |

### 3.4 风险级别自动判定（简化版）

按改动文件路径匹配规则：

```python
from pathlib import PurePath

# 高风险文件路径模式（使用 pathlib.PurePath.match，支持 ** 递归）
HIGH_PATTERNS = ["**/spawner*", "**/ticker*", "**/dispatcher*",
                 "**/router*", "**/guardrails*", "**/strategy*", "**/risk*"]

def calc_risk_level(changed_files: list[str]) -> str:
    for f in changed_files:
        for pattern in HIGH_PATTERNS:
            if PurePath(f).match(pattern):
                return "high"
    return "standard"
```

---

### §16.4. 技术设计

### 4.1 模块结构

```
src/config/
└── agents.py              # Agent ID 统一注册表

src/api/
├── toolchain_routes.py    # 事件中枢路由（~250行）
├── mail_routes.py         # 现有 Mail API
└── ...

src/daemon/
├── toolchain_templates.py # 模板加载+填充（~80行）
├── mail_notify.py         # 现有 Mail 失败通知
└── ...

templates/toolchain/
├── review_request.md
├── review_result.md
├── issue_assigned.md
├── ci_failure.md
└── deploy_failure.md
```

### 4.2 toolchain_routes.py 接口设计

```python
# src/api/toolchain_routes.py

import asyncio
from fastapi import APIRouter, Request, Response
from src.config.agents import AGENT_IDS
from src.daemon.toolchain_templates import TemplateEngine

router = APIRouter()
engine = TemplateEngine()

GITEA_WEBHOOK_SECRET = os.environ.get("GITEA_WEBHOOK_SECRET", "")
_idempotency_lock = asyncio.Lock()

@router.post("/webhook/gitea")
async def gitea_webhook(
    request: Request,
    x_gitea_event: str = Header(...),
    x_gitea_signature: str = Header(None),
    x_gitea_delivery: str = Header(None),
):
    """接收 Gitea Webhook，翻译成 Mail"""
    
    body = await request.body()
    
    # 1. 签名验证（HMAC-SHA256，Gitea 1.23.4 已确认）
    if GITEA_WEBHOOK_SECRET:
        expected = hmac.new(GITEA_WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
        if not hmac.compare_digest(expected, (x_gitea_signature or "")):
            return Response(status_code=403, content="signature verification failed")
    
    event = json.loads(body)
    
    # 2. 幂等检查（asyncio.Lock 防并发竞态）
    if x_gitea_event and x_gitea_delivery:
        async with _idempotency_lock:
            if _is_duplicate(x_gitea_event, x_gitea_delivery):
                return Response(status_code=200, content="duplicate")
    
    # 3. 路由到对应处理器
    handler = HANDLERS.get(x_gitea_event)
    if not handler:
        return Response(status_code=200, content="ignored")
    
    # 4. 处理事件 → 创建 Mail
    try:
        result = await handler(engine, event)
        return Response(status_code=200)
    except Exception as e:
        logger.exception("Failed to handle %s event", x_gitea_event)
        return Response(status_code=500, content=str(e))
```

### 4.3 事件处理器

每个事件类型一个处理函数，职责：解析 payload → 选模板 → 填充 → 创建 Mail Task。

```python
async def handle_issues(engine: TemplateEngine, event: dict) -> str:
    """issues 事件分发：assigned → 通知被指派人；opened + 标题匹配 → 部署失败通知"""
    action = event.get("action")
    
    if action == "assigned":
        return await _handle_issue_assigned(engine, event)
    
    if action == "opened":
        title = event.get("issue", {}).get("title", "")
        if "部署失败" in title:
            return await _handle_deploy_failure(engine, event)
    
    return None  # 忽略其他 action


async def handle_pull_request(engine: TemplateEngine, event: dict) -> str:
    """pull_request 事件分发：只处理 opened，忽略 synchronized/closed/reopened"""
    if event.get("action") != "opened":
        return None
    return await _handle_pr_opened(engine, event)


async def _handle_pr_opened(engine: TemplateEngine, event: dict) -> str:
    """PR opened → Review 请求给司马懿"""
    pr = event["pull_request"]
    
    # 提取变量
    variables = {
        "pr_number": pr["number"],
        "pr_title": pr["title"],
        "pr_author": to_agent_id(pr["user"]["login"]),
        "branch": pr["head"]["ref"],
        "repo": event["repository"]["full_name"],
        "risk_level": calc_risk_level(get_changed_files(pr)),
        "file_list": format_file_list(get_changed_files(pr)),
    }
    
    # 填充模板
    text = engine.render("review_request.md", variables)
    
    # 创建 Mail Task
    meta = {
        "source": "toolchain",
        "webhook_event": "pull_request_opened",
        "pr_number": pr["number"],
        "repo": variables["repo"],
    }
    
    return create_mail_task(
        to="simayi-challenger",
        title=f"Review 请求: PR #{pr['number']} {pr['title']}",
        text=text,
        meta=meta,
    )


async def handle_review_submitted(engine: TemplateEngine, event: dict) -> str:
    """Review submitted → 结果通知 PR 作者"""
    review = event["review"]
    pr = event["pull_request"]
    pr_author = to_agent_id(pr["user"]["login"])
    state = review["state"]  # APPROVED / REQUEST_CHANGES / COMMENTED
    
    if state == "COMMENTED":
        return None  # 普通评论不通知
    
    variables = {
        "pr_number": pr["number"],
        "pr_title": pr["title"],
        "result": "通过" if state == "APPROVED" else "不通过",
        "reviewer": to_agent_id(review["user"]["login"]),
        "review_body": review["body"] or "无",
        "repo": event["repository"]["full_name"],
    }
    
    template = "review_result.md"
    text = engine.render(template, variables)
    
    return create_mail_task(
        to=pr_author,
        title=f"Review {variables['result']}: PR #{pr['number']}",
        text=text,
        meta={"source": "toolchain", "webhook_event": "review_submitted", "pr_number": pr["number"]},
    )


async def handle_issue_comment(engine: TemplateEngine, event: dict) -> str:
    """issue_comment → 判断来源，路由到 CI/部署通知"""
    comment_body = event["comment"]["body"]
    
    # 匹配 CI workflow 写的失败评论（当前格式: "❌ **CI 失败**"，未来统一为 "[CI]"）
    if "[CI]" in comment_body or "CI 失败" in comment_body:
        return await handle_ci_comment(engine, event)
    # 部署失败当前走创建 Issue（不走 PR comment），见 §16.8 #6
    return None
    
    return None  # 非 CI/部署评论不处理


# HANDLERS 注册表
HANDLERS = {
    "pull_request": handle_pull_request,      # 只处理 opened
    "pull_request_review": handle_review_submitted,
    "issues": handle_issues,                  # 处理 assigned + opened(部署失败)
    "issue_comment": handle_issue_comment,    # 只处理 CI 失败评论
}
```

### 4.4 共用函数

```python
def to_agent_id(gitea_username: str) -> str:
    """Gitea 用户名 → Agent ID。已确认一致，直用。"""
    return gitea_username

def create_mail_task(to: str, title: str, text: str, meta: dict) -> str:
    """创建 Mail Task（from=system, type=inform）
    复用 mail_notify.py 的模式：直接通过 Blackboard 创建 Task。
    """
    # 从 mail_routes.py 提取共用逻辑，或直接复用 mail_notify 的方式
    ...

def is_duplicate(event_key: str) -> bool:
    """幂等检查：同一事件不重复创建 Mail
    存储在 SQLite events 表，daemon 重启后仍有效。
    保留最近 7 天记录，ticker 定期清理过期记录。
    """
    ...

def calc_risk_level(files: list[str]) -> str:
    """按文件路径规则判定风险级别"""
    ...

def get_changed_files(pr: dict) -> list[str]:
    """从 PR payload 提取改动文件列表
    payload 只有 changed_files 数量，需要额外调用 Gitea API:
    GET /repos/{owner}/{repo}/pulls/{number}/files
    """
    ...
```

### 4.5 模板引擎

```python
# src/daemon/toolchain_templates.py

from pathlib import Path
import string

TEMPLATE_DIR = Path(__file__).parent.parent.parent / "templates" / "toolchain"

class TemplateEngine:
    def __init__(self, template_dir: Path = TEMPLATE_DIR):
        self.template_dir = template_dir
        self._cache = {}
    
    def render(self, template_name: str, variables: dict) -> str:
        """加载模板文件并填充变量"""
        if template_name not in self._cache:
            path = self.template_dir / template_name
            self._cache[template_name] = path.read_text()
        
        template = self._cache[template_name]
        return template.format_map(defaultdict(str, variables))
```

---

### §16.5. 错误处理

| 场景 | 处理 | HTTP 返回 |
|------|------|----------|
| 签名验证失败 | 日志 warning | 403 |
| payload 解析失败 | 日志 error | 200（不触发 Gitea 重试） |
| 未知事件类型 | 忽略 + 日志 info | 200 |
| 幂等检测到重复 | 忽略 + 日志 info | 200 |
| 未知 Agent（不在映射表） | 忽略 + 日志 warning | 200 |
| PR 文件获取失败（3次重试后） | 降级为 risk_level=standard + 错误信息写入 Mail 正文 | 200（handler 内部处理） |
| 模板填充失败 | 日志 error | 500（触发 Gitea 重试） |
| Mail 创建失败 | 日志 error | 500（触发 Gitea 重试） |

**原则**：
- daemon 自身能处理的错误（格式、过滤、幂等）→ 返回 200，不让 Gitea 无意义重试
- daemon 处理不了的错误（数据库、内部异常）→ 返回 500，让 Gitea 重试

---

### §16.6. CI/Deploy Workflow 配合

### 6.1 CI 失败通知

CI workflow 已有 `notify-on-failure` job（ci.yml），当前格式：
```
❌ **CI 失败**

请检查 CI 日志并修复。

触发 commit: `abc1234`
```

**需要修改**：将 comment 前缀统一为 `[CI]`，方便中枢匹配：
```yaml
# .gitea/workflows/ci.yml — notify-on-failure job
-d "{\"body\": \"❌ **CI 失败**\\n\\n请检查 CI 日志并修复。\"}"
+d "{\"body\": \"[CI] 失败\\n\\n分支: ${{ gitea.ref_name }}\\n触发 commit: \\`${{ gitea.sha }}\\`\\n请检查日志并修复。\"}"
```

### 6.2 Deploy 结果通知

同理，deploy workflow 成功/失败时写 comment：

```yaml
# .gitea/workflows/deploy.yml
- name: Report success
  if: success()
  run: |
    curl -s -X POST \
      "http://192.168.2.154:3000/api/v1/repos/{owner}/{repo}/issues/{pr_number}/comments" \
      -H "Authorization: token ${{ secrets.CI_TOKEN }}" \
      -d "{\"body\": \"[Deploy:成功] 版本: $(bash scripts/deploy.sh --version)\\n请确认后关闭 Issue.\"}"

- name: Report failure
  if: failure()
  run: |
    curl -s -X POST \
      "http://192.168.2.154:3000/api/v1/repos/{owner}/{repo}/issues/{pr_number}/comments" \
      -H "Authorization: token ${{ secrets.CI_TOKEN }}" \
      -d "{\"body\": \"[Deploy:失败] 已回滚到上一版本。原因: ...\"}"
```

---

### §16.7. 配置

| 环境变量 | 用途 | 默认值 |
|---------|------|--------|
| `GITEA_WEBHOOK_SECRET` | Webhook HMAC 签名密钥（可选） | 空（跳过验签） |
| `BLACKBOARD_ROOT` | Blackboard 数据根目录（已有） | `~/.sanguo_projects/sanguo_moziplus_v2/data` |
| `TOOLCHAIN_TEMPLATES_DIR` | 模板文件目录（可选） | `templates/toolchain/` |

---

### §16.8. 待确认项

| # | 项 | 状态 | 结论 |
|---|------|------|------|
| 1 | Agent Gitea 用户名映射 | ✅ 姜维已确认 | 一致（用户名就是完整 Agent ID），直用，不需要映射表 |
| 2 | Gitea Webhook secret | ✅ 已配置 | 组织级 webhook secret = `22760993dff898a190731da43aa8d964`，daemon `GITEA_WEBHOOK_SECRET` 同步 |
| 3 | CI workflow PR comment | ✅ 已有 | ci.yml 已有 notify-on-failure job 写 PR comment，格式为 `❌ **CI 失败**...`，需改为 `[CI]` 前缀 |
| 4 | `from=system` 走内部函数 | ✅ 已确定 | 走内部函数（和 mail_notify.py 一致），不走 HTTP API |
| 5 | PR changed_files | ✅ 已实现 | `_fetch_pr_files()` 调用 Gitea API，3 次重试 + 失败信息写入 Mail正文 |
| 6 | Deploy workflow 通知方式 | ✅ 已确认 | 当前创建 Issue（非 PR comment）。部署通知走 `issues` Webhook（issue created）而非 `issue_comment` |
| 7 | 签名算法 | ✅ 已确认 | Gitea 使用 HMAC-SHA256，代码注释已补 |
| 8 | Webhook 作用范围 | ✅ 组织级 | Gitea 组织级 webhook（Hook ID=28），覆盖 sanguo 下所有仓库，新增仓库自动覆盖 |
| 9 | ALLOWED_HOST_LIST | ✅ 已修复 | Gitea 容器配置 `192.168.2.153, 127.0.0.1, localhost, 172.17.0.0/16, 192.168.2.0/24` |
| 10 | Gitea review payload 格式 | ✅ 姜维调研确认（2026-06-08） | Gitea v1.23.4 review payload 只有 `type` + `content`，没有 `state`/`body`/`user`，这不是 org vs repo 差异而是 Gitea 设计。v1.24.0 格式不变。双格式兼容是防御性编码，保持现状 |
| 11 | Spawner compact 检测窗口 | ✅ 已修复 | 窗口 300s→900s，尾部读取 50KB→1MB。实测长对话中 compact 记录被推出窗口导致漏检 |
| 12 | inform 类型 Mail crash 误标 done | ✅ 已修复 | `_mail_auto_complete` 增加 outcome 感知，inform 用白名单（completed/claimed/no_reply）控制 done 标记。spawner crash cooldown 300s→60s |

---

### §16.9. 实施计划

| 步骤 | 内容 | 依赖 |
|------|------|------|
| 1 | `toolchain_routes.py` 骨架 + Gitea Webhook 接收 + 签名验证 | 无 |
| 2 | `toolchain_templates.py` 模板引擎 | 无 |
| 3 | 5 个模板文件 | 步骤 2 |
| 4 | 4 个事件处理器（PR/Review/Issue/Comment） | 步骤 1+3 |
| 5 | 幂等检查（SQLite events 表，保留 7 天） | 步骤 1 |
| 6 | `create_mail_task` 共用函数（从 mail_routes.py 提取） | 无 |
| 7 | CI/Deploy workflow 加 comment step | 步骤 1+4 |
| 8 | Gitea Webhook 启用 + 测试 | 姜维 |
| 9 | 端到端测试 | 步骤 1-8 |

### 9.1 改动量估算

| 文件 | 行数 | 类型 |
|------|------|------|
| `src/api/toolchain_routes.py` | ~200 | 新增 |
| `src/daemon/toolchain_templates.py` | ~60 | 新增 |
| `templates/toolchain/*.md` | ~170（5个文件） | 新增 |
| `src/api/mail_routes.py` | ~20 | 修改（提取共用函数） |
| `.gitea/workflows/ci.yml` | ~15 | 修改（加 comment step） |
| `.gitea/workflows/deploy.yml` | ~15 | 修改（加 comment step） |
| **总计** | **~480** | |

---

### §16.10. 评审检查清单

- [ ] §0 讨论纪要是否准确反映了决策过程
- [ ] §1 两条线的边界是否清晰（出线不走中枢，入线统一走中枢）
- [ ] §2 事件矩阵是否有遗漏
- [ ] §3 模板内容是否完整，变量提取是否正确
- [ ] §4 接口设计是否合理，共用函数提取是否恰当
- [ ] §5 错误处理策略是否完备
- [ ] §6 CI/Deploy workflow 配合方案是否可行
- [ ] §8 待确认项是否完整
- [ ] §9 实施步骤是否合理


### v2.0 → v2.1 变更（事件中枢落地设计）

| 编号 | 变更内容 |
|------|---------|
| §16 | 新增事件中枢详细设计（§16.0-§16.10），基于 §15 串联架构 v2.0 的落地细节 |

### v2.1 → v2.2 变更（事件中枢完善 + E2E 验证通过）

| 编号 | 变更内容 |
|------|---------|
| §16.8 | #2 更新：组织级 webhook 已配置（Hook ID=28）+ secret 生效；#5 更新：PR 文件获取已实现 3 次重试；新增 #8（组织级覆盖）和 #9（ALLOWED_HOST_LIST 修复） |
| §16.4 | 技术设计更新：幂等检查加 `asyncio.Lock` 防并发竞态（T-02）；Agent ID 提取到 `config/agents.py` 统一管理（T-04）；`_fetch_pr_files` 返回 `Tuple[List[str], str]`，3 次重试 + 失败信息写入 Mail（T-05） |
| §16.5 | 错误处理更新：PR 文件获取失败不再静默，3 次重试后错误信息写入 Mail 正文 |
| 全局 | 广播风暴 bug 修复：`mail_notify.py` 校验 Agent ID，非有效 Agent（如 system）路由给庞统 |
| E2E | S22 模拟测试 13/13 通过；S23 真实 Gitea Webhook 8/8 通过；组织级 webhook 跨仓库投递 + 签名验证通过 |

### v2.2 → v3.0 变更（合并 §14 工具链 Skill + 自动部署）

| 编号 | 变更内容 |
|------|----------|
| §17 | 新增：原 `14-toolchain-skill-and-deploy.md` v1.1 全文合并为 §17「工具链落地设计」 |
| §17.1 | 目标：L1 TOOLS.md + L2 Skill 升级 + deploy.sh + 端到端验证 |
| §17.2 | 知识体系四层定位（L1 操作手段 + L2 流程规范） |
| §17.3 | L1 TOOLS.md：按 6 个角色定制 Gitea API 操作模板 |
| §17.4 | L2 Skill 升级：7 个现有 Skill 对齐事件中枢 + CI |
| §17.5 | deploy.sh + deploy.yml 统一接口（rsync + pm2 + health check + rollback） |
| §17.6 | 端到端验证：S1-S6 六个场景 |
| §17.7 | 覆盖率渐进策略（P1 只报告 → P2 40% → P3 60%） |
| §17.8 | 前端展示：Gitea 自带 CI 管理界面 |
| §17.9 | 实施路线（P1→P4） |
| §17.10 | 前置条件 Checklist（6 项） |
| §17.11 | 评审记录（M1/M2 修正 + S1-S4 采纳） |

---

## §17. 工具链落地设计（原 §14 合并）

> **来源**: 原 `14-toolchain-skill-and-deploy.md` v1.1（仲达评审 M1/M2 修正 + S1-S4 采纳）
> **合并日期**: 2026-06-08
> **定位**: 工具链从"设计完成"到"正式投入使用"的落地设计

---

### §17.1. 目标

把工具链从设计态推进到可使用态：
1. **L1 TOOLS.md**：每个 Agent 的 TOOLS.md 加入 Gitea API 操作模板，收到 Mail 后开箱即用
2. **L2 Skill 升级**：7 个现有 Skill 对齐事件中枢 + CI 实际运行
3. **deploy.sh + deploy.yml**：补完自动部署的实际脚本
4. **端到端验证**：用 sanguo/moziplus-v2 实验项目验证全链路

---

### §17.2. 知识体系四层定位

> 来源: `architecture-v3.0.md` §10 BootstrapBuilder

| 层 | 定位 | 载体 | Token | 工具链职责 |
|----|------|------|-------|-----------|
| **L0** | 铁律层 | AGENTS.md / MEMORY.md | ~500 | 不涉及 |
| **L1** | 角色层 | SOUL.md + **TOOLS.md** | ~2000 | **Gitea API 操作模板**（Agent 自带，开箱即用） |
| **L2** | 引擎注入 | BootstrapBuilder 注入 Skill 全文 | ~1500 | **7 个 Skill v2**（流程规范，Daemon 确定性注入） |
| **L3** | 被动参考 | Skill description，Agent 按需加载 | 按需 | 复用 L2 Skill，不新建 |

**核心原则**：
- L1 给操作手段（"怎么做"）：curl 命令模板、参数说明
- L2 给流程规范（"什么时候做什么"）：审查清单、分支规范、测试策略
- L3 是 L2 的被动触发版本，不需要单独维护

---

### §17.3. L1：TOOLS.md Gitea 操作模板

#### §17.3.1 设计原则

1. **开箱即用**：收到 Mail 后直接复制粘贴 curl 命令即可执行，不需要查文档
2. **按角色定制**：不是每个 Agent 都需要全套 API，只给该角色需要的
3. **统一格式**：所有模板使用相同的环境变量约定

#### §17.3.2 公共变量约定

每个 Agent 的 TOOLS.md 头部加入：

```markdown
## Gitea 工具链
- **地址**: http://192.168.2.154:3000
- **组织**: sanguo
- **认证**: `Authorization: token $GITEA_TOKEN`（各 Agent 使用自己的 token）
- **CI 管理界面**: http://192.168.2.154:3000/sanguo/{repo}/actions
```

#### §17.3.3 按角色模板

##### A. 开发者（张飞/关羽/赵云）— PR 创建 + Merge

```markdown
### 创建 PR
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls" \
  -H "Authorization: token $GITEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "head": "{branch}",
    "base": "main",
    "title": "{标题}"
  }'
# 返回 .number 即 PR 号
```

### 查看 PR diff
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}.diff" \
  -H "Authorization: token $GITEA_TOKEN"
```

### Merge PR（Review 通过后）
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}/merge" \
  -H "Authorization: token $GITEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"Do": "merge", "merge_title_field": "Merge PR #{pr_number}"}'
```

### 查看 CI 状态
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/commits/{sha}/status" \
  -H "Authorization: token $GITEA_TOKEN"
# .state = "success" | "pending" | "failure" | "error"
```
```

##### B. 审查者（司马懿）— Review 操作

```markdown
### 读取 PR diff
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}.diff" \
  -H "Authorization: token $GITEA_TOKEN"
```

### 查看 PR 改动文件列表
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}/files" \
  -H "Authorization: token $GITEA_TOKEN"
# 每个文件有 .filename, .additions, .deletions, .changes
```

### 提交 Review
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}/reviews" \
  -H "Authorization: token $GITEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "{审查意见}",
    "event": "APPROVED"
  }'
# event 可选: APPROVED | REQUEST_CHANGES | COMMENT
# ⚠️ 注意：是 APPROVED（过去式），不是 APPROVE
```

### 风险级别判定（自动 + 确认）
规则见 code-review Skill。改动者不能降级，只能维持或升级。
```

##### C. 协调者（庞统）— 全套管理

```markdown
### 创建 Issue + 指派
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/issues" \
  -H "Authorization: token $GITEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "{标题}",
    "body": "{描述}",
    "assignees": ["{agent_id}"],
    "labels": [1, 2]
  }'
# labels 需用数字 ID，先 GET /repos/{owner}/{repo}/labels 查询
```

### 查询仓库 Labels
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/labels" \
  -H "Authorization: token $GITEA_TOKEN"
```

### 查询 PR 列表
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls?state=open" \
  -H "Authorization: token $GITEA_TOKEN"
```

### 创建 Release
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/releases" \
  -H "Authorization: token $GITEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tag_name": "v{version}",
    "name": "v{version}",
    "body": "{changelog}",
    "target_commitish": "main"
  }'
```

### 关闭 Issue
```bash
curl -X PATCH "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/issues/{issue_number}" \
  -H "Authorization: token $GITEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"state": "closed"}'
```
```

##### D. 平台运维（姜维）— deploy + runner 管理

在开发者模板基础上追加：

```markdown
### 触发手动部署（需要 push 到 main 或手动 re-run）
```bash
# Re-run 最近一次 workflow
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/actions/runs/{run_id}/rerun" \
  -H "Authorization: token $GITEA_TOKEN"
```

### 查看 deploy-history
```bash
cat ~/.sanguo_projects/{project}/data/deploy-history.jsonl
```

### deploy.sh 规范
见 §17.5 deploy.sh 设计。所有项目必须遵循统一接口。
```

#### §17.3.4 实施清单

| Agent | TOOLS.md 新增 | 预计行数 |
|-------|--------------|---------|
| zhangfei-dev | 开发者模板（PR/Merge/CI状态） | ~30 行 |
| guanyu-dev | 开发者模板 | ~30 行 |
| zhaoyun-data | 开发者模板 | ~30 行 |
| simayi-challenger | 审查者模板（diff/review/风险判定） | ~40 行 |
| pangtong-fujunshi | 协调者模板（全套管理） | ~50 行 |
| jiangwei-infra | 开发者 + 平台运维模板 | ~45 行 |

---

### §17.4. L2：Skill 升级到 v2

#### §17.4.1 升级原则

1. **不重写已有内容**，在现有 Skill 基础上追加/修改
2. **对齐事件中枢**：每个 Skill 说明在事件中枢链路中的位置
3. **对齐 CI 实际运行**：ci.yml/deploy.yml 已有实际配置，Skill 要反映真实情况
4. **对齐 Gitea v1.23.4 限制**：不支持 failure()、concurrency、permissions 等

#### §17.4.2 升级内容

##### git-workflow（小改动）

| 改动 | 说明 |
|------|------|
| 追加"事件中枢集成"节 | PR 创建 → Gitea Webhook → 中枢通知司马懿 → Review → 中枢通知作者 → Merge → deploy.yml 自动触发 |
| 追加"CI 自动触发"说明 | push 非 main 分支自动触发 ci.yml；push main 自动触发 deploy.yml |
| 追加"分支感知"强化 | Agent spawn 后必须 `git branch --show-current`，确认分支正确 |

##### code-review（中等改动）

| 改动 | 说明 |
|------|------|
| 追加"事件中枢触发"节 | 收到 Mail → 读 PR diff → 审查 → 提交 Review → Webhook 自动通知作者 |
| 更新风险判定规则 | 对齐 §6.1 实际规则（按文件路径自动判定 + 只升不降） |
| 追加 Gitea Review API 操作 | curl 模板（和 L1 TOOLS.md 一致，这里放流程说明） |
| 追加审查结论格式 | APPROVE / REQUEST_CHANGES 的标准格式 |

##### testing-workflow（小改动）

| 改动 | 说明 |
|------|------|
| 追加"CI 集成"节 | UT 在 CI 自动跑（ci.yml test job）、coverage 在 deploy.yml ci job 跑 |
| 追加"E2E 触发方式" | 通过 e2e.yml 手动触发或 `RUN_INTEGRATION=1 pytest` 本地跑 |
| 追加"测试数据隔离" | CI 使用临时 venv + 临时 SQLite + 临时端口 |
| 追加"广播风暴禁止" | **禁止在 daemon 运行时跑含创建项目/Task/Mail 的测试**，否则会触发 Agent spawn 导致广播风暴。E2E 测试必须在 CI 隔离环境或 daemon 停止后跑 |

##### bugfix-workflow（小改动）

| 改动 | 说明 |
|------|------|
| 追加"事件中枢链路"节 | Bug Issue 创建/指派 → 中枢发 Mail → 修复 → PR → CI → Review → merge |
| 追加"CI 验证"步骤 | 修复后必须等 CI 通过再创建 PR |

##### hotfix-workflow（小改动）

| 改动 | 说明 |
|------|------|
| 追加"CI 自动跑"说明 | hotfix push main → deploy.yml 自动跑 CI + 部署 |
| 追加"失败自动创建 Issue" | deploy.yml notify-on-failure 已实现 |
| 更新 24h 复盘流程 | 复盘结论写到 Issue 评论中 |

##### ci-cd-ops（重写）

| 改动 | 说明 |
|------|------|
| 重写为 v2 | 对齐实际 ci.yml/deploy.yml 结构 |
| 新增 Gitea v1.23.4 限制清单 | 不支持 failure()/concurrency/permissions 等，workaround 方案 |
| 新增覆盖率渐进策略 | P1 只报告 → P2 40% 阈值 → P3 60% 阈值 |
| 新增 deploy.sh 规范 | 统一接口：--version / --source / --target / --health-check / --rollback |
| 新增 CI secret 配置 | CI_TOKEN 作为 repository secret 配置 |

##### release-workflow（中等改动）

| 改动 | 说明 |
|------|------|
| 追加"自动部署触发" | tag 创建 → deploy.yml 自动触发 |
| 追加 deploy-history.jsonl 规范 | 每次 deploy 记录 tag + commit + 时间戳 |
| 更新 schema 变更规范 | 向前兼容 Checklist（加列不加删、默认值、迁移脚本） |

#### §17.4.3 不新建 Skill

现有 7 个 Skill 覆盖所有工具链流程。不需要为事件中枢、CI 操作新建 Skill——这些流程固化在 Mail 模板（§16.4.3）中，Skill 只提供共识功能。

#### §17.4.4 实施清单

| Skill | 改动级别 | 预计改动行数 |
|-------|---------|------------|
| git-workflow | 小 | +15 行 |
| code-review | 中 | +40 行 |
| testing-workflow | 小 | +25 行（含广播风暴禁止约束） |
| bugfix-workflow | 小 | +15 行 |
| hotfix-workflow | 小 | +15 行 |
| ci-cd-ops | 重写 | ~120 行（原 114 行） |
| release-workflow | 中 | +30 行 |

---

### §17.5. deploy.sh + deploy.yml 补完

#### §17.5.1 deploy.sh 统一接口

每个项目的 `scripts/deploy.sh` 必须遵循以下接口：

```bash
#!/bin/bash
# scripts/deploy.sh — 项目部署脚本
# 用法:
#   bash scripts/deploy.sh --version              # 显示当前版本
#   bash scripts/deploy.sh --source=DIR --target=DIR --health-check  # 部署
#   bash scripts/deploy.sh --rollback             # 回滚到上一版本

set -euo pipefail

SOURCE_DIR=""
TARGET_DIR=""
HEALTH_CHECK=false
ACTION="deploy"

for arg in "$@"; do
    case $arg in
        --version) ACTION="version" ;;
        --source=*) SOURCE_DIR="${arg#*=}" ;;
        --target=*) TARGET_DIR="${arg#*=}" ;;
        --health-check) HEALTH_CHECK=true ;;
        --rollback) ACTION="rollback" ;;
    esac
done

PROJECT_NAME="{project_name}"
DEPLOY_HISTORY="${TARGET_DIR}/data/deploy-history.jsonl"

version() {
    echo "${PROJECT_NAME} deploy version: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
}

deploy() {
    echo "=== Deploying ${PROJECT_NAME} ==="
    echo "Source: ${SOURCE_DIR}"
    echo "Target: ${TARGET_DIR}"

    # 1. 同步文件（排除不需要部署的）
    # ⚠️ --delete 会删除目标中源没有的文件，必须排除 data/（生产数据）
    rsync -av --delete \
        --exclude='.git' \
        --exclude='__pycache__' \
        --exclude='.venv' \
        --exclude='data' \
        --exclude='tests' \
        --exclude='docs' \
        --exclude='.gitea' \
        --exclude='node_modules' \
        "${SOURCE_DIR}/" "${TARGET_DIR}/"

    # 2. 安装依赖
    if [ -f "${TARGET_DIR}/pyproject.toml" ]; then
        cd "${TARGET_DIR}"
        python3 -m venv .venv
        .venv/bin/pip install --quiet -e ".[dev]" 2>/dev/null || \
        .venv/bin/pip install --quiet -e . 2>/dev/null || true
    fi

    # 3. 重启服务
    if command -v pm2 &>/dev/null; then
        pm2 restart ${PROJECT_NAME} 2>/dev/null || true
    fi

    # 4. 健康检查
    if [ "$HEALTH_CHECK" = true ]; then
        sleep 3
        curl -sf http://localhost:8083/api/health && echo " ✓" || {
            echo " ✗ Health check failed!"
            exit 1
        }
    fi

    # 5. 记录版本（rollback 时可通过 DEPLOY_OVERRIDE_COMMIT 覆盖）
    local commit_hash
    if [ -n "${DEPLOY_OVERRIDE_COMMIT:-}" ]; then
        commit_hash="${DEPLOY_OVERRIDE_COMMIT}"
    else
        commit_hash=$(cd "${SOURCE_DIR}" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
    fi
    local timestamp
    timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    echo "{\"timestamp\": \"${timestamp}\", \"commit\": \"${commit_hash}\", \"source\": \"${SOURCE_DIR}\"}" >> "${DEPLOY_HISTORY}"

    # 保留最近 10 条
    tail -10 "${DEPLOY_HISTORY}" > "${DEPLOY_HISTORY}.tmp" && mv "${DEPLOY_HISTORY}.tmp" "${DEPLOY_HISTORY}"

    echo "=== Deploy complete: ${commit_hash} at ${timestamp} ==="
}

rollback() {
    if [ ! -f "${DEPLOY_HISTORY}" ]; then
        echo "No deploy history, cannot rollback"
        exit 1
    fi

    # 读取倒数第二行的 commit（不依赖 SOURCE_DIR 的 git 状态）
    local prev_line
    prev_line=$(tail -2 "${DEPLOY_HISTORY}" | head -1)
    local prev_commit
    local prev_source
    prev_commit=$(echo "${prev_line}" | python3 -c "import sys,json; print(json.load(sys.stdin)['commit'])" 2>/dev/null)
    prev_source=$(echo "${prev_line}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('source',''))" 2>/dev/null)

    if [ -z "${prev_commit}" ] || [ "${prev_commit}" = "unknown" ]; then
        echo "Cannot determine previous version"
        exit 1
    fi

    echo "=== Rolling back to ${prev_commit} ==="
    # checkout 到指定 commit，部署，然后回到 main
    cd "${SOURCE_DIR}"
    local current_branch
    current_branch=$(git branch --show-current)
    git checkout "${prev_commit}"
    # 部署时明确传 commit hash，不依赖 HEAD
    DEPLOY_OVERRIDE_COMMIT="${prev_commit}" deploy
    git checkout "${current_branch:-main}"
}

case $ACTION in
    version) version ;;
    deploy) deploy ;;
    rollback) rollback ;;
esac
```

#### §17.5.2 deploy.yml 更新

替换当前 placeholder：

```yaml
  deploy:
    runs-on: macos-arm64
    needs: ci
    steps:
      - uses: actions/checkout@v4

      - name: Deploy
        env:
          CI_TOKEN: ${{ secrets.CI_TOKEN }}
        run: |
          bash scripts/deploy.sh --source="$GITHUB_WORKSPACE" \
            --target="$HOME/.sanguo_projects/sanguo_moziplus_v2" \
            --health-check

      - name: Rollback on failure
        if: always()
        env:
          CI_TOKEN: ${{ secrets.CI_TOKEN }}
        run: |
          STATUS=$(curl -sf \
            -H "Authorization: token $CI_TOKEN" \
            "${{ gitea.api_url }}/repos/${{ gitea.repository }}/commits/${{ gitea.sha }}/status" \
            | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))" 2>/dev/null || echo "")
          if [ "$STATUS" != "success" ]; then
            echo "Deploy failed, rolling back..."
            bash scripts/deploy.sh --rollback || echo "Rollback failed, manual intervention needed"
          fi
```

#### §17.5.3 实施清单

| # | 内容 | 文件 | 说明 |
|---|------|------|------|
| D1 | 创建 deploy.sh | moziplus-v2 `scripts/deploy.sh` | 从模板创建，项目名填 sanguo_moziplus_v2 |
| D2 | 更新 deploy.yml | moziplus-v2 `.gitea/workflows/deploy.yml` | 替换 placeholder |
| D3 | 同步到 Gitea | push 到 moziplus-v2 | 触发 CI 验证 |

---

### §17.6. 端到端验证：sanguo/moziplus-v2 实验项目

#### §17.6.1 为什么用实验项目

- moziplus-v2 主项目已有正式数据（黑板项目、Mail、Task），不适合做破坏性验证
- sanguo/moziplus-v2 当前是空项目（只有 README.md + .gitea/workflows），可以随意实验

#### §17.6.2 验证场景

| 场景 | 操作 | 预期结果 |
|------|------|---------|
| **S1: CI 触发** | push 分支到实验项目 | ci.yml 自动跑 lint + test |
| **S2: PR Review 流程** | 创建 PR → 中枢 Mail → 司马懿 Review → 中枢通知作者 | 全链路 Mail 通知 |
| **S3: CI 失败通知** | push 含 lint 错误的代码 | ci.yml 失败 → 写 PR 评论 → 中枢发 Mail |
| **S4: 部署流程** | merge PR → push main | deploy.yml 自动跑 + 部署 + 健康检查 |
| **S5: Issue 指派** | 创建 Issue 并指派 | 中枢发 Mail 给被指派人 |
| **S6: 幂等验证** | 重复触发同一 delivery_id Webhook | 第二次返回 200 duplicate，不创建重复 Mail |

#### §17.6.4 P3 端到端验证结果（2026-06-08）

| 场景 | 结果 | 说明 |
|------|------|------|
| **S1: CI 触发** | ✅ | push test/p3-e2e-s1 → ci.yml 触发。首次因 act-runner 离线未触发，姜维修复后积压任务自动执行 |
| **S2: PR Review 流程** | ✅ | PR opened → 中枢发 Review 请求 Mail 给 simayi-challenger ✅。Review APPROVED → 中枢发 Mail 给 PR 作者 ✅（Gitea 1.23.4 支持此 webhook） |
| **S3: CI 失败通知** | ✅ | [CI] 评论 → 中枢发 Mail 给 PR作者 ✅ |
| **S4: 部署流程** | ⏭️ 跳过 | 依赖 CI 先跑通，未单独验证 |
| **S5: Issue 指派** | ✅ | Issue 指派 zhangfei-dev → 收到 Mail ✅ |
| **S6: 幂等验证** | ✅ | 同 delivery_id 第二次返回 `duplicate`，不创建重复 Mail ✅ |

##### 调研发现

**发现 1：Gitea Review API event 枚举值**

Gitea ReviewStateType 枚举值应为 `APPROVED`（不是 `APPROVE`）。使用错误的枚举值会创建 PENDING review，且 PENDING 不触发 webhook 通知。

- 正确：`{"event": "APPROVED"}`
- 错误：`{"event": "APPROVE"}` → 创建 PENDING review，webhook 不触发

已修正：TOOLS.md 模板 + code-review Skill 中的 event 值。

**发现 2：Gitea 1.23.4 支持 PullRequestReview webhook**

之前误判为"不支持"，实际原因同发现 1——错误的枚举值创建了 PENDING review，PENDING 不在 webhook switch 分支中导致报 "Unsupported review webhook type"。使用正确的 APPROVED event 后 webhook 正常触发。

daemon webhook handler 已正确覆盖：
- `pull_request_review` 事件 → `_handle_pull_request_review()`
- 只处理 APPROVED / REQUEST_CHANGES，跳过 COMMENTED 和 PENDING
- 通知对象：PR 作者

**发现 3：act-runner 进程管理**

act-runner 未纳入进程管理，崩溃后不会自恢复。姜维已修复：
- 纳入 PM2 托管（进程名：sanguo-act-runner，id=44）
- 启动命令：`pm2 start ~/bin/act_runner --name sanguo-act-runner --cwd ~/.config/act-runner/ -- daemon --config ~/.config/act-runner/config.yaml`
- 崩溃自动重启 ✅
- 开机自启：依赖 pm2 startup（需 sudo 配置 launchd）

#### §17.6.3 实验项目 CI 配置

从 moziplus-v2 的 ci.yml 精简，实验项目只需要基本验证：

```yaml
name: CI
on:
  push:
    branches: ['**', '!main']
  pull_request:
    types: [opened, synchronize]

jobs:
  lint:
    runs-on: macos-arm64
    steps:
      - uses: actions/checkout@v4
      - run: echo "lint placeholder"

  test:
    runs-on: macos-arm64
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - run: echo "test placeholder"
```

> **注意**：主项目 ci.yml/deploy.yml 的 runs-on 已从 `ubuntu-latest` 改为 `macos-arm64`，对齐 act-runner 实际 label。deploy.yml 的 placeholder 也已替换为实际 deploy.sh 命令。

---

### §17.7. 覆盖率渐进策略

#### §17.7.1 P1 阶段（启用后 2 周）— 只报告不阻断

**零改动**。当前 deploy.yml 的 ci job 已包含 `--cov=src --cov-report=term-missing`（push main 时自动跑）。

注意：ci.yml（非 main 分支）没有 coverage 配置——快速门控不需要覆盖率，这是有意为之。

收集基线数据后决定 P2 阈值。

#### §17.7.2 后续阶段（仅设计，暂不实施）

| 阶段 | 时间 | 策略 | 触发方式 |
|------|------|------|---------|
| P2 | 启用后 1 月 | 40% 阈值，低于警告不阻断 | ci.yml 加 coverage threshold check |
| P3 | 启用后 2 月+ | 60% 阈值，低于阻断 | ci.yml 加 `exit 1` |

---

### §17.8. 前端展示

#### §17.8.1 Gitea 自带管理界面

Gitea v1.23.4 自带完整的 CI 管理界面：

| 功能 | URL |
|------|-----|
| CI Runs 列表 | `http://192.168.2.154:3000/sanguo/{repo}/actions` |
| 单次 Run 日志 | `http://192.168.2.154:3000/sanguo/{repo}/actions/runs/{id}` |
| PR CI Status | PR 页面自动显示 CI 状态徽章 |
| Webhook 管理 | 仓库 Settings → Webhooks |

**不需要自己做 CI 前端。**

#### §17.8.2 moziplus v2 前端展示

工具链事件的 Mail 通知已在 moziplus v2 前端展示（Mail 列表页）。

如果未来要加，唯一值得做的是：在 moziplus 前端加一个「工具链状态」面板，聚合展示各仓库最近 CI 状态。这是 P4，不阻塞使用。

---

### §17.9. 实施路线

| 优先级 | 内容 | 耗时 | 前置 |
|--------|------|------|------|
| **P1** | L1 TOOLS.md（6 个 Agent） | 2h | 无 |
| **P1** | L2 Skill 升级（7 个） | 1d | 无 |
| **P2** | deploy.sh + deploy.yml | 2h | P1 Skill 升级（ci-cd-ops 定义了 deploy.sh 规范） |
| **P3** | sanguo/moziplus-v2 端到端验证 | 2h | P1 + P2 |
| **P4** | 前端工具链状态面板 | 按需 | 不阻塞 |

---

### §17.10. 前置条件 Checklist

工具链投入使用前必须确认：

| # | 条件 | 状态 | 谁确认 |
|---|------|------|--------|
| 1 | act-runner 已注册且 label = `macos-arm64` | ✅ PM2 托管（sanguo-act-runner, id=44），崩溃自动重启 | 姜维确认 |
| 2 | Gitea repository secrets 已配置（CI_TOKEN） | ✅ 姜维确认（sanguo/moziplus-v2 已配 CI_TOKEN） | 姜维 |
| 3 | Gitea 组织级 Webhook 已启用（Hook ID=28） | ✅ 已确认 | 已确认 |
| 4 | 各 Agent 的 GITEA_TOKEN 环境变量 | ✅ 已写入各 Agent TOOLS.md，姜维确认 token 记录存在 | 庞统+姜维 |
| 5 | main 分支保护规则（Review 才能 merge） | ✅ 姜维已配置（moziplus-v2 + sanguo_moziplus_v2，需1个approve） | 姜维 |
| 6 | 禁止在 daemon 运行时跑全量 E2E | ✅ 已警告司马懿 | 已确认 |

> 第 5 点很关键——如果 main 分支没有保护规则，开发者可以直接 push main 跳过 Review。

> act-runner 已于 2026-06-08 纳入 PM2 托管（姜维），崩溃自动重启 + pm2 save 已保存。开机自启依赖 pm2 startup（需 sudo 配置 launchd），P4 完成。

---

### §17.11. 评审记录（原 §14 评审）

#### v1.0 → v1.1 修订清单（仲达评审）

| 编号 | 类型 | 问题 | 修订内容 |
|------|------|------|----------|
| M1 | 必须修 | rsync --delete 会删 data/ | §17.5.1 --exclude 加 `data` + `node_modules` |
| M2 | 必须修 | runs-on 与实际环境不一致 | 实际 ci.yml/deploy.yml 已用 `macos-arm64`，CI 已跑通，属仲达误判。文档 §17.6.3 已明确说明 |
| S1 | 建议 | rollback commit 获取有竞态 | §17.5.1 rollback 改用 DEPLOY_OVERRIDE_COMMIT 显式传递，不依赖 git HEAD |
| S2 | 建议 | pip install 缺 lock file | 采纳但 P3，当前单一部署环境风险低 |
| S3 | 建议 | 缺前置条件 checklist | 新增 §17.10 前置条件 Checklist |
| S4 | 建议 | 验证场景补 S6 幂等 | §17.6.2 新增 S6 幂等验证场景 |
| — | 评审 | 庞统模板缺关闭 Issue curl | §17.3.3 C 节补关闭 Issue 模板 |
| — | 评审 | testing-workflow 加广播风暴禁止 | §17.4.2 testing-workflow 补充约束 |
| — | 评审 | ci-cd-ops 行数估算措辞 | 修正：重写后预计 ~120 行（原 114 行） |
| — | 评审 | 覆盖率 P1 前提需确认 | §17.7.1 明确指出 deploy.yml 已有 coverage，ci.yml 有意不加 |

---

### v3.0 → v3.1 变更（P3 端到端验证 + 调研结论）

| 编号 | 变更内容 |
|------|----------|
| §17.3.3 | Review API event 枚举值修正：APPROVE → APPROVED |
| §17.6.3 | 注意更新：runs-on 已改为 macos-arm64，placeholder 已替换 |
| §17.6.4 | 新增 P3 端到端验证结果（S1-S6 逐项） |
| §17.6.4 | 新增调研发现：Review API 枚举值、PullRequestReview webhook 支持、act-runner PM2 托管 |
| §17.10 | #1 状态更新：act-runner 已纳入 PM2 托管 |

### v3.1 → v3.2 变更（工具链修复 + Mail 投递 bug 修复）

| 编号 | 变更内容 |
|------|----------|
| §16.4 | Review handler 双格式兼容：HANDLERS 注册表同时注册 `pull_request_review` / `pull_request_approved` 等多种事件名；`_handle_pull_request_review` 兼容 repo webhook（review.state/body/user）和 org webhook（review.type/content/sender）两种 payload 格式 |
| §16.8 #10 | Gitea v1.23.4 review payload 调研结论（姜维 2026-06-08）：Gitea v1.23.4 review payload 只有 `type` + `content`，没有 `state`/`body`/`user`，这不是 org vs repo 差异而是 Gitea 设计。v1.24.0 格式不变。双格式兼容是防御性编码，保持现状 |
| §16.8 #11 | Spawner compact 检测窗口修复：窗口 300s→900s，尾部读取 50KB→1MB。实测长对话中 compact 记录被推出窗口导致漏检 |
| §16.8 #12 | inform 类型 Mail crash 误标 done bug 修复：`_mail_auto_complete` 增加 outcome 感知，inform 用白名单（completed/claimed/no_reply）控制 done 标记。spawner crash cooldown 300s→60s |
