@peterxiaoyang/superspec 0.1.2 → 0.1.3
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 +181 -116
- package/dist/src/core.js +5 -5
- package/dist/src/doctor.d.ts +44 -0
- package/dist/src/doctor.js +230 -0
- package/dist/src/evidence.d.ts +1 -0
- package/dist/src/evidence.js +7 -0
- package/dist/src/gates.d.ts +1 -0
- package/dist/src/gates.js +39 -10
- package/dist/src/git.js +2 -2
- package/dist/src/self_update.d.ts +14 -0
- package/dist/src/self_update.js +56 -0
- package/dist/src/util.js +4 -2
- package/dist/superspec.d.ts +2 -0
- package/dist/superspec.js +42 -3
- package/package.json +2 -2
- package/templates/workflow/skills/superspec-apply/SKILL.md +10 -2
- package/templates/workflow/skills/superspec-archive/SKILL.md +6 -0
- package/templates/workflow/skills/superspec-explore/SKILL.md +10 -1
- package/templates/workflow/skills/superspec-propose/SKILL.md +10 -2
- package/templates/workflow/skills/superspec-review/SKILL.md +10 -2
package/README.md
CHANGED
|
@@ -2,196 +2,261 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@peterxiaoyang/superspec)
|
|
4
4
|
[](https://nodejs.org)
|
|
5
|
-
[](https://github.com/Fission-AI/OpenSpec)
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> SuperSpec 是一套“先想清楚,再动手”的工作流。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
它解决的是一个很常见的问题:AI 编程工具写代码很快,但有时候还没搞清楚需求、现有代码和测试边界,就已经开始改文件了。
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
它做的是在 OpenSpec 现有变更生命周期之上,再加一层更强的执行纪律:
|
|
11
|
+
SuperSpec 会把一次需求变更拆成 5 步:
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- 先经过多角色审查,再允许宣布“完成”
|
|
18
|
-
- 先确认归档保全完整,再真正归档
|
|
13
|
+
```text
|
|
14
|
+
探索需求 -> 写方案 -> 做实现 -> 做审查 -> 归档收尾
|
|
15
|
+
```
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
这样做的目的很简单:
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
- 改代码前先弄清楚现状
|
|
20
|
+
- 写实现前先有方案和任务
|
|
21
|
+
- 任务完成前先有测试或验证记录
|
|
22
|
+
- 宣布完成前先经过审查
|
|
23
|
+
- 归档时保留关键过程记录
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
- **把执行约束收敛成可复用的叠加层**
|
|
26
|
-
- **让阶段推进依赖证据,而不是模型一句“done”**
|
|
25
|
+
## 适合谁
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
适合已经在用 AI 编程工具或命令行代理做项目开发,并希望流程更稳一点的团队或个人。
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
如果你遇到过这些情况,SuperSpec 会有帮助:
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
- 需求还没说清楚,AI 就开始写代码
|
|
32
|
+
- 方案写得很粗,后面实现时靠猜
|
|
33
|
+
- 测试只跑了命令,但没人说明它证明了什么
|
|
34
|
+
- 代码写完后缺少真正的审查
|
|
35
|
+
- 过几天想回看当时为什么这么改,却找不到过程记录
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
- 测试在,但没有证明真正重要的东西
|
|
36
|
-
- 任务打勾了,但缺少强校验
|
|
37
|
-
- 主线程自己写、自己审、自己宣布通过
|
|
38
|
-
- 归档做了,但辅助证据已经散了
|
|
37
|
+
如果你只是想让 AI 快速改一个很小的文件,且不需要完整方案、审查和记录,那 SuperSpec 可能会显得偏重。
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
## 和 OpenSpec 有什么区别
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
一句话区别:
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
```text
|
|
44
|
+
OpenSpec 管“这次要改什么”。
|
|
45
|
+
SuperSpec 管“AI 应该怎样把这次改动做稳”。
|
|
46
|
+
```
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
更具体一点:
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
| 问题 | OpenSpec 主要负责 | SuperSpec 额外补上 |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| 这次变更是什么 | 方案、规格、设计、任务和归档 | 要求 AI 在写方案前先调查现状 |
|
|
53
|
+
| 方案怎么写 | 提供标准的变更文档结构 | 要求方案前后有范围、风险、业务约束和测试思路 |
|
|
54
|
+
| 代码怎么做 | 记录任务清单和完成状态 | 要求按任务实现,并留下测试或验证记录 |
|
|
55
|
+
| 做完怎么算稳 | 可以校验规格和归档 | 增加代码审查、架构审查、反方审查和最终验证 |
|
|
56
|
+
| 以后怎么追溯 | 保留 OpenSpec 的变更文档 | 额外保留探索、测试、审查和收尾记录 |
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
举个例子:
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- `test-contract.md`:把测试覆盖范围、测试编号、约束映射和验证责任提前写清楚
|
|
60
|
+
OpenSpec 会帮你记录“要增加登录功能、需要哪些规格、设计和任务”。
|
|
61
|
+
SuperSpec 会进一步要求 AI 先看看现有登录/权限代码在哪里、哪些业务规则不能破坏、哪些场景必须测试、实现后要经过哪些审查,最后再归档。
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
所以 SuperSpec 不是 OpenSpec 的替代品。它更像是 OpenSpec 外面的一层执行纪律,专门约束 AI 编程工具不要跳过关键步骤。
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
## 快速开始
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
### 1. 安装
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
- `code-reviewer`
|
|
69
|
-
- `verifier`
|
|
69
|
+
```bash
|
|
70
|
+
npm install -g @peterxiaoyang/superspec@latest
|
|
71
|
+
```
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
需要 Node.js `>= 20.19.0`。
|
|
72
74
|
|
|
73
|
-
###
|
|
75
|
+
### 2. 初始化当前项目
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
进入你的项目根目录,然后运行:
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
- 可归档条件与归档保全条件
|
|
79
|
+
```bash
|
|
80
|
+
superspec init --scope project
|
|
81
|
+
```
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
这条命令的意思是:把 SuperSpec 当前可用的工作流入口安装到项目里。
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
Windows PowerShell 如果拦截 npm 的 `.ps1` 脚本,请改用:
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
```powershell
|
|
88
|
+
superspec.cmd init --scope project
|
|
89
|
+
```
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
- 证据:保存 RED/GREEN、审查、验证、确认等过程证据
|
|
90
|
-
- 审查输出:保存各角色的审查结果和主线程的汇总结论
|
|
91
|
-
- 状态文件:保存当前 gate、指纹和阶段状态,便于恢复和重算
|
|
92
|
-
- ledger:保存关键事件记录,方便追溯流程推进过程
|
|
93
|
-
- 归档保全元数据:确保归档前后能核对辅助目录是否完整保留
|
|
91
|
+
### 3. 按步骤使用
|
|
94
92
|
|
|
95
|
-
|
|
93
|
+
在你使用的 AI 编程工具或 CLI 里,按下面的阶段入口推进。不同工具的触发方式可以不同,但入口名和顺序保持一致。
|
|
96
94
|
|
|
97
|
-
|
|
95
|
+
开始时先探索需求:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
使用 superspec-explore,帮我梳理这个需求:……
|
|
99
|
+
```
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
探索完成后,写正式方案:
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
```text
|
|
104
|
+
使用 superspec-propose,把刚才的探索结果整理成方案。
|
|
105
|
+
```
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
方案确认后,开始实现:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
使用 superspec-apply,按任务实现。
|
|
111
|
+
```
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
实现完成后,审查:
|
|
108
114
|
|
|
109
|
-
|
|
115
|
+
```text
|
|
116
|
+
使用 superspec-review,检查实现、测试和风险。
|
|
117
|
+
```
|
|
110
118
|
|
|
111
|
-
|
|
119
|
+
审查通过后,归档:
|
|
112
120
|
|
|
113
|
-
```
|
|
114
|
-
|
|
121
|
+
```text
|
|
122
|
+
使用 superspec-archive,归档这个变更。
|
|
115
123
|
```
|
|
116
124
|
|
|
125
|
+
## 五个入口分别做什么
|
|
117
126
|
|
|
118
|
-
|
|
127
|
+
| 入口 | 什么时候用 | 它会要求做什么 |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `superspec-explore` | 需求刚开始时 | 读代码、查现状、整理范围和风险;这一步不改业务代码 |
|
|
130
|
+
| `superspec-propose` | 需求已经清楚后 | 写正式方案、规格、设计和任务,并提前规划测试 |
|
|
131
|
+
| `superspec-apply` | 方案通过后 | 按任务实现代码,记录测试或验证结果 |
|
|
132
|
+
| `superspec-review` | 实现完成后 | 做代码审查、架构审查、反方审查和最终验证 |
|
|
133
|
+
| `superspec-archive` | 审查通过后 | 用 OpenSpec 完成归档,并检查关键记录是否保留 |
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
你日常主要记住这五个入口就够了。
|
|
136
|
+
|
|
137
|
+
## 它会多保存哪些记录
|
|
138
|
+
|
|
139
|
+
SuperSpec 会在每次变更下面保存一些辅助记录,方便后续追溯。
|
|
140
|
+
|
|
141
|
+
主要包括:
|
|
142
|
+
|
|
143
|
+
- 探索记录:这次需求是什么、当前代码是什么情况、有哪些风险
|
|
144
|
+
- 业务约束:哪些业务规则不能被改坏
|
|
145
|
+
- 测试约定:哪些场景必须验证
|
|
146
|
+
- 实现记录:每个任务怎么验证通过
|
|
147
|
+
- 审查记录:谁检查了什么、发现了什么、最后为什么通过或退回
|
|
148
|
+
|
|
149
|
+
这些记录默认放在:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
openspec/changes/<变更ID>/.superspec/
|
|
122
153
|
```
|
|
123
154
|
|
|
124
|
-
|
|
155
|
+
这里的 `<变更ID>` 就是一次需求变更的名字。
|
|
156
|
+
|
|
157
|
+
`.superspec/` 要不要提交到 git,由你的团队决定。
|
|
158
|
+
如果不提交,删掉后就没有 git 历史可以恢复。
|
|
159
|
+
|
|
160
|
+
## 重要边界
|
|
161
|
+
|
|
162
|
+
SuperSpec 能让流程更规范,但它不是安全锁。
|
|
163
|
+
|
|
164
|
+
它能帮助你:
|
|
165
|
+
|
|
166
|
+
- 减少 AI 还没想清楚就改代码的情况
|
|
167
|
+
- 让测试、审查和用户确认留下记录
|
|
168
|
+
- 在进入下一步前提醒缺少什么
|
|
169
|
+
- 让一次变更之后更容易回看原因
|
|
170
|
+
|
|
171
|
+
它不能保证:
|
|
125
172
|
|
|
126
|
-
|
|
173
|
+
- 阻止人手动绕过流程直接改文件
|
|
174
|
+
- 阻止人删除过程记录
|
|
175
|
+
- 阻止恶意伪造记录
|
|
176
|
+
- 替代正式的安全审计、合规审计或法律证明
|
|
177
|
+
|
|
178
|
+
也就是说,SuperSpec v1 是“流程纪律工具”,不是“强制安全系统”。
|
|
179
|
+
|
|
180
|
+
## 常用命令
|
|
181
|
+
|
|
182
|
+
查看当前 SuperSpec CLI 版本:
|
|
127
183
|
|
|
128
184
|
```bash
|
|
129
|
-
superspec
|
|
185
|
+
superspec --version
|
|
130
186
|
```
|
|
131
187
|
|
|
132
|
-
|
|
188
|
+
安装到当前项目:
|
|
133
189
|
|
|
134
190
|
```bash
|
|
135
|
-
superspec init
|
|
191
|
+
superspec init --scope project
|
|
136
192
|
```
|
|
137
193
|
|
|
138
|
-
|
|
194
|
+
更新当前项目里的 SuperSpec 入口:
|
|
139
195
|
|
|
140
196
|
```bash
|
|
141
|
-
superspec
|
|
197
|
+
superspec update --scope project
|
|
142
198
|
```
|
|
143
199
|
|
|
144
|
-
|
|
145
|
-
如果项目里还没有对应的 OpenSpec 初始化内容,且本机可用 `openspec` CLI,SuperSpec 会自动尝试执行:
|
|
200
|
+
这条命令会先通过 npm 更新全局 `@peterxiaoyang/superspec`,再用新版本更新当前项目里的入口文件。只想使用当前已安装包更新项目文件时,可以运行:
|
|
146
201
|
|
|
147
|
-
|
|
148
|
-
|
|
202
|
+
```bash
|
|
203
|
+
superspec update --scope project --local-only
|
|
204
|
+
```
|
|
149
205
|
|
|
150
|
-
|
|
206
|
+
卸载当前项目里的 SuperSpec 入口:
|
|
151
207
|
|
|
152
|
-
|
|
208
|
+
```bash
|
|
209
|
+
superspec uninstall --scope project
|
|
210
|
+
```
|
|
153
211
|
|
|
154
|
-
|
|
212
|
+
这些命令默认不会删除已经生成的 `.superspec/` 过程记录。
|
|
155
213
|
|
|
156
|
-
|
|
157
|
-
- `superspec-propose`
|
|
158
|
-
- `superspec-apply`
|
|
159
|
-
- `superspec-review`
|
|
160
|
-
- `superspec-archive`
|
|
214
|
+
诊断全局安装、PATH、OpenSpec 依赖和 npm bin 指向问题:
|
|
161
215
|
|
|
162
|
-
|
|
216
|
+
```bash
|
|
217
|
+
superspec doctor
|
|
218
|
+
```
|
|
163
219
|
|
|
164
|
-
|
|
220
|
+
## 进阶信息
|
|
165
221
|
|
|
166
|
-
|
|
222
|
+
以当前内置的 Codex 适配器为例,初始化后项目里会出现这些入口文件:
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
.codex/
|
|
226
|
+
skills/superspec-explore/
|
|
227
|
+
skills/superspec-propose/
|
|
228
|
+
skills/superspec-apply/
|
|
229
|
+
skills/superspec-review/
|
|
230
|
+
skills/superspec-archive/
|
|
231
|
+
```
|
|
167
232
|
|
|
168
|
-
|
|
169
|
-
- `.superspec/artifacts/business-invariants.md`:记录本次变更必须守住的业务不变量
|
|
170
|
-
- `.superspec/artifacts/test-contract.md`:记录测试覆盖矩阵、测试编号和约束映射
|
|
171
|
-
- `.superspec/evidence/...`:保存测试、审查、验证、用户确认等证据文件
|
|
172
|
-
- `.superspec/superspec-state.json`:保存 guard 重算后的状态摘要和指纹
|
|
173
|
-
- `.superspec/ledger.jsonl`:保存关键流程事件,便于审计和追溯
|
|
233
|
+
SuperSpec 内部还有一些检查命令,例如:
|
|
174
234
|
|
|
175
|
-
|
|
235
|
+
```bash
|
|
236
|
+
superspec guard check-init --change <变更ID>
|
|
237
|
+
superspec guard check-apply-ready --change <变更ID>
|
|
238
|
+
superspec guard check-review-ready --change <变更ID>
|
|
239
|
+
superspec guard check-archive-ready --change <变更ID>
|
|
240
|
+
```
|
|
176
241
|
|
|
177
|
-
|
|
178
|
-
- `user` scope
|
|
179
|
-
- 基于清单的 `update`
|
|
180
|
-
- 基于清单的 `uninstall`
|
|
242
|
+
普通使用者通常不需要手动运行这些命令;对应的阶段入口会在需要时使用它们。
|
|
181
243
|
|
|
182
|
-
|
|
244
|
+
如果你要开发 SuperSpec 本身:
|
|
183
245
|
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
→ 更严格,但不过度做重
|
|
246
|
+
```bash
|
|
247
|
+
npm run build
|
|
248
|
+
npm run typecheck
|
|
249
|
+
npm test
|
|
250
|
+
npm run pack:dry-run
|
|
190
251
|
```
|
|
191
252
|
|
|
192
|
-
|
|
253
|
+
更多细节见:
|
|
254
|
+
|
|
255
|
+
- `docs/SPEC.md`:完整设计和规则
|
|
256
|
+
- `docs/DISTRIBUTION.md`:安装、升级、卸载和分发说明
|
|
257
|
+
- `.codex/skills/superspec-*/SKILL.md`:当前 Codex 适配器使用的阶段入口说明
|
|
193
258
|
|
|
194
|
-
|
|
259
|
+
## 致谢与灵感来源
|
|
195
260
|
|
|
196
261
|
- [OpenSpec](https://github.com/Fission-AI/OpenSpec)
|
|
197
262
|
- [oh-my-codex](https://github.com/Yeachan-Heo/oh-my-codex)
|
package/dist/src/core.js
CHANGED
|
@@ -16,12 +16,12 @@ import { relative } from "node:path";
|
|
|
16
16
|
import { GuardError, allow, block, deepEqual, reason, runtime, toPosix, trustWarnings } from "./util.js";
|
|
17
17
|
import { artifact_status_map, gate_route_phase, get_change_root, get_repo_root, openspec_status, openspec_status_shape_reasons, openspec_validate, openspec_version, repo_root_from_cwd, } from "./openspec.js";
|
|
18
18
|
import { config_file, load_config, project_config_file } from "./paths.js";
|
|
19
|
-
import { index_evidence,
|
|
19
|
+
import { index_evidence, live_user_confirmations } from "./evidence.js";
|
|
20
20
|
import { tasks_structure_hash } from "./tasks.js";
|
|
21
21
|
import { dirty_worktree_paths, dirty_worktree_reasons, dirty_write_scope_red_reasons, file_blob_sha, git_lines, review_diff_coverage_reasons, review_diff_paths, } from "./git.js";
|
|
22
22
|
import { append_ledger, load_state, compute_fingerprints, prepare_recomputed_state_write, read_ledger_text, record_supersede_ledger_events, restore_state_snapshot_locked, state_corrupt_reasons, state_file, state_file_corrupt, state_stale_reasons, force_unlock_state, with_state_lock, write_prepared_state_locked, } from "./state.js";
|
|
23
23
|
import { archive_manifest_path, begin_archive_preservation_bundle, check_archived, preset_upgrade_reasons, preset_upgrade_required_from_context, write_archive_preservation_bundle, } from "./archive.js";
|
|
24
|
-
import { check_archive_ready, check_artifact, check_init, check_superspec_gate, check_review_complete, check_review_ready, check_task_complete, check_task_edit, check_task_reopen, check_verify_complete, evidence_schema_guard, superspec_agent_reasons, superspec_workflow_skill_reasons, openspec_cli_capability_reasons, openspec_init_reasons, } from "./gates.js";
|
|
24
|
+
import { check_archive_ready, check_apply_ready, check_artifact, check_init, check_superspec_gate, check_review_complete, check_review_ready, check_task_complete, check_task_edit, check_task_reopen, check_verify_complete, evidence_schema_guard, superspec_agent_reasons, superspec_workflow_skill_reasons, openspec_cli_capability_reasons, openspec_init_reasons, } from "./gates.js";
|
|
25
25
|
const RETRY_WAIT = new Int32Array(new SharedArrayBuffer(4));
|
|
26
26
|
const DEFAULT_MAX_STATE_WRITE_RETRIES = 5;
|
|
27
27
|
function sleep_ms(ms) {
|
|
@@ -134,7 +134,7 @@ function dispatch_once(args) {
|
|
|
134
134
|
if (cmd !== "recompute") {
|
|
135
135
|
const changedPaths = String(config.preset ?? "full") !== "full" ? runtime.dirty_worktree_paths(repoRoot) : [];
|
|
136
136
|
presetRequired = preset_upgrade_required_from_context(config, changedPaths);
|
|
137
|
-
const presetHumanConfirmed =
|
|
137
|
+
const presetHumanConfirmed = live_user_confirmations(evidences, "preset_upgrade").length > 0;
|
|
138
138
|
presetProblems = preset_upgrade_reasons(config, changedPaths, presetHumanConfirmed);
|
|
139
139
|
if (cmd !== "init")
|
|
140
140
|
staleProblems = state_stale_reasons(changeRoot, status);
|
|
@@ -171,7 +171,7 @@ function dispatch_once(args) {
|
|
|
171
171
|
route = gate_route_phase(args.gate ?? "");
|
|
172
172
|
}
|
|
173
173
|
else if (cmd === "check-apply-ready") {
|
|
174
|
-
dec =
|
|
174
|
+
dec = check_apply_ready(change, status, changeRoot, evidences);
|
|
175
175
|
route = "propose";
|
|
176
176
|
}
|
|
177
177
|
else if (cmd === "check-task-edit") {
|
|
@@ -230,7 +230,7 @@ function dispatch_once(args) {
|
|
|
230
230
|
const lockedShapeProblems = openspec_status_shape_reasons(lockedStatus);
|
|
231
231
|
const lockedChangedPaths = String(lockedConfig.preset ?? "full") !== "full" ? runtime.dirty_worktree_paths(lockedRepoRoot) : [];
|
|
232
232
|
const lockedPresetRequired = preset_upgrade_required_from_context(lockedConfig, lockedChangedPaths);
|
|
233
|
-
const lockedPresetHumanConfirmed =
|
|
233
|
+
const lockedPresetHumanConfirmed = live_user_confirmations(lockedEvidences, "preset_upgrade").length > 0;
|
|
234
234
|
const lockedPresetProblems = preset_upgrade_reasons(lockedConfig, lockedChangedPaths, lockedPresetHumanConfirmed);
|
|
235
235
|
const lockedStaleProblems = state_stale_reasons(lockedChangeRoot, lockedStatus);
|
|
236
236
|
const lockedCorruptProblems = state_corrupt_reasons(lockedChangeRoot);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type JsonMap } from "./util.ts";
|
|
2
|
+
type RunResult = {
|
|
3
|
+
status: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
error?: Error;
|
|
7
|
+
};
|
|
8
|
+
type RunFn = (cmd: string, args: string[], opts?: {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
platform?: NodeJS.Platform;
|
|
12
|
+
}) => RunResult;
|
|
13
|
+
type CommandExistsFn = (cmd: string, meta?: {
|
|
14
|
+
cwd?: string;
|
|
15
|
+
}) => boolean;
|
|
16
|
+
type CheckStatus = "ok" | "warn" | "fail";
|
|
17
|
+
export type DoctorCheck = {
|
|
18
|
+
status: CheckStatus;
|
|
19
|
+
name: string;
|
|
20
|
+
detail: string;
|
|
21
|
+
refs?: string[];
|
|
22
|
+
};
|
|
23
|
+
export type DoctorReport = {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
cwd: string;
|
|
26
|
+
superspec: JsonMap;
|
|
27
|
+
node: JsonMap;
|
|
28
|
+
npm: JsonMap;
|
|
29
|
+
openspec: JsonMap;
|
|
30
|
+
checks: DoctorCheck[];
|
|
31
|
+
next_actions: string[];
|
|
32
|
+
};
|
|
33
|
+
export declare function superspec_package_version(packageRoot?: string): string;
|
|
34
|
+
export declare function build_doctor_report(opts?: {
|
|
35
|
+
cwd?: string;
|
|
36
|
+
packageRoot?: string;
|
|
37
|
+
argv0?: string;
|
|
38
|
+
platform?: NodeJS.Platform;
|
|
39
|
+
commandExistsFn?: CommandExistsFn;
|
|
40
|
+
run?: RunFn;
|
|
41
|
+
}): DoctorReport;
|
|
42
|
+
export declare function render_doctor_report(report: DoctorReport): string;
|
|
43
|
+
export declare function main_doctor(argv?: string[]): number;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { PACKAGE_ROOT } from "./install_engine.js";
|
|
4
|
+
import { openspec_cli_probe } from "./openspec.js";
|
|
5
|
+
import { commandExists, runCommand } from "./util.js";
|
|
6
|
+
function readPackageJson(packageRoot) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
9
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function superspec_package_version(packageRoot = PACKAGE_ROOT) {
|
|
16
|
+
const version = readPackageJson(packageRoot).version;
|
|
17
|
+
return typeof version === "string" && version ? version : "0.0.0";
|
|
18
|
+
}
|
|
19
|
+
function realpathMaybe(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
return realpathSync(filePath);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return resolve(filePath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function pathInfo(filePath) {
|
|
28
|
+
if (!filePath)
|
|
29
|
+
return null;
|
|
30
|
+
const info = { path: filePath, exists: existsSync(filePath) };
|
|
31
|
+
if (!info.exists)
|
|
32
|
+
return info;
|
|
33
|
+
try {
|
|
34
|
+
const stat = lstatSync(filePath);
|
|
35
|
+
info.kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : stat.isFile() ? "file" : "other";
|
|
36
|
+
if (stat.isSymbolicLink())
|
|
37
|
+
info.link_target = readlinkSync(filePath);
|
|
38
|
+
info.realpath = realpathMaybe(filePath);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
info.error = err.message;
|
|
42
|
+
}
|
|
43
|
+
return info;
|
|
44
|
+
}
|
|
45
|
+
function firstNonEmptyLine(text) {
|
|
46
|
+
return text.split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0) ?? null;
|
|
47
|
+
}
|
|
48
|
+
function commandPath(cmd, opts) {
|
|
49
|
+
if (!opts.commandExistsFn(cmd, { cwd: opts.cwd }))
|
|
50
|
+
return null;
|
|
51
|
+
const proc = opts.platform === "win32"
|
|
52
|
+
? opts.run("where.exe", [cmd], { cwd: opts.cwd, timeout: 15_000, platform: opts.platform })
|
|
53
|
+
: opts.run("sh", ["-c", `command -v ${cmd}`], { cwd: opts.cwd, timeout: 15_000, platform: opts.platform });
|
|
54
|
+
if (proc.error || proc.status !== 0)
|
|
55
|
+
return null;
|
|
56
|
+
return firstNonEmptyLine(proc.stdout);
|
|
57
|
+
}
|
|
58
|
+
function npmValue(args, opts) {
|
|
59
|
+
if (!opts.commandExistsFn("npm", { cwd: opts.cwd }))
|
|
60
|
+
return { available: false, value: null, error: "npm not found on PATH" };
|
|
61
|
+
const proc = opts.run("npm", args, { cwd: opts.cwd, timeout: 15_000, platform: opts.platform });
|
|
62
|
+
if (proc.error || proc.status !== 0) {
|
|
63
|
+
return {
|
|
64
|
+
available: true,
|
|
65
|
+
value: null,
|
|
66
|
+
error: (proc.error?.message ?? (proc.stderr || proc.stdout)).trim(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { available: true, value: firstNonEmptyLine(proc.stdout), error: null };
|
|
70
|
+
}
|
|
71
|
+
function containsUnscopedSuperspecNodeModule(value) {
|
|
72
|
+
if (!value)
|
|
73
|
+
return false;
|
|
74
|
+
return /[/\\]node_modules[/\\]superspec[/\\]/u.test(value);
|
|
75
|
+
}
|
|
76
|
+
function add(checks, status, name, detail, refs) {
|
|
77
|
+
checks.push({ status, name, detail, refs });
|
|
78
|
+
}
|
|
79
|
+
export function build_doctor_report(opts = {}) {
|
|
80
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
81
|
+
const packageRoot = opts.packageRoot ?? PACKAGE_ROOT;
|
|
82
|
+
const platform = opts.platform ?? process.platform;
|
|
83
|
+
const run = opts.run ?? runCommand;
|
|
84
|
+
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd, platform }));
|
|
85
|
+
const packageJson = readPackageJson(packageRoot);
|
|
86
|
+
const version = superspec_package_version(packageRoot);
|
|
87
|
+
const packageName = typeof packageJson.name === "string" ? packageJson.name : "unknown";
|
|
88
|
+
const expectedBin = join(packageRoot, "bin", "superspec.js");
|
|
89
|
+
const resolvedCommand = commandPath("superspec", { cwd, platform, commandExistsFn, run });
|
|
90
|
+
const command = pathInfo(resolvedCommand);
|
|
91
|
+
const prefix = npmValue(["prefix", "-g"], { cwd, platform, commandExistsFn, run });
|
|
92
|
+
const root = npmValue(["root", "-g"], { cwd, platform, commandExistsFn, run });
|
|
93
|
+
const npmRoot = typeof root.value === "string" ? root.value : null;
|
|
94
|
+
const scopedPackage = npmRoot ? pathInfo(join(npmRoot, "@peterxiaoyang", "superspec")) : null;
|
|
95
|
+
const unscopedPackage = npmRoot ? pathInfo(join(npmRoot, "superspec")) : null;
|
|
96
|
+
const openspecProbe = openspec_cli_probe({ cwd, commandExistsFn, run });
|
|
97
|
+
const checks = [];
|
|
98
|
+
const nextActions = [];
|
|
99
|
+
if (packageName === "@peterxiaoyang/superspec" && version !== "0.0.0") {
|
|
100
|
+
add(checks, "ok", "package metadata", `${packageName}@${version}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
add(checks, "fail", "package metadata", `could not identify @peterxiaoyang/superspec package metadata under ${packageRoot}`);
|
|
104
|
+
}
|
|
105
|
+
if (openspecProbe.ok) {
|
|
106
|
+
add(checks, "ok", "OpenSpec CLI", openspecProbe.message);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
add(checks, "fail", "OpenSpec CLI", openspecProbe.message);
|
|
110
|
+
nextActions.push("npm install -g @fission-ai/openspec@latest");
|
|
111
|
+
}
|
|
112
|
+
if (!command) {
|
|
113
|
+
add(checks, "warn", "superspec command", "`superspec` is not resolvable from PATH in this shell");
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const commandRealpath = typeof command.realpath === "string" ? command.realpath : undefined;
|
|
117
|
+
const commandTarget = typeof command.link_target === "string" ? command.link_target : undefined;
|
|
118
|
+
if (containsUnscopedSuperspecNodeModule(commandRealpath) || containsUnscopedSuperspecNodeModule(commandTarget)) {
|
|
119
|
+
add(checks, "fail", "superspec command owner", "`superspec` points at the unscoped `superspec` package, which conflicts with @peterxiaoyang/superspec", [String(command.path)]);
|
|
120
|
+
nextActions.push("npm uninstall -g superspec");
|
|
121
|
+
nextActions.push("npm install -g @peterxiaoyang/superspec@latest");
|
|
122
|
+
}
|
|
123
|
+
else if (existsSync(expectedBin) && commandRealpath && realpathMaybe(expectedBin) !== commandRealpath) {
|
|
124
|
+
add(checks, "warn", "superspec command owner", "`superspec` on PATH does not point at this package root", [String(command.path), expectedBin]);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
add(checks, "ok", "superspec command owner", "`superspec` resolves to this package");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (root.error) {
|
|
131
|
+
add(checks, "warn", "npm global root", String(root.error));
|
|
132
|
+
}
|
|
133
|
+
else if (npmRoot) {
|
|
134
|
+
add(checks, "ok", "npm global root", npmRoot);
|
|
135
|
+
}
|
|
136
|
+
if (unscopedPackage?.exists) {
|
|
137
|
+
add(checks, "warn", "unscoped superspec package", "global package `superspec` is installed and can claim the same `superspec` binary", [String(unscopedPackage.path)]);
|
|
138
|
+
nextActions.push("npm uninstall -g superspec");
|
|
139
|
+
}
|
|
140
|
+
if (scopedPackage && !scopedPackage.exists) {
|
|
141
|
+
add(checks, "warn", "scoped superspec package", "@peterxiaoyang/superspec is not present under npm global root", [String(scopedPackage.path)]);
|
|
142
|
+
}
|
|
143
|
+
const dedupedNextActions = [...new Set(nextActions)];
|
|
144
|
+
return {
|
|
145
|
+
ok: !checks.some((check) => check.status === "fail"),
|
|
146
|
+
cwd,
|
|
147
|
+
superspec: {
|
|
148
|
+
name: packageName,
|
|
149
|
+
version,
|
|
150
|
+
package_root: packageRoot,
|
|
151
|
+
expected_bin: expectedBin,
|
|
152
|
+
entry: opts.argv0 ?? process.argv[1] ?? null,
|
|
153
|
+
command,
|
|
154
|
+
scoped_global_package: scopedPackage,
|
|
155
|
+
unscoped_global_package: unscopedPackage,
|
|
156
|
+
},
|
|
157
|
+
node: {
|
|
158
|
+
version: process.versions.node,
|
|
159
|
+
exec_path: process.execPath,
|
|
160
|
+
platform,
|
|
161
|
+
},
|
|
162
|
+
npm: {
|
|
163
|
+
prefix,
|
|
164
|
+
root,
|
|
165
|
+
},
|
|
166
|
+
openspec: {
|
|
167
|
+
ok: openspecProbe.ok,
|
|
168
|
+
state: openspecProbe.state,
|
|
169
|
+
version: openspecProbe.version,
|
|
170
|
+
message: openspecProbe.message,
|
|
171
|
+
},
|
|
172
|
+
checks,
|
|
173
|
+
next_actions: dedupedNextActions,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function render_doctor_report(report) {
|
|
177
|
+
const lines = [
|
|
178
|
+
"SuperSpec doctor",
|
|
179
|
+
"",
|
|
180
|
+
`SuperSpec: ${report.superspec.name}@${report.superspec.version}`,
|
|
181
|
+
`Package root: ${report.superspec.package_root}`,
|
|
182
|
+
`Command path: ${report.superspec.command?.path ?? "not found"}`,
|
|
183
|
+
`OpenSpec: ${report.openspec.message}`,
|
|
184
|
+
`Node: ${report.node.version} (${report.node.platform})`,
|
|
185
|
+
`npm prefix: ${report.npm.prefix.value ?? "unknown"}`,
|
|
186
|
+
`npm root: ${report.npm.root.value ?? "unknown"}`,
|
|
187
|
+
"",
|
|
188
|
+
"Checks:",
|
|
189
|
+
...report.checks.map((check) => {
|
|
190
|
+
const refs = check.refs && check.refs.length > 0 ? ` (${check.refs.join(", ")})` : "";
|
|
191
|
+
return ` [${check.status}] ${check.name}: ${check.detail}${refs}`;
|
|
192
|
+
}),
|
|
193
|
+
];
|
|
194
|
+
if (report.next_actions.length > 0) {
|
|
195
|
+
lines.push("", "Next actions:", ...report.next_actions.map((action) => ` ${action}`));
|
|
196
|
+
}
|
|
197
|
+
lines.push("");
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
function doctorHelp() {
|
|
201
|
+
return [
|
|
202
|
+
"usage: superspec doctor [--json]",
|
|
203
|
+
"",
|
|
204
|
+
"diagnoses SuperSpec, OpenSpec, npm global package, and PATH wiring.",
|
|
205
|
+
"",
|
|
206
|
+
"options:",
|
|
207
|
+
" --json print machine-readable JSON",
|
|
208
|
+
" -h, --help show this help",
|
|
209
|
+
"",
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
212
|
+
export function main_doctor(argv = process.argv.slice(2)) {
|
|
213
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
214
|
+
process.stdout.write(doctorHelp());
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
const unknown = argv.filter((arg) => arg !== "--json");
|
|
218
|
+
if (unknown.length > 0) {
|
|
219
|
+
process.stderr.write(`superspec doctor: unknown option ${JSON.stringify(unknown[0])}\n\n${doctorHelp()}`);
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
const report = build_doctor_report();
|
|
223
|
+
if (argv.includes("--json")) {
|
|
224
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
process.stdout.write(render_doctor_report(report));
|
|
228
|
+
}
|
|
229
|
+
return report.ok ? 0 : 1;
|
|
230
|
+
}
|
package/dist/src/evidence.d.ts
CHANGED
package/dist/src/evidence.js
CHANGED
|
@@ -272,6 +272,9 @@ function finding_string_list_reasons(ev, finding, field, opts = {}) {
|
|
|
272
272
|
function human_confirmation_reasons(ev) {
|
|
273
273
|
const problems = [];
|
|
274
274
|
const gate = normalize_gate(String(ev.gate ?? ""));
|
|
275
|
+
if (String(ev.created_by ?? "") !== "user") {
|
|
276
|
+
problems.push(reason("human_confirmation_invalid", `${ev._path}: human_confirmation must be created_by user`));
|
|
277
|
+
}
|
|
275
278
|
if (!HUMAN_CONFIRMATION_GATES.has(gate)) {
|
|
276
279
|
problems.push(reason("human_confirmation_invalid", `${ev._path}: human_confirmation gate=${repr(ev.gate)} is not consumed by any guard gate; expected one of ${renderList([...HUMAN_CONFIRMATION_GATES].sort())}`));
|
|
277
280
|
}
|
|
@@ -847,3 +850,7 @@ export function live_pass(evidences, filters = {}) {
|
|
|
847
850
|
const dead = superseded_ids(evidences);
|
|
848
851
|
return find_pass(evidences, filters).filter((ev) => !dead.has(ev.evidence_id));
|
|
849
852
|
}
|
|
853
|
+
export function live_user_confirmations(evidences, gate) {
|
|
854
|
+
return live_pass(evidences, { gate, kind: "human_confirmation" })
|
|
855
|
+
.filter((ev) => String(ev.created_by ?? "") === "user");
|
|
856
|
+
}
|
package/dist/src/gates.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare function check_init(change: string, status: JsonMap, repoRoot: st
|
|
|
8
8
|
export declare function check_superspec_gate(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], gateRaw: string): Decision;
|
|
9
9
|
export declare function check_artifact(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], artifact: string): Decision;
|
|
10
10
|
export declare function check_task_reopen(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
|
|
11
|
+
export declare function check_apply_ready(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[]): Decision;
|
|
11
12
|
export declare function check_task_edit(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
|
|
12
13
|
export declare function check_task_complete(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
|
|
13
14
|
export declare function check_review_ready(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[]): Decision;
|
package/dist/src/gates.js
CHANGED
|
@@ -5,7 +5,7 @@ import { all_done, artifact_status_map, get_repo_root, is_done, normalize_gate,
|
|
|
5
5
|
import { read_agent_toml_name, read_skill_frontmatter_name, sidecar_business_invariants_path, sidecar_discovery_path, sidecar_test_contract_path } from "./paths.js";
|
|
6
6
|
import { business_invariant_ids, business_invariant_validation_reasons, automated_hard_business_invariant_ids, evidence_invariant_refs, evidence_invariant_ref_reasons, evidence_test_contract_invariant_reasons, human_confirmation_business_invariant_ids, invariant_matrix_coverage_reasons, post_implementation_business_invariant_ids, red_green_invariant_ids, test_contract_invariant_ids, } from "./invariants.js";
|
|
7
7
|
import { evidence_test_id_reasons, declared_test_evidence_reasons, parse_spec_scenarios, parse_tasks, parse_test_contract_ids, parse_test_contract_records, red_green_test_ids, splitList, tasks_structure_hash, task_alternative_verification, task_test_evidence, task_test_refs, test_contract_covers_scenario, test_contract_invariant_refs_by_test, write_scope_conflict_reasons, } from "./tasks.js";
|
|
8
|
-
import { duplicate_evidence_id_reasons, dangling_evidence_ref_reasons, final_verification_evidences, live_task_reopens, live_task_reopen_resolutions, live_pass, pass_task_reopens, supersede_reasons, unresolved_live_task_reopens, validate_evidence_schema, verify_reference_reasons, } from "./evidence.js";
|
|
8
|
+
import { duplicate_evidence_id_reasons, dangling_evidence_ref_reasons, final_verification_evidences, live_task_reopens, live_task_reopen_resolutions, live_pass, live_user_confirmations, pass_task_reopens, supersede_reasons, unresolved_live_task_reopens, validate_evidence_schema, verify_reference_reasons, } from "./evidence.js";
|
|
9
9
|
import { review_disclosure_reasons } from "./disclosure.js";
|
|
10
10
|
import { archive_manifest_path } from "./archive.js";
|
|
11
11
|
function action_list(...items) {
|
|
@@ -311,7 +311,7 @@ function archive_ready_actions(change, reasons, review) {
|
|
|
311
311
|
function default_gate_next_actions(gate) {
|
|
312
312
|
switch (gate) {
|
|
313
313
|
case "explore_complete":
|
|
314
|
-
return ["write .superspec/artifacts/discovery.md
|
|
314
|
+
return ["write .superspec/artifacts/discovery.md, record native_subagent critic evidence, and record explore_complete human confirmation"];
|
|
315
315
|
case "proposal_reviewed":
|
|
316
316
|
return ["run the proposal critic review (round-tagged, findings[]) and record a main_review_digest disclosing every finding"];
|
|
317
317
|
case "design_complete":
|
|
@@ -478,6 +478,9 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
478
478
|
reasons.push(reason("missing_native_subagent_evidence", `explore_complete requires native_subagent ${role} report`));
|
|
479
479
|
}
|
|
480
480
|
reasons.push(...stale_artifact_review_reasons(exploreReviews, changeRoot, ".superspec/artifacts/discovery.md", "stale_explore_review", "explore_complete"));
|
|
481
|
+
if (live_user_confirmations(evidences, "explore_complete").length === 0) {
|
|
482
|
+
reasons.push(reason("missing_human_confirmation", "explore_complete requires human confirmation before entering propose"));
|
|
483
|
+
}
|
|
481
484
|
// DISC Phase 1: material findings raised by explore reviews must be disclosed to the user
|
|
482
485
|
// (main_review_digest + user_review_decision) before the gate can pass.
|
|
483
486
|
reasons.push(...review_disclosure_reasons("explore_complete", changeRoot, evidences));
|
|
@@ -510,7 +513,7 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
510
513
|
if (!designReviews.some((ev) => ev.agent_role === role))
|
|
511
514
|
reasons.push(reason(`missing_${role}_review`, `design_complete requires native_subagent ${role} report`));
|
|
512
515
|
}
|
|
513
|
-
if (
|
|
516
|
+
if (live_user_confirmations(evidences, "design_complete").length === 0)
|
|
514
517
|
reasons.push(reason("missing_human_confirmation", "design_complete requires human confirmation"));
|
|
515
518
|
reasons.push(...stale_artifact_review_reasons(designReviews, changeRoot, "design.md", "stale_design_review", "design_complete"));
|
|
516
519
|
// DISC Phase 2: design reviews carrying round-tagged findings enter the disclosure loop
|
|
@@ -536,7 +539,7 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
536
539
|
const humanRequired = human_confirmation_business_invariant_ids(changeRoot);
|
|
537
540
|
if (humanRequired.size > 0) {
|
|
538
541
|
const confirmed = new Set();
|
|
539
|
-
for (const ev of
|
|
542
|
+
for (const ev of live_user_confirmations(evidences, "invariants_reviewed")) {
|
|
540
543
|
for (const id of evidence_invariant_refs(ev))
|
|
541
544
|
confirmed.add(id);
|
|
542
545
|
}
|
|
@@ -654,6 +657,9 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
654
657
|
}
|
|
655
658
|
}
|
|
656
659
|
}
|
|
660
|
+
else if (gate === "apply_ready") {
|
|
661
|
+
return check_apply_ready(change, status, changeRoot, evidences);
|
|
662
|
+
}
|
|
657
663
|
else {
|
|
658
664
|
return block(change, gate, [reason("unknown_gate", `unknown superspec gate: ${gate}`)]);
|
|
659
665
|
}
|
|
@@ -1029,6 +1035,7 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1029
1035
|
reasons.push(reason("propose_not_complete", "task_reopen requires propose_complete"));
|
|
1030
1036
|
reasons.push(...propose.block_reasons);
|
|
1031
1037
|
}
|
|
1038
|
+
reasons.push(...apply_scope_confirmation_reasons(changeRoot, evidences));
|
|
1032
1039
|
const tasks = parse_tasks(changeRoot);
|
|
1033
1040
|
const task = tasks[taskId];
|
|
1034
1041
|
if (!task)
|
|
@@ -1038,8 +1045,13 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1038
1045
|
reasons.push(...request_changes_round_reasons(evidences));
|
|
1039
1046
|
const reopenCheck = active_task_reopen_reasons(changeRoot, evidences, task, taskId, "pre_revert");
|
|
1040
1047
|
reasons.push(...reopenCheck.reasons);
|
|
1041
|
-
if (reasons.length > 0)
|
|
1042
|
-
|
|
1048
|
+
if (reasons.length > 0) {
|
|
1049
|
+
const reasonSet = reason_codes(reasons);
|
|
1050
|
+
return block(change, gate, reasons, {
|
|
1051
|
+
task_id: taskId,
|
|
1052
|
+
next_actions: action_list(reasonSet.has("apply_isolation_unconfirmed") ? "AskUserQuestion for apply isolation/execution mode and record gate=\"apply_isolation\" human_confirmation" : null, reasonSet.has("scope_expansion_unconfirmed") ? "stop: redesign/split the change or record gate=\"scope_expansion\" human_confirmation re-approving tasks.md structure" : null, `keep ${taskId} checked, fix task_reopen evidence / supersedes, then rerun check-task-reopen`),
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1043
1055
|
return allow(change, gate, { task_id: taskId });
|
|
1044
1056
|
}
|
|
1045
1057
|
// FIX-8 (audit A-5): apply work requires the user's explicit isolation/execution-mode choice,
|
|
@@ -1048,7 +1060,7 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1048
1060
|
// apply-phase scope expansion (SPEC §14.7) and demands explicit user re-approval — redesign,
|
|
1049
1061
|
// split into a new change, or record a scope_expansion confirmation re-pinning the structure.
|
|
1050
1062
|
function apply_scope_confirmation_reasons(changeRoot, evidences) {
|
|
1051
|
-
const isolation =
|
|
1063
|
+
const isolation = live_user_confirmations(evidences, "apply_isolation");
|
|
1052
1064
|
const currentHash = tasks_structure_hash(changeRoot);
|
|
1053
1065
|
if (isolation.length === 0) {
|
|
1054
1066
|
const hashHint = currentHash ? ` with tasks_structure_hash=${currentHash}` : "";
|
|
@@ -1056,11 +1068,28 @@ function apply_scope_confirmation_reasons(changeRoot, evidences) {
|
|
|
1056
1068
|
}
|
|
1057
1069
|
if (currentHash === null)
|
|
1058
1070
|
return [];
|
|
1059
|
-
const approvals = [...isolation, ...
|
|
1071
|
+
const approvals = [...isolation, ...live_user_confirmations(evidences, "scope_expansion")];
|
|
1060
1072
|
if (approvals.some((ev) => String(ev.tasks_structure_hash ?? "") === currentHash))
|
|
1061
1073
|
return [];
|
|
1062
1074
|
return [reason("scope_expansion_unconfirmed", `tasks.md structure changed after the last user-approved apply scope; ask the user to redesign/split the change or re-approve by recording gate="scope_expansion" human_confirmation with tasks_structure_hash=${currentHash}`)];
|
|
1063
1075
|
}
|
|
1076
|
+
export function check_apply_ready(change, status, changeRoot, evidences) {
|
|
1077
|
+
const gate = "apply_ready";
|
|
1078
|
+
const reasons = [];
|
|
1079
|
+
const propose = check_superspec_gate(change, status, changeRoot, evidences, "propose_complete");
|
|
1080
|
+
if (!propose.allowed) {
|
|
1081
|
+
reasons.push(reason("propose_not_complete", "apply_ready requires propose_complete"));
|
|
1082
|
+
reasons.push(...propose.block_reasons);
|
|
1083
|
+
}
|
|
1084
|
+
reasons.push(...apply_scope_confirmation_reasons(changeRoot, evidences));
|
|
1085
|
+
if (reasons.length > 0) {
|
|
1086
|
+
const reasonSet = reason_codes(reasons);
|
|
1087
|
+
return block(change, gate, reasons, {
|
|
1088
|
+
next_actions: action_list(!propose.allowed ? "pass propose_complete before apply" : null, reasonSet.has("apply_isolation_unconfirmed") ? "AskUserQuestion for apply isolation/execution mode and record gate=\"apply_isolation\" human_confirmation" : null, reasonSet.has("scope_expansion_unconfirmed") ? "stop: redesign/split the change or record gate=\"scope_expansion\" human_confirmation re-approving tasks.md structure" : null),
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
return allow(change, gate, { openspec_summary: artifact_status_map(status) });
|
|
1092
|
+
}
|
|
1064
1093
|
export function check_task_edit(change, status, changeRoot, evidences, taskId) {
|
|
1065
1094
|
const gate = "task_edit";
|
|
1066
1095
|
const reasons = [];
|
|
@@ -1378,7 +1407,7 @@ export function check_review_complete(change, status, changeRoot, evidences) {
|
|
|
1378
1407
|
&& (ev.kind === "verification_review" || ev.kind === "final_test")
|
|
1379
1408
|
&& ev.status === "fail");
|
|
1380
1409
|
if (failedVerifications.length > 0) {
|
|
1381
|
-
const dispositions = new Set(
|
|
1410
|
+
const dispositions = new Set(live_user_confirmations(evidences, "verify_failure_handling")
|
|
1382
1411
|
.flatMap((ev) => (Array.isArray(ev.confirmed_refs) ? ev.confirmed_refs.map((item) => String(item)) : [])));
|
|
1383
1412
|
const unhandled = failedVerifications
|
|
1384
1413
|
.map((ev) => String(ev.evidence_id ?? ev._path ?? "unknown"))
|
|
@@ -1456,7 +1485,7 @@ export function check_archive_ready(change, status, changeRoot, evidences) {
|
|
|
1456
1485
|
if (!ok && !reviewCodes.has("validate_failed")) {
|
|
1457
1486
|
reasons.push(reason("validate_failed", "openspec validate did not pass"));
|
|
1458
1487
|
}
|
|
1459
|
-
if (
|
|
1488
|
+
if (live_user_confirmations(evidences, "archive_ready").length === 0) {
|
|
1460
1489
|
reasons.push(reason("missing_final_confirmation", "archive_ready requires human confirmation evidence"));
|
|
1461
1490
|
}
|
|
1462
1491
|
if (reasons.length > 0)
|
package/dist/src/git.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { dirname, isAbsolute, relative } from "node:path";
|
|
3
3
|
import { GuardError, reason, renderList, runCommand, runtime } from "./util.js";
|
|
4
4
|
import { splitList, task_test_evidence } from "./tasks.js";
|
|
5
|
-
import {
|
|
5
|
+
import { live_user_confirmations } from "./evidence.js";
|
|
6
6
|
export function file_blob_sha(filePath) {
|
|
7
7
|
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
8
8
|
throw new GuardError(`git_blob_inspection_failure: reviewed target missing: ${filePath}`);
|
|
@@ -50,7 +50,7 @@ export function dirty_worktree_reasons(repoRoot, changeRoot, evidences) {
|
|
|
50
50
|
const unknown = dirty.filter((item) => !(changeRel && item.startsWith(`${changeRel}/`)));
|
|
51
51
|
if (unknown.length === 0)
|
|
52
52
|
return [];
|
|
53
|
-
const confirmations =
|
|
53
|
+
const confirmations = live_user_confirmations(evidences, "branch_handling");
|
|
54
54
|
const confirmedScopes = confirmations.flatMap((ev) => (Array.isArray(ev.confirmed_paths) ? ev.confirmed_paths.map((item) => String(item)).filter(Boolean) : []));
|
|
55
55
|
const unconfirmed = unknown.filter((item) => !confirmedScopes.some((scope) => pathInScope(item, scope)));
|
|
56
56
|
if (unconfirmed.length === 0)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runCommand } from "./util.ts";
|
|
2
|
+
type RunFn = typeof runCommand;
|
|
3
|
+
type CommandExistsFn = (cmd: string, meta?: {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
}) => boolean;
|
|
6
|
+
export declare function update_self_then_rerun(opts: {
|
|
7
|
+
args: string[];
|
|
8
|
+
cwd?: string;
|
|
9
|
+
run?: RunFn;
|
|
10
|
+
commandExistsFn?: CommandExistsFn;
|
|
11
|
+
writeStdout?: (text: string) => void;
|
|
12
|
+
writeStderr?: (text: string) => void;
|
|
13
|
+
}): number;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { commandExists, runCommand } from "./util.js";
|
|
2
|
+
const SUPERSPEC_NPM_PACKAGE = "@peterxiaoyang/superspec";
|
|
3
|
+
function renderCommand(cmd, args) {
|
|
4
|
+
return [cmd, ...args].join(" ");
|
|
5
|
+
}
|
|
6
|
+
function commandFailure(proc) {
|
|
7
|
+
const output = (proc.error?.message ?? (proc.stderr || proc.stdout)).trim();
|
|
8
|
+
if (output)
|
|
9
|
+
return output;
|
|
10
|
+
return proc.status === null ? "command failed" : `command exited with status ${proc.status}`;
|
|
11
|
+
}
|
|
12
|
+
function shouldRetrySuperSpecInstallWithForce(proc) {
|
|
13
|
+
const output = `${proc.error?.message ?? ""}\n${proc.stderr}\n${proc.stdout}`;
|
|
14
|
+
const binConflict = /\bEEXIST\b|already exists|file exists|Refusing to delete|will not overwrite|would overwrite/iu.test(output);
|
|
15
|
+
const superspecBin = /\bsuperspec(?:\.(?:cmd|ps1))?\b/iu.test(output);
|
|
16
|
+
return binConflict && superspecBin;
|
|
17
|
+
}
|
|
18
|
+
export function update_self_then_rerun(opts) {
|
|
19
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
20
|
+
const run = opts.run ?? runCommand;
|
|
21
|
+
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
|
|
22
|
+
const writeStdout = opts.writeStdout ?? ((text) => process.stdout.write(text));
|
|
23
|
+
const writeStderr = opts.writeStderr ?? ((text) => process.stderr.write(text));
|
|
24
|
+
if (!commandExistsFn("npm", { cwd })) {
|
|
25
|
+
writeStderr("SuperSpec update requires npm on PATH to install @peterxiaoyang/superspec@latest. Use `superspec update --local-only` to update surfaces from the currently installed package.\n");
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const installArgs = ["install", "-g", `${SUPERSPEC_NPM_PACKAGE}@latest`];
|
|
29
|
+
writeStderr(`Updating SuperSpec CLI: ${renderCommand("npm", installArgs)}\n`);
|
|
30
|
+
let install = run("npm", installArgs, { cwd, timeout: 300_000 });
|
|
31
|
+
if ((install.error || install.status !== 0) && shouldRetrySuperSpecInstallWithForce(install)) {
|
|
32
|
+
const forcedArgs = ["install", "-g", "--force", `${SUPERSPEC_NPM_PACKAGE}@latest`];
|
|
33
|
+
writeStderr(`SuperSpec CLI install hit a global bin conflict; retrying with: ${renderCommand("npm", forcedArgs)}\n`);
|
|
34
|
+
install = run("npm", forcedArgs, { cwd, timeout: 300_000 });
|
|
35
|
+
}
|
|
36
|
+
if (install.error || install.status !== 0) {
|
|
37
|
+
writeStderr(`SuperSpec CLI update failed: ${commandFailure(install)}\n`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
if (!commandExistsFn("superspec", { cwd })) {
|
|
41
|
+
writeStderr("SuperSpec CLI update completed, but `superspec` is not resolvable from PATH. Reopen the shell or check npm global bin configuration.\n");
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
const rerunArgs = ["update", ...opts.args, "--skip-self-update"];
|
|
45
|
+
const rerun = run("superspec", rerunArgs, { cwd, timeout: 300_000 });
|
|
46
|
+
if (rerun.stdout)
|
|
47
|
+
writeStdout(rerun.stdout);
|
|
48
|
+
if (rerun.stderr)
|
|
49
|
+
writeStderr(rerun.stderr);
|
|
50
|
+
if (rerun.error || rerun.status !== 0) {
|
|
51
|
+
if (rerun.error && !rerun.stderr)
|
|
52
|
+
writeStderr(`SuperSpec update rerun failed: ${rerun.error.message}\n`);
|
|
53
|
+
return rerun.status ?? 1;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
package/dist/src/util.js
CHANGED
|
@@ -78,6 +78,7 @@ export const EVIDENCE_KINDS = new Set([
|
|
|
78
78
|
// FIX-8 (audit A-5) adds the previously anchor-less human pause points:
|
|
79
79
|
// apply isolation choice, apply-phase scope expansion, and verify-failure disposition.
|
|
80
80
|
export const HUMAN_CONFIRMATION_GATES = new Set([
|
|
81
|
+
"explore_complete",
|
|
81
82
|
"design_complete",
|
|
82
83
|
"invariants_reviewed",
|
|
83
84
|
"archive_ready",
|
|
@@ -215,8 +216,8 @@ export const GATE_ALIASES = {
|
|
|
215
216
|
"propose.invariants_reviewed": "invariants_reviewed",
|
|
216
217
|
"propose.test_plan_drafted": "test_contract_drafted",
|
|
217
218
|
"propose.tasks_mapped": "tasks_complete",
|
|
218
|
-
"propose.apply_ready": "
|
|
219
|
-
apply_ready: "
|
|
219
|
+
"propose.apply_ready": "apply_ready",
|
|
220
|
+
apply_ready: "apply_ready",
|
|
220
221
|
};
|
|
221
222
|
export const GATE_ROUTE = {
|
|
222
223
|
explore_complete: "explore",
|
|
@@ -227,6 +228,7 @@ export const GATE_ROUTE = {
|
|
|
227
228
|
test_contract_honored: "propose",
|
|
228
229
|
tasks_complete: "propose",
|
|
229
230
|
propose_complete: "propose",
|
|
231
|
+
apply_ready: "propose",
|
|
230
232
|
review_complete: "review",
|
|
231
233
|
verify_complete: "review",
|
|
232
234
|
archive_ready: "archive",
|
package/dist/superspec.d.ts
CHANGED
package/dist/superspec.js
CHANGED
|
@@ -3,8 +3,12 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
export * from "./src/init_cli.js";
|
|
5
5
|
export * from "./src/cli.js";
|
|
6
|
+
export * from "./src/doctor.js";
|
|
7
|
+
export * from "./src/self_update.js";
|
|
6
8
|
import { main } from "./src/cli.js";
|
|
9
|
+
import { main_doctor, superspec_package_version } from "./src/doctor.js";
|
|
7
10
|
import { main_init_async } from "./src/init_cli.js";
|
|
11
|
+
import { update_self_then_rerun } from "./src/self_update.js";
|
|
8
12
|
function realpathMaybe(filePath) {
|
|
9
13
|
try {
|
|
10
14
|
return realpathSync(filePath);
|
|
@@ -19,14 +23,34 @@ function help() {
|
|
|
19
23
|
"",
|
|
20
24
|
"commands:",
|
|
21
25
|
" init install SuperSpec Codex surfaces (asks project/user; default project)",
|
|
22
|
-
" update update manifest-managed
|
|
26
|
+
" update update SuperSpec CLI, then update manifest-managed surfaces",
|
|
23
27
|
" uninstall remove manifest-managed SuperSpec surfaces",
|
|
24
28
|
" guard run the SuperSpec guard command surface",
|
|
29
|
+
" doctor diagnose SuperSpec/OpenSpec/npm/PATH wiring",
|
|
30
|
+
" version print SuperSpec CLI version",
|
|
25
31
|
"",
|
|
26
32
|
"examples:",
|
|
27
33
|
" superspec init --scope project",
|
|
28
34
|
" superspec init --scope user",
|
|
29
35
|
" superspec guard check-init --change <change>",
|
|
36
|
+
" superspec doctor",
|
|
37
|
+
"",
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
function updateHelp() {
|
|
41
|
+
return [
|
|
42
|
+
"usage: superspec update [--scope {project,user}] [--path PATH] [--codex-home PATH] [--local-only]",
|
|
43
|
+
"",
|
|
44
|
+
"updates the global SuperSpec CLI from npm, then updates manifest-managed SuperSpec surfaces.",
|
|
45
|
+
"",
|
|
46
|
+
"options:",
|
|
47
|
+
" --scope {project,user} update project .codex surfaces or user Codex home surfaces (default: project)",
|
|
48
|
+
" --project equivalent to --scope project",
|
|
49
|
+
" --user, --global equivalent to --scope user",
|
|
50
|
+
" --path PATH project root for project scope (default: current directory)",
|
|
51
|
+
" --codex-home PATH Codex user home for user scope (default: $CODEX_HOME or ~/.codex)",
|
|
52
|
+
" --local-only skip npm self-update and use the currently installed package",
|
|
53
|
+
" -h, --help show this help",
|
|
30
54
|
"",
|
|
31
55
|
].join("\n");
|
|
32
56
|
}
|
|
@@ -36,14 +60,29 @@ export async function main_superspec(argv = process.argv.slice(2)) {
|
|
|
36
60
|
process.stdout.write(help());
|
|
37
61
|
return 0;
|
|
38
62
|
}
|
|
63
|
+
if (command === "-v" || command === "--version" || command === "version") {
|
|
64
|
+
process.stdout.write(`${superspec_package_version()}\n`);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
39
67
|
if (command === "init")
|
|
40
68
|
return main_init_async(rest);
|
|
41
|
-
if (command === "update")
|
|
42
|
-
|
|
69
|
+
if (command === "update") {
|
|
70
|
+
if (rest.includes("-h") || rest.includes("--help")) {
|
|
71
|
+
process.stdout.write(updateHelp());
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const localOnly = rest.includes("--local-only") || rest.includes("--skip-self-update");
|
|
75
|
+
const updateArgs = rest.filter((arg) => arg !== "--local-only" && arg !== "--skip-self-update");
|
|
76
|
+
if (!localOnly)
|
|
77
|
+
return update_self_then_rerun({ args: updateArgs });
|
|
78
|
+
return main_init_async([...updateArgs, "--update"]);
|
|
79
|
+
}
|
|
43
80
|
if (command === "uninstall")
|
|
44
81
|
return main_init_async([...rest, "--uninstall"]);
|
|
45
82
|
if (command === "guard")
|
|
46
83
|
return main(rest);
|
|
84
|
+
if (command === "doctor")
|
|
85
|
+
return main_doctor(rest);
|
|
47
86
|
// Convenience fallback: `superspec check-init ...` behaves like `superspec guard check-init ...`.
|
|
48
87
|
if (command.startsWith("check-") || command === "status" || command === "recompute" || command === "init") {
|
|
49
88
|
return main(argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peterxiaoyang/superspec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "SuperSpec workflow package: guard runtime, generic workflow templates, and Codex adapter payload.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "node build.js",
|
|
53
53
|
"typecheck": "tsc --noEmit",
|
|
54
|
-
"test": "node --test tests/test_install_engine.test.ts tests/test_real_openspec_smoke.test.ts tests/test_superspec_guard.test.ts tests/test_superspec_skills.test.ts",
|
|
54
|
+
"test": "node --test tests/test_install_engine.test.ts tests/test_real_openspec_smoke.test.ts tests/test_superspec_cli.test.ts tests/test_superspec_guard.test.ts tests/test_superspec_skills.test.ts",
|
|
55
55
|
"prepack": "npm run build",
|
|
56
56
|
"prepublishOnly": "npm run build",
|
|
57
57
|
"pack:dry-run": "npm pack --dry-run"
|
|
@@ -23,6 +23,14 @@ metadata:
|
|
|
23
23
|
- Windows PowerShell 中执行 npm 全局 bin 时,必须显式使用 `.cmd` shim:`superspec.cmd ...`、`openspec.cmd ...`;不要运行 `superspec.ps1` 或 `openspec.ps1`。
|
|
24
24
|
- macOS、Linux、Git Bash、cmd.exe 或其他不会优先拦截 `.ps1` 的 shell 中,继续使用文档中的 `superspec ...`、`openspec ...` 命令。
|
|
25
25
|
|
|
26
|
+
## 上下文读取纪律 / Context Budget
|
|
27
|
+
|
|
28
|
+
- guard 可以在本地读取完整 `.superspec/evidence/**/*.json` 并重算判定;主流程默认不要打开完整 evidence JSON,除非正在排查 guard block、修复 schema,或用户明确要求诊断原文。
|
|
29
|
+
- 主流程默认只读取 guard decision、当前 task 必要 artifact、OpenSpec instructions apply 返回的 `contextFiles`、native subagent `output_ref` 的摘要/结论段,以及 task RED/GREEN 所需的最小测试摘要。
|
|
30
|
+
- raw log、长报告和历史 superseded evidence 默认只作为引用、hash 或摘要保留;不要把全文复制进对话上下文或新的 evidence。
|
|
31
|
+
- RED/GREEN 仍按 task 的 `test_refs` 记录 gate-driving evidence;同一次命令输出可以作为共享 raw log 被引用,但不要仅凭聚合日志替代每个 task/test 所需的 RED/GREEN 证据字段。
|
|
32
|
+
- 使用按运行合并建档的 `test_ids[]` 清单时,每个 claimed id 都必须出现在引用的 raw log 中;若当前 task gate 需要单个 `test_id` 覆盖,仍要补齐对应 gate-driving evidence。
|
|
33
|
+
|
|
26
34
|
在 propose package 完成后使用本 skill 执行实现任务。**Task context、ordering 和 progress 来自 OpenSpec native apply instructions**(`openspec instructions apply`);SuperSpec 为每个 task 包上一层 RED/GREEN guard checks。
|
|
27
35
|
|
|
28
36
|
## 边界 / Boundaries
|
|
@@ -41,7 +49,7 @@ metadata:
|
|
|
41
49
|
|
|
42
50
|
## 步骤 / Steps
|
|
43
51
|
|
|
44
|
-
1. 对实现隔离(apply isolation)和执行模式(execution mode)使用 AskUserQuestion
|
|
52
|
+
1. 对实现隔离(apply isolation)和执行模式(execution mode)使用 AskUserQuestion,并等待明确选择;记录 `gate:"apply_isolation"` 的 `human_confirmation` evidence,必须包含 `confirmation_text`、`confirmed_refs` 和当前 `tasks_structure_hash`。
|
|
45
53
|
2. 当 dirty worktree、untracked files 或 branch state 需要确认时,使用 AskUserQuestion 处理分支状态。
|
|
46
54
|
3. 验证 apply readiness:
|
|
47
55
|
```text
|
|
@@ -68,7 +76,7 @@ metadata:
|
|
|
68
76
|
```
|
|
69
77
|
7. 在 runtime/business implementation edits 前产出 RED evidence,除非有允许的 `no_tdd_reason` 或处于现状锁定测试模式(`characterization mode`)。这里的 `characterization` 指“先把当前真实行为测出来并锁住,重构后保持一致”。RED/GREEN evidence 必须引用 task 的 `test_refs`,并在 task 声明 `invariant_refs` 时同步记录 `invariant_refs`。若 task 来自 reopen,本轮 successor GREEN / alternative verification / manual verification 必须携带同一 `reopen_id`。
|
|
70
78
|
8. 按 native dynamic instruction 和 `contextFiles` 指引,实现最小 task scope。
|
|
71
|
-
9. 产出 GREEN evidence,保留 `test_id`、`invariant_refs`、命令、输出摘要和 raw log ref
|
|
79
|
+
9. 产出 GREEN evidence,保留 `test_id`、`invariant_refs`、命令、输出摘要和 raw log ref;raw log ref 指向原始输出文件,evidence 中只写必要摘要,不复制完整日志。
|
|
72
80
|
10. 勾选 task 前执行任务完成检查(`check-task-complete`):
|
|
73
81
|
```text
|
|
74
82
|
superspec guard check-task-complete --change "<change>" --task-id "<task-id>"
|
|
@@ -23,6 +23,12 @@ metadata:
|
|
|
23
23
|
- Windows PowerShell 中执行 npm 全局 bin 时,必须显式使用 `.cmd` shim:`superspec.cmd ...`、`openspec.cmd ...`;不要运行 `superspec.ps1` 或 `openspec.ps1`。
|
|
24
24
|
- macOS、Linux、Git Bash、cmd.exe 或其他不会优先拦截 `.ps1` 的 shell 中,继续使用文档中的 `superspec ...`、`openspec ...` 命令。
|
|
25
25
|
|
|
26
|
+
## 上下文读取纪律 / Context Budget
|
|
27
|
+
|
|
28
|
+
- guard 可以在本地读取完整 `.superspec/evidence/**/*.json` 并重算 archive 判定;主流程默认不要打开完整 evidence JSON,除非正在排查 guard block、修复 preservation manifest,或用户明确要求诊断原文。
|
|
29
|
+
- 主流程默认只读取 guard decision、archive preservation 摘要、OpenSpec archive 输出摘要,以及最终验证 archive 结果所需的最小 manifest 信息。
|
|
30
|
+
- raw log、长报告和历史 superseded evidence 默认只作为引用、hash 或摘要保留;不要把全文复制进对话上下文或新的 evidence。
|
|
31
|
+
|
|
26
32
|
仅在 `review_complete` passes(其中已经包含 final verification)后使用本 skill。
|
|
27
33
|
|
|
28
34
|
## 边界 / Boundaries
|
|
@@ -23,6 +23,14 @@ metadata:
|
|
|
23
23
|
- Windows PowerShell 中执行 npm 全局 bin 时,必须显式使用 `.cmd` shim:`superspec.cmd ...`、`openspec.cmd ...`;不要运行 `superspec.ps1` 或 `openspec.ps1`。
|
|
24
24
|
- macOS、Linux、Git Bash、cmd.exe 或其他不会优先拦截 `.ps1` 的 shell 中,继续使用文档中的 `superspec ...`、`openspec ...` 命令。
|
|
25
25
|
|
|
26
|
+
## 上下文读取纪律 / Context Budget
|
|
27
|
+
|
|
28
|
+
- guard 可以在本地读取完整 `.superspec/evidence/**/*.json` 并重算判定;主流程默认不要打开完整 evidence JSON,除非正在排查 guard block、修复 schema,或用户明确要求诊断原文。
|
|
29
|
+
- 主流程默认只读取 guard decision、当前 gate 必要 artifact、OpenSpec instructions 返回的必要 context、native subagent `output_ref` 的摘要/结论段,以及用户确认所需的最小原文。
|
|
30
|
+
- 当前轮披露循环所需的最小结构化字段必须读取,不能只看 `output_ref` 摘要;包括 role evidence 的 `findings[]`、`finding_uid`、`decision_scope_key`、逐字 `summary`、`target_refs`,以及本轮 digest 需要引用的 evidence id。
|
|
31
|
+
- raw log、长报告和历史 superseded evidence 默认只作为引用、hash 或摘要保留;不要把全文复制进对话上下文或新的 evidence。
|
|
32
|
+
- native subagent 的 `output_ref` 应指向简洁审查报告;原始命令输出或长日志放在 raw/report 文件中被引用,不作为默认阅读材料。
|
|
33
|
+
|
|
26
34
|
在 init 之后、编写 OpenSpec proposal package 之前使用本 skill。
|
|
27
35
|
|
|
28
36
|
## 边界 / Boundaries
|
|
@@ -74,7 +82,8 @@ metadata:
|
|
|
74
82
|
```
|
|
75
83
|
7. 在 `.superspec/evidence/discovery/` 记录 critic evidence,包含 `execution_mode:"native_subagent"`、`agent_role`、`agent_id`、`output_ref`、`source_anchors` 和 `target_refs`。
|
|
76
84
|
8. 按确认循环处理 findings:写本轮审查问题记录;存在关键问题时**停下来向用户说明并等待用户确认**,再按用户确认更新探索记录 / 重跑 critic / 写新一轮记录,直到最新轮 clean 且问题清单里没有未处理完的问题。
|
|
77
|
-
9.
|
|
85
|
+
9. 对探索结论、范围边界和进入 propose 的授权使用 AskUserQuestion,并等待明确选择;记录探索阶段人工确认 evidence(JSON 中为 `gate:"explore_complete"`、`kind:"human_confirmation"`、`created_by:"user"`),`confirmed_refs` 固定记录用户确认过的探索记录。
|
|
86
|
+
10. 运行进入阶段前检查(`check-enter`),验证 explore completion:
|
|
78
87
|
```text
|
|
79
88
|
superspec guard check-enter --change "<change>" --gate explore_complete
|
|
80
89
|
```
|
|
@@ -23,6 +23,14 @@ metadata:
|
|
|
23
23
|
- Windows PowerShell 中执行 npm 全局 bin 时,必须显式使用 `.cmd` shim:`superspec.cmd ...`、`openspec.cmd ...`;不要运行 `superspec.ps1` 或 `openspec.ps1`。
|
|
24
24
|
- macOS、Linux、Git Bash、cmd.exe 或其他不会优先拦截 `.ps1` 的 shell 中,继续使用文档中的 `superspec ...`、`openspec ...` 命令。
|
|
25
25
|
|
|
26
|
+
## 上下文读取纪律 / Context Budget
|
|
27
|
+
|
|
28
|
+
- guard 可以在本地读取完整 `.superspec/evidence/**/*.json` 并重算判定;主流程默认不要打开完整 evidence JSON,除非正在排查 guard block、修复 schema,或用户明确要求诊断原文。
|
|
29
|
+
- 主流程默认只读取 guard decision、当前 gate 必要 artifact、OpenSpec instructions 返回的必要 context、native subagent `output_ref` 的摘要/结论段,以及用户确认所需的最小原文。
|
|
30
|
+
- 当前轮披露循环所需的最小结构化字段必须读取,不能只看 `output_ref` 摘要;包括 role evidence 的 `findings[]`、`finding_uid`、`decision_scope_key`、逐字 `summary`、`target_refs`,以及本轮 digest 需要引用的 evidence id。
|
|
31
|
+
- raw log、长报告和历史 superseded evidence 默认只作为引用、hash 或摘要保留;不要把全文复制进对话上下文或新的 evidence。
|
|
32
|
+
- native subagent 的 `output_ref` 应指向简洁审查报告;原始命令输出或长日志放在 raw/report 文件中被引用,不作为默认阅读材料。
|
|
33
|
+
|
|
26
34
|
在 explore 完成后使用本 skill,用于把探索记录整理成可执行的正式方案包。OpenSpec 负责生成标准方案文件,具体写法必须通过它自带的指令(`openspec instructions`)获取;SuperSpec 负责在外层增加审查门禁(gates)、业务约束(`business-invariants.md`)、测试契约(`test-contract.md`)和任务元数据。
|
|
27
35
|
|
|
28
36
|
## 边界 / Boundaries
|
|
@@ -43,7 +51,7 @@ metadata:
|
|
|
43
51
|
|
|
44
52
|
## 步骤 / Steps
|
|
45
53
|
|
|
46
|
-
1. 运行前置门禁检查(`check-enter
|
|
54
|
+
1. 运行前置门禁检查(`check-enter`),确认探索阶段已经完成;该 gate 必须包含用户对探索结论和进入 propose 的明确确认,若 guard block 则停止并回到 explore 补确认:
|
|
47
55
|
```text
|
|
48
56
|
superspec guard check-enter --change "<change>" --gate explore_complete
|
|
49
57
|
```
|
|
@@ -79,7 +87,7 @@ metadata:
|
|
|
79
87
|
```text
|
|
80
88
|
superspec guard check-enter --change "<change>" --gate propose.test_plan_drafted
|
|
81
89
|
```
|
|
82
|
-
8. 通过 `openspec instructions tasks` 编写任务清单 `tasks.md` 时:为每个 task 补充 `requirement_refs`、`invariant_refs`(必须是 business-invariants `INV-*` ids 的子集)、`test_refs`(必须是 test-contract TEST ids 的子集)、`read_scope`、`write_scope`、dependencies、TDD metadata,以及需要时的 parallel group。若 reviewer 对 task 映射提出 round-tagged findings,走 `tasks_complete-r<N>` 确认循环(pinned target:tasks + test-contract + invariants + design + specs glob);验收标准问题 route 用 `return_test_contract_drafted`,映射问题用 `stay_same_gate_fix`。对于任务审查确认,使用 AskUserQuestion
|
|
90
|
+
8. 通过 `openspec instructions tasks` 编写任务清单 `tasks.md` 时:为每个 task 补充 `requirement_refs`、`invariant_refs`(必须是 business-invariants `INV-*` ids 的子集)、`test_refs`(必须是 test-contract TEST ids 的子集)、`read_scope`、`write_scope`、dependencies、TDD metadata,以及需要时的 parallel group。若 reviewer 对 task 映射提出 round-tagged findings,走 `tasks_complete-r<N>` 确认循环(pinned target:tasks + test-contract + invariants + design + specs glob);验收标准问题 route 用 `return_test_contract_drafted`,映射问题用 `stay_same_gate_fix`。对于任务审查确认,使用 AskUserQuestion 并等待明确选择;按披露循环记录用户裁决和审查问题处理结果,然后验证:
|
|
83
91
|
```text
|
|
84
92
|
superspec guard check-enter --change "<change>" --gate propose.tasks_mapped
|
|
85
93
|
```
|
|
@@ -25,6 +25,14 @@ metadata:
|
|
|
25
25
|
- Windows PowerShell 中执行 npm 全局 bin 时,必须显式使用 `.cmd` shim:`superspec.cmd ...`、`openspec.cmd ...`;不要运行 `superspec.ps1` 或 `openspec.ps1`。
|
|
26
26
|
- macOS、Linux、Git Bash、cmd.exe 或其他不会优先拦截 `.ps1` 的 shell 中,继续使用文档中的 `superspec ...`、`openspec ...` 命令。
|
|
27
27
|
|
|
28
|
+
## 上下文读取纪律 / Context Budget
|
|
29
|
+
|
|
30
|
+
- guard 可以在本地读取完整 `.superspec/evidence/**/*.json` 并重算判定;主流程默认不要打开完整 evidence JSON,除非正在排查 guard block、修复 schema,或用户明确要求诊断原文。
|
|
31
|
+
- 主流程默认只读取 guard decision、当前 review 必要 artifact、native subagent `output_ref` 的摘要/结论段、`required_load_refs` 指向的关键 source,以及 final verification 的摘要。
|
|
32
|
+
- `source_refs` 只是可追溯来源,不等于必须读取;只有 `required_load_refs` 是主流程必须亲自读取并写入 `loaded_refs` 的内容。
|
|
33
|
+
- raw log、长报告和历史 superseded evidence 默认只作为引用、hash 或摘要保留;不要把全文复制进对话上下文或新的 evidence。
|
|
34
|
+
- guard-only read 不能替代主流程的 `loaded_refs`:凡进入 `required_load_refs` 的材料,主流程必须真实读取后再写 `main_adjudication`。
|
|
35
|
+
|
|
28
36
|
## 硬边界
|
|
29
37
|
|
|
30
38
|
- `review_complete` 是 allow-only gate。只有 `main_adjudication.review_decision:"allow"` 才允许进入 `check-review-complete` / `archive_ready`。
|
|
@@ -58,13 +66,13 @@ Required project-scope files: `.codex/agents/code-reviewer.toml`、`.codex/promp
|
|
|
58
66
|
```text
|
|
59
67
|
superspec guard check-review-ready --change "<change>"
|
|
60
68
|
```
|
|
61
|
-
2. 从
|
|
69
|
+
2. 从 guard decision、`git diff`、OpenSpec artifacts、tasks、business invariants、test contract、RED/GREEN 摘要和 live role output 摘要构建审查范围;不要默认打开完整 `.superspec/evidence/**/*.json`。
|
|
62
70
|
3. 运行 repo-local review guidance:
|
|
63
71
|
- 启动 repo-local `code-reviewer` native subagent,记录审查指导证据(内部 JSON kind 为 `source_guidance`)。
|
|
64
72
|
- 启动 repo-local `architect` native subagent,记录审查指导证据(内部 JSON kind 为 `source_guidance`)。
|
|
65
73
|
- 启动独立 repo-local `critic` native subagent,审查 superspec-specific scope drift、隐藏假设、业务不变量是否被测试/实现扭曲、遗漏的 rollback targets 和 evidence 充分性,并记录审查指导证据(内部 JSON kind 为 `source_guidance`)。
|
|
66
74
|
4. 主流程读取关键 source,准备 `main_adjudication` 输入:
|
|
67
|
-
- 从每条 `source_guidance.source_refs`
|
|
75
|
+
- 从每条 `source_guidance.source_refs` 中判断哪些内容确实需要亲自加载,不要把所有来源自动升级为必须读取。
|
|
68
76
|
- `source_refs` / `required_load_refs` 使用 `pinned_ref = {path, blob_sha}`;其中 `pinned_ref.path` 一律是 repo-root relative path。`required_load_refs` 必须按 `(path, blob_sha)` 精确包含于 `source_refs`。
|
|
69
77
|
- 对所有 `required_load_refs` 做真实读取,并在 `loaded_refs` 中记录同样的 `pinned_ref`;`loaded_refs` 必须按 `(path, blob_sha)` 精确覆盖全部 `required_load_refs`,同一路径不同 blob 不算已加载。
|
|
70
78
|
- 对所有 `required_claim_ids` 写出结构化 `claim_adjudications[] = {claim_id, decision, rationale}`。
|