wiki-sync 是 OncallAgent 在通用 OpenSpec 之外增加的仓库级 Skill。OpenSpec 已经把 proposal、design、tasks 和 specs 保存进 Git,但直接在几十个目录里查找并不方便。wiki-sync 为它们生成 VitePress 浏览层,让变更可搜索、可导航,同时坚持 OpenSpec 文件是唯一事实来源。

📷 [图片 token=MXLtb8s8Bomd3KxvWnRcDzbvnRg(未能下载,见飞书原文)]

先区分三个容易混淆的概念

OpenSpec sync 把 change 下的 delta spec 合入 openspec/specs/ 主规格。

OpenSpec archive 把完整 change 移到 openspec/changes/archive/YYYY-MM-DD-name/

OncallAgent wiki-sync 根据 active/archive 目录生成 docs/changes/ 页面、索引和 VitePress Sidebar。它不实现业务功能,也不把内容发布到飞书。

三者顺序通常是:先验证实现,再 sync 主规格,再 archive 历史,最后 wiki-sync 文档展示。

📷 [图片 token=BiVqbBDw0opSzKxuHIZco8G4nGg(未能下载,见飞书原文)]

确定性入口

python3 .codex/skills/wiki-sync/scripts/sync_wiki.py active <change-name>
python3 .codex/skills/wiki-sync/scripts/sync_wiki.py archive <name-or-dated-name>
python3 .codex/skills/wiki-sync/scripts/sync_wiki.py all

active 用于完整规划产物生成后建立进行中页面;archive 用于归档后更新状态;all 全量重建。虽然 archive 命令接受一个名称,脚本仍会扫描全部 active 和 archive、校验全部归档并重建完整快照,避免人工维护造成漂移。

📷 [图片 token=CPxsbGiWPoJDeAx37XKcHaCdn1b(未能下载,见飞书原文)]

openspec-new-change 后通常只有 .openspec.yaml,而 wiki-sync 页面固定 include proposal、design、tasks 和 delta specs,因此不能在裸 new 后立即成功同步。应在 openspec-propose 完成后,或 new + continue 补齐产物后再执行。

📷 [图片 token=WQ7LbTQ9VobkDhxva5TcwGkpnRv(未能下载,见飞书原文)]

生成哪些文件

docs/
├── openspec -> ../openspec
├── changes/
│   ├── index.md
│   ├── active/<name>/index.md
│   └── archive/YYYY-MM-DD-<name>/index.md
└── .vitepress/
    └── config.mts

每个 change 只生成一个聚合 index.md,不会把四类 OpenSpec Markdown 再复制一份。归档页包含 titlestatus: archivedcreatedDatearchivedDate 等 frontmatter,以及 proposal、design、tasks 和每个 delta spec 的 include。

📷 [图片 token=J8NsbWveNo36E6xeHKUcDMEvnih(未能下载,见飞书原文)]

<!--@include:
../../../openspec/changes/archive/
2026-07-11-limit-qwen-embedding-batch-size/proposal.md-->

这里的 docs/openspec 是指向仓库根 openspec 的符号链接。VitePress 从 docs 内的路径读取,实际内容仍来自 OpenSpec 原文件。页面是“窗口”,不是第二份事实。

📷 [图片 token=Ln71bpRHnoKiZbxaxlpcUoDtnFb(未能下载,见飞书原文)]

为什么使用 symlink 加 @include

如果同步脚本把正文复制到 docs,归档中修复一个错字后还要同步副本;任何一次遗漏都会出现 OpenSpec 与 WIKI 内容不同。include 让页面每次构建都读取源文件,消除正文双写。

它还保留清晰责任:OpenSpec artifact 可以被 CLI、Codex 和 Git 直接处理;VitePress 只负责浏览体验。生成页只维护标题、状态、导航和 include 关系,脚本可以安全地确定性重建。

📷 [图片 token=U7f0bknMWoOgJOx7d7ccGU4cnsf(未能下载,见飞书原文)]

归档同步前的严格校验

目录布局。docs/openspec 必须是指向 ../openspec 的正确符号链接,openspec/changes 必须存在。

**归档名称解析。**可以传完整日期名称或短名;短名必须恰好匹配一个目录,否则失败,避免更新错误目标。

**delta/main spec 一致性。**每个 archive 至少有一个 delta spec。脚本解析 ADDED、MODIFIED、REMOVED、RENAMED:新增和修改的 requirement 必须在 main spec 中,删除的必须已消失。未同步默认阻断。

📷 [图片 token=LWKObnFq5o3gOSxkCOFcXiyXnZA(未能下载,见飞书原文)]

**include 完整性。**每页至少包含 proposal、design、tasks,并为所有 delta spec 生成 include;每个路径必须存在,归档页不能继续引用归档前的 active 路径。

镜像与导航。docs/changes/active、archive 页面目录必须与 OpenSpec 目录一一对应;docs/changes/index.md 和 Sidebar 必须来自同一顺序。陈旧页面会被删除,生成目录内不应手写额外内容。

--allow-unsynced 只能在明确批准后绕过 delta/main spec 同步问题,不能绕过 symlink、include、frontmatter、目录镜像或导航校验。

📷 [图片 token=RXUEbc2FzoqIotxrIcicdJfynaf(未能下载,见飞书原文)]

为什么还要单独运行 docs:build

wiki-sync 脚本检查业务完整性,但不在脚本内部运行 VitePress 生产构建。同步成功后仍须:

npm run docs:build

结构校验和构建验证解决不同问题。include 路径全部真实存在,不代表 Markdown 一定能被 VitePress 正确构建;VitePress 构建通过,也不代表页面没有漏 include。因此两道门禁都需要保留。生成的 docs/.vitepress/dist/ 和 cache 被 Git 忽略,不提交 HTML 产物。

📷 [图片 token=XahtbibFGo1L0XxXl5Pcivp7ngc(未能下载,见飞书原文)]

主案例的 WIKI 页面

归档 2026-07-11-limit-qwen-embedding-batch-size 对应:

docs/changes/archive/
2026-07-11-limit-qwen-embedding-batch-size/index.md

页面有 archived frontmatter,固定 include proposal、design、tasks,再 include qwen-openai-provider delta spec。.openspec.yaml 仍保存在 archive,但不面向 WIKI 读者展示。总索引和 Sidebar 中也存在同一条目。

📷 [图片 token=Ck1qbEHdHoVMFcxWW3acAd4Mnpb(未能下载,见飞书原文)]

另一个很适合说明机制的自举案例是 add-openspec-wiki:它通过 OpenSpec change 建立 wiki-sync,归档后又被自己建立的同步机制收录。面试中可以用“dogfooding”解释:工具自己的演进也遵守同一套规格、任务、验证和归档规则。

面试表达

我们没有维护第二份 Wiki 正文,而是让 VitePress 聚合页通过 symlink 和 @include 读取 OpenSpec 源文件。wiki-sync 全量扫描 active/archive,校验 delta 已进入主规格、include 真实存在、页面与导航严格镜像,再单独跑 docs build。这样 WIKI 是可浏览视图,OpenSpec 仍是唯一事实来源。

📷 [图片 token=BRYBbRIx2oYFSZxfcsec3FvQnTb(未能下载,见飞书原文)]