# 周复盘机制 · 架构

## 定位

每周六早 09:03（Asia/Shanghai）自动拉一周开发数据 → 生成 Gitea issue → AI 给出候选改进 → 人选定 + 执行 → 关 issue 闭环。

> **为什么选周六**：报告周六早上出来，团队周末有时间消化 + 讨论改进项，周一开工就能直接动手；放周一会挤掉一上午的工作时间。

**核心 invariant**：Gitea issue 的 `open / close` 状态本身即闭环载体。不再造文件级 closure tracker。

## 目标架构（target state）

skill 是编排层，yaml 退化为单步触发器；底层脚本是确定性工具，被 skill 通过 subprocess 调用。

```
触发入口（三选一，殊途同归）
  ① Gitea cron         周六 09:03 +0800（生产入口，自动）
  ② workflow_dispatch  Gitea Web UI 手动（验证 / 紧急触发）
  ③ @weekly-retro      Claude 会话内手动（开发期 / 测试 / 重跑）

         (① ②)                            (③)
            │                               │
            ▼                               │
  .gitea/workflows/weekly-retro.yml         │
    └─ 工作步骤：bash weekly-retro-runner.sh │
       （前置 step: checkout / setup-python / │
        verify claude，3 步纯准备）           │
         └─ claude -p (runner mode)         │
                  │                         │
                  ▼                         │
       .agents/skills/weekly-retro/SKILL.md ◄┘
            （orchestrator + LLM 分析）
                  │
       ┌──────────┼──────────┐
   subprocess    HTTP       LLM
       │          │          │
       ▼          ▼          ▼
  weekly-       Gitea     pattern +
  review.py     API       候选改进
  (拉数据)      (issues,    生成
       │       labels)
       ▼
  testing/reports/retros/YYYY-Www.md
  （事实-only markdown）
                  │
                  ▼
  Gitea Issue（label: weekly-retro）
    title : 周复盘 YYYY-Www
    body  : 事实 + 候选改进
    author: AIBot
                  │
                  ▼
                 人
                  ├─ 评论选定项 ──► Issue
                  └─ 做完关闭 ───► state = closed = 闭环
```

## 决策树（skill 进入后的分支）

skill 启动后第一件事是判断当前 ISO 周的 weekly-retro issue 状态，按下表分流。

```
skill 启动
   │
   ▼
本 ISO 周已有 weekly-retro issue ?
   │
   ├─ NO ──► CREATE
   │           └─ 跑 review.py + LLM 分析
   │              └─ POST issue（body = 事实 + 候选）
   │
   └─ YES
        │
        ▼
   body 含「候选改进」段 ?
        │
        ├─ YES ──► NOOP（什么都不做退出）
        │
        └─ NO
             │
             ▼
        有用户评论 ?
             │
             ├─ NO  ──► REPAIR
             │           └─ LLM 分析 + PATCH body 拼候选段
             │              （上次跑到一半挂了，自愈）
             │
             └─ YES ──► WARNING
                         └─ POST 警告评论，不动 body
                            （用户已 engage，body 必须冻结）
```

| 分支 | 触发条件 | 动作 | 为什么 |
|---|---|---|---|
| **CREATE** | 本周还没 issue | 全流程：脚本 + 分析 + POST | 周六例行 fresh start |
| **NOOP** | 已存在且完整 | 什么都不做退出 | 已经分析过了，幂等 |
| **REPAIR** | 存在但缺候选段 + 没人评论过 | PATCH body 拼候选段 | 上次跑到一半挂了，自愈 |
| **WARNING** | 缺候选段 + 已有评论 | 评论警告，不动 body | 用户已 engage，body 必须冻结 |

## 单周生命周期

