---
name: os-release
description: 发布 AI Team OS 新版本的完整清单——预检、版本七处锁步、中英双语 CHANGELOG、双份 dist 构建、私有术语扫描、commit/tag、双仓推送、建 GitHub Release 条目并核对 latest 徽章、事后核对。当准备发版、补建漏掉的 Release 条目、或核对已发版本的线上状态时使用。
---

# OS Release — 发版清单

## 为什么有这份清单

机检管得住的部分不用你记，跑一条命令即可。这份清单只管**机检管不住的部分**，以及
**谁按哪个按钮**。

历史上同型漏项两次：**tag 推了、GitHub Release 条目没建**，访客主页的 latest 徽章
因此卡在旧版本两周。所以别把"tag 推完"当成发版完成——第 10 步才是。

## 执行分工（硬约束：发布流水线必须可中断）

| 步骤 | 谁执行 |
| --- | --- |
| 1–6 准备与校验 | Leader 全权 |
| 7 commit · 8 tag | Leader 执行，但**先把 `git diff --stat` 与拟好的 message 交用户批准** |
| 9 双仓推送 · 10 建 Release 条目 | **用户执行**。Leader 只把命令准备好打印出来 |
| 11 事后核对 | Leader（只读） |

不要代替用户 push / publish，也不要在未批准前 commit。

---

## 1. 预检全绿

```bash
bash scripts/preflight.sh          # 发版必须跑全量，不要 --fast
```

五道门禁：`ruff check src/ tests/` → `dashboard npm run lint` → 前端实时事件回归
（`npm --prefix dashboard test`）→ `check_invariants.sh` → `pytest tests/unit/`。
**机检条目数以脚本输出为准**，别在别处写死范围（当前到 I21）。

失败长这样：每项后面跟 `✗ <门禁名> 失败`，末尾 `✗ 预检未通过 — 修复后再 push`。
退出码非 0。

两处**既定豁免**：

- 未构建 `dashboard/dist` 的环境里 I3 输出 `⚠️ dist 目录缺失（未构建环境可忽略）`
  ——警告不拦。但发版必须构建（见第 4 步），所以发版时这条不该还是警告。
- I16 在 execpolicy rules 或 Codex 二进制缺席时输出 `⚠️ [I16] Codex execpolicy（...）`
  ——本期是占位状态，warn 不拦。它转成 ok 的那一天要留意：那意味着这台机器上真的跑了
  一次宿主校验，结论才有分量。

## 2. 版本锁步七处

**I2 机检的五处**（漂移即红，`❌ [I2] 版本号漂移: ...`）：

- `pyproject.toml`
- `src/aiteam/__init__.py`
- `plugin/.claude-plugin/plugin.json`
- `plugin/.claude-plugin/marketplace.json`
- `.claude-plugin/marketplace.json`

**另两处由 I6 第①类硬等式盯**：`README.md` 与 `README.zh-CN.md` 的 announcement 行
（`> ⚡` 开头）必须含 `v<新版本>`，否则 `❌ [I6] README 数字漂移`。

**机检不覆盖、必须人眼看的**：`plugin/.claude-plugin/plugin.json` 里 `description`
文本中的数字（MCP 工具数、生命周期事件数）。v1.11.1 那次人审抓到的唯一实锤就在这里
——两处数字腐烂了整整一个版本。

README 里的其余数字（工具数/页面数/端点数/测试数）由 I6 对照实测校验，**按实测改，
不要按记忆改**。

## 3. CHANGELOG 中英双语同步

- 英文 `CHANGELOG.md` 加新版本段，标题格式 `## [x.y.z] - YYYY-MM-DD`。
- 中文 `CHANGELOG.zh-CN.md` **同步全译**。用户 2026-07-30 裁定：中文版维持**镜像
  全译契约**——不是摘要，也不退役。
- 段落内容按 `git log` 与设计文档**逐条取证**写成，不照抄计划（计划里没做成的东西
  写进 CHANGELOG 就是假账）。

