@house365/comate 0.9.18 → 0.9.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,45 +1,33 @@
1
- # comate 入门操作手册
1
+ # comate 入门指南
2
2
 
3
- 这是一份给第一次使用 comate 的同学看的操作手册。按“安装 → 初始化 → 输入 /comate”的顺序,就可以开始使用。
3
+ 这份指南覆盖 comate 的安装、初始化和日常使用。
4
4
 
5
5
  ## 1. comate 是什么
6
6
 
7
- comate 是一个 AI 编程流程编排器。它把一次需求变更拆成几个可以检查的阶段,让 AI 先理解需求和设计,再写代码,最后验证并归档。
8
-
9
- 完整流程:
7
+ comate 是一个面向 AI 编程工具的 SDD(规约驱动开发)编排器。它把一次需求变更按阶段推进:
10
8
 
11
9
  ```text
12
10
  原始 PRD(可选)
13
- ↓
14
- /comate-gate PRD 门禁和拆分
15
- ↓
16
- /comate-open 开题,创建 change
17
- ↓
18
- /comate-design 深度设计
19
- ↓
20
- /comate-build 计划和编码
21
- ↓
22
- /comate-verify 验证和收尾
23
- ↓
24
- /comate-archive 归档规格
11
+ -> gate -> open -> design -> build -> verify -> archive
25
12
  ```
26
13
 
27
- - **OpenSpec** 负责“要做什么”:proposal、spec、tasks 和归档。
28
- - **Superpowers** 负责“怎么做好”:头脑风暴、技术设计、计划、执行和验证。
29
- - **comate** 负责识别当前阶段,并把这些能力串起来。
14
+ 通常只需要使用 `/comate`,它会根据当前项目状态自动进入下一阶段。
30
15
 
31
- ## 2. 安装前准备
16
+ - `/comate-gate`:审阅 PRD,并在需要时拆分变更。
17
+ - `/comate-fix`:处理 Bug 或很小的文案、配置调整。
18
+ - `/comate-grill`:逐个澄清 PRD、设计或计划中的关键决策。
19
+ - `/comate-kb-integration`:根据第三方官方文档生成对接档案。
32
20
 
33
- ### 2.1 必需软件
21
+ ## 2. 安装前准备
34
22
 
35
- | 软件 | 建议版本 | 用途 |
36
- | --- | --- | --- |
37
- | Node.js | 20 或更高 | 运行 comate 和 npm |
38
- | npm | 随 Node.js 安装 | 安装 comate、OpenSpec |
39
- | Git | 任意现代版本 | 管理代码和分支 |
40
- | Bash | 3.2 或更高 | 执行 comate 的状态机脚本 |
23
+ 需要以下环境:
41
24
 
