From 4e554b5c079147e5bbd55fc7a2262008d8f7a044 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 18:56:44 +0000 Subject: [PATCH] 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. --- .../skills/software-engineering/CHANGELOG.md | 52 ++++ .../templates/roles/00-coordinator.md | 95 +++++++ .../templates/roles/01-product-manager.md | 36 +++ .../templates/roles/02-architect.md | 43 ++++ .../templates/roles/03-designer.md | 35 +++ .../templates/roles/04-client-developer.md | 70 ++++++ .../templates/roles/05-backend.md | 29 +++ .../templates/roles/06-qa.md | 38 +++ .../templates/roles/07-acceptance.md | 39 +++ .../templates/roles/08-shared-core.md | 31 +++ .../tools/check_appendix.py | 233 ++++++++++++++++++ cloud/tests/test_skill_appendix_provenance.py | 117 +++++++++ 12 files changed, 818 insertions(+) create mode 100644 cloud/skills/software-engineering/CHANGELOG.md create mode 100644 cloud/skills/software-engineering/templates/roles/00-coordinator.md create mode 100644 cloud/skills/software-engineering/templates/roles/01-product-manager.md create mode 100644 cloud/skills/software-engineering/templates/roles/02-architect.md create mode 100644 cloud/skills/software-engineering/templates/roles/03-designer.md create mode 100644 cloud/skills/software-engineering/templates/roles/04-client-developer.md create mode 100644 cloud/skills/software-engineering/templates/roles/05-backend.md create mode 100644 cloud/skills/software-engineering/templates/roles/06-qa.md create mode 100644 cloud/skills/software-engineering/templates/roles/07-acceptance.md create mode 100644 cloud/skills/software-engineering/templates/roles/08-shared-core.md create mode 100644 cloud/skills/software-engineering/tools/check_appendix.py create mode 100644 cloud/tests/test_skill_appendix_provenance.py diff --git a/cloud/skills/software-engineering/CHANGELOG.md b/cloud/skills/software-engineering/CHANGELOG.md new file mode 100644 index 0000000..62c6d42 --- /dev/null +++ b/cloud/skills/software-engineering/CHANGELOG.md @@ -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`(装机路径)未在本条目处理, + 仍缺失;本条目只补附录溯源,不扩大范围。 diff --git a/cloud/skills/software-engineering/templates/roles/00-coordinator.md b/cloud/skills/software-engineering/templates/roles/00-coordinator.md new file mode 100644 index 0000000..845248c --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/00-coordinator.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 + 覆盖密钥文件与本地配置文件。 diff --git a/cloud/skills/software-engineering/templates/roles/01-product-manager.md b/cloud/skills/software-engineering/templates/roles/01-product-manager.md new file mode 100644 index 0000000..c0b43d9 --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/01-product-manager.md @@ -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 里塞技术实现细节。 +- 不把猜测的用户需求写成事实,拿不准的写成“待用户确认的问题”。 diff --git a/cloud/skills/software-engineering/templates/roles/02-architect.md b/cloud/skills/software-engineering/templates/roles/02-architect.md new file mode 100644 index 0000000..c265793 --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/02-architect.md @@ -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 框架、写界面代码。 diff --git a/cloud/skills/software-engineering/templates/roles/03-designer.md b/cloud/skills/software-engineering/templates/roles/03-designer.md new file mode 100644 index 0000000..30b7aa0 --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/03-designer.md @@ -0,0 +1,35 @@ +# 角色模板:UI/UX 设计(1 个线程) + +## 输入 + +协调员给你的:PRD、客户端清单、架构师给出的跨端一致性规则、 +任务状态机、审批模型。你要和架构线程做一次联合评审, +审批界面和状态机对得上才能冻结。 + +## 只能写 + +`docs/design/`(流程、页面、设计规范)。不写业务代码,不改契约。 + +## 产出 + +1. 核心流程:每个核心能力从进入到完成的用户流程,含异常分支 + (失败、超时、没权限、没网络时界面说什么、给什么操作)。 + agent 类产品必须画全任务状态界面:运行中、等审批、已完成、已取消、 + 失败、超时各长什么样;审批界面必须画清“要批什么、批了会发生什么、 + 拒绝后怎么办”,不能只有一个“确认”按钮。 +2. 页面清单:每个客户端共有的页面和各端特有的页面,差异要说明原因。 +3. 设计规范:颜色、字体、间距、组件状态(正常 / 加载 / 空 / 报错) + 的基本规则,保证各端看起来是同一个产品。 +4. 跨端一致性清单:同一个任务、同一条数据在各端展示的字段、 + 文案、状态词是否一致。 + +## 完成定义 + +- 客户端线程拿到设计文档后能直接开工,不需要再问“这个页面长什么样”。 +- 异常分支已覆盖,不是只有 happy path。 +- 已交协调员,和架构契约一起进入冻结判定。 + +## 禁止 + +- 不把设计稿写成“大概这样”,含糊处要标明待确认。 +- 不在设计里改产品范围,新增页面要先经协调员确认在 MVP 内。 diff --git a/cloud/skills/software-engineering/templates/roles/04-client-developer.md b/cloud/skills/software-engineering/templates/roles/04-client-developer.md new file mode 100644 index 0000000..4c26c71 --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/04-client-developer.md @@ -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 只进子进程环境,不写进示例命令。 diff --git a/cloud/skills/software-engineering/templates/roles/05-backend.md b/cloud/skills/software-engineering/templates/roles/05-backend.md new file mode 100644 index 0000000..9d09a04 --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/05-backend.md @@ -0,0 +1,29 @@ +# 角色模板:后端 / API(可选,1 个线程) + +只在产品需要服务端时派生。纯本地工具、纯前端调用第三方 API 的项目 +不需要这个线程,协调员要在决策里写明“无后端”及原因。 + +## 只能写 + +`server/`。`contracts/` 冻结后只读,改契约走协调员的变更协议。 + +## 产出 + +按冻结契约实现的服务端:鉴权、核心业务接口、数据同步、 +任务状态与超时、失败重试、日志与限流(按 PRD 范围,不超做)。 +你在 G3 阶段就要参与契约测试,和共享核心线程一起把 mock 服务 +换成可跑的真实接口,不要等到 G4 才让客户端发现接口对不上。 +agent 类产品还要参与 G3.5 可行性 spike:任务能创建、能跑出进度、 +能取消、能回结果,spike 不通过不扩端。 + +## 完成定义 + +- 每个契约接口都有实现或明确标注“本期不实现,客户端要怎么降级”。 +- 有可跑的启动方式和接口自测证据(命令和结果)。 +- 鉴权和同步冲突规则按契约实现,不是自己另发明一套。 +- 已把“客户端怎么接你的服务”写给各客户端线程和 QA。 + +## 禁止 + +- 不改契约迎合实现;实现不了契约时向协调员提变更请求,不悄悄改接口。 +- 不在服务端里写客户端专属逻辑。 diff --git a/cloud/skills/software-engineering/templates/roles/06-qa.md b/cloud/skills/software-engineering/templates/roles/06-qa.md new file mode 100644 index 0000000..28d618c --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/06-qa.md @@ -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。 +- 跨端矩阵已覆盖全部已选客户端,缺失的端要写明为什么没测。 +- 缺陷清单已交协调员,严重缺陷未修好前不建议进入验收。 + +## 禁止 + +- 不替开发线程改代码“顺手修掉”,修了就测不出原缺陷。 +- 不把“没测”写成“通过”。没测的端和用例要单独列出。 diff --git a/cloud/skills/software-engineering/templates/roles/07-acceptance.md b/cloud/skills/software-engineering/templates/roles/07-acceptance.md new file mode 100644 index 0000000..3407cf4 --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/07-acceptance.md @@ -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 运行链接、执行人缺一项, + 验收就不算完成;已删远程分支的要确认合并提交已在主线。 diff --git a/cloud/skills/software-engineering/templates/roles/08-shared-core.md b/cloud/skills/software-engineering/templates/roles/08-shared-core.md new file mode 100644 index 0000000..7f358a7 --- /dev/null +++ b/cloud/skills/software-engineering/templates/roles/08-shared-core.md @@ -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`。 + +## 禁止 + +- 不在共享逻辑里写某端专属代码,专属逻辑留给各端适配器。 +- 不改契约迎合实现;实现不了契约时向协调员提变更请求。 diff --git a/cloud/skills/software-engineering/tools/check_appendix.py b/cloud/skills/software-engineering/tools/check_appendix.py new file mode 100644 index 0000000..bfecdea --- /dev/null +++ b/cloud/skills/software-engineering/tools/check_appendix.py @@ -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/.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/.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 ("## 附录角色:(来源 + templates/roles/.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 "" + a = act_lines[i] if i < len(act_lines) else "" + 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()) diff --git a/cloud/tests/test_skill_appendix_provenance.py b/cloud/tests/test_skill_appendix_provenance.py new file mode 100644 index 0000000..c46a0fe --- /dev/null +++ b/cloud/tests/test_skill_appendix_provenance.py @@ -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()