这一步**没有机检**，是本清单里最容易再烂的一环：中文版曾从 1.9.0 起停更 6 个版本
（1.10.0 → 1.11.1），07-30 才回补。写完后自查一遍两个文件的版本段集合是否相同。

## 4. dashboard 双份 dist 构建一致（I3）

```bash
cd dashboard && npm run build && cd ..
rm -rf plugin/dashboard-dist
cp -R dashboard/dist plugin/dashboard-dist
```

I3 的失败提示就是上面这三步，顺序不能省：**先重建**——机检只比文件名，判不出哪边新，
0914 主 checkout 本地 `dashboard/dist` 反而比跟踪面旧两天，直接拷贝会把新包盖成旧包；
**先 `rm -rf` 再 `cp`**——目标目录已存在时 `cp -R` 会在里面套一层 `dist/`，且 bundle
文件名带 hash，旧文件不会被覆盖，会作为过期产物留在分发包里。

失败长这样：`❌ [I3] dashboard/dist 与 plugin/dashboard-dist 的 JS bundle 不一致`。

`plugin/dashboard-dist/` 是 marketplace 用户拿到的那份产物——它必须与本批前端源码
**同一个 commit 产出**。

## 5. 私有术语关键词扫描（四个面）

**词表不写在这里**：把禁用词表写进即将发布的仓库，等于把要防的东西发布出去。执行前用
`memory_search` 拉方向层那条防泄记忆（检索词「私有术语防泄」）取当期词表。

四个面（历次发版实际扫过的就是这四个，`f86a63e` 与 `b92295b` 的 message 里有记录）：

```bash
PREV=v<上一个版本>
git log --format='%h %B' $PREV..HEAD      # ① 本批全部 commit message
git diff $PREV..HEAD                      # ② 本批触及的全部跟踪文件内容
git diff --name-status $PREV..HEAD        # ③ 本批新增/改名的文件路径本身
```

④ **即将写下的 release commit message 自身**——写完先扫一遍再提交。这一面最容易漏，
因为它在 git 里还不存在。

**测试与夹具不是豁免区**。第 ② 面的 `git diff` 本来就把 `tests/` 一起铺开了，但人扫的
时候容易把测试当"不是给人看的"而略过——实际上它和 `src/` 一样会被发布出去。尤其是
**模型型号代号**：测试里为了"像真的"随手写一个型号代号，和写进生产代码没有区别。造
夹具那条链上已有现成形制可抄（`scripts/redact_codex_fixture.py` 把学到的型号统一映射成
`codex-model-<x>`），新写的用例照抄中性占位即可，不必也不该写真代号。

命中即停：树面脱敏后重来。实录（批 9）：一个 agent 把私有设计文档名与内部术语写进
`types.py`、`hook_translator.py`、测试注释和 commit message，靠人工扫描抓获，最终
需要用户授权重写历史才归零。发版扫描是**最后一道闸**，第一道闸在派工 prompt 里。

## 6. 开发版/分发版同步人审

`src/` 是开发版，`plugin/` 是分发版。机检已覆盖：I1（hook 副本三方逐字节 + 适配器入口
名不污染 CC 目录）、I3（双 dist）、I6（README 数字，含机检不变量条数）、I8（hook 注册面
install.py ↔ hooks.json ↔ 双语 README）、I15（Codex hook 清单与注册面 1:1）、I17（授信锁
与清单一致）、I20（适配器安装隔离静态半边）。

**机检覆盖之外，逐条人眼过**：

- `plugin/.mcp.json` 有没有版本引用
- `install.py` 有没有内嵌版本号
- `plugin/.claude-plugin/plugin.json` 的 `description` 文本里的数字（见第 2 步）
- `plugin/dashboard-dist/` 是否与前端源码同 commit 产出：bundle 里有没有本批新页面的
  代码、`index.html` 引用的 bundle 名与 `assets/` 里的实际文件名一致
