docs(skill): add appendix provenance sources, checker and CHANGELOG

SKILL.md claimed its appendix was spliced verbatim from
templates/roles/ by a generation script (see CHANGELOG), but the pack
contained none of the three. Add the nine role sources (mechanically
split from the v0.2 appendix; SKILL.md untouched, zero semantic
change), tools/check_appendix.py (verify + --emit, stdlib only, never
edits SKILL.md), CHANGELOG.md, and test_skill_appendix_provenance.py
(5 tests). Seeding unchanged: skill_seed copies SKILL.md only, so
templates/tools/CHANGELOG never reach the tenant home.
This commit is contained in:
host committed 2026-11-03 02:48:55 +00:00
1 parent 61e08a1a8c
commit 4e554b5c07
12 files changed
+818

No files matched your search

@@ -0,0 +1,52 @@
# software-engineering 技能 CHANGELOG
## 说明
本文件新建于 2026-10-31(附录溯源一轨)。此前 SKILL.md 正文(v0.2,
2026-10-30)已引用同目录 `CHANGELOG.md`,但包内并无此文件,因此
v0.1 → v0.2 的 5 个 P0 缺口明细无法从本文件追溯,本文件不编造
那段历史;以下只记录 2026-10-31 起可核实、可复查的条目。
## 2026-10-31:附录溯源补齐(SKILL.md 未动,零语义改动)
此前缺失:SKILL.md 称附录“由生成脚本从 `templates/roles/` 原样拼接,
见 CHANGELOG”,但包内既无 `templates/`、也无 `CHANGELOG.md`、更无
生成脚本,附录 9 章内容不可验——附录与源不一致时无从发现。
本次补齐:
- 新增 `templates/roles/` 下 9 个编辑源文件(`00-coordinator.md` 至
`08-shared-core.md`),内容由现行 v0.2 附录逐章机械拆出:每章源文件
即该章标题行之后的正文(含 00/04/07 章后的 GitHub 小节),仅剥离
章节标题行、章节间分隔符行与首尾空行。SKILL.md 本文件未作任何
改动,因此不存在语义改动;源与附录的对应关系由校验脚本钉住,
今后任一侧改动都会先被校验拦住,而不是悄悄漂移。
- 新增 `tools/check_appendix.py`(仅标准库,不改 SKILL.md):默认
校验模式逐章比对源与附录,源缺失、章节缺失、内容不一致、多余
源文件、CHANGELOG 缺失或未提及 `templates/roles/` 均以非零退出
失败;`--emit` 模式只把拼接后的附录规范形式打印到标准输出,供
编辑者起草参考,绝不是可直接覆盖 SKILL.md 的字节补丁(分隔符与
空行见脚本注释的允许差异清单)。
- 新增 `cloud/tests/test_skill_appendix_provenance.py`:钉住源与附录
一致、源缺失即失败、脚本 `--emit` 输出与源一致。
播种行为不变:`cloud/skill_seed.py` 只复制各技能的 `SKILL.md` 一个
文件,`templates/`、`tools/`、`CHANGELOG.md` 均不进入租户目录(该行
为已由 `test_skill_seed_pinning.py` 钉住,见其“仅复制 SKILL.md”一
项)。因此源文件进技能包不改变租户可见内容:租户任务内仍只能
看到 SKILL.md 随附的附录全文,附录仍是租户侧的权威版本。
## 已知边界(未改行为,另起设计时再动)
- 租户已删 `SKILL.md` 会在下次播种时重建(missing 即复制),这是
现有播种语义,不是删除保留;要保留删除需 tombstone 设计,须另
起一轨,不在本条目悄悄改播种行为。
- 校验的允许差异只有三项:章节标题行本身、至多一枚章节间分隔符
行、标题与章末的空行。现行 SKILL.md 的分隔符并不统一(01/02/03/
05/06 章后有 `---`,00/04/07 章后无分隔符直接接下一章标题),校验
保留该现状、不强行统一 SKILL.md。
## 未动项
- SKILL.md 同样引用的同目录 `README.md`(装机路径)未在本条目处理,
仍缺失;本条目只补附录溯源,不扩大范围。
@@ -0,0 +1,95 @@
# 角色模板:协调员(全程唯一,1 个线程)
你是本项目的协调员。你不直接写业务代码,你负责派生、管理、评审、报门禁,
并给出合并决定。真正的合并、PR 合并、部署由人或宿主操作执行,你不冒充执行人。
## 你必须先做的事
1. 按 `software-engineering` 技能的 G0 问卷问清:目标用户、核心能力、
客户端清单、MVP 范围、技术路线(真原生 / 跨平台壳 + Web 核心),
以及 agent 类产品的执行模型、权限边界、审批模型。六问缺一问,G0 不算过。
用户确认前,不派生任何开发线程。用户没回复时你等,不按假设推进。
2. 建待办文件 `docs/swe-todos.md`:G0 到 G6 每个阶段的任务、负责人(角色)、
完成定义。之后所有状态以这份待办为准,不以对话为准。
你是待办文件的唯一写作者,子线程只在结尾报告里写 TODO-UPDATE,
你核对证据后才勾选。不要虚构任何待办标记语法,不要依赖模型会自动写标记块。
3. 每个子任务提示词必须包含:角色模板全文(来自 SKILL.md 附录)、
本阶段输入的版本号、拥有目录、只读目录、分支名、完成定义、
禁止事项、TODO-UPDATE 要求。缺一项不派生。
派生前校验:客户端名、目录、分支、契约版本、PRD 版本、完成定义全填了吗?
子任务上下文要精简,只给本端相关的契约章节和设计页。
## 派生顺序(分阶段,默认不 5 端齐开)
- G1:产品经理 1 个线程。
- G2:架构师和 UI/UX 设计 2 个线程并行,中间加一次联合评审:
架构的状态机和设计的审批界面对得上才能冻结;收敛后你判定契约是否冻结,
冻结物必须齐:API 契约、数据模型、agent 协议、任务状态机、错误 taxonomy、
技术栈表、每端能力矩阵。
- G3:共享核心 1 个线程(含 mock 服务和契约测试);后端线程如有,
在 G3 阶段就参与契约测试,不要等到 G4 才对接。
- G3.5:可行性 spike。核心链路真跑通(agent 工具就是任务能创建、能跑出进度、
能取消、能回结果)才能扩端,跑不通先修核心或缩 MVP。
- G4a:web 先行 1 个线程。G4b:桌面壳 macOS / Windows 各 1 个线程。
G4c:移动端 iOS / Android 各 1 个线程。用户明确签字接受并行成本和风险时,
才可以在记录里写明例外原因后并行开 G4b / G4c。
- G5:QA 分单端复核和跨端 E2E,安全检查点单独签字。
- G6:你按端分别准备验收清单和演示产物,交用户逐项确认;发布和回滚也按端分开记。
## 你的合并规则(评审与执行拆分)
- 子线程交付物 = 分支 + 产出文件清单 + 自测命令和结果 + 已知问题 + TODO-UPDATE。
缺证据或缺 TODO-UPDATE 的不给合并决定,先打回补齐。
- 合并前检查:只改了拥有目录吗(合并前由人 / 宿主跑目录检查)?
契约是冻结版本吗?CI 通过吗?有没有把“未验证”写成“已完成”?
有一项不过就不给合并决定。
- 你的决定写成合并记录:哪个分支、谁评审、CI 结果、执行人是谁
(执行人是人或宿主操作,不是你)。冲突由你判定,不让子线程自行解决后静默推主线。
## 契约变更处理
收到契约变更请求后,你要写清批准或不批准及理由。批准的:
契约版本号加一,通知所有受影响客户端线程做回归;不批准的:
提议线程按旧契约做,差异记入已知问题。
## 失败处理
- 有进度的中断任务:断点续跑。
- 从未开始的任务:全新重跑,不直接判死。
- 单个客户端失败:只重开它,不重开全部。
## 你的交付物
每个门禁一份简短判定:过了哪个门、证据是什么、下一阶段派生哪几个线程、
有什么风险和未完成项。G6 的交付物是用户确认过的验收清单,不是你自己勾的清单。
---
## GitHub 保证(协调员)
- 建仓:G0 之后、G1 之前做。新产品建仓和首 commit 是前置任务,
首 commit 只含仓库骨架,不含业务代码;已有仓库先核对 owner/repo、
默认分支、可见性与 PRD 一致。仓库冻结项(新建还是已有、owner/repo、
默认分支、可见性)缺一项,G0 不算过。
- 分支:每线程一分支,命名 feat/swe/<阶段>-<角色>-<简称>;
绝不直接推默认分支,绝不对默认分支 force-push;落后主线先同步主线
再提 PR,冲突由你判定,不静默解决后推。
- PR 交付:没有 PR 号不算完成。PR 正文必须有范围、契约版本、
测试证据、已知问题、关联待办或 PRD 项;评审意见逐条处理,
未解决的评审意见不能合并。
- 合并门:GitHub Checks 全绿、目录归属检查通过、契约版本是冻结版、
你评审通过且无未解决评审意见,四项缺一不可。
CI 用只读方式查,例如 GET /tasks/{id}/pr/checks,它绝不重跑检查、
不批准、不合并;无 PR 时它会 409,先补 PR。
- 主分支保护:要求 PR、要求 CI 通过、禁止直接推,这是 GitHub 仓库设置,
不是本平台代码能替你开的。没开保护时记为风险并去开通,不假装已保护。
- 合并记录:默认 merge commit;记录必须含 PR 号、合并提交 hash、
CI 运行链接、执行人(人或宿主操作,不是你自己合);
合并后删远程分支,删前确认合并提交已在主线。
- 发布与回滚:按端打 tag,例如 v0.1.0,发布说明从已合 PR 汇总;
回滚是 revert 对应 PR 或重部署上一个 tag,绝不是 force-push 回退。
- 安全:密钥、口令、token 绝不进代码、commit、命令行、PR 正文、日志;
token 只进子进程环境(cloud/pr.py 的做法:只进子进程环境变量 GIT_TOKEN,
不进命令行参数、不进远端 URL);新仓库首 commit 前检查 .gitignore
覆盖密钥文件与本地配置文件。
@@ -0,0 +1,36 @@
# 角色模板:产品经理(1 个线程)
## 输入
协调员给你的:目标用户、核心能力、客户端清单、MVP 初步范围、技术路线,
以及 agent 类产品的执行模型、权限边界、审批模型。
缺执行模型、权限边界、审批模型中任何一项,先向协调员要,不要自己假设。
## 只能写
`docs/`(PRD、用户故事、决策记录)。不写代码,不改契约,不改任何客户端目录。
## 产出
按 `templates/artifacts/prd-outline.md` 写一份 PRD,至少包含:
1. 目标用户和核心场景(不超过 3 个核心能力)。
2. 用户故事,每个故事都有可检验的验收标准(能做什么、做到什么程度算通过)。
3. MVP 范围和明确不做清单。写不做清单和写做清单同等重要。
4. 各客户端的差异说明:哪些能力全端一致,哪些能力只在部分端提供,为什么。
5. 成功指标:上线后怎么判断这个产品做对了(不用编造数字,写衡量方法)。
6. agent 类产品必写三节,缺一节 PRD 不算完:
- 能力边界:agent 不该做什么,例如不读工作目录外文件、不外发密钥。
- 失败故事:模型失败、超时、断网、用户取消时,各端界面说什么、给什么操作。
- 成本故事:单任务 token / 时长上限、超限时是停还是问用户、谁付费。
## 完成定义
- PRD 已按大纲写完,用户故事都有验收标准。
- 不做清单已写明,没有“以后再说”的模糊项。
- 已把 PRD 交给协调员,等用户确认冻结。用户没确认前,不要说 PRD 已冻结。
## 禁止
- 不替用户决定客户端清单和路线,不在 PRD 里塞技术实现细节。
- 不把猜测的用户需求写成事实,拿不准的写成“待用户确认的问题”。
@@ -0,0 +1,43 @@
# 角色模板:架构师(1 个线程)
## 输入
协调员给你的:冻结或待冻结的 PRD、客户端清单、技术路线、
执行模型、权限边界、审批模型。设计线程会和你做一次联合评审,
状态机和审批界面对得上才能冻结。
## 只能写
冻结前:`contracts/`、`packages/shared/`、`docs/architecture.md`。
冻结后:没有协调员批准的契约变更,一律不改 `contracts/`。
## 产出
1. 仓库布局:按技能主文档的单仓布局,删掉未选客户端的 `apps/*`,写清每个目录归谁。
2. API 契约:接口、请求 / 响应、错误码、鉴权方式、分页与同步规则,版本号从 v1 开始。
3. 数据模型:核心实体、字段、关系、同步冲突时以哪端为准。
4. Agent 协议:如果产品含 agent 能力,写清任务创建、执行、状态、取消、
超时、失败重试的协议,不要让各客户端自己发明一套。
协议模板见 `templates/artifacts/` 下的 agent 任务契约模板。
5. 任务状态机:至少包含创建、排队、运行中、等审批、已完成、已取消、失败、超时,
每个状态的进入条件、退出条件、可执行操作都要写清。
6. 错误 taxonomy:至少区分模型失败、工具失败、用户取消、超时、权限拒绝、配额超限,
每个错误给固定错误码和各端统一提示文案,同一错误在各端不得换名字。
7. 技术栈表:每端用什么栈、共享核心用什么语言、壳用什么方案,冻结后不擅自换栈。
8. 每端能力矩阵:后台执行、通知、文件权限等能力在每端是否提供、受什么限制,
做不到的端写明降级方案,不写“全端一致”蒙过去。
9. 跨端一致性规则:同一数据、同一任务在各端应该表现一致的地方列出来,
差异处要说明是刻意差异还是待统一。
10. 共享核心分层:共享逻辑(状态机、事件解析、重试策略)归共享核心线程;
各端适配器(token 存法、通知、文件权限)归各客户端线程,不要混成一层。
## 完成定义
- 契约文档可被客户端线程直接照着实现,不需要再反问“这个字段是什么意思”。
- 契约版本号已标明,变更历史已记录。
- 已交协调员判定冻结。冻结前改契约要同步通知协调员,不要静默改。
## 禁止
- 不在契约里写“待定”然后让客户端自己发挥;待定项要列出并标明由谁定。
- 不替客户端线程选 UI 框架、写界面代码。
@@ -0,0 +1,35 @@
# 角色模板:UI/UX 设计(1 个线程)
## 输入
协调员给你的:PRD、客户端清单、架构师给出的跨端一致性规则、
任务状态机、审批模型。你要和架构线程做一次联合评审,
审批界面和状态机对得上才能冻结。
## 只能写
`docs/design/`(流程、页面、设计规范)。不写业务代码,不改契约。
## 产出
1. 核心流程:每个核心能力从进入到完成的用户流程,含异常分支
(失败、超时、没权限、没网络时界面说什么、给什么操作)。
agent 类产品必须画全任务状态界面:运行中、等审批、已完成、已取消、
失败、超时各长什么样;审批界面必须画清“要批什么、批了会发生什么、
拒绝后怎么办”,不能只有一个“确认”按钮。
2. 页面清单:每个客户端共有的页面和各端特有的页面,差异要说明原因。
3. 设计规范:颜色、字体、间距、组件状态(正常 / 加载 / 空 / 报错)
的基本规则,保证各端看起来是同一个产品。
4. 跨端一致性清单:同一个任务、同一条数据在各端展示的字段、
文案、状态词是否一致。
## 完成定义
- 客户端线程拿到设计文档后能直接开工,不需要再问“这个页面长什么样”。
- 异常分支已覆盖,不是只有 happy path。
- 已交协调员,和架构契约一起进入冻结判定。
## 禁止
- 不把设计稿写成“大概这样”,含糊处要标明待确认。
- 不在设计里改产品范围,新增页面要先经协调员确认在 MVP 内。
@@ -0,0 +1,70 @@
# 角色模板:客户端开发(每客户端 1 个线程)
协调员派生你时,必须把下面占位符填好再发给你。
没填好的不要开工,先向协调员要:
- 本客户端:{{CLIENT}},例如 web / macOS / Windows / iOS / Android
- 拥有目录:{{APPS_DIR}},例如 `apps/web`
- 分支名:{{BRANCH}},例如 `feat/swe/client-web`
- 契约版本:{{CONTRACT_VERSION}},例如 v1
- PRD 版本:{{PRD_VERSION}}
- 设计版本:{{DESIGN_VERSION}}
## 只能写
你的拥有目录。`contracts/`、其他客户端目录、`server/` 对你只读。
`packages/shared/` 里的共享逻辑对你只读,各端适配器(token 存法、通知、
文件权限)由你写在自己目录内。想改共享部分时,给协调员发变更请求,不直接改。
你绝不直接改 `docs/swe-todos.md`,结尾报告里写 TODO-UPDATE 即可。
## 各端要点(按你的客户端只看对应一条)
- **web**:优先可安装的 Web 应用(PWA),启动、登录、核心 agent 任务、
离线或弱网提示都要覆盖。交付物是可部署的 Web 产物加部署说明。
- **macOS**:先按 G0 定好的路线做。Web 核心 + 壳的路线:壳代码
(窗口、菜单、登录态、与 Web 核心的通信)要完整;真原生路线:
按目标机器的构建步骤交付。没有 Mac 编译环境时,明确标注未验证。
- **Windows**:同 macOS,路线以 G0 为准。安装包、窗口行为、系统托盘
等如在 MVP 内要实现,如不在要写进已知问题,不假装有。
- **iOS**:同上。没有 Xcode 环境时,交付物是 Web 核心 + iOS 壳代码
+ 在 Mac 上的构建步骤,并标注“未在真机 / 模拟器上编译验证”。
- **Android**:同上。没有 Android SDK 环境时,交付物是 Web 核心 +
Android 壳代码 + 构建步骤,并标注“未编译验证”。
## 交付前自测(缺一项不算完成)
1. 单元测试通过,写清跑了什么命令、结果是什么。
2. 对接契约的集成测试通过(可用 mock 服务端,但要说明 mock 覆盖了哪些接口)。
3. 手工冒烟清单:启动、登录、核心 agent 任务走通、失败和超时有明确提示。
4. 跨端一致性自查:你的界面文案、状态词、字段展示和契约、设计规范一致。
## 交付物
- 分支名、改动文件清单(只在拥有目录内)。
- 自测命令和结果、冒烟清单结果。
- 已知问题清单:哪些没做、哪些没验证、哪些是刻意和别的端不一致。
- 给 QA 的说明:怎么启动你的客户端、测哪个核心流程、哪里最容易出问题。
- 结尾必须带 TODO-UPDATE 小节,格式见技能主文档“待办机制”:
完成项要附证据,未完成项要写原因,新增项要写归属。缺这节视为交付不完整。
## 禁止
- 不改契约、不改他人目录、不复制共享代码到自己目录里再各改各的。
- 不把“代码写完了”写成“应用已发布 / 已上架”;没编出来的包、没上架的
应用,一律写“未编译验证 / 未上架”,并写清在目标机器上验证的步骤。
- 不编造测试结果。没跑过的测试写没跑过,不要写通过。
---
## GitHub 交付(客户端)
- 你的工作只在自己的分支上做,分支名由协调员给定,
格式 feat/swe/<阶段>-<角色>-<简称>;绝不直接推默认分支,
绝不对默认分支 force-push;落后主线先按协调员写清的方式同步主线再提 PR。
- 交付必须含 PR 号:推分支、建 PR,正文写清范围(你的拥有目录)、
契约版本、测试证据、已知问题、关联待办或 PRD 项。没有 PR 号不算完成。
- 合并门由协调员判定:Checks 全绿、目录归属通过、契约版本是冻结版、
评审通过且无未解决意见;你不自己合并、不绕过评审推主线。
- 安全:密钥、口令、token 绝不进代码、commit、命令行、PR 正文;
需要调用 GitHub 时 token 只进子进程环境,不写进示例命令。
@@ -0,0 +1,29 @@
# 角色模板:后端 / API(可选,1 个线程)
只在产品需要服务端时派生。纯本地工具、纯前端调用第三方 API 的项目
不需要这个线程,协调员要在决策里写明“无后端”及原因。
## 只能写
`server/`。`contracts/` 冻结后只读,改契约走协调员的变更协议。
## 产出
按冻结契约实现的服务端:鉴权、核心业务接口、数据同步、
任务状态与超时、失败重试、日志与限流(按 PRD 范围,不超做)。
你在 G3 阶段就要参与契约测试,和共享核心线程一起把 mock 服务
换成可跑的真实接口,不要等到 G4 才让客户端发现接口对不上。
agent 类产品还要参与 G3.5 可行性 spike:任务能创建、能跑出进度、
能取消、能回结果,spike 不通过不扩端。
## 完成定义
- 每个契约接口都有实现或明确标注“本期不实现,客户端要怎么降级”。
- 有可跑的启动方式和接口自测证据(命令和结果)。
- 鉴权和同步冲突规则按契约实现,不是自己另发明一套。
- 已把“客户端怎么接你的服务”写给各客户端线程和 QA。
## 禁止
- 不改契约迎合实现;实现不了契约时向协调员提变更请求,不悄悄改接口。
- 不在服务端里写客户端专属逻辑。
@@ -0,0 +1,38 @@
# 角色模板:QA / 测试(单端复核 + 跨端 E2E,可分 2 个线程)
## 只能写
`tests/`(测试用例、跨端一致性矩阵、缺陷清单)。
不改业务代码,修 bug 回对应客户端或后端线程,你只负责复现和验证。
你绝不直接改 `docs/swe-todos.md`,结尾报告里写 TODO-UPDATE 即可。
## 产出
1. 测试用例:按 PRD 验收标准写,每个用例写清前置条件、步骤、期望结果。
单端复核先复核各客户端的自测证据,跨端 E2E 再做下面的矩阵,不要一个人包全端。
2. 跨端一致性矩阵:同一个 agent 任务、同一份数据,在已选各端的结果、
文案、状态词、同步状态是否一致;不一致的标明是缺陷还是刻意差异。
3. agent 专属测试(缺一项不算测完):沙箱越界、危险动作审批、
长任务超时和取消、断网续跑、模型 503 重试、token / 配额超限。
4. 安全检查点(G5 单独签字):权限边界是否按契约实现、密钥是否外泄、
agent 是否能读工作目录外文件。安全检查不算 QA 的附带项。
5. 缺陷清单:每个缺陷写清复现步骤、影响客户端、严重程度、归属线程。
6. 回归清单:契约从 v1 升到 v2 时,哪些用例必须重跑;G5 缺陷修完回 G5 回归,
回归范围按契约变更影响面定,不是每次都全量重测。
## 未编译端的口径
iOS / Android / Mac 在 Linux 主机上编不出来时,你的结论写
“代码评审 + Web 核心验证”,不写“构建通过”。没测的端和用例单独列出,
不要把“没测”写成“通过”。
## 完成定义
- 所有 PRD 验收标准都有对应测试用例,不是只测了 happy path。
- 跨端矩阵已覆盖全部已选客户端,缺失的端要写明为什么没测。
- 缺陷清单已交协调员,严重缺陷未修好前不建议进入验收。
## 禁止
- 不替开发线程改代码“顺手修掉”,修了就测不出原缺陷。
- 不把“没测”写成“通过”。没测的端和用例要单独列出。
@@ -0,0 +1,39 @@
# 角色模板:验收 / 发布(协调员兼任,用户拍板)
这不是一个可以自己给自己打分的开发线程。协调员准备材料,用户逐项确认。
## 协调员准备
1. 按 `templates/artifacts/acceptance-checklist.md` 生成本项目的验收清单,
每一项对应 PRD 里的一条验收标准,不额外加、不悄悄删。
2. 每个已选客户端准备一份演示产物:怎么启动、测哪个核心流程、
已知问题是什么、哪些平台未编译验证。web 演示可真跑,
未编译端审代码和构建步骤,两者效力不同,分开标注,不能混成一张“通过”表。
3. 发布说明草稿,按端分别写:新能力、已知问题、回滚方式
(至少写清“出问题先停哪个端”)。web 部署、桌面安装包分发、
应用商店审核是三条流水线,分开记录,不要写成一次全上。
## 用户确认规则
- 用户逐项勾选通过 / 不通过。不通过项写清原因,变成新一轮待办,
不算“基本通过”或“下次再说”。
- 协调员不能代替用户勾选,不能把 QA 通过等同于用户验收通过。
## 完成定义
- 验收清单全部通过,或不通过项已明确列入下一轮待办并经用户确认。
- 发布说明里的已知问题和未验证项与各客户端交付物一致,没有美化。
- 分支合并记录完整:哪个分支、何时、谁评审、CI 结果是什么、执行合并的人或宿主操作是谁。
协调员只给合并决定,不冒充执行人。
---
## 按端发布与回滚(验收)
- 发布按端分别做:每端发布时打 tag,例如 v0.1.0;
发布说明从已合 PR 汇总,不凭记忆重写;发布记录里写清端、tag、PR 号、执行人。
- 回滚是 revert 对应 PR,或重部署上一个 tag;绝不是 force-push 回退主线;
回滚后重新走合并门(CI、评审)才能再发布。
- 合并记录复核:PR 号、合并提交 hash、CI 运行链接、执行人缺一项,
验收就不算完成;已删远程分支的要确认合并提交已在主线。
@@ -0,0 +1,31 @@
# 角色模板:共享核心(G3,1 个线程)
## 只能写
`packages/shared/` 里的共享逻辑,以及 mock 服务和契约测试。
`contracts/` 冻结后只读,改契约走协调员的变更协议。
各端适配器不归你,归各客户端线程。
## 产出
1. 共享类型和 API SDK:按冻结契约实现,客户端照着调,不需要再问字段含义。
2. 共享逻辑:任务状态机、事件解析、重试策略、同步冲突处理,按契约实现。
3. 可跑的 mock 服务:G3 结束时客户端能对着 mock 跑集成测试,
mock 覆盖的接口清单要写清,不能只给 SDK 代码。
4. 契约测试:mock 和真实后端(后端线程参与)都要过同一套契约测试,
测试失败先定位是契约、SDK 还是后端的问题,不互相甩锅。
5. G3.5 可行性 spike 证据:核心链路真跑通的命令和结果。
agent 工具就是任务能创建、能跑出进度、能取消、能回结果。
## 完成定义
- 客户端线程拿到 SDK 和 mock 服务后能直接开工,不需要各写一套状态机。
- 契约测试通过,mock 覆盖清单已交协调员。
- spike 证据已交协调员,spike 不通过不进入 G4。
- 结尾报告带 TODO-UPDATE 小节,格式见技能主文档“待办机制”。
你绝不直接改 `docs/swe-todos.md`。
## 禁止
- 不在共享逻辑里写某端专属代码,专属逻辑留给各端适配器。
- 不改契约迎合实现;实现不了契约时向协调员提变更请求。
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""Verify the software-engineering skill appendix against its sources.
Provenance (see CHANGELOG.md in the skill directory): SKILL.md claims
its appendix is "spliced verbatim from templates/roles/ by a generation
script". The sources live at templates/roles/<id>.md next to SKILL.md;
this script is the check that makes that claim testable. It never edits
SKILL.md: the fix for a mismatch is an editorial decision (edit the
source, then re-issue the appendix through the skill's own track),
never a silent rewrite by this script.
What it checks, in order:
1. SKILL.md contains the appendix intro and exactly the nine
chapters 00..08, in order, each with a source note naming its own
templates/roles/<id>.md file.
2. templates/roles/ contains exactly those nine source files:
a missing source fails, an unexpected extra file fails.
3. Each source, in canonical form, equals the matching appendix
chapter in canonical form (see canonical() below).
4. CHANGELOG.md exists in the skill directory and mentions
templates/roles/, so the "see CHANGELOG" pointer in SKILL.md
resolves to a provenance record, not a dangling reference.
Canonical form (the ONLY allowed differences between a source file
and its appendix chapter -- anything else is a failure):
a. The chapter heading line itself ("## 附录角色:<id>(来源
templates/roles/<id>.md)") exists only in SKILL.md; it is
derived from the file name, not stored in the source.
b. One inter-chapter separator line "---": in the current SKILL.md
it appears after chapters 01/02/03/05/06 only. Chapters
00/04/07 end with their GitHub subsection and run straight into
the next heading with no separator, and 08 is the end of file.
The check strips at most one trailing separator; it does NOT
normalise SKILL.md to a uniform separator, so the current
inconsistency stays visible and untouched.
c. Blank lines around the heading and at the chapter end: a source
file is stored with exactly one trailing newline and no leading
blank line; the check strips surrounding blank lines before
comparing.
Any other difference -- wording, punctuation, order, a dropped or
added line -- fails with the first differing line number.
Usage:
python3 tools/check_appendix.py [--skill-dir DIR]
python3 tools/check_appendix.py --emit [--skill-dir DIR]
--emit prints the spliced appendix in canonical form (heading line +
source body per chapter) to stdout. It is a drafting aid for an
editor, NOT a byte patch for SKILL.md: because of (b) and (c) above,
its output differs from SKILL.md in separators and blank lines only,
and it must never be pasted over SKILL.md without a review pass.
Exit codes: 0 = all chapters match; 1 = any check failed; 2 = usage
error (argparse). Only the standard library is used, so the script
runs standalone without importing cloud/.
"""
import argparse
import re
import sys
from pathlib import Path
EXPECTED_IDS = ["00-coordinator", "01-product-manager", "02-architect",
"03-designer", "04-client-developer", "05-backend",
"06-qa", "07-acceptance", "08-shared-core"]
CHAPTER_RE = re.compile(
r"^## 附录角色:(\d{2}-[a-z0-9-]+)"
r"(来源 templates/roles/(\d{2}-[a-z0-9-]+\.md))",
flags=re.M)
TITLE_RE = re.compile(r"^# 角色模板:", flags=re.M)
APPENDIX_MARKER = "## 附录:角色模板"
def canonical(segment):
"""Canonical chapter body: strip surrounding blanks and at most
one trailing inter-chapter separator line, end with one newline."""
lines = segment.split("\n")
while lines and lines[0] == "":
lines.pop(0)
while lines and lines[-1] == "":
lines.pop()
if lines and lines[-1] == "---":
lines.pop()
while lines and lines[-1] == "":
lines.pop()
return "\n".join(lines) + "\n"
def first_diff(expected, actual):
"""Return (line_no_1_based, expected_line, actual_line) or None."""
exp_lines = expected.split("\n")
act_lines = actual.split("\n")
for i, (e, a) in enumerate(zip(exp_lines, act_lines)):
if e != a:
return i + 1, e, a
if len(exp_lines) != len(act_lines):
i = min(len(exp_lines), len(act_lines))
e = exp_lines[i] if i < len(exp_lines) else "<end of source>"
a = act_lines[i] if i < len(act_lines) else "<end of appendix>"
return i + 1, e, a
return None
def load_chapters(skill_dir):
"""Return (errors, chapters) where chapters maps id -> body segment
(raw text between its heading line and the next heading / EOF)."""
errors = []
skill_path = skill_dir / "SKILL.md"
if not skill_path.is_file():
return ["SKILL.md 缺失:%s" % skill_path], {}
text = skill_path.read_text(encoding="utf-8")
if APPENDIX_MARKER not in text:
return ["SKILL.md 缺失附录引言节 %r" % APPENDIX_MARKER], {}
appendix = text.split(APPENDIX_MARKER, 1)[1]
matches = list(CHAPTER_RE.finditer(appendix))
found = [m.group(1) for m in matches]
if found != EXPECTED_IDS:
errors.append(
"附录章节不符:应为 %s,实际为 %s" % (EXPECTED_IDS, found))
chapters = {}
for i, m in enumerate(matches):
end = matches[i + 1].start() if i + 1 < len(matches) else len(appendix)
if m.group(2) != m.group(1) + ".md":
errors.append(
"章节 %s 的来源注不符:应为 %s,实际为 %s"
% (m.group(1), m.group(1) + ".md", m.group(2)))
chapters[m.group(1)] = appendix[m.end():end]
return errors, chapters
def check(skill_dir):
"""Return a list of failure strings (empty means pass)."""
errors, chapters = load_chapters(skill_dir)
roles_dir = skill_dir / "templates" / "roles"
if not roles_dir.is_dir():
errors.append("源目录缺失:%s" % roles_dir)
sources = {}
else:
on_disk = sorted(p.name for p in roles_dir.iterdir()
if p.is_file())
expected_files = [cid + ".md" for cid in EXPECTED_IDS]
if on_disk != expected_files:
errors.append(
"源文件集合不符:应为 %s,实际为 %s"
% (expected_files, on_disk))
sources = {}
for name in on_disk:
sources[name[:-3]] = (roles_dir / name).read_text(
encoding="utf-8")
for cid in EXPECTED_IDS:
if cid not in sources:
errors.append("源缺失:templates/roles/%s.md 不存在" % cid)
continue
source = sources[cid]
if not TITLE_RE.match(source):
errors.append(
"源格式不符:%s.md 首行不是“# 角色模板:”" % cid)
if canonical(source) != source:
errors.append(
"源非规范形式:%s.md 含首尾空行或尾部分隔符"
"(源文件须以正文起、以单个换行收)" % cid)
if cid not in chapters:
errors.append("附录缺失章节:%s" % cid)
continue
expected = source
actual = canonical(chapters[cid])
if actual != expected:
diff = first_diff(expected, actual)
if diff is not None:
lineno, exp_line, act_line = diff
errors.append(
"内容不一致:章节 %s 第 %d 行不同\n"
" 源:%s\n 附录:%s"
% (cid, lineno, exp_line[:120], act_line[:120]))
else:
errors.append("内容不一致:章节 %s(未定位首异行)" % cid)
changelog = skill_dir / "CHANGELOG.md"
if not changelog.is_file():
errors.append("CHANGELOG.md 缺失:%s" % changelog)
elif "templates/roles/" not in changelog.read_text(encoding="utf-8"):
errors.append(
"CHANGELOG.md 未提及 templates/roles/,溯源记录不完整")
return errors
def emit(skill_dir):
"""Print the spliced appendix in canonical form to stdout."""
_errors, chapters = load_chapters(skill_dir)
roles_dir = skill_dir / "templates" / "roles"
parts = []
for cid in EXPECTED_IDS:
source = (roles_dir / (cid + ".md")).read_text(encoding="utf-8")
heading = ("## 附录角色:%s(来源 templates/roles/%s.md)"
% (cid, cid))
parts.append(heading + "\n\n" + source.rstrip("\n"))
return "\n\n".join(parts) + "\n"
def main(argv=None):
parser = argparse.ArgumentParser(
description="Verify the software-engineering skill appendix "
"against templates/roles/ sources.")
parser.add_argument(
"--skill-dir", default=None,
help="skill directory (default: parent of this script's "
"tools/ directory)")
parser.add_argument(
"--emit", action="store_true",
help="print the spliced appendix in canonical form instead "
"of verifying (drafting aid only, never a byte patch)")
args = parser.parse_args(argv)
if args.skill_dir:
skill_dir = Path(args.skill_dir)
else:
skill_dir = Path(__file__).resolve().parents[1]
if args.emit:
sys.stdout.write(emit(skill_dir))
return 0
errors = check(skill_dir)
if errors:
for err in errors:
print("FAIL: " + err, file=sys.stderr)
print("appendix provenance FAILED: %d problem(s)" % len(errors),
file=sys.stderr)
return 1
print("appendix provenance OK: %d chapters match templates/roles/"
% len(EXPECTED_IDS))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,117 @@
"""Appendix provenance for the software-engineering skill.
Background (see cloud/skills/software-engineering/CHANGELOG.md):
SKILL.md claimed its appendix was "spliced verbatim from
templates/roles/ by a generation script, see CHANGELOG", but the pack
contained none of the three: no templates/, no CHANGELOG.md, no
script. The appendix was therefore unverifiable -- a drifted chapter
would pass CI silently.
This file pins the repair, and nothing else:
- tools/check_appendix.py passes on the real pack (all nine chapters
match their templates/roles/ sources, CHANGELOG present and
mentioning templates/roles/);
- the same script FAILS (nonzero exit) on a temp copy with a source
deleted, a source tampered, or an extra source added -- so a future
"source missing" or "appendix drifted" state cannot slip through;
- --emit output is exactly the nine headings plus the nine source
bodies, so the spliced form is itself derived from the sources.
Scope guard: these tests never edit SKILL.md or the sources. A
failure here is an editorial decision for the skill's own track
(edit the source, then re-issue the appendix), not a silent rewrite.
Seeding behaviour is pinned elsewhere (test_skill_seed_pinning.py):
templates/, tools/ and CHANGELOG.md never reach the tenant home,
because skill_seed copies SKILL.md only.
"""
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILL_DIR = (REPO_ROOT / "cloud" / "skills" / "software-engineering")
SCRIPT = SKILL_DIR / "tools" / "check_appendix.py"
EXPECTED_IDS = ["00-coordinator", "01-product-manager", "02-architect",
"03-designer", "04-client-developer", "05-backend",
"06-qa", "07-acceptance", "08-shared-core"]
def _run(skill_dir=None):
cmd = [sys.executable, str(SCRIPT)]
if skill_dir is not None:
cmd += ["--skill-dir", str(skill_dir)]
return subprocess.run(cmd, capture_output=True, text=True)
def _temp_skill_copy(testcase):
tmp = tempfile.TemporaryDirectory()
testcase.addCleanup(tmp.cleanup)
dest = Path(tmp.name) / "software-engineering"
shutil.copytree(SKILL_DIR, dest)
return dest
class AppendixProvenanceTestCase(unittest.TestCase):
def test_check_script_passes_on_real_pack(self):
self.assertTrue(SCRIPT.is_file(), "校验脚本缺失:%s" % SCRIPT)
self.assertTrue(
(SKILL_DIR / "CHANGELOG.md").is_file(),
"CHANGELOG.md 缺失,SKILL.md 的“见 CHANGELOG”仍是悬空引用")
result = _run()
self.assertEqual(
result.returncode, 0,
"校验脚本应在真实包上通过,stderr:%s" % result.stderr)
self.assertIn("9 chapters match", result.stdout)
def test_missing_source_fails(self):
skill = _temp_skill_copy(self)
(skill / "templates" / "roles" / "03-designer.md").unlink()
result = _run(skill)
self.assertNotEqual(
result.returncode, 0, "源缺失时校验必须失败退出非零")
self.assertIn("03-designer", result.stderr)
def test_tampered_source_fails(self):
skill = _temp_skill_copy(self)
target = skill / "templates" / "roles" / "06-qa.md"
text = target.read_text(encoding="utf-8")
target.write_text(text.replace("不把“没测”写成“通过”",
"不把“没测”写成“通过X”"),
encoding="utf-8")
result = _run(skill)
self.assertNotEqual(
result.returncode, 0, "源被改动时校验必须失败退出非零")
self.assertIn("06-qa", result.stderr)
def test_extra_source_fails(self):
skill = _temp_skill_copy(self)
(skill / "templates" / "roles" / "99-extra.md").write_text(
"# 角色模板:多余\n", encoding="utf-8")
result = _run(skill)
self.assertNotEqual(
result.returncode, 0, "多余源文件时校验必须失败退出非零")
self.assertIn("99-extra", result.stderr)
def test_emit_is_headings_plus_sources(self):
result = subprocess.run(
[sys.executable, str(SCRIPT), "--emit"],
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
expected_parts = []
for cid in EXPECTED_IDS:
heading = ("## 附录角色:%s(来源 templates/roles/%s.md)"
% (cid, cid))
source = (SKILL_DIR / "templates" / "roles"
/ (cid + ".md")).read_text(encoding="utf-8")
expected_parts.append(heading + "\n\n" + source.rstrip("\n"))
self.assertEqual(result.stdout, "\n\n".join(expected_parts) + "\n",
"--emit 输出须恰为 9 章标题 + 9 份源正文")
if __name__ == "__main__":
unittest.main()