@peterxiaoyang/superspec 0.1.25-beta.0 → 0.1.27
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 +10 -2
- package/dist/cli.js +34 -12
- package/dist/install.d.ts +1 -0
- package/dist/install.js +37 -1
- package/dist/job_action.d.ts +6 -0
- package/dist/job_action.js +21 -0
- package/dist/next.js +37 -48
- package/dist/record.js +2 -0
- package/dist/transition.d.ts +11 -4
- package/dist/transition.js +19 -2
- package/dist/types.d.ts +10 -6
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +11 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +2 -0
- package/templates/workflow/skills/superspec-archive/SKILL.md +2 -0
- package/templates/workflow/skills/superspec-explore/SKILL.md +10 -10
- package/templates/workflow/skills/superspec-propose/SKILL.md +3 -1
- package/templates/workflow/skills/superspec-review/SKILL.md +3 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ superspec install
|
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
这条命令的意思是:把 SuperSpec 当前可用的工作流入口安装到项目里。
|
|
84
|
-
当前 beta 会安装 `.superspec/` 引擎目录、`.codex/skills/superspec-*` 阶段入口、`.codex/prompts/*.md` 角色 prompt、`.codex/agents/*.toml`
|
|
84
|
+
当前 beta 会安装 `.superspec/` 引擎目录、`.codex/skills/superspec-*` 阶段入口、`.codex/prompts/*.md` 角色 prompt、`.codex/agents/*.toml` 子智能体配置,补齐 `.codex/config.toml` 的多 agent 开关,并在项目根 `AGENTS.md` 中维护 SuperSpec 轻量门禁片段。`superspec init --scope project` 仍作为兼容别名可用。
|
|
85
85
|
|
|
86
86
|
Windows PowerShell 如果拦截 npm 的 `.ps1` 脚本,请改用:
|
|
87
87
|
|
|
@@ -160,6 +160,14 @@ openspec/changes/<变更ID>/.superspec/
|
|
|
160
160
|
`.superspec/` 要不要提交到 git,由你的团队决定。
|
|
161
161
|
如果不提交,删掉后就没有 git 历史可以恢复。
|
|
162
162
|
|
|
163
|
+
## 流程门禁
|
|
164
|
+
|
|
165
|
+
SuperSpec 的阶段入口由 `superspec transition next --change <变更ID>` 驱动。
|
|
166
|
+
|
|
167
|
+
当当前阶段还有未完成的用户确认、审查、验证或工作项时,`next` 会先返回这些事项,不会把下一阶段命令作为推荐路径。重复运行会创建审查工作项的 transition 时,如果同阶段工作项已经存在,CLI 会返回正常的门禁结果,不会写入新事件,也不会把它当作程序错误。
|
|
168
|
+
|
|
169
|
+
这仍然是轻量流程控制,不是写入拦截。它约束按 SuperSpec 正常入口执行时的下一步建议和状态提交结果,不承诺阻止绕过流程的手动编辑。
|
|
170
|
+
|
|
163
171
|
## Hook 会做什么
|
|
164
172
|
|
|
165
173
|
SuperSpec 默认安装的 hook 只在子智能体启动和停止时运行:
|
|
@@ -238,7 +246,7 @@ superspec status
|
|
|
238
246
|
superspec update
|
|
239
247
|
```
|
|
240
248
|
|
|
241
|
-
这条命令会先检查 npm 上的 latest 版本;如果有新版,会自动执行全局升级并用新版 CLI 重新同步项目入口。同步内容包括补齐 `.superspec/changes`
|
|
249
|
+
这条命令会先检查 npm 上的 latest 版本;如果有新版,会自动执行全局升级并用新版 CLI 重新同步项目入口。同步内容包括补齐 `.superspec/changes` 运行时目录,把当前 CLI 内置的 `.codex/skills/superspec-*`、`.codex/prompts/*.md`、`.codex/agents/*.toml` 同步到项目里,并更新 `AGENTS.md` 中 marker 包裹的 SuperSpec 轻量门禁片段。
|
|
242
250
|
|
|
243
251
|
## 进阶信息
|
|
244
252
|
|
package/dist/cli.js
CHANGED
|
@@ -54,6 +54,16 @@ function readStdinRecordContent(flag) {
|
|
|
54
54
|
}
|
|
55
55
|
return readFileSync(0, "utf8");
|
|
56
56
|
}
|
|
57
|
+
function transitionExitCode(result) {
|
|
58
|
+
if (result.outcome === "blocked")
|
|
59
|
+
return 0;
|
|
60
|
+
return result.events_written === 0 ? 1 : 0;
|
|
61
|
+
}
|
|
62
|
+
function proposeReadyExitCode(result) {
|
|
63
|
+
if (result.outcome === "blocked")
|
|
64
|
+
return 0;
|
|
65
|
+
return result.events_written === 0 && result.message.includes("不能") ? 1 : 0;
|
|
66
|
+
}
|
|
57
67
|
function parseVersion(version) {
|
|
58
68
|
const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
59
69
|
if (!match)
|
|
@@ -106,6 +116,18 @@ function isTestMode() {
|
|
|
106
116
|
function testEnv(name) {
|
|
107
117
|
return isTestMode() ? process.env[name] : undefined;
|
|
108
118
|
}
|
|
119
|
+
function runtimePlatform() {
|
|
120
|
+
return testEnv("SUPERSPEC_TEST_PLATFORM") ?? process.platform;
|
|
121
|
+
}
|
|
122
|
+
function windowsAwareCommand(name) {
|
|
123
|
+
return runtimePlatform() === "win32" ? `${name}.cmd` : name;
|
|
124
|
+
}
|
|
125
|
+
function npmCommand() {
|
|
126
|
+
return windowsAwareCommand("npm");
|
|
127
|
+
}
|
|
128
|
+
function superspecCommand() {
|
|
129
|
+
return windowsAwareCommand("superspec");
|
|
130
|
+
}
|
|
109
131
|
function selfUpdateError(phase, err, latest = null) {
|
|
110
132
|
return new SelfUpdateError(phase, commandErrorMessage(err), latest);
|
|
111
133
|
}
|
|
@@ -147,7 +169,7 @@ function npmLatestVersion() {
|
|
|
147
169
|
if (testLatest)
|
|
148
170
|
return testLatest;
|
|
149
171
|
try {
|
|
150
|
-
const output = execFileSync(
|
|
172
|
+
const output = execFileSync(npmCommand(), ["view", PACKAGE_NAME, "version"], { encoding: "utf8" });
|
|
151
173
|
return output.trim().replace(/^"|"$/g, "");
|
|
152
174
|
}
|
|
153
175
|
catch (err) {
|
|
@@ -161,7 +183,7 @@ function installLatestGlobal() {
|
|
|
161
183
|
if (testEnv("SUPERSPEC_TEST_SKIP_GLOBAL_INSTALL") === "1")
|
|
162
184
|
return;
|
|
163
185
|
try {
|
|
164
|
-
execFileSync(
|
|
186
|
+
execFileSync(npmCommand(), ["install", "-g", `${PACKAGE_NAME}@latest`], {
|
|
165
187
|
encoding: "utf8",
|
|
166
188
|
stdio: ["ignore", "pipe", "pipe"],
|
|
167
189
|
});
|
|
@@ -188,7 +210,7 @@ function pathCliVersion() {
|
|
|
188
210
|
throw new Error(`无法解析 superspec --version 输出:${testOutput}`);
|
|
189
211
|
return parsed;
|
|
190
212
|
}
|
|
191
|
-
const output = execFileSync(
|
|
213
|
+
const output = execFileSync(superspecCommand(), ["--version"], {
|
|
192
214
|
encoding: "utf8",
|
|
193
215
|
env: process.env,
|
|
194
216
|
});
|
|
@@ -217,7 +239,7 @@ function rerunUpdatedCli(projectRoot, args) {
|
|
|
217
239
|
if (testOutput)
|
|
218
240
|
return testOutput;
|
|
219
241
|
try {
|
|
220
|
-
return execFileSync(
|
|
242
|
+
return execFileSync(superspecCommand(), args, {
|
|
221
243
|
cwd: projectRoot,
|
|
222
244
|
encoding: "utf8",
|
|
223
245
|
env: process.env,
|
|
@@ -494,12 +516,12 @@ jobs 子命令:
|
|
|
494
516
|
const risk = opts.risk ?? "strict";
|
|
495
517
|
const result = proposeReady(projectRoot, change, cr, risk);
|
|
496
518
|
console.log(JSON.stringify(result, null, 2));
|
|
497
|
-
return result
|
|
519
|
+
return proposeReadyExitCode(result);
|
|
498
520
|
}
|
|
499
521
|
case "start-apply": {
|
|
500
522
|
const result = startApply(projectRoot, change, cr);
|
|
501
523
|
console.log(JSON.stringify(result, null, 2));
|
|
502
|
-
return result
|
|
524
|
+
return transitionExitCode(result);
|
|
503
525
|
}
|
|
504
526
|
case "task-start": {
|
|
505
527
|
const taskId = opts.task;
|
|
@@ -509,7 +531,7 @@ jobs 子命令:
|
|
|
509
531
|
}
|
|
510
532
|
const result = taskStart(projectRoot, change, cr, taskId);
|
|
511
533
|
console.log(JSON.stringify(result, null, 2));
|
|
512
|
-
return result
|
|
534
|
+
return transitionExitCode(result);
|
|
513
535
|
}
|
|
514
536
|
case "task-complete": {
|
|
515
537
|
const taskId = opts.task;
|
|
@@ -519,7 +541,7 @@ jobs 子命令:
|
|
|
519
541
|
}
|
|
520
542
|
const result = taskComplete(projectRoot, change, cr, taskId);
|
|
521
543
|
console.log(JSON.stringify(result, null, 2));
|
|
522
|
-
return result
|
|
544
|
+
return transitionExitCode(result);
|
|
523
545
|
}
|
|
524
546
|
case "reopen": {
|
|
525
547
|
const to = opts.to;
|
|
@@ -534,23 +556,23 @@ jobs 子命令:
|
|
|
534
556
|
}
|
|
535
557
|
const result = reopen(projectRoot, change, cr, to, reason);
|
|
536
558
|
console.log(JSON.stringify(result, null, 2));
|
|
537
|
-
return result
|
|
559
|
+
return transitionExitCode(result);
|
|
538
560
|
}
|
|
539
561
|
case "review-ready": {
|
|
540
562
|
const risk = opts.risk ?? "strict";
|
|
541
563
|
const result = reviewReady(projectRoot, change, cr, risk);
|
|
542
564
|
console.log(JSON.stringify(result, null, 2));
|
|
543
|
-
return result
|
|
565
|
+
return transitionExitCode(result);
|
|
544
566
|
}
|
|
545
567
|
case "accept": {
|
|
546
568
|
const result = accept(projectRoot, change, cr);
|
|
547
569
|
console.log(JSON.stringify(result, null, 2));
|
|
548
|
-
return result
|
|
570
|
+
return transitionExitCode(result);
|
|
549
571
|
}
|
|
550
572
|
case "archive": {
|
|
551
573
|
const result = archive(projectRoot, change, cr);
|
|
552
574
|
console.log(JSON.stringify(result, null, 2));
|
|
553
|
-
return result
|
|
575
|
+
return transitionExitCode(result);
|
|
554
576
|
}
|
|
555
577
|
default:
|
|
556
578
|
console.error(`未知的 transition 子命令:${subcommand}`);
|
package/dist/install.d.ts
CHANGED
package/dist/install.js
CHANGED
|
@@ -55,6 +55,9 @@ function assertWorkflowTemplates(templateRoot) {
|
|
|
55
55
|
if (!existsSync(file))
|
|
56
56
|
missing.push(file);
|
|
57
57
|
}
|
|
58
|
+
const agentsMdTemplate = join(templateRoot, "AGENTS.md");
|
|
59
|
+
if (!existsSync(agentsMdTemplate))
|
|
60
|
+
missing.push(agentsMdTemplate);
|
|
58
61
|
if (missing.length > 0) {
|
|
59
62
|
throw new Error(`workflow templates missing: ${missing.join(", ")}`);
|
|
60
63
|
}
|
|
@@ -66,7 +69,6 @@ function writeBundledFile(src, dest) {
|
|
|
66
69
|
const current = readFileSync(dest, "utf8");
|
|
67
70
|
if (current === next)
|
|
68
71
|
return;
|
|
69
|
-
writeFileSync(`${dest}.bak`, current);
|
|
70
72
|
}
|
|
71
73
|
writeFileSync(dest, next);
|
|
72
74
|
}
|
|
@@ -242,6 +244,38 @@ function ensureOpenSpecChineseContext(projectRoot) {
|
|
|
242
244
|
writeFileSync(configPath, next);
|
|
243
245
|
return OPENSPEC_CONFIG_PATH;
|
|
244
246
|
}
|
|
247
|
+
const AGENTS_MD_PATH = "AGENTS.md";
|
|
248
|
+
const SUPERSPEC_AGENTS_START = "<!-- SUPERSPEC:AGENTS:START -->";
|
|
249
|
+
const SUPERSPEC_AGENTS_END = "<!-- SUPERSPEC:AGENTS:END -->";
|
|
250
|
+
function readAgentsMdTemplate(templateRoot) {
|
|
251
|
+
const template = readFileSync(join(templateRoot, AGENTS_MD_PATH), "utf8").trimEnd();
|
|
252
|
+
if (!template.includes(SUPERSPEC_AGENTS_START) || !template.includes(SUPERSPEC_AGENTS_END)) {
|
|
253
|
+
throw new Error(`workflow AGENTS.md template missing SuperSpec markers: ${join(templateRoot, AGENTS_MD_PATH)}`);
|
|
254
|
+
}
|
|
255
|
+
return template;
|
|
256
|
+
}
|
|
257
|
+
function replaceMarkerBlock(content, block) {
|
|
258
|
+
const start = content.indexOf(SUPERSPEC_AGENTS_START);
|
|
259
|
+
const end = content.indexOf(SUPERSPEC_AGENTS_END);
|
|
260
|
+
if (start < 0 || end < 0 || end < start)
|
|
261
|
+
return null;
|
|
262
|
+
const afterEnd = end + SUPERSPEC_AGENTS_END.length;
|
|
263
|
+
return `${content.slice(0, start)}${block}${content.slice(afterEnd)}`;
|
|
264
|
+
}
|
|
265
|
+
function ensureAgentsMd(projectRoot, block) {
|
|
266
|
+
const agentsPath = join(projectRoot, AGENTS_MD_PATH);
|
|
267
|
+
if (!existsSync(agentsPath)) {
|
|
268
|
+
writeFileSync(agentsPath, `${block}\n`);
|
|
269
|
+
return AGENTS_MD_PATH;
|
|
270
|
+
}
|
|
271
|
+
const current = readFileSync(agentsPath, "utf8");
|
|
272
|
+
const replaced = replaceMarkerBlock(current, block);
|
|
273
|
+
const next = replaced ?? `${current.trimEnd()}\n\n${block}\n`;
|
|
274
|
+
if (next === current)
|
|
275
|
+
return AGENTS_MD_PATH;
|
|
276
|
+
writeFileSync(agentsPath, next);
|
|
277
|
+
return AGENTS_MD_PATH;
|
|
278
|
+
}
|
|
245
279
|
export function installProject(projectRoot, options = {}) {
|
|
246
280
|
if (!options.allowLegacyState && legacyStateFound(projectRoot)) {
|
|
247
281
|
throw new Error("检测到老版 SuperSpec (0.x) 的状态文件。\n" +
|
|
@@ -251,6 +285,7 @@ export function installProject(projectRoot, options = {}) {
|
|
|
251
285
|
}
|
|
252
286
|
const templateRoot = options.templateRoot ?? defaultTemplateRoot();
|
|
253
287
|
assertWorkflowTemplates(templateRoot);
|
|
288
|
+
const agentsMdTemplate = readAgentsMdTemplate(templateRoot);
|
|
254
289
|
const engineDir = join(projectRoot, ".superspec");
|
|
255
290
|
mkdirSync(join(engineDir, "changes"), { recursive: true });
|
|
256
291
|
const gitignorePath = join(engineDir, ".gitignore");
|
|
@@ -266,6 +301,7 @@ export function installProject(projectRoot, options = {}) {
|
|
|
266
301
|
prompts: copyPrompts(templateRoot, projectRoot),
|
|
267
302
|
agents: copyAgents(templateRoot, projectRoot),
|
|
268
303
|
config: ensureCodexConfig(projectRoot),
|
|
304
|
+
agents_md: ensureAgentsMd(projectRoot, agentsMdTemplate),
|
|
269
305
|
openspec_config: ensureOpenSpecChineseContext(projectRoot),
|
|
270
306
|
},
|
|
271
307
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Job, RequiredJobAction } from "./types.ts";
|
|
2
|
+
export declare function jobPacketCommand(change: string, jobId: string): string;
|
|
3
|
+
export declare function jobPacketArgv(change: string, jobId: string): string[];
|
|
4
|
+
export declare function requiredJobAction(change: string, job: Job): RequiredJobAction;
|
|
5
|
+
export declare function requiredJobActions(change: string, jobs: Job[]): RequiredJobAction[];
|
|
6
|
+
export declare function jobSubmitArgv(change: string, jobId: string): string[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SuperSpec workflow engine - shared job action protocol helpers.
|
|
2
|
+
export function jobPacketCommand(change, jobId) {
|
|
3
|
+
return `superspec jobs packet --change "${change}" --job "${jobId}"`;
|
|
4
|
+
}
|
|
5
|
+
export function jobPacketArgv(change, jobId) {
|
|
6
|
+
return ["superspec", "jobs", "packet", "--change", change, "--job", jobId];
|
|
7
|
+
}
|
|
8
|
+
export function requiredJobAction(change, job) {
|
|
9
|
+
return {
|
|
10
|
+
job_id: job.job_id,
|
|
11
|
+
role: job.role,
|
|
12
|
+
packet_command: jobPacketCommand(change, job.job_id),
|
|
13
|
+
packet_argv: jobPacketArgv(change, job.job_id),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function requiredJobActions(change, jobs) {
|
|
17
|
+
return jobs.map(job => requiredJobAction(change, job));
|
|
18
|
+
}
|
|
19
|
+
export function jobSubmitArgv(change, jobId) {
|
|
20
|
+
return ["superspec", "record", "job-submit", "--change", change, "--job", jobId, "--report", "-"];
|
|
21
|
+
}
|
package/dist/next.js
CHANGED
|
@@ -4,13 +4,22 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { readEvents, sha256Text } from "./store.js";
|
|
6
6
|
import { isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest } from "./review.js";
|
|
7
|
+
import { requiredJobActions } from "./job_action.js";
|
|
7
8
|
import { validateDiscovery, countDiscoveryOpenQuestions, collectProposeOpenQuestions, parseTasksMd, pendingTasksInContent } from "./format.js";
|
|
8
9
|
const ACTIVE_PROPOSAL_REVIEW_ROLES = new Set(["critic", "architect", "test-engineer"]);
|
|
9
10
|
function isActiveProposalReviewJob(job) {
|
|
10
11
|
return job.created_from_transition === "propose-ready" && ACTIVE_PROPOSAL_REVIEW_ROLES.has(job.role);
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
-
return
|
|
13
|
+
function isExploreReviewJob(job) {
|
|
14
|
+
return job.created_from_transition === "explore" && job.role === "critic";
|
|
15
|
+
}
|
|
16
|
+
function requiredJobsOutput(state, change, jobs, reason) {
|
|
17
|
+
return {
|
|
18
|
+
state,
|
|
19
|
+
path: "required_job",
|
|
20
|
+
required_jobs: requiredJobActions(change, jobs),
|
|
21
|
+
reason,
|
|
22
|
+
};
|
|
14
23
|
}
|
|
15
24
|
function transitionCommand(change, name, extra = "") {
|
|
16
25
|
return `superspec transition ${name} --change "${change}"${extra ? " " + extra : ""}`;
|
|
@@ -67,6 +76,9 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
67
76
|
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
68
77
|
switch (snapshot.state) {
|
|
69
78
|
case "init":
|
|
79
|
+
if (snapshot.open_jobs.length > 0) {
|
|
80
|
+
return requiredJobsOutput("init", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
81
|
+
}
|
|
70
82
|
return {
|
|
71
83
|
state: "init",
|
|
72
84
|
path: "next_command",
|
|
@@ -75,6 +87,10 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
75
87
|
missing_inputs: [],
|
|
76
88
|
};
|
|
77
89
|
case "explore": {
|
|
90
|
+
const exploreReviewJobs = snapshot.open_jobs.filter(isExploreReviewJob);
|
|
91
|
+
if (exploreReviewJobs.length > 0) {
|
|
92
|
+
return requiredJobsOutput("explore", change, exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
93
|
+
}
|
|
78
94
|
// Phase 2:检查 discovery.md
|
|
79
95
|
const discoveryCheck = validateDiscovery(changeRoot);
|
|
80
96
|
if (!discoveryCheck.ok) {
|
|
@@ -117,17 +133,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
117
133
|
}
|
|
118
134
|
const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
|
|
119
135
|
if (proposalReviewJobs.length > 0) {
|
|
120
|
-
|
|
121
|
-
job_id: j.job_id,
|
|
122
|
-
role: j.role,
|
|
123
|
-
packet_command: packetCommand(change, j.job_id),
|
|
124
|
-
}));
|
|
125
|
-
return {
|
|
126
|
-
state: "propose",
|
|
127
|
-
path: "required_job",
|
|
128
|
-
required_jobs: jobs,
|
|
129
|
-
reason: `有 ${jobs.length} 个待完成工作项`,
|
|
130
|
-
};
|
|
136
|
+
return requiredJobsOutput("propose", change, proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
131
137
|
}
|
|
132
138
|
return {
|
|
133
139
|
state: "propose",
|
|
@@ -140,16 +146,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
140
146
|
case "propose_ready": {
|
|
141
147
|
const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
|
|
142
148
|
if (proposalReviewJobs.length > 0) {
|
|
143
|
-
return {
|
|
144
|
-
state: "propose_ready",
|
|
145
|
-
path: "required_job",
|
|
146
|
-
required_jobs: proposalReviewJobs.map(j => ({
|
|
147
|
-
job_id: j.job_id,
|
|
148
|
-
role: j.role,
|
|
149
|
-
packet_command: packetCommand(change, j.job_id),
|
|
150
|
-
})),
|
|
151
|
-
reason: `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`,
|
|
152
|
-
};
|
|
149
|
+
return requiredJobsOutput("propose_ready", change, proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
153
150
|
}
|
|
154
151
|
return {
|
|
155
152
|
state: "propose_ready",
|
|
@@ -160,6 +157,9 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
160
157
|
};
|
|
161
158
|
}
|
|
162
159
|
case "apply": {
|
|
160
|
+
if (snapshot.open_jobs.length > 0) {
|
|
161
|
+
return requiredJobsOutput("apply", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
162
|
+
}
|
|
163
163
|
// 检查是否所有任务已完成
|
|
164
164
|
const pending = pendingTaskIds(changeRoot);
|
|
165
165
|
if (pending.length > 0) {
|
|
@@ -190,15 +190,6 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
190
190
|
missing_inputs: [],
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
-
// 有 open job → 做
|
|
194
|
-
if (snapshot.open_jobs.length > 0) {
|
|
195
|
-
return {
|
|
196
|
-
state: "apply",
|
|
197
|
-
path: "required_job",
|
|
198
|
-
required_jobs: snapshot.open_jobs.map(j => ({ job_id: j.job_id, role: j.role, packet_command: packetCommand(change, j.job_id) })),
|
|
199
|
-
reason: `有 ${snapshot.open_jobs.length} 个待完成工作项`,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
193
|
return {
|
|
203
194
|
state: "apply",
|
|
204
195
|
path: "next_command",
|
|
@@ -219,12 +210,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
219
210
|
};
|
|
220
211
|
}
|
|
221
212
|
if (snapshot.open_jobs.length > 0) {
|
|
222
|
-
return {
|
|
223
|
-
state: "apply_done",
|
|
224
|
-
path: "required_job",
|
|
225
|
-
required_jobs: snapshot.open_jobs.map(j => ({ job_id: j.job_id, role: j.role, packet_command: packetCommand(change, j.job_id) })),
|
|
226
|
-
reason: `有 ${snapshot.open_jobs.length} 个待完成工作项`,
|
|
227
|
-
};
|
|
213
|
+
return requiredJobsOutput("apply_done", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
228
214
|
}
|
|
229
215
|
return {
|
|
230
216
|
state: "apply_done",
|
|
@@ -247,16 +233,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
247
233
|
}
|
|
248
234
|
const reviewVerifierJobs = snapshot.open_jobs.filter(isReviewReadyVerifier);
|
|
249
235
|
if (reviewVerifierJobs.length > 0) {
|
|
250
|
-
return {
|
|
251
|
-
state: "review",
|
|
252
|
-
path: "required_job",
|
|
253
|
-
required_jobs: reviewVerifierJobs.map(j => ({
|
|
254
|
-
job_id: j.job_id,
|
|
255
|
-
role: j.role,
|
|
256
|
-
packet_command: packetCommand(change, j.job_id),
|
|
257
|
-
})),
|
|
258
|
-
reason: `有 ${reviewVerifierJobs.length} 个待完成最终验证工作项`,
|
|
259
|
-
};
|
|
236
|
+
return requiredJobsOutput("review", change, reviewVerifierJobs, `有 ${reviewVerifierJobs.length} 个待完成最终验证工作项`);
|
|
260
237
|
}
|
|
261
238
|
const events = readEvents(projectRoot, change);
|
|
262
239
|
const policy = readReviewPolicyFromEvents(events);
|
|
@@ -291,6 +268,9 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
291
268
|
};
|
|
292
269
|
}
|
|
293
270
|
case "accepted":
|
|
271
|
+
if (snapshot.open_jobs.length > 0) {
|
|
272
|
+
return requiredJobsOutput("accepted", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不归档`);
|
|
273
|
+
}
|
|
294
274
|
return {
|
|
295
275
|
state: "accepted",
|
|
296
276
|
path: "next_command",
|
|
@@ -299,11 +279,20 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
299
279
|
missing_inputs: [],
|
|
300
280
|
};
|
|
301
281
|
case "archive":
|
|
282
|
+
if (snapshot.open_jobs.length > 0) {
|
|
283
|
+
return requiredJobsOutput("archive", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不结束`);
|
|
284
|
+
}
|
|
302
285
|
return {
|
|
303
286
|
state: "archive",
|
|
304
287
|
path: "done",
|
|
305
288
|
reason: "已归档,流程完成。",
|
|
306
289
|
};
|
|
290
|
+
case "abandoned":
|
|
291
|
+
return {
|
|
292
|
+
state: "abandoned",
|
|
293
|
+
path: "done",
|
|
294
|
+
reason: "变更已放弃,流程终止。",
|
|
295
|
+
};
|
|
307
296
|
default:
|
|
308
297
|
return {
|
|
309
298
|
state: snapshot.state,
|
package/dist/record.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha256Text, withLock, appendRawRecord, } from "./store.js";
|
|
5
5
|
import { reviewEvidenceDigest, reviewVerifierStaleReason } from "./review.js";
|
|
6
|
+
import { jobSubmitArgv } from "./job_action.js";
|
|
6
7
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
7
8
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
8
9
|
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
@@ -321,6 +322,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
321
322
|
required_output_kind: "job_report_json",
|
|
322
323
|
preferred_input_mode: "stdin",
|
|
323
324
|
submission_command: `superspec record job-submit --change "${change}" --job "${job.job_id}" --report -`,
|
|
325
|
+
submission_argv: jobSubmitArgv(change, job.job_id),
|
|
324
326
|
file_fallback: true,
|
|
325
327
|
output_contract_fields: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
326
328
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
package/dist/transition.d.ts
CHANGED
|
@@ -13,15 +13,22 @@ interface Decision {
|
|
|
13
13
|
details?: Record<string, unknown>;
|
|
14
14
|
postCommit?: (projectRoot: string, change: string, changeRoot: string) => void;
|
|
15
15
|
}
|
|
16
|
+
interface SkipDecision {
|
|
17
|
+
skip: true;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
interface BlockedDecision {
|
|
21
|
+
blocked: true;
|
|
22
|
+
reason: string;
|
|
23
|
+
jobs: Job[];
|
|
24
|
+
details?: Record<string, unknown>;
|
|
25
|
+
}
|
|
16
26
|
/**
|
|
17
27
|
* 统一 transition 提交协议——所有校验在锁内。
|
|
18
28
|
*/
|
|
19
29
|
export declare function commitTransition(projectRoot: string, change: string, changeRoot: string, opts: {
|
|
20
30
|
name: string;
|
|
21
|
-
decide: (snapshot: Snapshot) => Decision |
|
|
22
|
-
skip: true;
|
|
23
|
-
message: string;
|
|
24
|
-
};
|
|
31
|
+
decide: (snapshot: Snapshot) => Decision | SkipDecision | BlockedDecision;
|
|
25
32
|
idempotencyInputs?: Record<string, unknown>;
|
|
26
33
|
}): TransitionResult;
|
|
27
34
|
export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
package/dist/transition.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, sha256File, sha256Text, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
|
+
import { requiredJobActions } from "./job_action.js";
|
|
6
7
|
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, } from "./review.js";
|
|
7
8
|
import { validateDiscovery, collectProposeOpenQuestions, findTaskInLines, parseTasksMd, pendingTasksInContent, tasksStructureDigest } from "./format.js";
|
|
8
9
|
let transitionSeq = 0;
|
|
@@ -31,7 +32,7 @@ function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, tr
|
|
|
31
32
|
for (const role of requiredRoles) {
|
|
32
33
|
const openForRole = snapshot.open_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
33
34
|
if (openForRole)
|
|
34
|
-
return {
|
|
35
|
+
return { blocked: true, reason: `状态未推进;已有待完成工作项 ${role}(${openForRole.job_id})`, jobs: [openForRole] };
|
|
35
36
|
const fresh = snapshot.accepted_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
36
37
|
if (!fresh) {
|
|
37
38
|
staleRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
|
|
@@ -114,13 +115,28 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
114
115
|
const existing = events.find(e => e.idempotency_key === idemKey && e.event_type === "transition_commit");
|
|
115
116
|
if (existing) {
|
|
116
117
|
const p = existing.payload;
|
|
118
|
+
const newJobs = p.new_jobs ?? [];
|
|
117
119
|
return {
|
|
118
120
|
transition: name, outcome: p.outcome,
|
|
119
121
|
from_state: p.from_state, to_state: p.to_state,
|
|
120
122
|
created_jobs: p.created_job_ids ?? [], message: "幂等返回", events_written: 0,
|
|
123
|
+
...(newJobs.length > 0 ? { required_jobs: requiredJobActions(change, newJobs) } : {}),
|
|
121
124
|
};
|
|
122
125
|
}
|
|
123
126
|
const decision = opts.decide(snapshot);
|
|
127
|
+
if ("blocked" in decision) {
|
|
128
|
+
return {
|
|
129
|
+
transition: name,
|
|
130
|
+
outcome: "blocked",
|
|
131
|
+
from_state: snapshot.state,
|
|
132
|
+
to_state: snapshot.state,
|
|
133
|
+
created_jobs: [],
|
|
134
|
+
required_jobs: requiredJobActions(change, decision.jobs),
|
|
135
|
+
message: decision.reason,
|
|
136
|
+
events_written: 0,
|
|
137
|
+
...(decision.details ? { details: decision.details } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
124
140
|
if ("skip" in decision) {
|
|
125
141
|
return {
|
|
126
142
|
transition: name, outcome: "advanced",
|
|
@@ -163,6 +179,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
163
179
|
return {
|
|
164
180
|
transition: name, outcome, from_state: fromState, to_state: toState,
|
|
165
181
|
created_jobs: newJobs.map(j => j.job_id),
|
|
182
|
+
...(newJobs.length > 0 ? { required_jobs: requiredJobActions(change, newJobs) } : {}),
|
|
166
183
|
message: outcome === "advanced" ? `状态推进:${fromState} → ${toState}` : `状态不变(${fromState}),创建了 ${newJobs.length} 个工作项`,
|
|
167
184
|
events_written: 1 + extraEvents.length,
|
|
168
185
|
...(details ? { details } : {}),
|
|
@@ -348,7 +365,7 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
348
365
|
if (snapshot.state === "apply_done" || snapshot.state === "review") {
|
|
349
366
|
const verifierOpen = snapshot.open_jobs.find(isReviewReadyVerifier);
|
|
350
367
|
if (verifierOpen)
|
|
351
|
-
return {
|
|
368
|
+
return { blocked: true, reason: `状态未推进;已有待完成最终验证工作项 ${verifierOpen.job_id}`, jobs: [verifierOpen] };
|
|
352
369
|
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
|
|
353
370
|
if (!policy.requires_verifier) {
|
|
354
371
|
if (snapshot.state === "apply_done") {
|
package/dist/types.d.ts
CHANGED
|
@@ -26,12 +26,19 @@ export interface JobPacket {
|
|
|
26
26
|
required_output_kind: string;
|
|
27
27
|
preferred_input_mode?: "stdin" | "file";
|
|
28
28
|
submission_command?: string;
|
|
29
|
+
submission_argv?: string[];
|
|
29
30
|
file_fallback?: boolean;
|
|
30
31
|
output_contract_fields?: string[];
|
|
31
32
|
output_contract_optional_fields?: string[];
|
|
32
33
|
stop_conditions: string[];
|
|
33
34
|
created_from_transition: string;
|
|
34
35
|
}
|
|
36
|
+
export interface RequiredJobAction {
|
|
37
|
+
job_id: string;
|
|
38
|
+
role: JobRole;
|
|
39
|
+
packet_command: string;
|
|
40
|
+
packet_argv: string[];
|
|
41
|
+
}
|
|
35
42
|
export type EventType = "transition_prepare" | "transition_commit" | "job_requested" | "job_invalidated" | "reopen" | "abandon" | "task_started" | "task_completed" | "task_abandoned" | "job_accepted" | "job_rejected" | "user_decision_recorded" | "test_run_recorded" | "task_activation_recorded" | "artifact_recorded";
|
|
36
43
|
export interface Event {
|
|
37
44
|
event_id: string;
|
|
@@ -120,11 +127,7 @@ export type NextOutput = {
|
|
|
120
127
|
missing_inputs: MissingInput[];
|
|
121
128
|
} | {
|
|
122
129
|
path: "required_job";
|
|
123
|
-
required_jobs:
|
|
124
|
-
job_id: string;
|
|
125
|
-
role: JobRole;
|
|
126
|
-
packet_command: string;
|
|
127
|
-
}[];
|
|
130
|
+
required_jobs: RequiredJobAction[];
|
|
128
131
|
reason: string;
|
|
129
132
|
} | {
|
|
130
133
|
path: "ask_user";
|
|
@@ -136,10 +139,11 @@ export type NextOutput = {
|
|
|
136
139
|
});
|
|
137
140
|
export interface TransitionResult {
|
|
138
141
|
transition: string;
|
|
139
|
-
outcome: "advanced" | "job_created";
|
|
142
|
+
outcome: "advanced" | "job_created" | "blocked";
|
|
140
143
|
from_state: State;
|
|
141
144
|
to_state: State;
|
|
142
145
|
created_jobs: string[];
|
|
146
|
+
required_jobs?: RequiredJobAction[];
|
|
143
147
|
message: string;
|
|
144
148
|
events_written: number;
|
|
145
149
|
details?: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- SUPERSPEC:AGENTS:START -->
|
|
2
|
+
# SuperSpec
|
|
3
|
+
|
|
4
|
+
本项目启用 SuperSpec。使用 `superspec-*` 工作流时,以 `superspec transition next --change "<change>"` 返回的下一步为准;流程完成前不得跳阶段、不得自称完成。
|
|
5
|
+
|
|
6
|
+
SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
|
|
7
|
+
|
|
8
|
+
审查/验证工作项只授权处理该工作项绑定的内容,不得扩大范围、跳过阶段或代替后续流程。若当前环境无法启动 subagent,只能使用本地或用户已授权的独立来源;没有可用独立来源时停在当前工作项并说明阻塞,主会话不得因此自批。
|
|
9
|
+
|
|
10
|
+
执行 CLI 返回命令时优先使用 `*_argv` 字段。用户可见回复使用自然语言;除非用户要求调试信息,不复述内部 JSON 字段。
|
|
11
|
+
<!-- SUPERSPEC:AGENTS:END -->
|
|
@@ -19,7 +19,9 @@ metadata:
|
|
|
19
19
|
3. 登记结果
|
|
20
20
|
4. 回到 1
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
如果下一步提示当前阶段还有用户确认、审查或验证事项,先完成这些事项。完成前不要进入下一阶段,也不要修改业务代码;对用户说明时使用自然语言,不默认复述内部 JSON 字段或完整 packet。
|
|
23
|
+
|
|
24
|
+
如果下一步说明 discovery 不完整或有未确认问题,先检查并填写 discovery 草稿,不要把草稿占位内容直接转问用户;只有真实阻塞问题才向用户提问,收到回答后优先用 `superspec record user-decision --change "<change>" --input -` 从 stdin 登记 JSON 内容;文件路径模式仍可作为 fallback。
|
|
23
25
|
|
|
24
26
|
本技能默认走完整审查路径。探索完成后,`explore → propose` 会先创建 `critic` 工作项,由 Critic 角色审查需求澄清记录。审查完成后优先通过 `superspec record job-submit --change "<change>" --job <JOB> --report -` 从 stdin 登记 JSON 报告内容;文件路径模式仍可作为 fallback。
|
|
25
27
|
|
|
@@ -52,22 +54,20 @@ next 返回 `ask_user` 说明 discovery 不完整或有未确认问题。若 sco
|
|
|
52
54
|
|
|
53
55
|
写入 `openspec/changes/<change>/.superspec/artifacts/discovery.md`:
|
|
54
56
|
|
|
55
|
-
如果文件已经存在并包含 `<!-- superspec:discovery-draft -->` 或“待探索后...”占位文本,说明它是引擎生成的草稿。完成探索后必须删除草稿标记并替换所有占位内容,否则引擎会继续阻止推进。
|
|
56
|
-
|
|
57
57
|
```markdown
|
|
58
58
|
# Discovery
|
|
59
59
|
|
|
60
|
-
## 当前代码事实
|
|
61
|
-
- src/path.ts:10 当前系统怎么工作
|
|
62
|
-
|
|
63
60
|
## 需求理解
|
|
64
|
-
|
|
61
|
+
- 用户目标
|
|
62
|
+
|
|
63
|
+
## 现状
|
|
64
|
+
- 当前系统怎么工作 src/path.ts:10
|
|
65
65
|
|
|
66
|
-
##
|
|
67
|
-
-
|
|
66
|
+
## 影响范围
|
|
67
|
+
- 可能受影响的代码表面和相邻风险
|
|
68
68
|
|
|
69
69
|
## 风险和边界
|
|
70
|
-
|
|
70
|
+
- 技术风险、依赖、兼容性;尽量绑定代码或文档锚点
|
|
71
71
|
|
|
72
72
|
## 待确认问题
|
|
73
73
|
- [ ] 问题1的描述
|
|
@@ -19,7 +19,9 @@ metadata:
|
|
|
19
19
|
3. 登记结果
|
|
20
20
|
4. 回到 1
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
如果下一步提示当前阶段还有用户确认、审查或验证事项,先完成这些事项。完成前不要进入下一阶段,也不要修改业务代码;对用户说明时使用自然语言,不默认复述内部 JSON 字段或完整 packet。
|
|
23
|
+
|
|
24
|
+
如果下一步需要审查,先按返回的审查说明完成对应审查,再优先用 `superspec record job-submit --change "<change>" --job <JOB> --report -` 从 stdin 提交 JSON 审查报告内容;文件路径模式仍可作为 fallback。
|
|
23
25
|
|
|
24
26
|
人类可读正文默认使用简体中文;OpenSpec 结构标题、规范关键字、命令、路径、JSON 字段、代码标识符保留原文。
|
|
25
27
|
如果 OpenSpec 生成文档语言不符合预期,先检查 `openspec/config.yaml` 的官方 `context` 设置;不要在变更文档里添加自定义 `language` 字段。
|
|
@@ -19,7 +19,9 @@ metadata:
|
|
|
19
19
|
3. 登记结果
|
|
20
20
|
4. 回到 1
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
如果下一步提示当前阶段还有用户确认、审查或验证事项,先完成这些事项。完成前不要 accept 或 archive;对用户说明时使用自然语言,不默认复述内部 JSON 字段或完整 packet。
|
|
23
|
+
|
|
24
|
+
如果下一步需要 verifier 工作项,先按返回的验证说明执行核对,再优先用 `superspec record job-submit --change "<change>" --job <JOB> --report -` 从 stdin 提交 JSON 验证报告内容;文件路径模式仍可作为 fallback。
|
|
23
25
|
|
|
24
26
|
`record job-submit` 沿用现有 raw 归档:报告追加到 `raw/review-reports.jsonl`,不会为 review gate 新增 raw 文件类型。
|
|
25
27
|
|