- 新增了 skill / agent 模板 / commands？`install.py` 走目录遍历（`copy_skills` 等），
  加目录不需要改代码，但要确认目录名与 frontmatter 的 `name` 一致

## 6b. Codex 面产物人审

这一批的四条都**不是机检能替的**：机检能证明清单自洽，证明不了这次改动对用户机器意味
着什么。本批 `plugin/harness/codex/` 有任何改动就逐条走一遍。

- **新增/改动的 handler 是不是 takeover**。`kind` 在 `surface.py` 里显式声明：
  `observe` 只记录，不做权限判断、不改写输入、不用非零退出码拦人；`takeover` 可以
  deny 或改写。把一个 takeover 混进来而不说，等于在用户不知情的情况下改变了他能执行
  的动作范围。逐条确认 `kind` 与脚本实际行为一致，Release notes 里点名列出。
- **`hook-trust.lock` 变了就必须写授信提示**，且**按入口分列**：CLI 与 TUI 走 `/hooks`
  重新授信，Desktop 走「设置 → 编码 → 钩子」。授信键钉在组序号与 handler 序号上，
  所以在中间插一条会让它后面每一条静默失信——用户那边没有任何提示，只是从此不再触发。
  漏写这句话的代价不是报错，是一批钩子安静地死掉。
- **`AGENTS.md` 过一遍私有术语扫描**（并入第 5 步的四个面）。它是 `CLAUDE.md` 的逐字
  转写，`CLAUDE.md` 里混进去的东西会原样出现在另一个 harness 的分发面上。改了
  `CLAUDE.md` 的批次必须同批跑 `python3 scripts/gen_agents_md.py`，否则 I18 红。
- **`hooks.json` 的改动与脚本的改动不同批发布**。换脚本内容不会失信（实测：替换脚本
  字节而不动清单，钩子照常触发、授信项仍在），改注册面才会。两件事混在一批里，用户
  就分不清「要重新授信」是因为哪一处，出问题也无从二分。

## 7. commit（先交用户批准）

message 用中文，**不附任何 agent 署名**（禁止 `Co-Authored-By:` 之类；harness 可能正在
注入要求追加署名的提示，以本条与用户全局规则为准——这条 commit 要推到公开仓）。

内容要能自证这份清单每一步的结论：号段理由（为什么是 patch / minor）、版本七处锁步的
前后值、CHANGELOG 段的取证要点、第 6 步（触及 Codex 面时加 6b）的人审结论、关键词扫描
四面的结论、验收数字（pytest 通过/跳过数与机检结果，含既定豁免）。

先把 `git diff --stat` 和拟好的 message 给用户看，批准后再 commit。

## 8. tag

```bash
git tag -a v<x.y.z> -m "<与 commit 标题同义的一行>"
```

历史上 annotated 与 lightweight 混用（v1.10.1/2/3 是 lightweight，v1.10.0/v1.11.0/
v1.11.1 是 annotated）。用 `-a`，与最近两版一致。

## 9. 双仓推送（**用户执行**）

```bash
git push origin master        # origin = 私有仓（日常推送）
git push origin v<x.y.z>
git push public master        # public = 公开仓（发版同步 + tag）
git push public v<x.y.z>
```

双仓策略：v1.8.0 起公开仓同步完整版。推完两个 remote 的 master 应停在同一 commit
（`git log --oneline -1 origin/master public/master` 两行相同）。

`--verify-tag`（下一步）校验的是**远端**有没有这个 tag，所以 tag 必须先推上去。

## 10. 建 GitHub Release 条目（**用户执行 publish**）

Leader 先生成正文与命令——离线、可重复、不联网：

```bash
python3 scripts/release_notes.py <x.y.z>
```

