@house365/comate 0.3.0 → 0.4.0
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.
- package/README.md +11 -4
- package/bin/init-run.mjs +16 -0
- package/bin/init.mjs +43 -1
- package/docs/COMATE_MANUAL.md +48 -2
- package/package.json +1 -1
- package/skills/comate/SKILL.md +8 -1
- package/skills/comate/scripts/comate-lib.sh +94 -0
- package/skills/comate/scripts/comate.sh +204 -0
- package/skills/comate/tests/comate.bats +197 -0
- package/skills/comate-grill/SKILL.md +185 -0
- package/skills/comate-intake/SKILL.md +222 -0
- package/skills/comate-intake/scaffold/docs/README.md +36 -0
- package/skills/comate-intake/scaffold/docs/architecture/README.md +31 -0
- package/skills/comate-intake/scaffold/docs/knowledge-base/README.md +48 -0
- package/skills/comate-intake/scaffold/docs/knowledge-base/integrations/_TEMPLATE/README.md +49 -0
- package/skills/comate-intake/scaffold/docs/knowledge-base/integrations/_TEMPLATE/api-snapshot.md +47 -0
- package/skills/comate-intake/scaffold/docs/knowledge-base/requirements/_templates/PRD.md +97 -0
- package/skills/comate-open/SKILL.md +19 -0
package/README.md
CHANGED
|
@@ -7,13 +7,14 @@ comate 把 **OpenSpec**(WHAT:提案、规约生命周期、归档)和 **Superpow
|
|
|
7
7
|
## 五阶段
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
/comate
|
|
11
|
-
|
|
12
|
-
open → design → build → verify → archive
|
|
10
|
+
[原始 PRD] ─→ /comate-intake ─┐ (阶段 0:门禁+拆分)
|
|
11
|
+
▼
|
|
12
|
+
/comate ──自动探测阶段──────→ open → design → build → verify → archive
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
| 阶段 | skill | 职责 |
|
|
16
16
|
|------|-------|------|
|
|
17
|
+
| intake | `/comate-intake` | (阶段 0,可选)对原始 PRD 做质量门禁 + 拆分成 N 个 change |
|
|
17
18
|
| open | `/comate-open` | 探索想法,创建 change 结构并初始化 `.comate.yaml` |
|
|
18
19
|
| design | `/comate-design` | 技术设计与方案 brainstorming |
|
|
19
20
|
| build | `/comate-build` | 按任务实现并提交 |
|
|
@@ -22,6 +23,10 @@ open → design → build → verify → archive
|
|
|
22
23
|
|
|
23
24
|
预设路径:`/comate-hotfix`(热修)、`/comate-tweak`(文案/配置小调整)。
|
|
24
25
|
|
|
26
|
+
工具 skill:`/comate-grill`(决策点穷追式澄清)——对计划/设计/PRD 的模糊点逐个深挖,每问附推荐答案、一次一问,直到歧义清零。comate-intake 在阻断问题多、有依赖链时会自动提供「深度澄清」选项调用它;也可单独 `/comate-grill <话题>` 挑战自己的计划草稿。
|
|
27
|
+
|
|
28
|
+
**阶段 0(PRD 门禁与拆分)**:需求若来自一份**原始 PRD**,先过 `/comate-intake`——它审阅 PRD 质量、百分制评分、建阻断台账,门禁通过前禁止建任何 change;通过后按依赖/独立性判定把 PRD 拆成 1 或 N 个 change(粒度由用户定),逐个交给 `/comate-open`。门禁状态钉在 PRD 同级 `<PRD>.gate.yaml`,PRD 通过后被改会自动失效需重跑。需求来自对话时无需此阶段,直接从 open 起步。
|
|
29
|
+
|
|
25
30
|
## 安装
|
|
26
31
|
|
|
27
32
|
```bash
|
|
@@ -35,7 +40,7 @@ cd your-project
|
|
|
35
40
|
comate init
|
|
36
41
|
```
|
|
37
42
|
|
|
38
|
-
`init` 会装好 comate 及其依赖(OpenSpec CLI、Superpowers
|
|
43
|
+
`init` 会装好 comate 及其依赖(OpenSpec CLI、Superpowers),创建 `openspec/` 项目结构,并铺出 `docs/` 知识库骨架(`docs/knowledge-base/requirements/` 放 PRD + PRD 模板、`docs/architecture/` 架构规范占位、integrations 对接模板)——`/comate-intake` 从这里读 PRD。支持 Claude Code、Codex、Cursor 等 70+ agent。可重复执行,已装的自动跳过,已有的 docs 文件不覆盖。
|
|
39
44
|
|
|
40
45
|
skills 装到用户级(`~/.agents/skills`),装一次所有项目共用;`openspec/` 是项目自己的规约,建在当前目录,所以 `init` 需要在项目目录下运行。
|
|
41
46
|
|
|
@@ -63,7 +68,9 @@ skills/
|
|
|
63
68
|
reference/ 决策点、脏工作树协议、前置条件
|
|
64
69
|
tests/ bats 测试
|
|
65
70
|
comate-open/ design/ build/ verify/ archive/ 各阶段 skill
|
|
71
|
+
comate-intake/ 阶段 0:PRD 门禁与拆分
|
|
66
72
|
comate-hotfix/ tweak/ 预设路径 skill
|
|
73
|
+
comate-grill/ 决策点穷追式澄清工具
|
|
67
74
|
docs/
|
|
68
75
|
COMATE_MANUAL.md 完整操作手册
|
|
69
76
|
```
|
package/bin/init-run.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
npmInvocation,
|
|
12
12
|
openSpecState,
|
|
13
13
|
run,
|
|
14
|
+
scaffoldDocs,
|
|
14
15
|
skillsInvocation,
|
|
15
16
|
superpowersInstalled,
|
|
16
17
|
} from './init.mjs';
|
|
@@ -147,6 +148,21 @@ export async function init(pkgRoot) {
|
|
|
147
148
|
results.push(['openspec/', r2.ok ? 'installed' : 'failed', r2.ok ? '' : r2.reason, r2.output]);
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
// 5. docs/ knowledge-base scaffold. /comate-intake reads PRDs from
|
|
152
|
+
// docs/knowledge-base/requirements/ (comate_kb_root is hardcoded there), so a
|
|
153
|
+
// fresh project has nowhere to put a PRD. Idempotent — existing files are left
|
|
154
|
+
// alone, only missing pieces are filled in, so re-running never clobbers a
|
|
155
|
+
// project's real docs. A failure here is non-fatal to the rest of the install.
|
|
156
|
+
const sc = scaffoldDocs(pkgRoot);
|
|
157
|
+
if (!sc.ok) {
|
|
158
|
+
results.push(['docs/', 'failed', sc.reason]);
|
|
159
|
+
} else if (sc.created === 0) {
|
|
160
|
+
results.push(['docs/', 'skipped', '已存在']);
|
|
161
|
+
} else {
|
|
162
|
+
const note = sc.skipped > 0 ? `新建 ${sc.created} 个文件,保留 ${sc.skipped} 个已有` : '';
|
|
163
|
+
results.push(['docs/', 'installed', note]);
|
|
164
|
+
}
|
|
165
|
+
|
|
150
166
|
stopSpinner();
|
|
151
167
|
return report(results);
|
|
152
168
|
}
|
package/bin/init.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// ponytail: no SKILLS_AGENT_MAP here. The skills CLI detects the running agent
|
|
10
10
|
// on its own, so translating platform ids to agent names would be dead code.
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
|
-
import { existsSync } from 'node:fs';
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, copyFileSync, statSync } from 'node:fs';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
|
|
@@ -172,6 +172,45 @@ function superpowersInstalled(scope) {
|
|
|
172
172
|
return roots.some((root) => SUPERPOWERS_MARKERS.some((m) => existsSync(join(root, ...m))));
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// The docs scaffold ships inside the comate skill, so it is present both in dev
|
|
176
|
+
// (repo checkout) and after `skills add` copies the skill to ~/.agents/skills.
|
|
177
|
+
// Locating it relative to pkgRoot means init reads the same tree it packed.
|
|
178
|
+
function scaffoldSourceDir(pkgRoot) {
|
|
179
|
+
return join(pkgRoot, 'skills', 'comate-intake', 'scaffold', 'docs');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// copyTreeSkipExisting — recursively copy src→dst, never overwriting a file that
|
|
183
|
+
// already exists. Returns { created, skipped } counts so init can report whether
|
|
184
|
+
// it actually laid anything down (matching the openspec/ step's idempotence:
|
|
185
|
+
// an existing docs/ is left alone, only missing pieces are filled in).
|
|
186
|
+
function copyTreeSkipExisting(src, dst, acc = { created: 0, skipped: 0 }) {
|
|
187
|
+
mkdirSync(dst, { recursive: true });
|
|
188
|
+
for (const entry of readdirSync(src)) {
|
|
189
|
+
const s = join(src, entry);
|
|
190
|
+
const d = join(dst, entry);
|
|
191
|
+
if (statSync(s).isDirectory()) {
|
|
192
|
+
copyTreeSkipExisting(s, d, acc);
|
|
193
|
+
} else if (existsSync(d)) {
|
|
194
|
+
acc.skipped++;
|
|
195
|
+
} else {
|
|
196
|
+
copyFileSync(s, d);
|
|
197
|
+
acc.created++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return acc;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// scaffoldDocs — lay down docs/ (knowledge-base + architecture placeholder + PRD
|
|
204
|
+
// template) so /comate-intake has somewhere to read PRDs from. comate_kb_root in
|
|
205
|
+
// comate-lib.sh is hardcoded to docs/knowledge-base, so this path is not
|
|
206
|
+
// configurable — it must match. Idempotent: existing files are never clobbered.
|
|
207
|
+
function scaffoldDocs(pkgRoot, cwd = process.cwd()) {
|
|
208
|
+
const src = scaffoldSourceDir(pkgRoot);
|
|
209
|
+
if (!existsSync(src)) return { ok: false, reason: `scaffold source missing: ${src}` };
|
|
210
|
+
const { created, skipped } = copyTreeSkipExisting(src, join(cwd, 'docs'));
|
|
211
|
+
return { ok: true, created, skipped };
|
|
212
|
+
}
|
|
213
|
+
|
|
175
214
|
export {
|
|
176
215
|
MIN_OPENSPEC,
|
|
177
216
|
NPM_TIMEOUT_MS,
|
|
@@ -186,4 +225,7 @@ export {
|
|
|
186
225
|
atLeast,
|
|
187
226
|
openSpecState,
|
|
188
227
|
superpowersInstalled,
|
|
228
|
+
scaffoldSourceDir,
|
|
229
|
+
copyTreeSkipExisting,
|
|
230
|
+
scaffoldDocs,
|
|
189
231
|
};
|
package/docs/COMATE_MANUAL.md
CHANGED
|
@@ -33,13 +33,17 @@ comate 是一个规约驱动、AI 辅助的开发编排器。它将 **OpenSpec**
|
|
|
33
33
|
### 1.2 五阶段流程
|
|
34
34
|
|
|
35
35
|
```
|
|
36
|
+
[原始 PRD] -> /comate-intake -┐ (阶段 0:门禁 + 拆分)
|
|
37
|
+
v
|
|
36
38
|
/comate
|
|
37
39
|
auto-detect
|
|
38
40
|
/comate-open -> /comate-design -> /comate-build -> /comate-verify -> /comate-archive
|
|
39
41
|
(OpenSpec) (Superpowers) (Superpowers) (两者) (OpenSpec)
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
流程链:open -> design -> build -> verify -> archive
|
|
44
|
+
流程链:(intake ->) open -> design -> build -> verify -> archive
|
|
45
|
+
|
|
46
|
+
**阶段 0(可选,PRD 门禁与拆分)**:需求来自一份**原始 PRD**时,先过 `/comate-intake`:审阅 PRD 质量、百分制评分、建阻断台账,门禁通过前禁止建任何 change;通过后按依赖/整体性/独立性/规模把 PRD 拆成 1 或 N 个 change(粒度由用户定),逐个交给 `/comate-open`。门禁状态钉在 PRD 同级 `<PRD>.gate.yaml`(机器真相源),评审报告为 `PRD-REVIEW-<PRD名>.md`(人类可读)。PRD 通过门禁后被改会因哈希漂移自动失效(stale),需重跑 intake。需求来自对话时无需此阶段。
|
|
43
47
|
|
|
44
48
|
单次 `/comate` 调用从检测到的阶段开始,当退出条件满足时推进到下一阶段。智能体会自动推进后续阶段,但必须在决策阻塞点暂停,直到收到用户确认。
|
|
45
49
|
|
|
@@ -48,10 +52,15 @@ comate 是一个规约驱动、AI 辅助的开发编排器。它将 **OpenSpec**
|
|
|
48
52
|
- **hotfix**:Bug 修复 / 热修复 —— 跳过头脑风暴,直接走 open -> build -> verify -> archive
|
|
49
53
|
- **tweak**:非 Bug 的微调(文本、配置、文档、提示词)—— 跳过头脑风暴和完整计划,走 open -> 轻量 build -> 轻量 verify -> archive
|
|
50
54
|
|
|
55
|
+
### 1.3.1 工具 skill
|
|
56
|
+
|
|
57
|
+
- **grill**(`/comate-grill`):决策点穷追式澄清。对计划/设计/PRD 的模糊点做深度优先遍历,逐个决策点给推荐答案并等用户确认,直到歧义清零。不是阶段,是跨阶段可复用的工具。comate-intake 在阻断问题 > 3 或有依赖链时会提供「深度澄清」选项自动调用它;用户也可 `/comate-grill <话题>` 直接挑战自己的计划草稿。算法内化自社区 grill-me pattern,避免外部依赖漂移。
|
|
58
|
+
|
|
51
59
|
### 1.4 命令快速参考
|
|
52
60
|
|
|
53
61
|
| 命令 | 阶段 | 归属 | 产出物 |
|
|
54
62
|
|---------|-------|-------|-----------|
|
|
63
|
+
| `/comate-intake` | 0. PRD 门禁与拆分 | Claude | `<PRD>.gate.yaml`, 评审报告, N 个 change |
|
|
55
64
|
| `/comate-open` | 1. 启动 | OpenSpec | proposal.md, design.md, tasks.md |
|
|
56
65
|
| `/comate-design` | 2. 深度设计 | Superpowers | 设计文档, delta spec |
|
|
57
66
|
| `/comate-build` | 3. 规划与构建 | Superpowers | 实现计划, 代码提交 |
|
|
@@ -59,6 +68,7 @@ comate 是一个规约驱动、AI 辅助的开发编排器。它将 **OpenSpec**
|
|
|
59
68
|
| `/comate-archive` | 5. 归档 | OpenSpec | delta->main spec 同步, 设计文档标注, 归档 |
|
|
60
69
|
| `/comate-hotfix` | 预设 | 两者 | 快速修复(跳过头脑风暴) |
|
|
61
70
|
| `/comate-tweak` | 预设 | 两者 | 微调(跳过头脑风暴 + 完整计划) |
|
|
71
|
+
| `/comate-grill` | 工具 | Claude | 决策台账(逐决策点澄清,附推荐 + 用户答案) |
|
|
62
72
|
|
|
63
73
|
---
|
|
64
74
|
|
|
@@ -69,7 +79,7 @@ npm install -g @house365/comate
|
|
|
69
79
|
cd your-project && comate init
|
|
70
80
|
```
|
|
71
81
|
|
|
72
|
-
`init` 会装好下面 2.2–2.4
|
|
82
|
+
`init` 会装好下面 2.2–2.4 的全部内容,并铺出 `docs/` 知识库骨架(2.5)。本节余下部分用于排查:init 报某项失败时,对照确认要装什么、怎么自查。
|
|
73
83
|
|
|
74
84
|
### 2.1 运行环境
|
|
75
85
|
|
|
@@ -113,6 +123,27 @@ comate 使用以下 OpenSpec 能力(以技能形式调用):`openspec-explo
|
|
|
113
123
|
|
|
114
124
|
comate 以 npm 包分发(`npm install -g @house365/comate`),`comate init` 通过 skills CLI 装到用户级 `~/.agents/skills`,一次装完所有项目共用,支持 Claude Code、Codex、Cursor 等 70+ agent。
|
|
115
125
|
|
|
126
|
+
### 2.5 docs/ 知识库骨架(阶段 0 用)
|
|
127
|
+
|
|
128
|
+
`comate init` 会在项目里铺出 `docs/` 骨架,供 `/comate-intake` 读写 PRD:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
docs/
|
|
132
|
+
├── README.md # docs 目录说明
|
|
133
|
+
├── architecture/README.md # 架构规范占位(契约事实源,需项目自建)
|
|
134
|
+
└── knowledge-base/
|
|
135
|
+
├── README.md # PRD → comate 流程说明 + 命名约定
|
|
136
|
+
├── requirements/_templates/PRD.md # PRD 模板(comate 版,无 MRD/FRD 层)
|
|
137
|
+
└── integrations/_TEMPLATE/ # 第三方对接档案模板(README + api-snapshot)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- 路径与 `comate-lib.sh` 硬编码的 `comate_kb_root=docs/knowledge-base` 对齐,PRD 放 `requirements/<YYYYMMDD-功能名>/` 下。
|
|
141
|
+
- **幂等且不覆盖**:逐文件跳过已存在的,重跑 `comate init` 不动你已改过的 docs。
|
|
142
|
+
- `architecture/` 只给占位 README(架构内容项目专属),是 PRD 门禁「契约对齐」的事实源,需按项目实际补齐。
|
|
143
|
+
- 不含 MRD/FRD 模板——comate 只保留 PRD 层,FRD 内容消解进 change 的 proposal/design/tasks。
|
|
144
|
+
|
|
145
|
+
### 2.6 comate 本身的技能布局
|
|
146
|
+
|
|
116
147
|
采用扁平布局——每个 phase 是顶层技能目录,而非 `comate/<phase>/` 嵌套。部分 agent(含 Claude Code)只发现顶层目录,扁平是共同可用的最小公倍数:
|
|
117
148
|
|
|
118
149
|
```
|
|
@@ -511,6 +542,21 @@ bash `""` doctor
|
|
|
511
542
|
|
|
512
543
|
清单文件位于 `openspec/changes/<name>/.comate/sources.manifest`,随 change 进版本库、随归档保留。design 与 build 阶段 guard 会复算知识库文件哈希、与清单比对,漂移则阻塞。
|
|
513
544
|
|
|
545
|
+
### gate - PRD 门禁(阶段 0)
|
|
546
|
+
|
|
547
|
+
对**原始 PRD** 做质量门禁,通过前禁止从该 PRD 建 change。状态钉在 PRD 同级 `<PRD>.gate.yaml`(独立于 change 的 `.comate.yaml`),评审报告为 `PRD-REVIEW-<PRD名>.md`。由 `/comate-intake` 驱动。
|
|
548
|
+
|
|
549
|
+
| 命令 | 描述 |
|
|
550
|
+
|---------|-------------|
|
|
551
|
+
| `gate <prd> review` | 建/刷新 gate.yaml,钉当前 PRD sha256,回显评审报告路径。PRD 变了则状态重置为 `reviewing` |
|
|
552
|
+
| `gate <prd> set <field> <value>` | 写字段:`score` / `blocking_open` / `gate_status` 等 |
|
|
553
|
+
| `gate <prd> pass [--tbd]` | 置 `passed`(`--tbd` 置 `passed-with-tbd`)。硬校验 `blocking_open=0` 且评审报告非空,并重钉哈希 |
|
|
554
|
+
| `gate <prd> spawned <change>` | 记录据此 PRD 扇出的 change(逗号拼接) |
|
|
555
|
+
| `gate <prd> status [--json]` | 查状态;哈希漂移时自动判 `stale`(不改文件) |
|
|
556
|
+
| `gate <prd> check` | comate-open 建 change 前的前置守卫。PASS 要求 gate 存在、状态 ∈ {passed, passed-with-tbd}、哈希未漂移;否则退出码 1 阻塞 |
|
|
557
|
+
|
|
558
|
+
`gate_status` 取值:`reviewing`(审阅中)/ `blocked`(有未关闭阻断项)/ `passed`(阻断清零,可扇出)/ `passed-with-tbd`(用户授权带 TBD,change 需标 ⚠)/ `stale`(PRD 通过后被改,须重过门禁)。退出码契约同 guard:0 通过 / 1 阻塞 / 2 用法 / 3 环境 / 4 状态损坏。
|
|
559
|
+
|
|
514
560
|
### archive - 归档
|
|
515
561
|
|
|
516
562
|
| 命令 | 描述 |
|
package/package.json
CHANGED
package/skills/comate/SKILL.md
CHANGED
|
@@ -24,10 +24,12 @@ agent 做决策只需读本节,其余参见末尾「参考附录」。决策
|
|
|
24
24
|
**预设探测优先级最高**:
|
|
25
25
|
- 用户明确描述 bug 修复/热修 + 满足 hotfix 条件 → 直接调用 `/comate-hotfix`
|
|
26
26
|
- 用户明确描述文案/配置/文档/prompt 小调整 + 满足 tweak 条件 → 直接调用 `/comate-tweak`
|
|
27
|
+
- **用户输入指向一份原始 PRD**(给了 PRD 路径/编号,或明确说「这是 PRD / 从这份需求文档开始」,典型路径 `docs/knowledge-base/**/PRD-*.md`)**且无对应活跃 change** → 直接调用 `/comate-intake`(阶段 0:先过门禁,再拆分建 change)。不要直接调 `/comate-open`——PRD 必须先过质量门禁。
|
|
27
28
|
- 无预设命中 → 按下表处理
|
|
28
29
|
|
|
29
30
|
| 活跃 change | 用户输入 | 行为 |
|
|
30
31
|
|-------------|----------|------|
|
|
32
|
+
| 无 | 指向原始 PRD | → 调用 `/comate-intake` |
|
|
31
33
|
| 无 | 非预设输入 | → 调用 `/comate-open` |
|
|
32
34
|
| 恰好 1 个 | `/comate <描述>` | → **询问**:继续该 change 还是新建 |
|
|
33
35
|
| 多个 | `/comate <描述>` | → **询问**:继续已有还是新建;继续则列出供选择 |
|
|
@@ -99,6 +101,7 @@ agent 做决策只需读本节,其余参见末尾「参考附录」。决策
|
|
|
99
101
|
|
|
100
102
|
| 命令 | 阶段 | Owner | 产物 |
|
|
101
103
|
|------|------|-------|------|
|
|
104
|
+
| `/comate-intake` | 0. PRD 门禁与拆分 | Claude | `<PRD>.gate.yaml`, 评审报告, N 个 change |
|
|
102
105
|
| `/comate-open` | 1. 开题 | OpenSpec | proposal.md, design.md, tasks.md |
|
|
103
106
|
| `/comate-design` | 2. 深度设计 | Superpowers | Design Doc, delta spec |
|
|
104
107
|
| `/comate-build` | 3. 计划与构建 | Superpowers | 实施计划, 代码提交 |
|
|
@@ -108,12 +111,16 @@ agent 做决策只需读本节,其余参见末尾「参考附录」。决策
|
|
|
108
111
|
| `/comate-tweak` | 预设 | 双方 | 小调整(跳过 brainstorming + 完整 plan) |
|
|
109
112
|
|
|
110
113
|
```
|
|
111
|
-
/comate
|
|
114
|
+
[原始 PRD] ──→ /comate-intake ──┐ (门禁+拆分,扇出 N 个 change)
|
|
115
|
+
(Claude) │
|
|
116
|
+
/comate ▼
|
|
112
117
|
↓ 自动探测
|
|
113
118
|
/comate-open ──→ /comate-design ──→ /comate-build ──→ /comate-verify ──→ /comate-archive
|
|
114
119
|
(OpenSpec) (Superpowers) (Superpowers) (双方) (OpenSpec)
|
|
115
120
|
```
|
|
116
121
|
|
|
122
|
+
需求来自对话时从 `/comate-open` 起步;来自原始 PRD 时先过 `/comate-intake` 门禁再拆分。
|
|
123
|
+
|
|
117
124
|
---
|
|
118
125
|
|
|
119
126
|
## 参考附录
|
|
@@ -428,3 +428,97 @@ sources_drift() {
|
|
|
428
428
|
|
|
429
429
|
# json_escape <string> — escape backslash and double-quote for JSON values.
|
|
430
430
|
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
|
431
|
+
|
|
432
|
+
# --- PRD gate (phase 0: quality gate + split) ---
|
|
433
|
+
#
|
|
434
|
+
# gate.yaml lives NEXT TO the PRD (<prd>.gate.yaml), not under openspec/changes,
|
|
435
|
+
# so it has its own flat schema kept separate from the change .comate.yaml
|
|
436
|
+
# FIELD_ORDER (mixing them would pollute every change state file with gate keys).
|
|
437
|
+
# It is the machine truth for "may a change be spawned from this PRD"; the
|
|
438
|
+
# markdown review report is the human-readable derivative.
|
|
439
|
+
|
|
440
|
+
readonly GATE_SCHEMA_VERSION=1
|
|
441
|
+
|
|
442
|
+
# Canonical field order for gate.yaml (same full-rewrite discipline as yaml_set).
|
|
443
|
+
GATE_FIELD_ORDER=(
|
|
444
|
+
schema_version
|
|
445
|
+
prd_path
|
|
446
|
+
prd_hash
|
|
447
|
+
gate_status
|
|
448
|
+
score
|
|
449
|
+
blocking_open
|
|
450
|
+
review_report
|
|
451
|
+
spawned_changes
|
|
452
|
+
gated_at
|
|
453
|
+
created_at
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# gate_status is the only enum. `stale` is computed (hash mismatch) but also
|
|
457
|
+
# storable so a detected-stale gate persists across runs.
|
|
458
|
+
declare -A GATE_ENUMS=(
|
|
459
|
+
[schema_version]="1"
|
|
460
|
+
[gate_status]="reviewing blocked passed passed-with-tbd stale"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# gate_path_for <prd> — echo the gate.yaml path adjacent to the PRD.
|
|
464
|
+
# Strips a trailing .md/.markdown and appends .gate.yaml.
|
|
465
|
+
gate_path_for() {
|
|
466
|
+
local prd="$1" base
|
|
467
|
+
base="${prd%.markdown}"
|
|
468
|
+
base="${base%.md}"
|
|
469
|
+
echo "${base}.gate.yaml"
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# gate_report_for <prd> — echo the review report path (PRD same dir).
|
|
473
|
+
gate_report_for() {
|
|
474
|
+
local prd="$1" dir base
|
|
475
|
+
dir=$(dirname "$prd")
|
|
476
|
+
base=$(basename "$prd")
|
|
477
|
+
base="${base%.markdown}"
|
|
478
|
+
base="${base%.md}"
|
|
479
|
+
echo "${dir}/PRD-REVIEW-${base}.md"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# gate_yaml_set <file> <key> <value> — gate-file analogue of yaml_set: validate
|
|
483
|
+
# enum, read the whole file, set one key, rewrite every GATE_FIELD_ORDER field,
|
|
484
|
+
# atomic mv, write-back verify.
|
|
485
|
+
gate_yaml_set() {
|
|
486
|
+
local file="$1" key="$2" value="$3"
|
|
487
|
+
[ -f "$file" ] || die "$COMATE_CORRUPT" "ERROR: gate file not found: $file"
|
|
488
|
+
|
|
489
|
+
local known=0 k
|
|
490
|
+
for k in "${GATE_FIELD_ORDER[@]}"; do
|
|
491
|
+
[ "$k" = "$key" ] && { known=1; break; }
|
|
492
|
+
done
|
|
493
|
+
[ "$known" -eq 1 ] || die "$COMATE_USAGE" "ERROR: unknown gate field '$key'"
|
|
494
|
+
|
|
495
|
+
local legal="${GATE_ENUMS[$key]:-}"
|
|
496
|
+
if [ -n "$legal" ] && [ -n "$value" ] && [ "$value" != "null" ]; then
|
|
497
|
+
local ok=0 v
|
|
498
|
+
for v in $legal; do [ "$value" = "$v" ] && { ok=1; break; }; done
|
|
499
|
+
[ "$ok" -eq 1 ] || die "$COMATE_USAGE" "ERROR: invalid value '$value' for '$key' (expected: $legal)"
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
declare -A _g
|
|
503
|
+
for k in "${GATE_FIELD_ORDER[@]}"; do
|
|
504
|
+
local cv; cv=$(yaml_get "$file" "$k")
|
|
505
|
+
[ -z "$cv" ] && cv="null"
|
|
506
|
+
_g[$k]="$cv"
|
|
507
|
+
done
|
|
508
|
+
_g[$key]="$value"
|
|
509
|
+
|
|
510
|
+
local tmp
|
|
511
|
+
tmp=$(mktemp "${TMPDIR:-/tmp}/comate-gate.XXXXXX") || die "$COMATE_ENV" "ERROR: mktemp failed"
|
|
512
|
+
{
|
|
513
|
+
printf '# comate PRD gate — flat key:value only, no nesting\n'
|
|
514
|
+
for k in "${GATE_FIELD_ORDER[@]}"; do
|
|
515
|
+
printf '%s: %s\n' "$k" "${_g[$k]:-null}"
|
|
516
|
+
done
|
|
517
|
+
} > "$tmp"
|
|
518
|
+
mv "$tmp" "$file" || { rm -f "$tmp"; die "$COMATE_ENV" "ERROR: failed to replace $file"; }
|
|
519
|
+
|
|
520
|
+
local readback; readback=$(yaml_get "$file" "$key")
|
|
521
|
+
if [ "$readback" != "$value" ]; then
|
|
522
|
+
die "$COMATE_CORRUPT" "ERROR: gate write-back mismatch for '$key': wrote '$value', read '$readback'"
|
|
523
|
+
fi
|
|
524
|
+
}
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
# sources <name> check — drift check tracked source files
|
|
18
18
|
# sources <name> list — print the source manifest
|
|
19
19
|
# sources find <file> — reverse lookup: changes citing a file
|
|
20
|
+
# gate <prd> review — create/refresh gate.yaml next to the PRD, pin hash
|
|
21
|
+
# gate <prd> set <field> <value> — write score | blocking_open | gate_status | …
|
|
22
|
+
# gate <prd> pass [--tbd] — mark passed (needs blocking_open=0 + report)
|
|
23
|
+
# gate <prd> spawned <change> — record a change fanned out from this PRD
|
|
24
|
+
# gate <prd> status [--json] — print gate status (computes stale on hash drift)
|
|
25
|
+
# gate <prd> check — precondition guard before spawning a change
|
|
20
26
|
# archive <name> [--dry-run] — sync delta specs + annotate + move + mark archived
|
|
21
27
|
# status [path] — list active changes + next command
|
|
22
28
|
# doctor — prerequisite self-check
|
|
@@ -66,6 +72,12 @@ Usage: comate.sh <command> ...
|
|
|
66
72
|
sources <name> check
|
|
67
73
|
sources <name> list
|
|
68
74
|
sources find <file>
|
|
75
|
+
gate <prd> review create/refresh gate.yaml next to PRD, pin hash
|
|
76
|
+
gate <prd> set <field> <value> score | blocking_open | gate_status | …
|
|
77
|
+
gate <prd> pass [--tbd] mark passed (needs blocking_open=0 + report)
|
|
78
|
+
gate <prd> spawned <change> record a change fanned out from this PRD
|
|
79
|
+
gate <prd> status [--json]
|
|
80
|
+
gate <prd> check precondition guard before spawning a change
|
|
69
81
|
archive <name> [--dry-run]
|
|
70
82
|
status [path]
|
|
71
83
|
doctor
|
|
@@ -1332,6 +1344,184 @@ sources_find() {
|
|
|
1332
1344
|
return 0
|
|
1333
1345
|
}
|
|
1334
1346
|
|
|
1347
|
+
# =====================================================================
|
|
1348
|
+
# gate — phase 0: PRD quality gate + split precondition
|
|
1349
|
+
# =====================================================================
|
|
1350
|
+
|
|
1351
|
+
# require_prd <prd> — ensure the PRD path is a real, non-empty file; echo it.
|
|
1352
|
+
require_prd() {
|
|
1353
|
+
local prd="$1"
|
|
1354
|
+
[ -n "$prd" ] || die "$COMATE_USAGE" "ERROR: PRD path required"
|
|
1355
|
+
case "$prd" in *..*) die "$COMATE_USAGE" "ERROR: PRD path cannot contain '..': $prd" ;; esac
|
|
1356
|
+
[ -f "$prd" ] || die "$COMATE_CORRUPT" "ERROR: PRD not found: $prd"
|
|
1357
|
+
file_nonempty "$prd" || die "$COMATE_CORRUPT" "ERROR: PRD is empty: $prd"
|
|
1358
|
+
echo "$prd"
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
# gate <prd> review — create or refresh gate.yaml next to the PRD and pin the
|
|
1362
|
+
# current PRD hash. Existing passed gates are re-opened to `reviewing` only when
|
|
1363
|
+
# the PRD changed (hash drift); otherwise the review report path is (re)emitted
|
|
1364
|
+
# so the skill knows where to write its human-readable audit. The skill does the
|
|
1365
|
+
# semantic scoring and writes score/blocking_open/gate_status via `gate set`.
|
|
1366
|
+
gate_review() {
|
|
1367
|
+
local prd; prd=$(require_prd "$1")
|
|
1368
|
+
local gate report cur_hash
|
|
1369
|
+
gate=$(gate_path_for "$prd")
|
|
1370
|
+
report=$(gate_report_for "$prd")
|
|
1371
|
+
cur_hash=$(hash_file "$prd")
|
|
1372
|
+
|
|
1373
|
+
if [ ! -f "$gate" ]; then
|
|
1374
|
+
cat > "$gate" <<EOF
|
|
1375
|
+
# comate PRD gate — flat key:value only, no nesting
|
|
1376
|
+
schema_version: $GATE_SCHEMA_VERSION
|
|
1377
|
+
prd_path: $prd
|
|
1378
|
+
prd_hash: $cur_hash
|
|
1379
|
+
gate_status: reviewing
|
|
1380
|
+
score: null
|
|
1381
|
+
blocking_open: null
|
|
1382
|
+
review_report: $report
|
|
1383
|
+
spawned_changes: null
|
|
1384
|
+
gated_at: null
|
|
1385
|
+
created_at: $(today)
|
|
1386
|
+
EOF
|
|
1387
|
+
log_pass "[GATE] created $gate (status=reviewing)"
|
|
1388
|
+
else
|
|
1389
|
+
local old_hash old_status
|
|
1390
|
+
old_hash=$(yaml_get "$gate" prd_hash)
|
|
1391
|
+
old_status=$(yaml_get "$gate" gate_status)
|
|
1392
|
+
gate_yaml_set "$gate" prd_path "$prd"
|
|
1393
|
+
gate_yaml_set "$gate" review_report "$report"
|
|
1394
|
+
if [ "$old_hash" != "$cur_hash" ]; then
|
|
1395
|
+
gate_yaml_set "$gate" prd_hash "$cur_hash"
|
|
1396
|
+
gate_yaml_set "$gate" gate_status reviewing
|
|
1397
|
+
log_warn "[GATE] PRD changed since last review — status reset to reviewing (was $old_status)"
|
|
1398
|
+
else
|
|
1399
|
+
log_info "[GATE] $gate (status=$old_status, unchanged PRD)"
|
|
1400
|
+
fi
|
|
1401
|
+
fi
|
|
1402
|
+
echo "review_report: $report"
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
# gate <prd> set <field> <value> — write a gate field (score/blocking_open/
|
|
1406
|
+
# gate_status/…) with the same validation as gate_yaml_set.
|
|
1407
|
+
gate_set() {
|
|
1408
|
+
local prd; prd=$(require_prd "$1")
|
|
1409
|
+
local field="${2:-}" value="${3:-}"
|
|
1410
|
+
[ -n "$field" ] || die "$COMATE_USAGE" "Usage: comate.sh gate <prd> set <field> <value>"
|
|
1411
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1412
|
+
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found (run 'gate review' first): $gate"
|
|
1413
|
+
gate_yaml_set "$gate" "$field" "$value"
|
|
1414
|
+
log_pass "[GATE] set $field=$value"
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
# gate <prd> pass [--tbd] — flip to passed/passed-with-tbd. Hard precondition:
|
|
1418
|
+
# blocking_open must be 0 (no open blocking issues). Re-pins prd_hash so a later
|
|
1419
|
+
# edit invalidates the pass. --tbd records that the user authorised proceeding
|
|
1420
|
+
# with unresolved TBDs (spawned changes must carry ⚠ markers).
|
|
1421
|
+
gate_pass() {
|
|
1422
|
+
local prd; prd=$(require_prd "$1")
|
|
1423
|
+
local tbd=0
|
|
1424
|
+
[ "${2:-}" = "--tbd" ] && tbd=1
|
|
1425
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1426
|
+
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found (run 'gate review' first): $gate"
|
|
1427
|
+
|
|
1428
|
+
local blocking; blocking=$(yaml_get "$gate" blocking_open)
|
|
1429
|
+
if [ "$blocking" != "0" ]; then
|
|
1430
|
+
die "$COMATE_BLOCK" "BLOCK: blocking_open='$blocking' — resolve all blocking issues (set blocking_open 0) before passing the gate"
|
|
1431
|
+
fi
|
|
1432
|
+
local report; report=$(yaml_get "$gate" review_report)
|
|
1433
|
+
if [ -z "$report" ] || [ "$report" = "null" ] || ! file_nonempty "$report"; then
|
|
1434
|
+
die "$COMATE_BLOCK" "BLOCK: review report missing or empty ($report) — the gate report is mandatory"
|
|
1435
|
+
fi
|
|
1436
|
+
|
|
1437
|
+
gate_yaml_set "$gate" prd_hash "$(hash_file "$prd")"
|
|
1438
|
+
if [ "$tbd" -eq 1 ]; then
|
|
1439
|
+
gate_yaml_set "$gate" gate_status passed-with-tbd
|
|
1440
|
+
else
|
|
1441
|
+
gate_yaml_set "$gate" gate_status passed
|
|
1442
|
+
fi
|
|
1443
|
+
gate_yaml_set "$gate" gated_at "$(today)"
|
|
1444
|
+
log_pass "[GATE] $(yaml_get "$gate" gate_status) — changes may now be spawned from $prd"
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
# gate <prd> spawned <change> — append a change name to spawned_changes
|
|
1448
|
+
# (comma-joined), recording which changes this PRD fanned out into.
|
|
1449
|
+
gate_spawned() {
|
|
1450
|
+
local prd; prd=$(require_prd "$1")
|
|
1451
|
+
local change="${2:-}"
|
|
1452
|
+
validate_change_name "$change"
|
|
1453
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1454
|
+
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found: $gate"
|
|
1455
|
+
local cur; cur=$(yaml_get "$gate" spawned_changes)
|
|
1456
|
+
case ",$cur," in
|
|
1457
|
+
*",$change,"*) log_info "[GATE] $change already recorded" ; return 0 ;;
|
|
1458
|
+
esac
|
|
1459
|
+
if [ -z "$cur" ] || [ "$cur" = "null" ]; then
|
|
1460
|
+
gate_yaml_set "$gate" spawned_changes "$change"
|
|
1461
|
+
else
|
|
1462
|
+
gate_yaml_set "$gate" spawned_changes "$cur,$change"
|
|
1463
|
+
fi
|
|
1464
|
+
log_pass "[GATE] recorded spawned change: $change"
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
# _gate_is_stale <gate> <prd> — true (0) if the recorded hash no longer matches
|
|
1468
|
+
# the PRD on disk.
|
|
1469
|
+
_gate_is_stale() {
|
|
1470
|
+
local gate="$1" prd="$2" recorded actual
|
|
1471
|
+
recorded=$(yaml_get "$gate" prd_hash)
|
|
1472
|
+
[ -f "$prd" ] || return 0
|
|
1473
|
+
actual=$(hash_file "$prd")
|
|
1474
|
+
[ "$recorded" != "$actual" ]
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
# gate <prd> status [--json] — print current gate status, computing stale on the
|
|
1478
|
+
# fly (hash drift). Does not mutate the file.
|
|
1479
|
+
gate_status() {
|
|
1480
|
+
local prd="$1" json="${2:-}"
|
|
1481
|
+
[ -n "$prd" ] || die "$COMATE_USAGE" "Usage: comate.sh gate <prd> status [--json]"
|
|
1482
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1483
|
+
if [ ! -f "$gate" ]; then
|
|
1484
|
+
if [ "$json" = "--json" ]; then
|
|
1485
|
+
printf '{"gate":"none","prd":"%s"}\n' "$(json_escape "$prd")"
|
|
1486
|
+
else
|
|
1487
|
+
echo "gate: none ($prd) — run 'gate review' to start"
|
|
1488
|
+
fi
|
|
1489
|
+
return "$COMATE_OK"
|
|
1490
|
+
fi
|
|
1491
|
+
local st score blocking; st=$(yaml_get "$gate" gate_status)
|
|
1492
|
+
score=$(yaml_get "$gate" score); blocking=$(yaml_get "$gate" blocking_open)
|
|
1493
|
+
if [ -f "$prd" ] && _gate_is_stale "$gate" "$prd"; then st="stale"; fi
|
|
1494
|
+
if [ "$json" = "--json" ]; then
|
|
1495
|
+
printf '{"gate":"%s","status":"%s","score":"%s","blocking_open":"%s","report":"%s"}\n' \
|
|
1496
|
+
"$(json_escape "$gate")" "$(json_escape "$st")" "$(json_escape "$score")" \
|
|
1497
|
+
"$(json_escape "$blocking")" "$(json_escape "$(yaml_get "$gate" review_report)")"
|
|
1498
|
+
else
|
|
1499
|
+
echo "gate: $st score=$score blocking_open=$blocking ($gate)"
|
|
1500
|
+
fi
|
|
1501
|
+
return "$COMATE_OK"
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
# gate <prd> check — the precondition guard comate-open runs before spawning a
|
|
1505
|
+
# change. PASS (exit 0) requires: gate.yaml exists, status ∈ {passed,
|
|
1506
|
+
# passed-with-tbd}, and prd_hash matches the current PRD. Anything else BLOCKs.
|
|
1507
|
+
gate_check() {
|
|
1508
|
+
local prd; prd=$(require_prd "$1")
|
|
1509
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1510
|
+
if [ ! -f "$gate" ]; then
|
|
1511
|
+
die "$COMATE_BLOCK" "BLOCK: no gate for $prd — run /comate-intake to review the PRD before creating a change"
|
|
1512
|
+
fi
|
|
1513
|
+
local st; st=$(yaml_get "$gate" gate_status)
|
|
1514
|
+
case "$st" in
|
|
1515
|
+
passed|passed-with-tbd) ;;
|
|
1516
|
+
*) die "$COMATE_BLOCK" "BLOCK: gate_status='$st' (not passed) — a change cannot be spawned until the PRD gate passes" ;;
|
|
1517
|
+
esac
|
|
1518
|
+
if _gate_is_stale "$gate" "$prd"; then
|
|
1519
|
+
die "$COMATE_BLOCK" "BLOCK: PRD changed since it passed the gate (hash mismatch) — re-run /comate-intake to re-gate before spawning a change"
|
|
1520
|
+
fi
|
|
1521
|
+
log_pass "[GATE] check PASS ($st) — $prd"
|
|
1522
|
+
return "$COMATE_OK"
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1335
1525
|
# =====================================================================
|
|
1336
1526
|
# archive
|
|
1337
1527
|
# =====================================================================
|
|
@@ -1678,6 +1868,20 @@ case "$CMD" in
|
|
|
1678
1868
|
esac
|
|
1679
1869
|
fi
|
|
1680
1870
|
;;
|
|
1871
|
+
gate)
|
|
1872
|
+
PRD="${1:-}"; SUB="${2:-}"
|
|
1873
|
+
[ -n "$PRD" ] && [ -n "$SUB" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
1874
|
+
shift 2 || true
|
|
1875
|
+
case "$SUB" in
|
|
1876
|
+
review) gate_review "$PRD" ;;
|
|
1877
|
+
set) gate_set "$PRD" "${1:-}" "${2:-}" ;;
|
|
1878
|
+
pass) gate_pass "$PRD" "${1:-}" ;;
|
|
1879
|
+
spawned) gate_spawned "$PRD" "${1:-}" ;;
|
|
1880
|
+
status) gate_status "$PRD" "${1:-}" ;;
|
|
1881
|
+
check) gate_check "$PRD" ;;
|
|
1882
|
+
*) die "$COMATE_USAGE" "Unknown gate subcommand: $SUB" ;;
|
|
1883
|
+
esac
|
|
1884
|
+
;;
|
|
1681
1885
|
archive)
|
|
1682
1886
|
[ -n "${1:-}" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
1683
1887
|
archive "$1" "${2:-}"
|