@house365/comate 0.9.18 → 0.9.20
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/bin/cli.mjs +56 -12
- package/docs/COMATE_BEGINNER_GUIDE.md +76 -317
- package/docs/COMATE_INIT_DIRECTORY_GUIDE.md +201 -0
- package/docs/COMATE_MANUAL.md +2 -1
- package/package.json +1 -1
- package/skills/comate/scripts/comate.sh +163 -0
- package/skills/comate/tests/comate.bats +204 -0
- package/skills/comate-gate/SKILL.md +27 -12
- package/skills/comate-open/SKILL.md +31 -11
|
@@ -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`。
|
package/docs/COMATE_MANUAL.md
CHANGED
|
@@ -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>` |
|
|
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
|
@@ -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
|