42
- Windows 用户建议安装 [Git for Windows](https://git-scm.com/download/win),它会提供 Git Bash。PowerShell 可以运行 `comate`,但流程脚本仍需要 Bash。
25
+ | 软件 | 要求 |
26
+ | --- | --- |
27
+ | Node.js | 20 或更高版本 |
28
+ | npm | 随 Node.js 安装 |
29
+ | Git | 任意现代版本 |
30
+ | Bash | 3.2 或更高版本 |
43
31
 
44
32
  检查环境:
45
33
 
@@ -50,380 +38,151 @@ git --version
50
38
  bash --version
51
39
  ```
52
40
 
53
- ### 2.2 哪些步骤需要网络
41
+ Windows 用户请安装 [Git for Windows](https://git-scm.com/download/win),以提供 Git Bash。PowerShell 可以运行 `comate`,但 comate 的流程脚本仍需要 Bash。
54
42
 
55
- 首次初始化时,`comate init` 可能需要访问 npm registry 安装 OpenSpec CLI,并访问 GitHub 安装 Superpowers。
43
+ 首次初始化会访问 npm registry 和 GitHub,用于安装 OpenSpec CLI 与 Superpowers。
56
44
 
57
- comate 自己的 npm 包会随包携带 comate skill。npm 安装钩子只会尽力同步 comate 自身 skill,不会安装 Superpowers,也不会创建项目的 `openspec/` 和 `docs/`;这些由项目目录中的 `comate init` 完成。
45
+ ## 3. 安装并初始化项目
58
46
 
59
- ## 3. 安装 comate
60
-
61
- 全局安装:
47
+ 先安装 comate:
62
48
 
63
49
  ```bash
64
50
  npm install -g @house365/comate
65
51
  ```
66
52
 
67
- 确认命令可用:
68
-
69
- ```bash
70
- comate --version
71
- comate --help
72
- ```
73
-
74
- 安装后,skill 通常会同步到用户级目录:
75
-
76
- - macOS/Linux:`~/.agents/skills/`
77
- - Windows:`%USERPROFILE%\.agents\skills\`
78
-
79
- 用户级 skill 可以被多个项目共用。项目自己的规格文件放在项目目录下的 `openspec/`。
80
-
81
- ## 4. 初始化项目:`comate init`
82
-
83
- 进入项目根目录再运行:
53
+ 然后进入业务项目根目录初始化:
84
54
 
85
55
  ```bash
86
56
  cd path/to/your-project
87
57
  comate init
58
+ comate doctor
88
59
  ```
89
60
 
90
- Windows PowerShell 示例:
91
-
92
- ```powershell
93
- Set-Location D:\work\my-project
94
- comate init
95
- ```
96
-
97
- 请不要在用户主目录直接运行。comate 会检查当前目录是否有 `.git`、`package.json`、`pyproject.toml`、`Cargo.toml` 等项目标记;空目录会先询问是否确认初始化。
98
-
99
- ### 4.1 `init` 会做什么
100
-
101
- 1. 检查 Bash 版本。
102
- 2. 检查 OpenSpec,不存在或版本过旧时执行 `npm install -g @fission-ai/openspec@latest`。
103
- 3. 检查 Superpowers,不存在时执行 `skills add obra/superpowers -g -y`。
104
- 4. 把 comate 自身 skill 同步到用户级 skill 目录。
105
- 5. 创建项目级 `openspec/`(已经存在则保留)。
106
- 6. 创建 `docs/` 知识库骨架(只补缺少的文件,不覆盖已有文件)。
107
-
108
- 某一步失败时,修好网络或环境后重新运行 `comate init` 即可。它是幂等的,已完成的步骤会跳过。
109
-
110
- ## 5. 初始化后的目录结构
111
-
112
- ```text
113
- your-project/
114
- ├─ openspec/
115
- │ ├─ changes/ # 每次需求变更的工作区
116
- │ └─ specs/ # 已沉淀的主规格
117
- ├─ docs/
118
- │ ├─ README.md
119
- │ ├─ architecture/
120
- │ │ └─ README.md
121
- │ └─ knowledge-base/
122
- │ ├─ README.md
123
- │ ├─ requirements/
124
- │ │ └─ _templates/PRD.md
125
- │ └─ integrations/
126
- │ ├─ README.md
127
- │ └─ _TEMPLATE/
128
- └─ 你的业务代码...
129
- ```
130
-
131
- ### 5.1 `openspec/`:项目规格
132
-
133
- - `changes/<change-name>/`:正在进行的变更,包含 `proposal.md`、`design.md`、`tasks.md`、delta spec 和 `.comate.yaml`。
134
- - `specs/<capability>/spec.md`:归档后长期维护的能力规格。
135
- - `changes/archive/`:已经完成并归档的 change。
136
-
137
- `.comate.yaml` 是 comate 的状态真相源,记录当前阶段、构建模式、验证结果等。
138
-
139
- ### 5.2 `docs/`:长期知识库
140
-
141
- - `docs/`:项目背景、架构约束、原始 PRD、第三方对接资料。
142
- - `openspec/`:某次变更的提案、设计、任务、能力规格和归档记录。
143
-
144
- ### 5.3 用户级 skill 目录
145
-
146
- ```text
147
- ~/.agents/skills/ # macOS/Linux
148
- %USERPROFILE%\.agents\skills\ # Windows
149
- ```
150
-
151
- 已有的 Superpowers 或其他 skill 不会因为 `comate init` 被覆盖。
152
-
153
- ## 6. `docs/` 目录详解
154
-
155
- ### 6.1 `docs/README.md`
156
-
157
- 说明知识库整体规则、`architecture/` 与 `knowledge-base/` 的边界,以及它们和 `openspec/` 的关系。
158
-
159
- ### 6.2 `docs/architecture/`
160
-
161
- 放项目架构约束,例如前后端分层、API/数据库/错误码约定、命名规范和部署约束。`comate init` 只创建占位 README,真正的规则需要项目自己补充。这里是 `/comate-gate` 检查契约时的事实来源。
162
-
163
- ### 6.3 `docs/knowledge-base/`
164
-
165
- 放长期维护的需求和外部服务资料:
166
-
167
- - `requirements/`:原始 PRD;
168
- - `integrations/`:第三方服务对接档案;
169
- - `README.md`:命名、溯源和 PRD 到 change 的流程说明。
170
-
171
- ### 6.4 `docs/knowledge-base/requirements/`
172
-
173
- 这里是 PRD 的固定位置。模板位于:
174
-
175
- ```text
176
- docs/knowledge-base/requirements/_templates/PRD.md
177
- ```
178
-
179
- 复制模板到功能目录,填写后运行 `/comate-gate`。
180
-
181
- ### 6.5 `docs/knowledge-base/integrations/`
182
-
183
- 每个第三方平台建立一个子目录,例如:
184
-
185
- ```text
186
- docs/knowledge-base/integrations/alipay/README.md
187
- docs/knowledge-base/integrations/alipay/api-snapshot.md
188
- ```
61
+ `comate init` 会:
189
62
 
190
- 需要从官方文档生成对接档案时,使用 `/comate-kb-integration`。
63
+ 1. 安装或检查 OpenSpec CLI、Superpowers 和 comate skills。
64
+ 2. 在用户级目录安装 skills(通常是 `~/.agents/skills/`;Windows 为 `%USERPROFILE%\.agents\skills\`)。
65
+ 3. 在当前项目创建 `openspec/` 和 `docs/` 知识库骨架。
191
66
 
192
- ## 7. PRD 存放位置和命名规范
67
+ 已有文件不会被覆盖,初始化可以重复执行。请在项目目录运行,不要直接在用户主目录运行。
193
68
 
194
- 推荐格式:
69
+ 初始化后,项目通常会多出:
195
70
 
196
71
  ```text
197
- docs/knowledge-base/requirements/<YYYYMMDD-功能名>/PRD-<三位序号>-<功能名>.md
72
+ openspec/ # 变更和长期规格
73
+ docs/knowledge-base/requirements/ # PRD
74
+ docs/architecture/ # 架构约束
198
75
  ```
199
76
 
200
- 示例:
77
+ ## 4. 第一次使用
201
78
 
202
- ```text
203
- docs/knowledge-base/requirements/20260820-用户登录/PRD-001-用户登录.md
204
- ```
205
-
206
- 规则:
207
-
208
- 1. 功能目录使用“日期 + 功能名”,日期格式为 `YYYYMMDD`。
209
- 2. PRD 文件以 `PRD-001`、`PRD-002` 这样的三位序号开头。
210
- 3. 文件名中的功能名应和目录功能名保持一致。
211
- 4. 同一个功能目录中的序号不要重复。
212
- 5. 不建议使用 `PRD-1.md` 或把 PRD 放到 `openspec/changes/`。
213
-
214
- `/comate-gate` 会在 PRD 同级自动生成:
79
+ 在 Claude Code、Codex、Cursor 等支持 Agent Skills 的工具中输入:
215
80
 
216
81
  ```text
217
- PRD-001.gate.yaml # 机器读取的门禁状态
218
- PRD-001-REVIEW.md # 人类可读的评审报告
82
+ /comate 我想增加用户登录功能,支持邮箱和密码登录
219
83
  ```
220
84
 
221
- PRD 通过门禁后如果又被修改,哈希会变化,门禁会变成过期状态,需要重新运行 `/comate-gate`。
85
+ comate 会创建并推进一个 change。过程中只在需要你决定时提问;确认设计、实现和验证结果后,流程会完成归档。
222
86
 
223
- ## 8. 从需求到代码的 Skill 流程
87
+ ### 需求来自 PRD
224
88
 
225
- ### 8.1 需求来自一份 PRD
226
-
227
- 先把 PRD 放到 `docs/knowledge-base/requirements/`,然后直接指定 PRD 路径:
89
+ 复杂需求可以先把 PRD 放在 `docs/knowledge-base/requirements/`,然后直接指定文件:
228
90
 
229
91
  ```text
230
92
  /comate docs/knowledge-base/requirements/20260820-用户登录/PRD-001-用户登录.md
231
93
  ```
232
94
 
233
- `/comate` 会识别这是原始 PRD,自动进入 PRD 门禁和拆分流程;不需要先手动调用 `/comate-gate`。门禁通过后,它会继续处理拆分出来的 change。
234
-
235
- 如果只想单独审阅或重新运行门禁,也可以直接输入:
95
+ 这会自动进入 PRD 门禁和拆分流程。只想单独审阅 PRD 时使用:
236
96
 
237
97
  ```text
238
- /comate-gate docs/knowledge-base/requirements/20260820-用户登录/PRD-001-用户登录.md
98
+ /comate-gate path/to/PRD.md
239
99
  ```
240
100
 
241
- 对已有 change 继续工作时,输入:
242
-
243
- ```text
244
- /comate
245
- ```
246
-
247
- comate 会自动从当前阶段继续:`open → design → build → verify → archive`。
248
-
249
- ### 8.2 需求来自普通对话
250
-
251
- 不需要先写 PRD,直接输入:
252
-
253
- ```text
254
- /comate 我想增加用户登录功能,支持邮箱和密码登录
255
- ```
256
-
257
- 复杂需求仍建议先整理成 PRD,便于评审和追踪。
258
-
259
- ### 8.3 Bug 或很小的调整
101
+ ### Bug 或小调整
260
102
 
261
103
  ```text
262
104
  /comate-fix 修复登录失败时页面没有提示的问题
263
105
  ```
264
106
 
265
- 它走简化流程:`open → build → verify → archive`,跳过完整 brainstorming。涉及新能力、架构变化、数据库 schema 或多个模块时,应升级回完整 `/comate` 流程。
266
-
267
- ## 9. Skill 介绍
268
-
269
- | Skill | 作用 |
270
- | --- | --- |
271
- | `/comate` | 总入口,自动检测当前 change 阶段并分派 |
272
- | `/comate-gate` | 审阅 PRD、评分、记录阻断问题并拆分 change |
273
- | `/comate-open` | 探索需求,创建 change 和 proposal/design/tasks 骨架 |
274
- | `/comate-design` | 技术设计和方案头脑风暴,形成 Design Doc 和 delta spec |
275
- | `/comate-build` | 制定实施计划、按任务编码并更新 tasks |
276
- | `/comate-verify` | 运行测试和检查,生成验证报告并处理分支收尾 |
277
- | `/comate-archive` | 同步 delta spec、标记设计文档并归档 change |
278
- | `/comate-fix` | Bug 或小调整的快速路径 |
279
- | `/comate-grill` | 对 PRD、计划或设计中的模糊决策逐个追问 |
280
- | `/comate-kb-integration` | 根据第三方官方文档生成 integrations 对接档案 |
281
-
282
- ### 9.1 底层依赖分工
107
+ 如果改动涉及新能力、架构、数据库结构或多个模块,请改用完整的 `/comate` 流程。
283
108
 
284
- - **OpenSpec**:负责 `openspec/`、change 规格和归档。
285
- - **Superpowers**:由 `comate init` 通过 `skills` CLI 从 `obra/superpowers` 安装,负责设计、规划、执行和收尾。
286
- - **comate skill**:本项目提供的编排规则和状态机。
109
+ ### 中断后继续
287
110
 
288
- 首次使用时,如果 Superpowers 安装失败,design/build/verify 相关流程不能完整运行。先修复网络或手工安装,再重跑 `comate init`。
111
+ 回到同一个项目目录,再输入:
289
112
 
290
- ## 10. 常用命令
291
-
292
- ### 10.1 安装和检查
293
-
294
- ```bash
295
- comate --help
296
- comate --version
297
- comate init
298
- comate doctor
113
+ ```text
114
+ /comate
299
115
  ```
300
116
 
301
- ### 10.2 查看 token 用量
117
+ comate 会读取 `openspec/changes/<change-name>/.comate.yaml`,从当前阶段继续。
302
118
 
303
- ```bash
304
- comate usage
305
- comate usage daily
306
- comate usage --global
307
- comate usage --json
308
- ```
309
-
310
- ## 11. 配套 UI 查看器
119
+ ## 5. 配套 UI(可选)
311
120
 
312
- `comate-ui` 是 comate 的本地只读浏览器工作区,用来可视化查看项目中的变更状态、OpenSpec 规格、PRD、Superpowers 设计文档、计划和验证报告。它不会修改项目文件,也不需要数据库。
121
+ Comate UI 是本地只读的可视化工作台,用于查看 change 状态、规格、PRD、实施计划和验证结果,不会修改项目文件。
313
122
 
314
- 全局安装:
123
+ 安装:
315
124
 
316
125
  ```bash
317
126
  npm install -g @house365/comate-ui@latest
318
127
  ```
319
128
 
320
- 安装后,在包含 `openspec/` 的项目根目录运行:
129
+ 在已经执行过 `comate init` 的项目根目录启动:
321
130
 
322
131
  ```bash
323
132
  comate-ui
324
133
  ```
325
134
 
326
- 也可以指定项目路径、端口或禁止自动打开浏览器:
135
+ 启动后会自动打开浏览器。服务只监听本机 `127.0.0.1`,按 `Ctrl+C` 停止。也可以不安装直接运行,或指定项目和端口:
327
136
 
328
137
  ```bash
329
- comate-ui D:\work\my-project
330
- comate-ui D:\work\my-project --port 4300
331
- comate-ui --no-open
138
+ npx @house365/comate-ui@latest
139
+ comate-ui path/to/your-project --port 4300
140
+ comate-ui --no-open # 不自动打开浏览器
141
+ comate-ui --demo # 查看内置示例
332
142
  ```
333
143
 
334
- 查看内置示例:
144
+ ## 6. 常用命令
335
145
 
336
146
  ```bash
337
- comate-ui --demo
147
+ comate --help # 查看命令帮助
148
+ comate --version # 查看版本
149
+ comate doctor # 检查 Bash、OpenSpec 和 skills
150
+ comate update # 更新到 npm 最新版本
151
+ comate usage # 查看当前项目 token 用量
152
+ comate usage daily # 按天查看
153
+ comate usage --global # 查看全部项目
338
154
  ```
339
155
 
340
- 查看器只绑定本机 `127.0.0.1`。默认会自动打开浏览器;端口被占用时会自动选择下一个可用端口,按 `Ctrl+C` 停止。
341
-
342
- ## 12. 更新 comate
156
+ 更新后建议在各项目根目录重新执行一次 `comate init`,把新版 skills 同步到用户级目录:
343
157
 
344
158
  ```bash
345
- npm install -g @house365/comate@latest
159
+ comate update
346
160
  cd path/to/your-project
347
161
  comate init
348
162
  ```
349
163
 
350
- npm 更新 CLI 和包内文件;`comate init` 负责把新版 comate skill 同步到用户级目录,并补齐当前项目缺少的 `openspec/`、`docs/` 文件。
351
-
352
- 更新是幂等的:已有 Superpowers 不覆盖,已有项目规格和 docs 文件不覆盖。若使用 `npm install --ignore-scripts`,请手动执行 `comate init`。
353
-
354
- ## 13. 网络问题和手工补装
355
-
356
- ### OpenSpec 安装失败
357
-
358
- ```bash
359
- npm install -g @fission-ai/openspec@latest
360
- openspec --version
361
- ```
362
-
363
- ### Superpowers 安装失败
164
+ ## 7. 常见问题
364
165
 
365
- ```bash
366
- skills add obra/superpowers -g -y
367
- ```
166
+ ### `comate init` 失败
368
167
 
369
- GitHub 无法访问时,可以配置 npm/Git 代理、使用公司网络或 Git Bash/WSL,安装成功后再运行:
168
+ 先运行:
370
169
 
371
170
  ```bash
372
- comate init
373
171
  comate doctor
374
172
  ```
375
173
 
376
- 当前 npm 包没有内置 Superpowers 离线快照;不要把“npm 安装成功”理解成 Superpowers 已安装完成。
377
-
378
- ## 14. 常见问题
379
-
380
- ### `comate init` 应该在哪运行?
381
-
382
- 在业务项目根目录运行,也就是能看到 `.git` 或 `package.json` 的目录。不要在 `C:\Users\你的用户名` 或 `~` 直接运行。
383
-
384
- ### npm 安装后为什么还要 `comate init`?
385
-
386
- npm 只安装 CLI 和包本身。`init` 才会准备 OpenSpec、Superpowers 与 comate skill,并创建当前项目的 `openspec/`、`docs/`。
387
-
388
- ### 重跑 init 会不会覆盖我的文件?
389
-
390
- 不会。已有 skill、`openspec/`、docs 文件会保留,只补缺少的内容。
391
-
392
- ### 找不到 `openspec` 怎么办?
393
-
394
- 重开终端让全局 npm PATH 生效,然后运行 `openspec --version`。仍找不到就执行 `npm install -g @fission-ai/openspec@latest`,再运行 `comate init`。
395
-
396
- ### 找不到 Bash 怎么办?
397
-
398
- Windows 安装 Git for Windows;macOS 自带的 Bash 3.2 可以直接使用,不需要另外安装 Bash 4+。
399
-
400
- ### 流程中断了怎么办?
401
-
402
- 回到同一个项目目录,再输入 `/comate`。comate 会读取 `openspec/changes/<name>/.comate.yaml` 和已有产物,从当前阶段继续。先运行 `comate doctor` 可以排除环境问题。
403
-
404
- ### 有多个 change 怎么办?
405
-
406
- 在对话中明确 change 名称。不要同时修改多个 change 的 `tasks.md`。
407
-
408
- ## 15. 新手最短上手路径
174
+ 如果是网络问题,确认 npm registry 和 GitHub 可访问,修复代理或网络后重新运行 `comate init`。也可以手动补装:
409
175
 
410
176
  ```bash
411
- npm install -g @house365/comate
412
- cd your-project
177
+ npm install -g @fission-ai/openspec@latest
178
+ skills add obra/superpowers -g -y
413
179
  comate init
414
- comate doctor
415
180
  ```
416
181
 
417
- 然后在 Claude Code、Codex、Cursor 等支持 Agent Skills 的工具中输入:
418
-
419
- ```text
420
- /comate 我想实现一个用户登录功能
421
- ```
182
+ ### 找不到 `openspec` 或 Bash
422
183
 
423
- 等待 AI 在需要你决定的地方提问,确认设计和构建选项后,让流程走到验证和归档。
184
+ 重开终端让全局 npm PATH 生效。Windows 请安装 Git for Windows,并确认 `bash --version` 不低于 3.2。
424
185
 
425
- ## 16. 进一步阅读
186
+ ### 重跑 `init` 会覆盖项目文件吗?
426
187
 
427
- - [完整技术手册](COMATE_MANUAL.md):状态字段、门禁规则、脚本接口和故障排查。
428
- - [项目 README](../README.md):项目概览和快速安装。
429
- - 初始化后生成的 `docs/knowledge-base/README.md`:PRD 溯源和命名规则。
188
+ 不会。`init` 只补充缺少的 skills、`openspec/` 和 `docs/` 文件,已有内容会保留。
@@ -0,0 +1,201 @@
1
+ # comate init 目录说明
2
+
3
+ `comate init` 会同时准备两类内容:用户级 skills,以及当前项目的 OpenSpec 和知识库目录。
4
+
5
+ ## 1. 初始化时会发生什么
6
+
7
+ 在项目根目录运行:
8
+
9
+ ```bash
10
+ comate init
11
+ ```
12
+
13
+ 它会:
14
+
15
+ - 将 comate、OpenSpec 和 Superpowers 相关 skills 安装到用户级 skills 目录。
16
+ - 在当前项目创建 `openspec/`。
17
+ - 在当前项目补齐 `docs/` 知识库骨架。
18
+ - 保留已经存在的文件,不覆盖项目已有内容。
19
+
20
+ 项目目录通常会得到以下结构:
21
+
22
+ ```text
23
+ your-project/
24
+ ├── openspec/
25
+ │ ├── config.yaml
26
+ │ ├── changes/
27
+ │ │ └── archive/
28
+ │ └── specs/
29
+ └── docs/
30
+ ├── README.md
31
+ ├── architecture/
32
+ │ └── README.md
33
+ └── knowledge-base/
34
+ ├── README.md
35
+ ├── requirements/
36
+ │ └── _templates/
37
+ │ └── PRD.md
38
+ └── integrations/
39
+ ├── README.md
40
+ └── _TEMPLATE/
41
+ ├── README.md
42
+ └── api-snapshot.md
43
+ ```
44
+
45
+ ## 2. 用户级 skills
46
+
47
+ 默认目录:
48
+
49
+ ```text
50
+ macOS/Linux: ~/.agents/skills/
51
+ Windows: %USERPROFILE%\.agents\skills\
52
+ ```
53
+
54
+ 这里保存可被多个项目共用的 skills。常见目录包括:
55
+
56
+ | 目录 | 用途 |
57
+ | --- | --- |
58
+ | `comate/` | comate 总入口、状态脚本和共享规则 |
59
+ | `comate-open/` | 创建 change,生成初始产物 |
60
+ | `comate-design/` | 技术设计和方案确认 |
61
+ | `comate-build/` | 计划和代码实现 |
62
+ | `comate-verify/` | 测试、验证和分支收尾 |
63
+ | `comate-archive/` | 归档 change 并同步规格 |
64
+ | `comate-fix/` | Bug 或小调整的快速流程 |
65
+ | `comate-gate/` | PRD 门禁和拆分 |
66
+ | `comate-grill/` | 澄清关键决策 |
67
+ | `comate-kb-integration/` | 生成第三方对接档案 |
68
+ | `openspec-*/` | OpenSpec 提供的配套 skills |
69
+
70
+ 通常不需要手动修改这些目录。升级 comate 后,在项目根目录重新运行 `comate init`,即可同步新版 comate skills。
71
+
72
+ ## 3. 项目级 `openspec/`
73
+
74
+ `openspec/` 保存当前项目的变更规格,是项目自己的文件,应纳入版本控制。
75
+
76
+ ### `openspec/config.yaml`
77
+
78
+ OpenSpec 的项目配置文件,记录当前项目使用的规格 schema。通常由初始化命令创建;已有配置会保留,不要随意删除。
79
+
80
+ ### `openspec/changes/`
81
+
82
+ 每个进行中的需求或修复对应一个 change:
83
+
84
+ ```text
85
+ openspec/changes/<change-name>/
86
+ ├── .comate.yaml
87
+ ├── proposal.md
88
+ ├── design.md
89
+ ├── tasks.md
90
+ ├── specs/<capability>/spec.md
91
+ └── .comate/
92
+ ├── handoff/
93
+ └── sources.manifest
94
+ ```
95
+
96
+ 这些文件由 `/comate`、`/comate-fix` 及各阶段 skill 逐步生成:
97
+
98
+ - `.comate.yaml`:comate 当前阶段和验证状态的事实来源。
99
+ - `proposal.md`:为什么做、做什么、范围和非目标。
100
+ - `design.md`:高层设计、方案选择和数据流。
101
+ - `tasks.md`:实施任务清单,完成一项勾选一项。
102
+ - `specs/<capability>/spec.md`:本次变更的增量能力规格。
103
+ - `.comate/handoff/`:阶段之间自动生成的交接上下文。
104
+ - `.comate/sources.manifest`:需求源文件的追踪清单,仅在需求来自文件时出现。
105
+
106
+ `openspec/changes/archive/` 在初始化时就会创建。`<change-name>` 完成并归档后,会移动到:
107
+
108
+ ```text
109
+ openspec/changes/archive/YYYY-MM-DD-<change-name>/
110
+ ```
111
+
112
+ ### `openspec/specs/`
113
+
114
+ 这里保存已经沉淀的长期能力规格:
115
+
116
+ ```text
117
+ openspec/specs/<capability>/spec.md
118
+ ```
119
+
120
+ 归档 change 时,增量规格会合并到对应的主规格中。
121
+
122
+ ## 4. 项目级 `docs/`
123
+
124
+ `docs/` 保存跨多个 change 长期有效的项目文档。初始化只创建骨架,具体内容需要项目团队补充。
125
+
126
+ ### `docs/README.md`
127
+
128
+ 说明 `docs/` 的整体边界,以及架构文档、知识库和 `openspec/` 的分工。一般不需要频繁修改。
129
+
130
+ ### `docs/architecture/`
131
+
132
+ 保存项目架构约束,例如:
133
+
134
+ - 前后端分层和模块依赖;
135
+ - API、DTO、数据库和错误码约定;
136
+ - 命名、日志、测试和部署规范。
137
+
138
+ 初始化时只有占位说明。项目应根据实际技术栈补充正式规则。涉及架构约束的 PRD,门禁会以这里的内容作为对齐依据。
139
+
140
+ ### `docs/knowledge-base/`
141
+
142
+ 保存需求和第三方服务资料。
143
+
144
+ #### `requirements/`
145
+
146
+ 存放原始 PRD。推荐按功能和日期组织:
147
+
148
+ ```text
149
+ docs/knowledge-base/requirements/<YYYYMMDD-功能名>/
150
+ ├── PRD-001-功能名.md
151
+ ├── PRD-001.gate.yaml
152
+ └── PRD-001-REVIEW.md
153
+ ```
154
+
155
+ `_templates/PRD.md` 是新建 PRD 的模板。`.gate.yaml` 和 `-REVIEW.md` 由 `/comate-gate` 生成,不需要手动创建。
156
+
157
+ #### `integrations/`
158
+
159
+ 保存第三方服务的长期对接档案:
160
+
161
+ ```text
162
+ docs/knowledge-base/integrations/<service-id>/
163
+ ├── README.md
164
+ └── api-snapshot.md
165
+ ```
166
+
167
+ `_TEMPLATE/` 只存模板,不代表真实服务。接入新服务时,复制模板或使用 `/comate-kb-integration` 生成档案,并维护 `integrations/README.md` 中的服务索引。
168
+
169
+ ## 5. 后续流程会新增的目录
170
+
171
+ 以下目录不是 `comate init` 必然立即创建的,而是在对应阶段产生:
172
+
173
+ ```text
174
+ docs/superpowers/
175
+ ├── specs/ # design 阶段的设计文档
176
+ ├── plans/ # build 阶段的实施计划
177
+ └── reports/ # verify 阶段的验证报告
178
+ ```
179
+
180
+ 如果项目暂时没有进行中的 change,这些目录不存在是正常的。
181
+
182
+ ## 6. 哪些内容应提交到 Git
183
+
184
+ 建议提交项目级内容:
185
+
186
+ - `openspec/`
187
+ - `docs/architecture/`
188
+ - `docs/knowledge-base/`
189
+ - 已生成的 `docs/superpowers/`
190
+
191
+ 用户级 `~/.agents/skills/` 或 `%USERPROFILE%\.agents\skills\` 属于本机环境,不放进项目仓库。
192
+
193
+ ## 7. 重跑 init
194
+
195
+ `comate init` 是幂等的:
196
+
197
+ - 已安装的依赖和 skills 会跳过或复用;
198
+ - 已存在的 `openspec/` 和 `docs/` 文件会保留;
199
+ - 缺少的目录或模板文件会补齐。
200
+
201
+ 因此升级 comate 后,可以安全地在每个项目根目录重新运行一次 `comate init`。
@@ -595,7 +595,8 @@ bash "$COMATE" doctor
595
595
  | `gate <prd> review` | 建/刷新 gate.yaml,钉当前 PRD sha256,回显评审报告路径。PRD 变了则状态重置为 `reviewing` |
596
596
  | `gate <prd> set <field> <value>` | 写字段:`score` / `blocking_open` / `gate_status` 等 |
597
597
  | `gate <prd> pass [--tbd]` | 校验评分、拆分映射和报告。普通 pass 要求 `blocking_open=0`;`--tbd` 要求至少一个经用户接受的未解决阻断项 |
598
- | `gate <prd> spawned <change>` | 记录据此 PRD 扇出的 change(逗号拼接) |
598
+ | `gate <prd> spawned <change>` | 校验 change 属于确认映射、gate 未 stale、open 骨架与 PRD 源追踪完整后,记录据此 PRD 扇出的 change(逗号拼接,幂等) |
599
+ | `gate <prd> fanout-status [--json]` | 对比 `changes_map` 与 `spawned_changes`,输出 expected/spawned/complete/missing/invalid,用于多 change 中断恢复与完整性校验 |
599
600
  | `gate <prd> status [--json]` | 查状态;哈希漂移时自动判 `stale`(不改文件) |
600
601
  | `gate <prd> check` | comate-open 建 change 前的前置守卫。PASS 要求 gate 存在、状态 ∈ {passed, passed-with-tbd}、哈希未漂移;否则退出码 1 阻塞 |
601
602
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@house365/comate",
3
- "version": "0.9.18",
3
+ "version": "0.9.19",
4
4
  "description": "comate —— SDD (规约驱动开发) 的 AI 开发编排器(Agent Skills)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
  # gate <prd> set <field> <value> — write score | blocking_open | gate_status | …
22
22
  # gate <prd> pass [--tbd] — pass clean gate or explicitly accepted TBD blockers
23
23
  # gate <prd> spawned <change> — record a change fanned out from this PRD
24
+ # gate <prd> fanout-status [--json] — report mapped/spawned fanout progress
24
25
  # gate <prd> status [--json] — print gate status (computes stale on hash drift)
25
26
  # gate <prd> check — precondition guard before spawning a change
26
27
  # archive <name> [--dry-run] — sync delta specs + annotate + move + mark archived
@@ -62,6 +63,7 @@ Usage: comate.sh <command> ...
62
63
  gate <prd> set <field> <value> score | blocking_open | gate_status | …
63
64
  gate <prd> pass [--tbd] pass: zero blockers; --tbd: accepted blockers
64
65
  gate <prd> spawned <change> record a change fanned out from this PRD
66
+ gate <prd> fanout-status [--json] report mapped/spawned fanout progress
65
67
  gate <prd> status [--json]
66
68
  gate <prd> check precondition guard before spawning a change
67
69
  archive <name> [--dry-run]
@@ -1471,10 +1473,52 @@ gate_set() {
1471
1473
  [ -n "$field" ] || die "$COMATE_USAGE" "Usage: comate.sh gate <prd> set <field> <value>"
1472
1474
  local gate; gate=$(gate_path_for "$prd")
1473
1475
  [ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found (run 'gate review' first): $gate"
1476
+ if [ "$field" = "changes_map" ]; then
1477
+ gate_validate_changes_map "$value"
1478
+ local current_map current_status
1479
+ current_map=$(yaml_get "$gate" changes_map)
1480
+ current_status=$(yaml_get "$gate" gate_status)
1481
+ case "$current_status" in
1482
+ passed|passed-with-tbd)
1483
+ [ "$value" = "$current_map" ] || \
1484
+ die "$COMATE_BLOCK" "BLOCK: changes_map is immutable after gate pass; re-run gate review before remapping"
1485
+ ;;
1486
+ esac
1487
+ fi
1474
1488
  gate_yaml_set "$gate" "$field" "$value"
1475
1489
  log_pass "[GATE] set $field=$value"
1476
1490
  }
1477
1491
 
1492
+ # gate_validate_changes_map <changes_map> — reject malformed entries and
1493
+ # duplicate names when the user-confirmed mapping is first persisted.
1494
+ gate_validate_changes_map() {
1495
+ local map="$1" rest entry name items seen=""
1496
+ [ -n "$map" ] && [ "$map" != "null" ] || \
1497
+ die "$COMATE_USAGE" "ERROR: changes_map cannot be empty"
1498
+ case "$map" in
1499
+ \;*|*\;\;*|*\;) die "$COMATE_USAGE" "ERROR: changes_map contains an empty entry" ;;
1500
+ esac
1501
+ rest="$map"
1502
+ while [ -n "$rest" ]; do
1503
+ case "$rest" in
1504
+ *\;*) entry="${rest%%;*}"; rest="${rest#*;}" ;;
1505
+ *) entry="$rest"; rest="" ;;
1506
+ esac
1507
+ name="${entry%%:*}"
1508
+ items="${entry#*:}"
1509
+ [ -n "$name" ] && [ "$name" != "$entry" ] || \
1510
+ die "$COMATE_USAGE" "ERROR: malformed changes_map entry '$entry'"
1511
+ [ -n "$(printf '%s' "$items" | tr -d '[:space:]')" ] || \
1512
+ die "$COMATE_USAGE" "ERROR: empty function mapping for change '$name'"
1513
+ validate_change_name "$name"
1514
+ case ",$seen," in
1515
+ *",$name,"*) die "$COMATE_USAGE" "ERROR: duplicate change '$name' in changes_map" ;;
1516
+ esac
1517
+ [ -n "$seen" ] && seen="$seen,"
1518
+ seen="$seen$name"
1519
+ done
1520
+ }
1521
+
1478
1522
  # gate <prd> pass [--tbd] — flip to passed/passed-with-tbd. Normal pass requires
1479
1523
  # zero blockers; --tbd requires at least one explicitly accepted blocker.
1480
1524
  gate_pass() {
@@ -1501,6 +1545,7 @@ gate_pass() {
1501
1545
  if [ -z "$changes_map" ] || [ "$changes_map" = "null" ]; then
1502
1546
  die "$COMATE_BLOCK" "BLOCK: changes_map is required before passing the gate"
1503
1547
  fi
1548
+ gate_validate_changes_map "$changes_map"
1504
1549
  local report; report=$(yaml_get "$gate" review_report)
1505
1550
  if [ -z "$report" ] || [ "$report" = "null" ] || ! file_nonempty "$report"; then
1506
1551
  die "$COMATE_BLOCK" "BLOCK: review report missing or empty ($report) — the gate report is mandatory"
@@ -1524,10 +1569,15 @@ gate_spawned() {
1524
1569
  validate_change_name "$change"
1525
1570
  local gate; gate=$(gate_path_for "$prd")
1526
1571
  [ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found: $gate"
1572
+ local changes_map; changes_map=$(yaml_get "$gate" changes_map)
1573
+ gate_change_is_mapped "$changes_map" "$change" || \
1574
+ die "$COMATE_BLOCK" "BLOCK: change '$change' is not present in changes_map"
1527
1575
  local cur; cur=$(yaml_get "$gate" spawned_changes)
1528
1576
  case ",$cur," in
1529
1577
  *",$change,"*) log_info "[GATE] $change already recorded" ; return 0 ;;
1530
1578
  esac
1579
+ gate_check "$prd" >/dev/null
1580
+ gate_require_skeleton "$prd" "$change"
1531
1581
  if [ -z "$cur" ] || [ "$cur" = "null" ]; then
1532
1582
  gate_yaml_set "$gate" spawned_changes "$change"
1533
1583
  else
@@ -1536,6 +1586,118 @@ gate_spawned() {
1536
1586
  log_pass "[GATE] recorded spawned change: $change"
1537
1587
  }
1538
1588
 
1589
+ # gate_require_skeleton <prd> <change> — prove that comate-open finished the
1590
+ # gate skeleton before it is counted as spawned.
1591
+ gate_require_skeleton() {
1592
+ local prd="$1" change="$2" dir yaml manifest tracked_prd f
1593
+ dir=$(comate_change_dir "$change")
1594
+ for f in .openspec.yaml proposal.md design.md tasks.md .comate.yaml; do
1595
+ file_nonempty "$dir/$f" || \
1596
+ die "$COMATE_BLOCK" "BLOCK: change '$change' skeleton incomplete: missing or empty $dir/$f"
1597
+ done
1598
+ yaml="$dir/.comate.yaml"
1599
+ validate_flat_yaml "$yaml"
1600
+ [ "$(yaml_get "$yaml" phase)" = "open" ] || \
1601
+ die "$COMATE_BLOCK" "BLOCK: change '$change' skeleton must remain in phase=open"
1602
+ [ "$(yaml_get "$yaml" source_kind)" = "file" ] || \
1603
+ die "$COMATE_BLOCK" "BLOCK: change '$change' skeleton has not tracked its PRD source"
1604
+ manifest=$(yaml_get "$yaml" source_manifest)
1605
+ [ -n "$manifest" ] && [ "$manifest" != "null" ] && [ -f "$manifest" ] || \
1606
+ die "$COMATE_BLOCK" "BLOCK: change '$change' source manifest is missing"
1607
+ tracked_prd=$(validate_kb_path "$prd") || \
1608
+ die "$COMATE_BLOCK" "BLOCK: PRD source path is outside the knowledge base: $prd"
1609
+ local record_ok=0 line record_path="" record_hash=""
1610
+ while IFS= read -r line || [ -n "$line" ]; do
1611
+ case "$line" in
1612
+ path:*) record_path="${line#path:}"; record_hash="" ;;
1613
+ sha256:*)
1614
+ record_hash="${line#sha256:}"
1615
+ if [ "$record_path" = "$tracked_prd" ] && [ -n "$record_hash" ]; then record_ok=1; fi
1616
+ ;;
1617
+ ---|"") record_path=""; record_hash="" ;;
1618
+ esac
1619
+ done < "$manifest"
1620
+ [ "$record_ok" -eq 1 ] || \
1621
+ die "$COMATE_BLOCK" "BLOCK: change '$change' source manifest lacks a complete path/sha256 record for $tracked_prd"
1622
+ local drift rc=0
1623
+ drift=$(sources_drift "$manifest") || rc=$?
1624
+ [ "$rc" -eq 0 ] || die "$COMATE_BLOCK" "BLOCK: change '$change' source evidence is stale: $drift"
1625
+ }
1626
+
1627
+ # gate_change_names <changes_map> — emit mapped change names, one per line.
1628
+ # The persisted format is `name:function item,...;name:function item,...`.
1629
+ gate_change_names() {
1630
+ local map="$1" entry name
1631
+ [ -n "$map" ] && [ "$map" != "null" ] || return 0
1632
+ gate_validate_changes_map "$map"
1633
+ while [ -n "$map" ]; do
1634
+ case "$map" in
1635
+ *\;*) entry="${map%%;*}"; map="${map#*;}" ;;
1636
+ *) entry="$map"; map="" ;;
1637
+ esac
1638
+ name="${entry%%:*}"
1639
+ [ -n "$name" ] || die "$COMATE_USAGE" "ERROR: malformed changes_map entry '$entry'"
1640
+ [ "$name" != "$entry" ] || die "$COMATE_USAGE" "ERROR: malformed changes_map entry '$entry'"
1641
+ validate_change_name "$name"
1642
+ printf '%s\n' "$name"
1643
+ done
1644
+ }
1645
+
1646
+ # gate_change_is_mapped <changes_map> <change> — membership test for a mapped name.
1647
+ gate_change_is_mapped() {
1648
+ local map="$1" wanted="$2" name
1649
+ gate_validate_changes_map "$map"
1650
+ while IFS= read -r name; do
1651
+ [ "$name" = "$wanted" ] && return 0
1652
+ done <<EOF
1653
+ $(gate_change_names "$map")
1654
+ EOF
1655
+ return 1
1656
+ }
1657
+
1658
+ # gate_fanout_status <prd> [--json] — calculate persisted fanout progress.
1659
+ # This is deliberately derived from changes_map and spawned_changes so an
1660
+ # interrupted run can resume from the missing names instead of relying on
1661
+ # conversation/task state.
1662
+ gate_fanout_status() {
1663
+ local prd; prd=$(require_prd "$1")
1664
+ local json="${2:-}" gate; gate=$(gate_path_for "$prd")
1665
+ [ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found: $gate"
1666
+ local map spawned name expected=0 count=0 missing="" invalid=""
1667
+ map=$(yaml_get "$gate" changes_map); spawned=$(yaml_get "$gate" spawned_changes)
1668
+ gate_validate_changes_map "$map"
1669
+ while IFS= read -r name; do
1670
+ [ -n "$name" ] || continue
1671
+ expected=$((expected + 1))
1672
+ case ",$spawned," in
1673
+ *",$name,"*) count=$((count + 1)) ;;
1674
+ *) [ -n "$missing" ] && missing="$missing,"; missing="$missing$name" ;;
1675
+ esac
1676
+ done <<EOF
1677
+ $(gate_change_names "$map")
1678
+ EOF
1679
+ if [ -n "$spawned" ] && [ "$spawned" != "null" ]; then
1680
+ local rest="$spawned" item
1681
+ while [ -n "$rest" ]; do
1682
+ case "$rest" in
1683
+ *,*) item="${rest%%,*}"; rest="${rest#*,}" ;;
1684
+ *) item="$rest"; rest="" ;;
1685
+ esac
1686
+ gate_change_is_mapped "$map" "$item" || { [ -n "$invalid" ] && invalid="$invalid,"; invalid="$invalid$item"; }
1687
+ done
1688
+ fi
1689
+ local complete=false
1690
+ [ "$expected" -gt 0 ] && [ "$count" -eq "$expected" ] && [ -z "$invalid" ] && complete=true
1691
+ if [ "$json" = "--json" ]; then
1692
+ printf '{"expected":%s,"spawned":%s,"complete":%s,"missing":"%s","invalid":"%s"}\n' \
1693
+ "$expected" "$count" "$complete" "$(json_escape "$missing")" "$(json_escape "$invalid")"
1694
+ else
1695
+ [ -z "$missing" ] && missing=none
1696
+ [ -z "$invalid" ] && invalid=none
1697
+ echo "fanout: expected=$expected spawned=$count complete=$complete missing=$missing invalid=$invalid"
1698
+ fi
1699
+ }
1700
+
1539
1701
  # _gate_is_stale <gate> <prd> — true (0) if the recorded hash no longer matches
1540
1702
  # the PRD on disk.
1541
1703
  _gate_is_stale() {
@@ -1956,6 +2118,7 @@ case "$CMD" in
1956
2118
  set) gate_set "$PRD" "${1:-}" "${2:-}" ;;
1957
2119
  pass) gate_pass "$PRD" "${1:-}" ;;
1958
2120
  spawned) gate_spawned "$PRD" "${1:-}" ;;
2121
+ fanout-status) gate_fanout_status "$PRD" "${1:-}" ;;
1959
2122
  status) gate_status "$PRD" "${1:-}" ;;
1960
2123
  check) gate_check "$PRD" ;;
1961
2124
  *) die "$COMATE_USAGE" "Unknown gate subcommand: $SUB" ;;
@@ -565,6 +565,17 @@ seed_prd() {
565
565
  echo "docs/knowledge-base/requirements/test/PRD-001-demo.md"
566
566
  }
567
567
 
568
+ seed_gate_skeleton() {
569
+ local prd="$1" change="$2" d="openspec/changes/$2"
570
+ mkdir -p "$d"
571
+ printf 'schema: specs-driven\n' > "$d/.openspec.yaml"
572
+ printf '# proposal\n' > "$d/proposal.md"
573
+ printf '# design\n' > "$d/design.md"
574
+ printf -- '- [ ] task\n' > "$d/tasks.md"
575
+ comate state "$change" init full >/dev/null
576
+ comate sources "$change" track "$prd" >/dev/null
577
+ }
578
+
568
579
  @test "gate review creates gate.yaml next to PRD" {
569
580
  local prd; prd=$(seed_prd)
570
581
  run comate gate "$prd" review
@@ -643,6 +654,71 @@ seed_prd() {
643
654
  [[ "$output" == *"changes_map"* ]]
644
655
  }
645
656
 
657
+ @test "gate rejects changes_map entries without a function mapping" {
658
+ local prd; prd=$(seed_prd)
659
+ comate gate "$prd" review >/dev/null
660
+ run comate gate "$prd" set changes_map "login-flow"
661
+ [ "$status" -eq 2 ]
662
+ [[ "$output" == *"malformed changes_map"* ]]
663
+ }
664
+
665
+ @test "gate rejects duplicate change names in changes_map" {
666
+ local prd; prd=$(seed_prd)
667
+ comate gate "$prd" review >/dev/null
668
+ run comate gate "$prd" set changes_map "login-flow:功能项1;login-flow:功能项2"
669
+ [ "$status" -eq 2 ]
670
+ [[ "$output" == *"duplicate change"* ]]
671
+ }
672
+
673
+ @test "gate rejects an empty middle entry in changes_map" {
674
+ local prd; prd=$(seed_prd)
675
+ comate gate "$prd" review >/dev/null
676
+ run comate gate "$prd" set changes_map "login-flow:功能项1;;logout-flow:功能项2"
677
+ [ "$status" -eq 2 ]
678
+ [[ "$output" == *"empty entry"* ]]
679
+ }
680
+
681
+ @test "gate rejects a trailing empty entry in changes_map" {
682
+ local prd; prd=$(seed_prd)
683
+ comate gate "$prd" review >/dev/null
684
+ run comate gate "$prd" set changes_map "login-flow:功能项1;"
685
+ [ "$status" -eq 2 ]
686
+ [[ "$output" == *"empty entry"* ]]
687
+ }
688
+
689
+ @test "gate rejects an empty function mapping" {
690
+ local prd; prd=$(seed_prd)
691
+ comate gate "$prd" review >/dev/null
692
+ run comate gate "$prd" set changes_map "login-flow:"
693
+ [ "$status" -eq 2 ]
694
+ [[ "$output" == *"empty function mapping"* ]]
695
+ }
696
+
697
+ @test "gate rejects an all-whitespace function mapping" {
698
+ local prd; prd=$(seed_prd)
699
+ comate gate "$prd" review >/dev/null
700
+ run comate gate "$prd" set changes_map "login-flow: "
701
+ [ "$status" -eq 2 ]
702
+ [[ "$output" == *"empty function mapping"* ]]
703
+ }
704
+
705
+ @test "gate keeps a passed changes_map immutable except for idempotent writes" {
706
+ local prd; prd=$(seed_prd)
707
+ comate gate "$prd" review >/dev/null
708
+ comate gate "$prd" set score 90 >/dev/null
709
+ comate gate "$prd" set blocking_open 0 >/dev/null
710
+ comate gate "$prd" set changes_map "a:功能项1;b:功能项2;c:功能项3;d:功能项4" >/dev/null
711
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
712
+ comate gate "$prd" pass >/dev/null
713
+ run comate gate "$prd" set changes_map "a:功能项1;b:功能项2;c:功能项3;d:功能项4"
714
+ [ "$status" -eq 0 ]
715
+ run comate gate "$prd" set changes_map "a:功能项1"
716
+ [ "$status" -eq 1 ]
717
+ [[ "$output" == *"immutable after gate pass"* ]]
718
+ run comate gate "$prd" fanout-status --json
719
+ [[ "$output" == *'"expected":4'* ]]
720
+ }
721
+
646
722
  @test "gate pass --tbd requires unresolved blockers" {
647
723
  local prd; prd=$(seed_prd)
648
724
  comate gate "$prd" review >/dev/null
@@ -727,6 +803,7 @@ seed_prd() {
727
803
  printf '# 报告\n' > \
728
804
  "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
729
805
  comate gate "$prd" pass >/dev/null
806
+ seed_gate_skeleton "$prd" login-flow
730
807
  run comate gate "$prd" spawned login-flow
731
808
  [ "$status" -eq 0 ]
732
809
  local gate="docs/knowledge-base/requirements/test/PRD-001.gate.yaml"
@@ -743,6 +820,8 @@ seed_prd() {
743
820
  printf '# 报告\n' > \
744
821
  "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
745
822
  comate gate "$prd" pass >/dev/null
823
+ seed_gate_skeleton "$prd" login-flow
824
+ seed_gate_skeleton "$prd" logout-flow
746
825
  comate gate "$prd" spawned login-flow >/dev/null
747
826
  comate gate "$prd" spawned logout-flow >/dev/null
748
827
  local gate="docs/knowledge-base/requirements/test/PRD-001.gate.yaml"
@@ -751,6 +830,131 @@ seed_prd() {
751
830
  [[ "$sc" == *"logout-flow"* ]]
752
831
  }
753
832
 
833
+ @test "gate spawned remains idempotent after an already recorded change leaves the active directory" {
834
+ local prd; prd=$(seed_prd)
835
+ comate gate "$prd" review >/dev/null
836
+ comate gate "$prd" set score 90 >/dev/null
837
+ comate gate "$prd" set blocking_open 0 >/dev/null
838
+ comate gate "$prd" set changes_map "login-flow:功能项1" >/dev/null
839
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
840
+ comate gate "$prd" pass >/dev/null
841
+ seed_gate_skeleton "$prd" login-flow
842
+ comate gate "$prd" spawned login-flow >/dev/null
843
+ mkdir -p openspec/changes/archive
844
+ mv openspec/changes/login-flow openspec/changes/archive/2026-08-28-login-flow
845
+ run comate gate "$prd" spawned login-flow
846
+ [ "$status" -eq 0 ]
847
+ [[ "$output" == *"already recorded"* ]]
848
+ }
849
+
850
+ @test "gate fanout status reports missing changes instead of treating one of four as complete" {
851
+ local prd; prd=$(seed_prd)
852
+ comate gate "$prd" review >/dev/null
853
+ comate gate "$prd" set score 90 >/dev/null
854
+ comate gate "$prd" set blocking_open 0 >/dev/null
855
+ comate gate "$prd" set changes_map "login-flow:功能项1;logout-flow:功能项2;ops:功能项3;report:功能项4" >/dev/null
856
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
857
+ comate gate "$prd" pass >/dev/null
858
+ seed_gate_skeleton "$prd" login-flow
859
+ comate gate "$prd" spawned login-flow >/dev/null
860
+ run comate gate "$prd" fanout-status --json
861
+ [ "$status" -eq 0 ]
862
+ [[ "$output" == *'"expected":4'* ]]
863
+ [[ "$output" == *'"spawned":1'* ]]
864
+ [[ "$output" == *'"complete":false'* ]]
865
+ [[ "$output" == *'logout-flow'* ]]
866
+ }
867
+
868
+ @test "gate spawned rejects a change outside the confirmed map" {
869
+ local prd; prd=$(seed_prd)
870
+ comate gate "$prd" review >/dev/null
871
+ comate gate "$prd" set score 90 >/dev/null
872
+ comate gate "$prd" set blocking_open 0 >/dev/null
873
+ comate gate "$prd" set changes_map "login-flow:功能项1" >/dev/null
874
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
875
+ comate gate "$prd" pass >/dev/null
876
+ seed_gate_skeleton "$prd" other-flow
877
+ run comate gate "$prd" spawned other-flow
878
+ [ "$status" -eq 1 ]
879
+ [[ "$output" == *"not present in changes_map"* ]]
880
+ }
881
+
882
+ @test "gate fanout status becomes complete only after every mapped change is spawned" {
883
+ local prd; prd=$(seed_prd)
884
+ comate gate "$prd" review >/dev/null
885
+ comate gate "$prd" set score 90 >/dev/null
886
+ comate gate "$prd" set blocking_open 0 >/dev/null
887
+ comate gate "$prd" set changes_map "login-flow:功能项1;logout-flow:功能项2" >/dev/null
888
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
889
+ comate gate "$prd" pass >/dev/null
890
+ seed_gate_skeleton "$prd" login-flow
891
+ seed_gate_skeleton "$prd" logout-flow
892
+ comate gate "$prd" spawned login-flow >/dev/null
893
+ comate gate "$prd" spawned logout-flow >/dev/null
894
+ run comate gate "$prd" fanout-status
895
+ [ "$status" -eq 0 ]
896
+ [[ "$output" == *"complete=true"* ]]
897
+ [[ "$output" == *"missing=none"* ]]
898
+ }
899
+
900
+ @test "gate spawned rejects an incomplete change skeleton" {
901
+ local prd; prd=$(seed_prd)
902
+ comate gate "$prd" review >/dev/null
903
+ comate gate "$prd" set score 90 >/dev/null
904
+ comate gate "$prd" set blocking_open 0 >/dev/null
905
+ comate gate "$prd" set changes_map "login-flow:功能项1" >/dev/null
906
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
907
+ comate gate "$prd" pass >/dev/null
908
+ mkdir -p openspec/changes/login-flow
909
+ run comate gate "$prd" spawned login-flow
910
+ [ "$status" -eq 1 ]
911
+ [[ "$output" == *"skeleton incomplete"* ]]
912
+ }
913
+
914
+ @test "gate spawned blocks a new registration after the PRD becomes stale" {
915
+ local prd; prd=$(seed_prd)
916
+ comate gate "$prd" review >/dev/null
917
+ comate gate "$prd" set score 90 >/dev/null
918
+ comate gate "$prd" set blocking_open 0 >/dev/null
919
+ comate gate "$prd" set changes_map "login-flow:功能项1" >/dev/null
920
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
921
+ comate gate "$prd" pass >/dev/null
922
+ seed_gate_skeleton "$prd" login-flow
923
+ printf '\nchanged\n' >> "$prd"
924
+ run comate gate "$prd" spawned login-flow
925
+ [ "$status" -eq 1 ]
926
+ [[ "$output" == *"hash mismatch"* ]]
927
+ }
928
+
929
+ @test "gate spawned accepts a normalized PRD path recorded in the source manifest" {
930
+ local prd; prd=$(seed_prd)
931
+ prd="./$prd"
932
+ comate gate "$prd" review >/dev/null
933
+ comate gate "$prd" set score 90 >/dev/null
934
+ comate gate "$prd" set blocking_open 0 >/dev/null
935
+ comate gate "$prd" set changes_map "login-flow:功能项1" >/dev/null
936
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
937
+ comate gate "$prd" pass >/dev/null
938
+ seed_gate_skeleton "$prd" login-flow
939
+ run comate gate "$prd" spawned login-flow
940
+ [ "$status" -eq 0 ]
941
+ }
942
+
943
+ @test "gate spawned rejects a truncated PRD source record without sha256" {
944
+ local prd; prd=$(seed_prd)
945
+ comate gate "$prd" review >/dev/null
946
+ comate gate "$prd" set score 90 >/dev/null
947
+ comate gate "$prd" set blocking_open 0 >/dev/null
948
+ comate gate "$prd" set changes_map "login-flow:功能项1" >/dev/null
949
+ printf '# 报告\n' > "docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
950
+ comate gate "$prd" pass >/dev/null
951
+ seed_gate_skeleton "$prd" login-flow
952
+ printf 'path:%s\n' "$prd" > openspec/changes/login-flow/.comate/sources.manifest
953
+ run comate gate "$prd" spawned login-flow
954
+ [ "$status" -eq 1 ]
955
+ [[ "$output" == *"complete path/sha256 record"* ]]
956
+ }
957
+
754
958
  @test "gate status returns none when gate does not exist" {
755
959
  mkdir -p docs/knowledge-base/requirements/test
756
960
  run comate gate "docs/knowledge-base/requirements/test/PRD-001-demo.md" status
@@ -41,8 +41,9 @@ gate 命令族(退出码:`0` 通过 / `1` 阻塞 / `2` 用法 / `3` 环境 /
41
41
  bash "$COMATE" gate <prd> review # 建/刷新 gate.yaml,钉 PRD 哈希,回报告路径
42
42
  bash "$COMATE" gate <prd> set <field> <value> # 写 score | blocking_open | gate_status
43
43
  bash "$COMATE" gate <prd> pass [--tbd] # 普通通过要求 0 阻断;--tbd 要求有已授权的未解决阻断
44
- bash "$COMATE" gate <prd> spawned <change> # 记录据此 PRD 扇出的 change
45
- bash "$COMATE" gate <prd> status [--json] # 查状态(哈希漂移自动判 stale)
44
+ bash "$COMATE" gate <prd> spawned <change> # 记录据此 PRD 扇出的 change
45
+ bash "$COMATE" gate <prd> fanout-status [--json] # 查看映射/已创建/待创建进度
46
+ bash "$COMATE" gate <prd> status [--json] # 查状态(哈希漂移自动判 stale)
46
47
  bash "$COMATE" gate <prd> check # comate-open 建 change 前的前置守卫
47
48
  ```
48
49
 
@@ -205,21 +206,33 @@ bash "$COMATE" gate <prd> pass --tbd
205
206
 
206
207
  `pass` 会硬校验评分为 0–100、`changes_map` 非空、报告非空;普通 pass 要求 `blocking_open=0`,`--tbd` 则要求存在至少 1 个已记录且经用户明确接受的未解决阻断项。之后 PRD 再被改,哈希漂移会使门禁 `stale`,须重跑 `/comate-gate`。
207
208
 
208
- ### 6. 逐 Change 初始化(调 comate-open)
209
-
210
- 对映射里每个 change,逐个调用 comate-open。**每次都在 change 描述里点明**:本 change 承接 PRD 的哪些功能项、范围边界、以及(多 change 时)相邻职责归哪个 change(In/Out 显式互引,防重复防遗漏)。
211
-
212
- **立即执行**:用 Skill 工具加载 `comate-open`,传入 change 名与来源 PRD。comate-open 内部会:
209
+ ### 6. 逐 Change 初始化(调 comate-open)
210
+
211
+ 这是一个可恢复的扇出循环,不是一次性只执行首项的提示。每次进入本步骤先运行:
212
+
213
+ ```bash
214
+ bash "$COMATE" gate <prd路径> fanout-status --json
215
+ ```
216
+
217
+ 只处理返回 JSON 中 `missing` 列出的 change;不要重新处理 `spawned` 中已有的 change。每创建一个 change 后立即运行 `gate spawned`,再重新运行 `fanout-status`,直到 `complete: true`。如果本轮因上下文、用户决策或子 skill 中断,下一轮必须从 `fanout-status` 恢复,不能根据对话记忆猜测剩余项。
218
+
219
+ 对 `missing` 里的每个 change,逐个调用 comate-open。**每次都在 change 描述里点明**:本 change 承接 PRD 的哪些功能项、范围边界、以及(多 change 时)相邻职责归哪个 change(In/Out 显式互引,防重复防遗漏)。若同名目录或 `.comate.yaml` 已存在,视为中断恢复:让 comate-open 复用现有状态、补齐缺失产物和源追踪后登记,不得重复执行 `state init`。
220
+
221
+ **立即执行**:用 Skill 工具加载 `comate-open`,传入 change 名与来源 PRD,并明确使用“gate 扇出骨架模式”。该模式创建/补齐并校验当前 change 的 `.openspec.yaml`、open 三件套、`.comate.yaml` 和 PRD 源追踪,最后登记 `gate spawned` 并保持 `phase: open` 返回本 gate 循环;不得在同一次 gate 调用中自动进入该 change 的 design/build。
222
+
223
+ comate-open 内部会:
213
224
  - 先跑 `gate check <prd>` 前置守卫(未通过则阻断);
214
225
  - 生成 proposal.md(范围/目标/非目标承接该 change 的功能项,不脱离 PRD 另起炉灶);
215
226
  - `sources track <prd>` 钉住 PRD 为源(`source_kind=file`);
216
227
  - design.md / tasks.md 由后续 comate-design 细化。
217
228
 
218
- 每建成一个,记录到门禁:
229
+ 每建成一个,记录到门禁(目录已存在后才能登记):
219
230
 
220
231
  ```bash
221
- bash "$COMATE" gate <prd> spawned <change-name>
222
- ```
232
+ bash "$COMATE" gate <prd> spawned <change-name>
233
+ ```
234
+
235
+ 登记后继续处理 `fanout-status` 的 `missing` 列表。只有 `complete: true` 才能进入步骤 7,并且收尾报告中的“已创建 N 个 change”必须等于映射项数量;部分完成必须明确报告剩余项,不能宣称扇出完成。
223
236
 
224
237
  若门禁是 `passed-with-tbd`,在该 change 的 proposal/design 受影响处标 **「⚠ 待确认」** 并引用评审报告。
225
238
 
@@ -229,7 +242,8 @@ bash "$COMATE" gate <prd> spawned <change-name>
229
242
  - PRD 审阅评分 + 评级、阻断问题关闭情况;
230
243
  - 评审报告路径、gate.yaml 路径与最终 `gate_status`;
231
244
  - 拆分映射(功能项 → N 个 change);
232
- - 已建的 change 列表(名称 + 承接功能项 + 是否带 ⚠ TBD);
245
+ - 已建的 change 列表(名称 + 承接功能项 + 是否带 ⚠ TBD);
246
+ - 扇出完整性(`fanout-status` 的 expected/spawned/missing/invalid);
233
247
  - 待用户补的字段(如技术负责人)。
234
248
 
235
249
  提示:「已过门禁并创建 N 个 change。对每个 change 调用 `/comate` 继续 design→build→verify→archive。」
@@ -245,7 +259,8 @@ bash "$COMATE" gate <prd> spawned <change-name>
245
259
  - AskUserQuestion 是真实阻塞点:文字输出/推荐/默认值/历史选择/用户未反对都不能替代明确回答。
246
260
  - 拆分粒度(1 PRD → 几个 change)由用户定,AI 只给建议不拍板。
247
261
  - 每个 change 承接 PRD 对应功能项,不另起炉灶;范围/依赖/验收与 PRD 对齐。
248
- - 多 change 时在各 change 的 In/Out Scope 显式互引边界职责归属。
262
+ - 多 change 时在各 change 的 In/Out Scope 显式互引边界职责归属。
263
+ - 扇出必须以 `gate fanout-status` 为持久化进度真相;`TaskCreate` 仅作展示,不得替代 gate.yaml 状态。
249
264
  - 门禁通过后 PRD 若被改(哈希漂移),gate 转 stale,禁止再扇出 change,须重跑本 skill。
250
265
  - 契约以项目架构文档(如 `docs/architecture/`,若存在)为事实源,change 里点名遵循了哪条,不硬编码具体值、不自造。
251
266
  - 不写应用代码——本 skill 止于「过门禁 + 拆分 + 建 change 骨架」,实现由后续 comate 阶段驱动。
@@ -36,7 +36,23 @@ bash "$COMATE" gate <prd路径> check
36
36
 
37
37
  需求**来自对话**(非 PRD)时跳过本步——门禁只约束「PRD → change」这条路径。
38
38
 
39
- ### 3. 创建 Change 结构 + 初始化状态
39
+ ### 3. 创建 Change 结构 + 初始化状态
40
+
41
+ #### Gate 扇出骨架模式
42
+
43
+ 当调用方明确说明本次来自 `comate-gate` 的批量扇出时,使用独立的骨架模式:
44
+
45
+ 1. 运行 gate check;
46
+ 2. 创建或补齐 `.openspec.yaml`、proposal.md、design.md、tasks.md;
47
+ 3. `.comate.yaml` 不存在时才执行 `state init full`,已存在时读取并复用,不能重复 init;
48
+ 4. 追踪源 PRD;
49
+ 5. 运行 `state check open` 并检查所有骨架文件非空、proposal 范围忠实承接映射;
50
+ 6. 最后执行 `gate spawned`。脚本会再次校验 gate 未 stale、骨架完整且源 manifest 确实追踪该 PRD;
51
+ 7. 登记成功后保持 `phase: open`,立即返回 comate-gate 的扇出循环。
52
+
53
+ 骨架模式不执行 Step 4 后的 `phase_step` 记录、Step 5 产物确认、open guard 或本文“自动流转”;**不得**在同一次 gate 调用中加载 `comate-design`。下一项由 gate 根据 `fanout-status` 的 `missing` 列表继续处理。
54
+
55
+ 若不是 gate 扇出调用,保持普通 open 流程并按本文退出条件进入 design。
40
56
 
41
57
  **立即执行:** 使用 Skill 工具加载 `openspec-new-change` 技能。若用户意图未明确、需先形成建议,改为加载 `openspec-propose`。禁止跳过。
42
58
 
@@ -63,10 +79,12 @@ if [ -z "$COMATE" ]; then
63
79
  return 1 2>/dev/null || exit 1
64
80
  fi
65
81
 
66
- bash "$COMATE" state <name> init full
67
- ```
68
-
69
- 这会创建 `openspec/changes/<name>/.comate.yaml`,初始状态:`workflow: full`, `phase: open`, `phase_step: null`。
82
+ bash "$COMATE" state <name> init full
83
+ ```
84
+
85
+ 这会创建 `openspec/changes/<name>/.comate.yaml`,初始状态:`workflow: full`, `phase: open`, `phase_step: null`。
86
+
87
+ **恢复规则**:若 change 目录或 `.comate.yaml` 已存在,逐项检查并补齐缺失产物;`.comate.yaml` 存在时跳过 `state init`。这覆盖“骨架已创建但尚未 gate spawned”中断点,完成校验后直接补登记。
70
88
 
71
89
  ### 3b. 判定需求来源(源文件追踪)
72
90
 
@@ -84,13 +102,13 @@ bash "$COMATE" sources <name> track docs/knowledge-base/<path>.md --note "<可
84
102
 
85
103
  ### 3c. PRD 扇出记录(从 PRD 建 change 时必须登记)
86
104
 
87
- 若本 change 来自 PRD,在创建完成后把该 change 记回门禁:
105
+ 若本 change 来自 PRD,在创建、源追踪和完整性校验全部完成后,把该 change 记回门禁:
88
106
 
89
107
  ```bash
90
108
  bash "$COMATE" gate <prd路径> spawned <name>
91
109
  ```
92
110
 
93
- 此步将 change 名称追加到 `<PRD>.gate.yaml` 的 `spawned_changes` 列表,用于追溯 PRD → change 的完整扇出关系。
111
+ 此步将 change 名称追加到 `<PRD>.gate.yaml` 的 `spawned_changes` 列表,用于追溯 PRD → change 的完整扇出关系。不得在文件不完整或源追踪前提前登记。
94
112
 
95
113
  需求**来自对话**时跳过本步。
96
114
 
@@ -187,7 +205,9 @@ bash "$COMATE" state <name> set phase_step open.review-confirmed
187
205
 
188
206
  然后继续执行退出条件(guard)。
189
207
 
190
- ## 退出条件
208
+ ## 退出条件
209
+
210
+ 以下退出条件仅适用于普通 open;gate 扇出骨架模式使用 Step 3 的专用返回条件。
191
211
 
192
212
  - 三件套已创建且内容完整
193
213
  - **用户已确认**
@@ -201,9 +221,9 @@ bash "$COMATE" guard <name> open --apply
201
221
 
202
222
  full 流程自动更新为 `phase: design`;fix 预设自动更新为 `phase: build`。
203
223
 
204
- ## 自动流转
205
-
206
- 用户确认后退出条件满足,自动流转:
224
+ ## 自动流转
225
+
226
+ 仅普通 open 在用户确认后自动流转:
207
227
 
208
228
  > **REQUIRED NEXT SKILL(完整流程):** 调用 `comate-design` skill 进入深度设计。
209
229
  >