```
时间                       操作                                状态
─────────────────────────────────────────────────────────────────────
周六 09:03 +0800   ───►   Gitea cron 触发 workflow           [open]
                            │
                            ├─ subprocess weekly-review.py
                            │     → testing/reports/retros/YYYY-Www.md
                            │
                            ├─ GET Gitea /issues?labels=weekly-retro
                            │     → 没找到本周 issue
                            │
                            ├─ LLM 分析 → ≥3 候选改进
                            │
                            └─ POST issue（body = 事实 + 候选）
                                                ↓
                                          Gitea 主页可见

任意时刻           ───►   你打开 Gitea，读 issue              [open]
                            ├─ 读 candidates
                            └─ 评论 "做 1 + 3"

本周内             ───►   做工作 + 链相关 PR                  [open]

完成时             ───►   关闭 issue                         [closed]
                                                              = 闭环 ✓

下周六 09:03       ───►   cron 创建 W+1 新 issue              [open]
                          （上周 closed issue 入历史，可
                           GET issues?state=closed&labels=weekly-retro 查）
```

## 数据流（数据从哪来 / 到哪去）

```
数据源                       采集层                  产出
─────────────────────────────────────────────────────────────────
git log origin/develop  ─┐
Gitea /pulls             │
Gitea /issues            ├──► weekly-review.py ──► YYYY-Www.md
Gitea /actions/runs      │     (拉数据，纯函数)      （事实 only）
Gitea /branches          │                                  │
.learnings/ 本周新增 md   ─┘                                 │
                                                            │
                          ┌─────────────────────────────────┤
                          ▼                                 │
             Issue body 上半段                              │
             （事实 sections 1-7）                          │
                          │                                 ▼
                          │                          LLM 分析
                          │                                 │
                          │                                 ▼
                          │                       Issue body 下半段
                          │                       （候选改进段）
                          │                                 │
                          └─────────────┬───────────────────┘
                                        ▼
                           Gitea Issue（state = open）

  用户互动 ──► 评论 + 做工作 ──► 关 issue ──► state = closed ＝ 闭环
```

## 角色与权限

| 角色 | 是谁 | 用途 | Token |
|---|---|---|---|
| **AIBot** | Gitea 用户 `AIBot`（专用 bot 账号） | 创建 issue、改 issue body、发评论 | `WEEKLY_RETRO_TOKEN` (repo secret) |
| **act_runner** | self-hosted Gitea Action runner（dedicated-runner-1） | 跑 workflow 步骤、claude CLI | runner 镜像内置（见 ai-review-runner 同款） |
| **用户** | 项目 dev | 决定本周做哪些候选项、做工作、关 issue | 无（操作通过 Gitea web UI） |

**注意**：bot 不能跨 scope——只有 repo 内 `issues:write` + `repository:write`，碰不到生产 / 部署 / 数据库。撤销 bot 等于撤销整个自动化，blast radius 可控。

## 关键设计决策（图里看不到的）

| 决策 | 为什么 |
|---|---|
| **数据用脚本，分析用 LLM** | 数据采集是确定性、纯函数——脚本快稳便宜；pattern + 候选生成需要语义判断——LLM 必须 |
| **body 一次性写入，不再反复 PATCH** | 用户基于 candidates 做选择后不能被覆盖；只有自愈（repair）才会改 body |
| **完整性检查只看一段（"## 候选改进"）** | 单 marker 简单，避免 schema 校验过度工程；现实失败 99% 是 "skill 跑到一半崩了"，单 marker 就够覆盖 |
| **用户评论后停止改 body** | 评论是人类 engagement 信号；body 必须冻结，否则用户的"做 1+3"会跟新 candidates 对不上 |
| **issue 关闭 = 闭环** | 用 Gitea 原生 lifecycle，不自造 closure tracker |
| **AIBot 而非个人 PAT** | 审计干净（grep author 一目了然）+ 撤销可控 + 去掉 PR 自审批 dance（后续） |
| **title 只含 ISO 周不含日期范围** | ISO 周和 "now" 无关，幂等键稳定（见 [learning](.learnings/2026-05-10-rolling-window-title-idempotency-trap.md)） |

