@peterxiaoyang/superspec 0.1.15-alpha → 0.1.17-alpha
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 +48 -31
- package/dist/cli.js +41 -34
- package/dist/format.d.ts +9 -0
- package/dist/format.js +35 -5
- package/dist/install.d.ts +19 -0
- package/dist/install.js +185 -0
- package/dist/next.js +42 -11
- package/dist/record.js +81 -6
- package/dist/transition.d.ts +2 -1
- package/dist/transition.js +55 -25
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
- package/templates/workflow/agents/architect.toml +13 -0
- package/templates/workflow/agents/code-reviewer.toml +13 -0
- package/templates/workflow/agents/critic.toml +13 -0
- package/templates/workflow/agents/executor.toml +13 -0
- package/templates/workflow/agents/explore.toml +13 -0
- package/templates/workflow/agents/test-engineer.toml +13 -0
- package/templates/workflow/agents/test-runner.toml +13 -0
- package/templates/workflow/agents/verifier.toml +13 -0
- package/templates/workflow/prompts/architect.md +44 -0
- package/templates/workflow/prompts/code-reviewer.md +34 -0
- package/templates/workflow/prompts/critic.md +47 -0
- package/templates/workflow/prompts/executor.md +32 -0
- package/templates/workflow/prompts/explore.md +27 -0
- package/templates/workflow/prompts/test-engineer.md +45 -0
- package/templates/workflow/prompts/test-runner.md +35 -0
- package/templates/workflow/prompts/verifier.md +53 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +8 -18
- package/templates/workflow/skills/superspec-archive/SKILL.md +2 -9
- package/templates/workflow/skills/superspec-explore/SKILL.md +5 -10
- package/templates/workflow/skills/superspec-propose/SKILL.md +28 -13
- package/templates/workflow/skills/superspec-review/SKILL.md +6 -28
package/README.md
CHANGED
|
@@ -77,17 +77,16 @@ npm install -g @peterxiaoyang/superspec@latest
|
|
|
77
77
|
进入你的项目根目录,然后运行:
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
|
-
superspec
|
|
80
|
+
superspec install
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
这条命令的意思是:把 SuperSpec 当前可用的工作流入口安装到项目里。
|
|
84
|
-
|
|
85
|
-
初始化还会安装托管的 `.codex/hooks.json`。默认 manifest 只在子智能体启动和停止时调用 SuperSpec,记录 best-effort 审计信息;它不会拦截普通文件写入,也不会在测试命令后自动记录结果。
|
|
84
|
+
当前 beta 会安装 `.superspec/` 引擎目录、`.codex/skills/superspec-*` 阶段入口、`.codex/prompts/*.md` 角色 prompt、`.codex/agents/*.toml` 子智能体配置,并补齐 `.codex/config.toml` 的多 agent 开关。`superspec init --scope project` 仍作为兼容别名可用。
|
|
86
85
|
|
|
87
86
|
Windows PowerShell 如果拦截 npm 的 `.ps1` 脚本,请改用:
|
|
88
87
|
|
|
89
88
|
```powershell
|
|
90
|
-
superspec.cmd
|
|
89
|
+
superspec.cmd install
|
|
91
90
|
```
|
|
92
91
|
|
|
93
92
|
### 3. 按步骤使用
|
|
@@ -136,6 +135,8 @@ superspec.cmd init --scope project
|
|
|
136
135
|
|
|
137
136
|
你日常主要记住这五个入口就够了。
|
|
138
137
|
|
|
138
|
+
CLI 不带 `--risk` 时默认走完整审查路径;需要轻量路径时显式传 `--risk normal` 或 `--risk minimal`。探索阶段会创建 `critic` 工作项审查需求澄清记录;计划阶段会创建 `critic`、`architect` 和 `test-engineer` 工作项后再进入实现准备。
|
|
139
|
+
|
|
139
140
|
## 它会多保存哪些记录
|
|
140
141
|
|
|
141
142
|
SuperSpec 会在每次变更下面保存一些辅助记录,方便后续追溯。
|
|
@@ -200,59 +201,76 @@ superspec --version
|
|
|
200
201
|
安装到当前项目:
|
|
201
202
|
|
|
202
203
|
```bash
|
|
203
|
-
superspec
|
|
204
|
+
superspec install
|
|
204
205
|
```
|
|
205
206
|
|
|
206
|
-
|
|
207
|
+
`superspec init --scope project` 是兼容别名,也会执行同一套安装逻辑。
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
### OpenSpec 中文输出
|
|
210
|
+
|
|
211
|
+
OpenSpec 生成文档的语言应通过官方项目配置控制。在 `openspec/config.yaml` 中使用 `context`:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
schema: spec-driven
|
|
215
|
+
|
|
216
|
+
context: |
|
|
217
|
+
语言:中文(简体)
|
|
218
|
+
所有产出物必须用简体中文撰写。
|
|
210
219
|
```
|
|
211
220
|
|
|
212
|
-
|
|
221
|
+
`superspec install` 会创建缺失的 `openspec/config.yaml`,或在没有顶层 `context` 时追加这段官方中文 context。如果文件已经有顶层 `context`,SuperSpec 不会覆盖它。
|
|
222
|
+
|
|
223
|
+
可以用下面的命令检查生成的 instructions 是否包含语言上下文:
|
|
213
224
|
|
|
214
225
|
```bash
|
|
215
|
-
|
|
226
|
+
openspec instructions proposal --change <change>
|
|
216
227
|
```
|
|
217
228
|
|
|
218
|
-
|
|
229
|
+
检查当前项目的 OpenSpec 探测结果:
|
|
219
230
|
|
|
220
231
|
```bash
|
|
221
|
-
superspec
|
|
232
|
+
superspec status
|
|
222
233
|
```
|
|
223
234
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
诊断全局安装、PATH、OpenSpec 依赖和 npm bin 指向问题:
|
|
235
|
+
更新当前 beta CLI:
|
|
227
236
|
|
|
228
237
|
```bash
|
|
229
|
-
superspec
|
|
238
|
+
superspec update
|
|
230
239
|
```
|
|
231
240
|
|
|
241
|
+
这条命令只检查当前 `.superspec/changes` 运行时是否已经是新引擎布局;真正升级全局 npm 包仍需使用 `npm install -g @peterxiaoyang/superspec@<version>`。
|
|
242
|
+
|
|
232
243
|
## 进阶信息
|
|
233
244
|
|
|
234
245
|
以当前内置的 Codex 适配器为例,初始化后项目里会出现这些入口文件:
|
|
235
246
|
|
|
236
247
|
```text
|
|
237
248
|
.codex/
|
|
238
|
-
hooks.json
|
|
239
249
|
skills/superspec-explore/
|
|
240
250
|
skills/superspec-propose/
|
|
241
251
|
skills/superspec-apply/
|
|
242
252
|
skills/superspec-review/
|
|
243
253
|
skills/superspec-archive/
|
|
254
|
+
prompts/architect.md
|
|
255
|
+
prompts/code-reviewer.md
|
|
256
|
+
prompts/critic.md
|
|
257
|
+
prompts/executor.md
|
|
258
|
+
prompts/explore.md
|
|
259
|
+
prompts/test-engineer.md
|
|
260
|
+
prompts/test-runner.md
|
|
261
|
+
prompts/verifier.md
|
|
262
|
+
agents/architect.toml
|
|
263
|
+
agents/code-reviewer.toml
|
|
264
|
+
agents/critic.toml
|
|
265
|
+
agents/executor.toml
|
|
266
|
+
agents/explore.toml
|
|
267
|
+
agents/test-engineer.toml
|
|
268
|
+
agents/test-runner.toml
|
|
269
|
+
agents/verifier.toml
|
|
270
|
+
config.toml
|
|
244
271
|
```
|
|
245
272
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
superspec check check-init --change <变更ID>
|
|
250
|
-
superspec check check-apply-ready --change <变更ID>
|
|
251
|
-
superspec check check-review-ready --change <变更ID>
|
|
252
|
-
superspec check check-archive-ready --change <变更ID>
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
普通使用者通常不需要手动运行这些命令;对应的阶段入口会在需要时使用它们。
|
|
273
|
+
当前 beta 的阶段入口由 `superspec transition next --change <变更ID>` 驱动,不再提供旧版 `superspec check ...` surface。
|
|
256
274
|
|
|
257
275
|
如果你要开发 SuperSpec 本身:
|
|
258
276
|
|
|
@@ -260,14 +278,13 @@ superspec check check-archive-ready --change <变更ID>
|
|
|
260
278
|
npm run build
|
|
261
279
|
npm run typecheck
|
|
262
280
|
npm test
|
|
263
|
-
npm
|
|
281
|
+
npm pack --dry-run
|
|
264
282
|
```
|
|
265
283
|
|
|
266
284
|
更多细节见:
|
|
267
285
|
|
|
268
|
-
- `docs/
|
|
269
|
-
- `
|
|
270
|
-
- `.codex/skills/superspec-*/SKILL.md`:当前 Codex 适配器使用的阶段入口说明
|
|
286
|
+
- `docs/plans/SUPERSPEC_TRANSITION_ENGINE_SPEC_LITE.md`:transition engine 设计
|
|
287
|
+
- `templates/workflow/skills/superspec-*/SKILL.md`:当前 Codex 适配器使用的阶段入口说明
|
|
271
288
|
|
|
272
289
|
## 致谢与灵感来源
|
|
273
290
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SuperSpec 流程引擎 — CLI 入口
|
|
3
|
-
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import { installProject } from "./install.js";
|
|
5
5
|
import { writeSnapshot } from "./store.js";
|
|
6
6
|
import { rebuildSnapshot } from "./sync.js";
|
|
7
7
|
import { next as nextCmd } from "./next.js";
|
|
@@ -44,7 +44,7 @@ function parseFlags(args) {
|
|
|
44
44
|
async function main(argv) {
|
|
45
45
|
// --help / 无参数 → 打印用法
|
|
46
46
|
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
47
|
-
console.log(`SuperSpec 流程引擎 0.1.
|
|
47
|
+
console.log(`SuperSpec 流程引擎 0.1.16-alpha
|
|
48
48
|
|
|
49
49
|
用法:superspec <命令> [选项]
|
|
50
50
|
|
|
@@ -53,7 +53,8 @@ async function main(argv) {
|
|
|
53
53
|
transition <子命令> --change <C> 状态流转(见下)
|
|
54
54
|
record <子命令> --change <C> 登记证据(见下)
|
|
55
55
|
jobs <子命令> --change <C> 工作项管理(见下)
|
|
56
|
-
install
|
|
56
|
+
install 安装项目工作流入口
|
|
57
|
+
init --scope project install 的兼容别名
|
|
57
58
|
update 更新 SuperSpec
|
|
58
59
|
version 版本号
|
|
59
60
|
|
|
@@ -74,36 +75,31 @@ jobs 子命令:
|
|
|
74
75
|
}
|
|
75
76
|
// version
|
|
76
77
|
if (argv[0] === "version" || argv[0] === "--version" || argv[0] === "-v") {
|
|
77
|
-
console.log("SuperSpec 0.1.
|
|
78
|
+
console.log("SuperSpec 0.1.16-alpha");
|
|
78
79
|
return 0;
|
|
79
80
|
}
|
|
80
81
|
const { command, subcommand, opts } = parseArgs(argv);
|
|
81
82
|
const projectRoot = process.cwd();
|
|
82
|
-
// install / update 不需要 --change
|
|
83
|
-
if (command === "install") {
|
|
84
|
-
|
|
85
|
-
const engineDir = join(projectRoot, ".superspec");
|
|
86
|
-
// 检测老版残留(0.x 的 superspec-state.json / superspec-state.lock)
|
|
87
|
-
const oldStateFiles = ["superspec-state.json", "superspec-state.lock", "ledger.jsonl"];
|
|
88
|
-
const foundOld = oldStateFiles.some(f => existsSync(join(projectRoot, "openspec", "changes")) &&
|
|
89
|
-
readdirSync(join(projectRoot, "openspec", "changes")).some(c => existsSync(join(projectRoot, "openspec", "changes", c, ".superspec", f))));
|
|
90
|
-
if (foundOld) {
|
|
83
|
+
// install / init / update 不需要 --change
|
|
84
|
+
if (command === "install" || command === "init") {
|
|
85
|
+
if (command === "init" && opts.scope && opts.scope !== "project") {
|
|
91
86
|
console.log(JSON.stringify({
|
|
92
87
|
ok: false,
|
|
93
|
-
message: "
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
message: "当前 beta 只支持 init --scope project",
|
|
89
|
+
}));
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
console.log(JSON.stringify(installProject(projectRoot)));
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.log(JSON.stringify({
|
|
98
|
+
ok: false,
|
|
99
|
+
message: err.message,
|
|
97
100
|
}));
|
|
98
101
|
return 1;
|
|
99
102
|
}
|
|
100
|
-
if (!existsSync(engineDir))
|
|
101
|
-
mkdirSync(join(engineDir, "changes"), { recursive: true });
|
|
102
|
-
const gitignorePath = join(engineDir, ".gitignore");
|
|
103
|
-
if (!existsSync(gitignorePath))
|
|
104
|
-
writeFileSync(gitignorePath, "changes/\n*.log\n*.tmp\n");
|
|
105
|
-
console.log(JSON.stringify({ ok: true, message: "SuperSpec 0.1.15-alpha 已安装" }));
|
|
106
|
-
return 0;
|
|
107
103
|
}
|
|
108
104
|
if (command === "update") {
|
|
109
105
|
// 检测是否从老版 update 过来
|
|
@@ -112,13 +108,13 @@ jobs 子命令:
|
|
|
112
108
|
if (isLegacyUpdate) {
|
|
113
109
|
console.log(JSON.stringify({
|
|
114
110
|
ok: false,
|
|
115
|
-
message: "SuperSpec 0.1.
|
|
116
|
-
"请用 npm install -g @peterxiaoyang/superspec@0.1.
|
|
111
|
+
message: "SuperSpec 0.1.16-alpha 是全新引擎,不能从 0.x 直接 update。\n" +
|
|
112
|
+
"请用 npm install -g @peterxiaoyang/superspec@0.1.16-alpha 手动安装。\n" +
|
|
117
113
|
"现有 change 请先用 0.1.x 完成归档。",
|
|
118
114
|
}));
|
|
119
115
|
return 1;
|
|
120
116
|
}
|
|
121
|
-
console.log(JSON.stringify({ ok: true, message: "已是最新版本 0.1.
|
|
117
|
+
console.log(JSON.stringify({ ok: true, message: "已是最新版本 0.1.16-alpha" }));
|
|
122
118
|
return 0;
|
|
123
119
|
}
|
|
124
120
|
const change = opts.change;
|
|
@@ -138,12 +134,20 @@ jobs 子命令:
|
|
|
138
134
|
const cr = changeRoot(projectRoot, change);
|
|
139
135
|
const snapshot = rebuildSnapshot(projectRoot, change, cr);
|
|
140
136
|
const jobs = jobsList(projectRoot, change);
|
|
137
|
+
const staleAcceptedJobs = Math.max(0, jobs.accepted.length - snapshot.accepted_jobs.length);
|
|
141
138
|
console.log(JSON.stringify({
|
|
142
139
|
change,
|
|
143
140
|
state: snapshot.state,
|
|
144
|
-
open_jobs:
|
|
145
|
-
accepted_jobs:
|
|
141
|
+
open_jobs: snapshot.open_jobs.length,
|
|
142
|
+
accepted_jobs: snapshot.accepted_jobs.length,
|
|
143
|
+
historical_accepted_jobs: jobs.accepted.length,
|
|
144
|
+
stale_accepted_jobs: staleAcceptedJobs,
|
|
146
145
|
rejected_jobs: jobs.rejected.length,
|
|
146
|
+
active_task_attempts: snapshot.active_task_attempts.map(a => ({
|
|
147
|
+
attempt_id: a.attempt_id,
|
|
148
|
+
task_id: a.task_id,
|
|
149
|
+
state: a.state,
|
|
150
|
+
})),
|
|
147
151
|
document_digests: snapshot.document_digests,
|
|
148
152
|
last_transition: snapshot.last_transition,
|
|
149
153
|
}, null, 2));
|
|
@@ -156,7 +160,10 @@ jobs 子命令:
|
|
|
156
160
|
console.log(JSON.stringify(transitionInit(projectRoot, change, cr), null, 2));
|
|
157
161
|
return 0;
|
|
158
162
|
case "explore":
|
|
159
|
-
|
|
163
|
+
{
|
|
164
|
+
const risk = opts.risk ?? "strict";
|
|
165
|
+
console.log(JSON.stringify(transitionExplore(projectRoot, change, cr, risk), null, 2));
|
|
166
|
+
}
|
|
160
167
|
return 0;
|
|
161
168
|
case "sync": {
|
|
162
169
|
const snapshot = rebuildSnapshot(projectRoot, change, cr, openspecStatus(projectRoot, change));
|
|
@@ -170,13 +177,13 @@ jobs 子命令:
|
|
|
170
177
|
return 0;
|
|
171
178
|
}
|
|
172
179
|
case "next": {
|
|
173
|
-
const risk = opts.risk ?? "
|
|
180
|
+
const risk = opts.risk ?? "strict";
|
|
174
181
|
const result = nextCmd(projectRoot, change, cr, risk);
|
|
175
182
|
console.log(JSON.stringify(result, null, 2));
|
|
176
183
|
return 0;
|
|
177
184
|
}
|
|
178
185
|
case "propose-ready": {
|
|
179
|
-
const risk = opts.risk ?? "
|
|
186
|
+
const risk = opts.risk ?? "strict";
|
|
180
187
|
const result = proposeReady(projectRoot, change, cr, risk);
|
|
181
188
|
console.log(JSON.stringify(result, null, 2));
|
|
182
189
|
return result.events_written === 0 && result.message.includes("不能") ? 1 : 0;
|
|
@@ -207,7 +214,7 @@ jobs 子命令:
|
|
|
207
214
|
return result.events_written === 0 ? 1 : 0;
|
|
208
215
|
}
|
|
209
216
|
case "review-ready": {
|
|
210
|
-
const risk = opts.risk ?? "
|
|
217
|
+
const risk = opts.risk ?? "strict";
|
|
211
218
|
const result = reviewReady(projectRoot, change, cr, risk);
|
|
212
219
|
console.log(JSON.stringify(result, null, 2));
|
|
213
220
|
return result.events_written === 0 ? 1 : 0;
|
|
@@ -289,7 +296,7 @@ jobs 子命令:
|
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
default:
|
|
292
|
-
console.error(`未知命令:${command}。可用:status, transition, record, jobs`);
|
|
299
|
+
console.error(`未知命令:${command}。可用:status, transition, record, jobs, install, init, update, version`);
|
|
293
300
|
return 1;
|
|
294
301
|
}
|
|
295
302
|
}
|
package/dist/format.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ export declare function validateDiscovery(changeRoot: string): {
|
|
|
6
6
|
message: string;
|
|
7
7
|
openCount: number;
|
|
8
8
|
};
|
|
9
|
+
export interface ProposeOpenQuestionFile {
|
|
10
|
+
path: string;
|
|
11
|
+
openCount: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function countProposeOpenQuestionsInContent(content: string): number;
|
|
14
|
+
export declare function collectProposeOpenQuestions(changeRoot: string): {
|
|
15
|
+
openCount: number;
|
|
16
|
+
files: ProposeOpenQuestionFile[];
|
|
17
|
+
};
|
|
9
18
|
export interface ParsedTask {
|
|
10
19
|
taskId: string;
|
|
11
20
|
lineIdx: number;
|
package/dist/format.js
CHANGED
|
@@ -12,10 +12,12 @@ import { join } from "node:path";
|
|
|
12
12
|
// - [ ] 问题2的描述
|
|
13
13
|
//
|
|
14
14
|
// 引擎只解析"待确认问题"段内的 `- [ ]`,不误判正常 checklist。
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
function escapeRegex(value) {
|
|
16
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
|
+
}
|
|
18
|
+
function countOpenChecklistItemsInSection(content, headings) {
|
|
19
|
+
const headingPattern = headings.map(escapeRegex).join("|");
|
|
20
|
+
const sectionMatch = new RegExp(`^#{1,6}\\s*(?:${headingPattern})\\s*$`, "im").exec(content);
|
|
19
21
|
if (!sectionMatch)
|
|
20
22
|
return 0;
|
|
21
23
|
const sectionStart = sectionMatch.index + sectionMatch[0].length;
|
|
@@ -24,9 +26,13 @@ export function countDiscoveryOpenQuestions(content) {
|
|
|
24
26
|
const nextHeadingMatch = restContent.match(/^#{1,6}\s+/m);
|
|
25
27
|
const sectionBody = nextHeadingMatch ? restContent.slice(0, nextHeadingMatch.index) : restContent;
|
|
26
28
|
// 数未确认项
|
|
27
|
-
const matches = sectionBody.match(
|
|
29
|
+
const matches = sectionBody.match(/^\s*-\s+\[ \]/gm);
|
|
28
30
|
return matches ? matches.length : 0;
|
|
29
31
|
}
|
|
32
|
+
/** 从 discovery.md 提取"待确认问题"段内的未确认项数量 */
|
|
33
|
+
export function countDiscoveryOpenQuestions(content) {
|
|
34
|
+
return countOpenChecklistItemsInSection(content, ["待确认问题", "Open Questions", "Pending Questions"]);
|
|
35
|
+
}
|
|
30
36
|
/** 完整校验 discovery.md:存在 + 非空 + 无未确认问题 */
|
|
31
37
|
export function validateDiscovery(changeRoot) {
|
|
32
38
|
const path = join(changeRoot, ".superspec", "artifacts", "discovery.md");
|
|
@@ -40,6 +46,30 @@ export function validateDiscovery(changeRoot) {
|
|
|
40
46
|
return { ok: false, message: `discovery.md 有 ${openCount} 个未确认问题`, openCount };
|
|
41
47
|
return { ok: true, message: "discovery.md 就绪", openCount: 0 };
|
|
42
48
|
}
|
|
49
|
+
const PROPOSE_CONFIRMATION_DOCS = [
|
|
50
|
+
"proposal.md",
|
|
51
|
+
"design.md",
|
|
52
|
+
".superspec/artifacts/test-contract.md",
|
|
53
|
+
];
|
|
54
|
+
const PROPOSE_CONFIRMATION_HEADINGS = ["待用户确认", "待确认问题", "Open Questions", "Pending Questions"];
|
|
55
|
+
export function countProposeOpenQuestionsInContent(content) {
|
|
56
|
+
return countOpenChecklistItemsInSection(content, PROPOSE_CONFIRMATION_HEADINGS);
|
|
57
|
+
}
|
|
58
|
+
export function collectProposeOpenQuestions(changeRoot) {
|
|
59
|
+
const files = [];
|
|
60
|
+
for (const path of PROPOSE_CONFIRMATION_DOCS) {
|
|
61
|
+
const fullPath = join(changeRoot, path);
|
|
62
|
+
if (!existsSync(fullPath))
|
|
63
|
+
continue;
|
|
64
|
+
const openCount = countProposeOpenQuestionsInContent(readFileSync(fullPath, "utf8"));
|
|
65
|
+
if (openCount > 0)
|
|
66
|
+
files.push({ path, openCount });
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
openCount: files.reduce((sum, file) => sum + file.openCount, 0),
|
|
70
|
+
files,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
43
73
|
const TASK_LINE_RE = /^(- \[([ xX])\])\s+(\S+)/;
|
|
44
74
|
/** 解析 tasks.md 的全部任务行 */
|
|
45
75
|
export function parseTasksMd(content) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const WORKFLOW_SKILLS: readonly ["superspec-explore", "superspec-propose", "superspec-apply", "superspec-review", "superspec-archive"];
|
|
2
|
+
export declare const WORKFLOW_PROMPTS: readonly ["architect.md", "code-reviewer.md", "critic.md", "executor.md", "explore.md", "test-engineer.md", "test-runner.md", "verifier.md"];
|
|
3
|
+
export declare const WORKFLOW_AGENTS: readonly ["architect.toml", "code-reviewer.toml", "critic.toml", "executor.toml", "explore.toml", "test-engineer.toml", "test-runner.toml", "verifier.toml"];
|
|
4
|
+
export interface InstallResult {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
installed: {
|
|
8
|
+
engine_dir: string;
|
|
9
|
+
skills: string[];
|
|
10
|
+
prompts: string[];
|
|
11
|
+
agents: string[];
|
|
12
|
+
config: string;
|
|
13
|
+
openspec_config: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface InstallOptions {
|
|
17
|
+
templateRoot?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function installProject(projectRoot: string, options?: InstallOptions): InstallResult;
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const WORKFLOW_SKILLS = [
|
|
4
|
+
"superspec-explore",
|
|
5
|
+
"superspec-propose",
|
|
6
|
+
"superspec-apply",
|
|
7
|
+
"superspec-review",
|
|
8
|
+
"superspec-archive",
|
|
9
|
+
];
|
|
10
|
+
export const WORKFLOW_PROMPTS = [
|
|
11
|
+
"architect.md",
|
|
12
|
+
"code-reviewer.md",
|
|
13
|
+
"critic.md",
|
|
14
|
+
"executor.md",
|
|
15
|
+
"explore.md",
|
|
16
|
+
"test-engineer.md",
|
|
17
|
+
"test-runner.md",
|
|
18
|
+
"verifier.md",
|
|
19
|
+
];
|
|
20
|
+
export const WORKFLOW_AGENTS = [
|
|
21
|
+
"architect.toml",
|
|
22
|
+
"code-reviewer.toml",
|
|
23
|
+
"critic.toml",
|
|
24
|
+
"executor.toml",
|
|
25
|
+
"explore.toml",
|
|
26
|
+
"test-engineer.toml",
|
|
27
|
+
"test-runner.toml",
|
|
28
|
+
"verifier.toml",
|
|
29
|
+
];
|
|
30
|
+
function defaultTemplateRoot() {
|
|
31
|
+
return join(import.meta.dirname, "..", "templates", "workflow");
|
|
32
|
+
}
|
|
33
|
+
function legacyStateFound(projectRoot) {
|
|
34
|
+
const changesRoot = join(projectRoot, "openspec", "changes");
|
|
35
|
+
if (!existsSync(changesRoot))
|
|
36
|
+
return false;
|
|
37
|
+
const oldStateFiles = ["superspec-state.json", "superspec-state.lock", "ledger.jsonl"];
|
|
38
|
+
return oldStateFiles.some(file => readdirSync(changesRoot).some(change => existsSync(join(changesRoot, change, ".superspec", file))));
|
|
39
|
+
}
|
|
40
|
+
function assertWorkflowTemplates(templateRoot) {
|
|
41
|
+
const missing = [];
|
|
42
|
+
for (const skill of WORKFLOW_SKILLS) {
|
|
43
|
+
const file = join(templateRoot, "skills", skill, "SKILL.md");
|
|
44
|
+
if (!existsSync(file))
|
|
45
|
+
missing.push(file);
|
|
46
|
+
}
|
|
47
|
+
for (const prompt of WORKFLOW_PROMPTS) {
|
|
48
|
+
const file = join(templateRoot, "prompts", prompt);
|
|
49
|
+
if (!existsSync(file))
|
|
50
|
+
missing.push(file);
|
|
51
|
+
}
|
|
52
|
+
for (const agent of WORKFLOW_AGENTS) {
|
|
53
|
+
const file = join(templateRoot, "agents", agent);
|
|
54
|
+
if (!existsSync(file))
|
|
55
|
+
missing.push(file);
|
|
56
|
+
}
|
|
57
|
+
if (missing.length > 0) {
|
|
58
|
+
throw new Error(`workflow templates missing: ${missing.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function copySkills(templateRoot, projectRoot) {
|
|
62
|
+
const skillsDest = join(projectRoot, ".codex", "skills");
|
|
63
|
+
const installedSkills = [];
|
|
64
|
+
for (const skill of WORKFLOW_SKILLS) {
|
|
65
|
+
const src = join(templateRoot, "skills", skill, "SKILL.md");
|
|
66
|
+
mkdirSync(join(skillsDest, skill), { recursive: true });
|
|
67
|
+
copyFileSync(src, join(skillsDest, skill, "SKILL.md"));
|
|
68
|
+
installedSkills.push(skill);
|
|
69
|
+
}
|
|
70
|
+
return installedSkills;
|
|
71
|
+
}
|
|
72
|
+
function copyPrompts(templateRoot, projectRoot) {
|
|
73
|
+
const promptsDest = join(projectRoot, ".codex", "prompts");
|
|
74
|
+
const installedPrompts = [];
|
|
75
|
+
for (const prompt of WORKFLOW_PROMPTS) {
|
|
76
|
+
const src = join(templateRoot, "prompts", prompt);
|
|
77
|
+
mkdirSync(promptsDest, { recursive: true });
|
|
78
|
+
copyFileSync(src, join(promptsDest, prompt));
|
|
79
|
+
installedPrompts.push(prompt);
|
|
80
|
+
}
|
|
81
|
+
return installedPrompts;
|
|
82
|
+
}
|
|
83
|
+
function copyAgents(templateRoot, projectRoot) {
|
|
84
|
+
const agentsDest = join(projectRoot, ".codex", "agents");
|
|
85
|
+
const installedAgents = [];
|
|
86
|
+
for (const agent of WORKFLOW_AGENTS) {
|
|
87
|
+
const src = join(templateRoot, "agents", agent);
|
|
88
|
+
mkdirSync(agentsDest, { recursive: true });
|
|
89
|
+
copyFileSync(src, join(agentsDest, agent));
|
|
90
|
+
installedAgents.push(agent);
|
|
91
|
+
}
|
|
92
|
+
return installedAgents;
|
|
93
|
+
}
|
|
94
|
+
function insertMissingTableEntries(content, table, entries) {
|
|
95
|
+
const tablePattern = new RegExp(`^\\[${table}\\]\\s*$`, "m");
|
|
96
|
+
const tableMatch = tablePattern.exec(content);
|
|
97
|
+
const lines = Object.entries(entries).map(([key, value]) => `${key} = ${value}`);
|
|
98
|
+
if (!tableMatch) {
|
|
99
|
+
const separator = content.trim().length === 0 ? "" : "\n\n";
|
|
100
|
+
return `${content.trimEnd()}${separator}[${table}]\n${lines.join("\n")}\n`;
|
|
101
|
+
}
|
|
102
|
+
const tableStart = tableMatch.index;
|
|
103
|
+
const headerEnd = tableStart + tableMatch[0].length;
|
|
104
|
+
const nextTable = /^\[[^\]]+\]\s*$/m.exec(content.slice(headerEnd));
|
|
105
|
+
const tableEnd = nextTable ? headerEnd + nextTable.index : content.length;
|
|
106
|
+
const tableBody = content.slice(headerEnd, tableEnd);
|
|
107
|
+
const missing = lines.filter(line => {
|
|
108
|
+
const key = line.split(" = ", 1)[0];
|
|
109
|
+
return !new RegExp(`^\\s*${key}\\s*=`, "m").test(tableBody);
|
|
110
|
+
});
|
|
111
|
+
if (missing.length === 0)
|
|
112
|
+
return content;
|
|
113
|
+
return `${content.slice(0, headerEnd)}\n${missing.join("\n")}${content.slice(headerEnd)}`;
|
|
114
|
+
}
|
|
115
|
+
function ensureCodexConfig(projectRoot) {
|
|
116
|
+
const codexDir = join(projectRoot, ".codex");
|
|
117
|
+
const configPath = join(codexDir, "config.toml");
|
|
118
|
+
mkdirSync(codexDir, { recursive: true });
|
|
119
|
+
const current = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
120
|
+
let next = insertMissingTableEntries(current, "features", {
|
|
121
|
+
multi_agent: "true",
|
|
122
|
+
child_agents_md: "true",
|
|
123
|
+
});
|
|
124
|
+
next = insertMissingTableEntries(next, "agents", {
|
|
125
|
+
max_threads: "12",
|
|
126
|
+
max_depth: "1",
|
|
127
|
+
});
|
|
128
|
+
if (next !== current)
|
|
129
|
+
writeFileSync(configPath, next);
|
|
130
|
+
return ".codex/config.toml";
|
|
131
|
+
}
|
|
132
|
+
const OPENSPEC_CONFIG_PATH = "openspec/config.yaml";
|
|
133
|
+
const OPENSPEC_CHINESE_CONTEXT_BLOCK = "context: |\n" +
|
|
134
|
+
" 语言:中文(简体)\n" +
|
|
135
|
+
" 所有产出物必须用简体中文撰写。\n";
|
|
136
|
+
const OPENSPEC_DEFAULT_CONFIG = "schema: spec-driven\n\n" +
|
|
137
|
+
OPENSPEC_CHINESE_CONTEXT_BLOCK;
|
|
138
|
+
function hasTopLevelContext(content) {
|
|
139
|
+
return /^context\s*:/m.test(content);
|
|
140
|
+
}
|
|
141
|
+
function ensureOpenSpecChineseContext(projectRoot) {
|
|
142
|
+
const openspecDir = join(projectRoot, "openspec");
|
|
143
|
+
const configPath = join(projectRoot, OPENSPEC_CONFIG_PATH);
|
|
144
|
+
mkdirSync(openspecDir, { recursive: true });
|
|
145
|
+
if (!existsSync(configPath)) {
|
|
146
|
+
writeFileSync(configPath, OPENSPEC_DEFAULT_CONFIG);
|
|
147
|
+
return OPENSPEC_CONFIG_PATH;
|
|
148
|
+
}
|
|
149
|
+
const current = readFileSync(configPath, "utf8");
|
|
150
|
+
if (hasTopLevelContext(current))
|
|
151
|
+
return OPENSPEC_CONFIG_PATH;
|
|
152
|
+
const trimmed = current.trimEnd();
|
|
153
|
+
const next = trimmed.length > 0
|
|
154
|
+
? `${trimmed}\n\n${OPENSPEC_CHINESE_CONTEXT_BLOCK}`
|
|
155
|
+
: OPENSPEC_DEFAULT_CONFIG;
|
|
156
|
+
writeFileSync(configPath, next);
|
|
157
|
+
return OPENSPEC_CONFIG_PATH;
|
|
158
|
+
}
|
|
159
|
+
export function installProject(projectRoot, options = {}) {
|
|
160
|
+
if (legacyStateFound(projectRoot)) {
|
|
161
|
+
throw new Error("检测到老版 SuperSpec (0.x) 的状态文件。\n" +
|
|
162
|
+
"SuperSpec 0.1.16-alpha 是全新引擎,不兼容 0.x 的状态格式。\n" +
|
|
163
|
+
"请先用老版(0.1.x)完成或归档现有 change,再安装 0.1.16-alpha。\n" +
|
|
164
|
+
"或在全新项目目录中安装。");
|
|
165
|
+
}
|
|
166
|
+
const templateRoot = options.templateRoot ?? defaultTemplateRoot();
|
|
167
|
+
assertWorkflowTemplates(templateRoot);
|
|
168
|
+
const engineDir = join(projectRoot, ".superspec");
|
|
169
|
+
mkdirSync(join(engineDir, "changes"), { recursive: true });
|
|
170
|
+
const gitignorePath = join(engineDir, ".gitignore");
|
|
171
|
+
if (!existsSync(gitignorePath))
|
|
172
|
+
writeFileSync(gitignorePath, "changes/\n*.log\n*.tmp\n");
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
message: "SuperSpec 0.1.16-alpha 已安装",
|
|
176
|
+
installed: {
|
|
177
|
+
engine_dir: ".superspec/",
|
|
178
|
+
skills: copySkills(templateRoot, projectRoot),
|
|
179
|
+
prompts: copyPrompts(templateRoot, projectRoot),
|
|
180
|
+
agents: copyAgents(templateRoot, projectRoot),
|
|
181
|
+
config: ensureCodexConfig(projectRoot),
|
|
182
|
+
openspec_config: ensureOpenSpecChineseContext(projectRoot),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|