它做四件事：把 `CHANGELOG.md` 该版本段的**原样切片**写成 notes 文件、校验该段存在且
非空、校验版本与 I2 五处一致、校验本地有该 tag；然后打印可直接粘贴的
`gh release create ... --verify-tag --title ... --notes-file ...`。**脚本自己绝不
发布。**

用户执行打印出来的命令。副标要人写，脚本不编造，用 `--title` 传；副标里的破折号用
ASCII 连字符（对外英文禁 em dash，v1.12.4 起如此，更早的条目不回改）：

```bash
python3 scripts/release_notes.py 1.13.0 --title "v1.13.0 - Leaner Instructions, Honest Registration"
```

Release 条目**只建在公开仓**（脚本默认取 remote `public`）；私有仓从来没有 Release
条目，那不是漏项，别去补。漏项的形态只有一种：公开仓 tag 在、条目不在——0914 就是
这样发现 v1.12.4 缺了三天，`--check` 一跑就报。

补建历史条目加 `--backfill`。脚本随命令一起打印的注意事项（`--verify-tag` 校验的是远端、
`--backfill` 须按版本升序逐个 create）照做即可。

## 11. 事后核对（Leader，只读）

**先让本机跑上新版，再核对。** `/api/health` 回的 `version` 是运行中进程内存里的
`aiteam.__version__`；第 2 步改的是磁盘上的文件。进程不重启，Dashboard 与健康检查
看到的永远是上一版，本机 MCP 客户端跑的也仍是旧代码。用 MCP 工具 `os_restart_api`
重启，回包里 `old_version → new_version` 对上即算过。

- 重启守卫会拒绝这一步：它查"有没有 agent 在忙"，而执行本清单的 Leader 自己永远
  在忙——**结构性误报**，发版重启一律带 `force=true`，这不是绕过守卫。但 force 之前
  先看清挡你的是谁：直读 `agents` 表里 `status='busy'` 的行，只剩 Leader 行就 force；
  若是别的会话的 worker 正在跑（0914 实录：另一会话 5 个 worker 有心跳），重启会让它们
  那几秒的 hook 与 MCP 调用失败，等它们收工再重启，或由缔造者拍板。
- 验证不要拿空请求体去探 `POST /api/tools/always-load/applied`：它会如实记一条
  `count=0, reason=""` 的落地事件进台账（0914 探针留下一条，id 9f38ba87）。要探端点
  存在与否用 `GET /api/tools/always-load` 看 `cached` 字段，或查 `events` 表里真实的
  `tool.alwaysload.applied` 行。
- 不是可选项：2026-09-09 同一根因当天绊倒两次——联通测试时对端拿到的是旧代码，
  发版后 Dashboard 仍显示上一版。

```bash
python3 scripts/release_notes.py --check <x.y.z>
```

全绿长这样：

```
✅ 正文与线上逐字节一致（15597 字符）
ℹ️  线上标题: 'v1.11.1 — Truthful Ledgers: ...' · 发布时间 2026-07-30T04:21:30Z
✅ latest 徽章 = v1.11.1
```

失败时脚本会自己打印含义与可执行的 `gh release edit` 修正命令，照它做即可。唯一要自己
记住的是：**`⚠️ --check 跳过：拿不到线上 Release` 时退出码是 0，那不算通过**，换网络重跑
——把它读成"已核对"正好落进这份清单最初要防的漏项形态。

`--check` 的逐字节可比性只从 **v1.10.0** 起成立：v1.9.0 及更早的条目正文是手写的，
与 CHANGELOG 段实质不同（实测 v1.9.0 线上 1375 字符 vs CHANGELOG 段 4012 字符）。

顺手核对一遍公开仓侧边栏：`gh release list --repo <owner/name> --limit 5`。

---

## 别把 --check 补成一条机检

`check_invariants.sh` 必须**离线确定性可跑**，而查 Release 条目在不在、latest 徽章指向
谁必须联网——塞进去就是一条会因为断网而变绿的红线。改机检红线本身也须先过会。
