@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.
Files changed (33) hide show
  1. package/README.md +48 -31
  2. package/dist/cli.js +41 -34
  3. package/dist/format.d.ts +9 -0
  4. package/dist/format.js +35 -5
  5. package/dist/install.d.ts +19 -0
  6. package/dist/install.js +185 -0
  7. package/dist/next.js +42 -11
  8. package/dist/record.js +81 -6
  9. package/dist/transition.d.ts +2 -1
  10. package/dist/transition.js +55 -25
  11. package/dist/types.d.ts +5 -1
  12. package/package.json +1 -1
  13. package/templates/workflow/agents/architect.toml +13 -0
  14. package/templates/workflow/agents/code-reviewer.toml +13 -0
  15. package/templates/workflow/agents/critic.toml +13 -0
  16. package/templates/workflow/agents/executor.toml +13 -0
  17. package/templates/workflow/agents/explore.toml +13 -0
  18. package/templates/workflow/agents/test-engineer.toml +13 -0
  19. package/templates/workflow/agents/test-runner.toml +13 -0
  20. package/templates/workflow/agents/verifier.toml +13 -0
  21. package/templates/workflow/prompts/architect.md +44 -0
  22. package/templates/workflow/prompts/code-reviewer.md +34 -0
  23. package/templates/workflow/prompts/critic.md +47 -0
  24. package/templates/workflow/prompts/executor.md +32 -0
  25. package/templates/workflow/prompts/explore.md +27 -0
  26. package/templates/workflow/prompts/test-engineer.md +45 -0
  27. package/templates/workflow/prompts/test-runner.md +35 -0
  28. package/templates/workflow/prompts/verifier.md +53 -0
  29. package/templates/workflow/skills/superspec-apply/SKILL.md +8 -18
  30. package/templates/workflow/skills/superspec-archive/SKILL.md +2 -9
  31. package/templates/workflow/skills/superspec-explore/SKILL.md +5 -10
  32. package/templates/workflow/skills/superspec-propose/SKILL.md +28 -13
  33. 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 init --scope project
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 init --scope project
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 init --scope project
204
+ superspec install
204
205
  ```
205
206
 
206
- 更新当前项目里的 SuperSpec 入口:
207
+ `superspec init --scope project` 是兼容别名,也会执行同一套安装逻辑。
207
208
 
208
- ```bash
209
- superspec update --scope project
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
- 这条命令会先通过 npm 更新全局 `@peterxiaoyang/superspec`,再用新版本更新当前项目里的入口文件。只想使用当前已安装包更新项目文件时,可以运行:
221
+ `superspec install` 会创建缺失的 `openspec/config.yaml`,或在没有顶层 `context` 时追加这段官方中文 context。如果文件已经有顶层 `context`,SuperSpec 不会覆盖它。
222
+
223
+ 可以用下面的命令检查生成的 instructions 是否包含语言上下文:
213
224
 
214
225
  ```bash
215
- superspec update --scope project --local-only
226
+ openspec instructions proposal --change <change>
216
227
  ```
217
228
 
218
- 卸载当前项目里的 SuperSpec 入口:
229
+ 检查当前项目的 OpenSpec 探测结果:
219
230
 
220
231
  ```bash
221
- superspec uninstall --scope project
232
+ superspec status
222
233
  ```
223
234
 
224
- 这些命令默认不会删除已经生成的 `.superspec/` 过程记录。
225
-
226
- 诊断全局安装、PATH、OpenSpec 依赖和 npm bin 指向问题:
235
+ 更新当前 beta CLI:
227
236
 
228
237
  ```bash
229
- superspec doctor
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
- SuperSpec 内部还有一些检查命令,例如:
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 run pack:dry-run
281
+ npm pack --dry-run
264
282
  ```
265
283
 
266
284
  更多细节见:
267
285
 
268
- - `docs/SPEC.md`:完整设计和规则
269
- - `docs/DISTRIBUTION.md`:安装、升级、卸载和分发说明
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.15-alpha
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 [--global] 安装到项目或全局
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.15-alpha");
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
- const { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync } = await import("node:fs");
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: "检测到老版 SuperSpec (0.x) 的状态文件。\n" +
94
- "SuperSpec 0.1.15-alpha 是全新引擎,不兼容 0.x 的状态格式。\n" +
95
- "请先用老版(0.1.x)完成或归档现有 change,再安装 0.1.15-alpha。\n" +
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.15-alpha 是全新引擎,不能从 0.x 直接 update。\n" +
116
- "请用 npm install -g @peterxiaoyang/superspec@0.1.15-alpha 手动安装。\n" +
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.15-alpha" }));
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: jobs.open.length,
145
- accepted_jobs: jobs.accepted.length,
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
- console.log(JSON.stringify(transitionExplore(projectRoot, change, cr), null, 2));
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 ?? "normal";
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 ?? "normal";
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 ?? "normal";
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
- /** discovery.md 提取"待确认问题"段内的未确认项数量 */
16
- export function countDiscoveryOpenQuestions(content) {
17
- // 找"待确认问题"标题(中英文兼容)
18
- const sectionMatch = content.match(/^#{1,6}\s*(待确认问题|Open Questions|Pending Questions)\s*$/im);
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(/- \[ \]/g);
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;
@@ -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
+ }