## 实现状态

| 组件 | 文件 | 角色 |
|---|---|---|
| 数据采集 | `scripts/ops/weekly-review.py` | 拉 git + Gitea API → 事实型 markdown |
| Issue 工具（standalone） | `scripts/ops/weekly-retro-issue.py` | 兜底/手工创建 issue；body 含 `## 候选改进` 时自动 noop |
| Workflow | `.gitea/workflows/weekly-retro.yml` | 单步：cron / dispatch → `bash weekly-retro-runner.sh` |
| Runner 包装 | `scripts/ops/weekly-retro-runner.sh` | `claude -p` 调用 + token 兜底链 + dry-run flag |
| Skill（orchestrator） | `.agents/skills/weekly-retro/SKILL.md` | runner mode + 决策树 4 分支（CREATE/NOOP/REPAIR/WARNING） |
| Bot 账号 | Gitea 用户 `AIBot` | 自动化身份（评论/创建 issue 的 author） |
| Secret | repo `WEEKLY_RETRO_TOKEN` | bot PAT，scope: `write:repository` + `write:issue` |

整套机制由 [PR #281](http://43.130.59.228/FFAIWorkspace/workspace/pulls/281) 落地（架构 doc + skill 升级 + runner 一并进入 develop）。

## 验证方式

### 端到端

```bash
# 手动触发 workflow
curl -X POST -H "Authorization: token $GITEA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ref":"develop","inputs":{"since-days":"7"}}' \
  http://43.130.59.228/api/v1/repos/FFAIWorkspace/workspace/actions/workflows/weekly-retro.yml/dispatches

# 看 run 状态
curl -fsS -H "Authorization: token $GITEA_API_TOKEN" \
  http://43.130.59.228/api/v1/repos/FFAIWorkspace/workspace/actions/runs?limit=5 | jq

# 看 weekly-retro 当前 issue
curl -fsS -H "Authorization: token $GITEA_API_TOKEN" \
  "http://43.130.59.228/api/v1/repos/FFAIWorkspace/workspace/issues?state=open&labels=weekly-retro&type=issues"
```

### 本地 dry-run

```bash
# 数据脚本
python3 scripts/ops/weekly-review.py --print | head

# Issue 脚本（dry-run）
python3 scripts/ops/weekly-retro-issue.py --dry-run
```

## 故障排查

| 症状 | 定位 |
|---|---|
| cron 没触发 | 看 `nightly-snapshot-check.yml` 是否也没跑（Gitea 实例 schedule 全局问题）；登 Gitea Actions runner 看 `act_runner` 服务状态 |
| issue body 不含候选段 | runner 步骤崩了——拉 job logs `GET /actions/jobs/{id}/logs` 看 claude 调用失败原因；通常是 claude 没登录或 token 不对 |
| 创建了重复 issue | 见 [learning](.learnings/2026-05-10-rolling-window-title-idempotency-trap.md)——title 应该只含 ISO 周 |
| 候选改进绑了不存在的 PR/run id | LLM 幻觉——用 `--dry-run` 在本地复现，调 prompt |

## 相关 learnings

- [`2026-05-09-gitea-actions-runs-api-field-quirks.md`](../../.learnings/2026-05-09-gitea-actions-runs-api-field-quirks.md)：`/actions/runs` API 字段坑
- [`2026-05-10-gitea-secret-pat-api-opaqueness.md`](../../.learnings/2026-05-10-gitea-secret-pat-api-opaqueness.md)：Gitea Secret/PAT API 不透明
- [`2026-05-10-rolling-window-title-idempotency-trap.md`](../../.learnings/2026-05-10-rolling-window-title-idempotency-trap.md)：滚动窗口 title 不能做幂等键
- [`2026-05-10-resvg-js-svg-rasterize-fallback.md`](../../.learnings/2026-05-10-resvg-js-svg-rasterize-fallback.md)：SVG→PNG 兜底（用于 bot 头像）
