@peterxiaoyang/superspec 0.1.16-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 +21 -5
- package/dist/cli.js +14 -6
- package/dist/format.d.ts +9 -0
- package/dist/format.js +35 -5
- package/dist/install.d.ts +3 -2
- package/dist/install.js +28 -4
- package/dist/next.js +42 -11
- package/dist/record.js +28 -8
- package/dist/transition.d.ts +1 -0
- package/dist/transition.js +51 -21
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/templates/workflow/agents/architect.toml +2 -2
- package/templates/workflow/agents/code-reviewer.toml +1 -1
- package/templates/workflow/agents/critic.toml +2 -2
- package/templates/workflow/agents/executor.toml +2 -2
- package/templates/workflow/agents/explore.toml +1 -1
- package/templates/workflow/agents/test-engineer.toml +2 -2
- package/templates/workflow/agents/test-runner.toml +3 -3
- package/templates/workflow/agents/verifier.toml +4 -4
- package/templates/workflow/prompts/architect.md +7 -6
- package/templates/workflow/prompts/code-reviewer.md +6 -6
- package/templates/workflow/prompts/critic.md +8 -7
- package/templates/workflow/prompts/executor.md +7 -7
- package/templates/workflow/prompts/explore.md +3 -3
- package/templates/workflow/prompts/test-engineer.md +7 -6
- package/templates/workflow/prompts/test-runner.md +12 -12
- package/templates/workflow/prompts/verifier.md +26 -8
- package/templates/workflow/skills/superspec-apply/SKILL.md +8 -8
- package/templates/workflow/skills/superspec-archive/SKILL.md +2 -2
- package/templates/workflow/skills/superspec-explore/SKILL.md +6 -6
- package/templates/workflow/skills/superspec-propose/SKILL.md +27 -11
- package/templates/workflow/skills/superspec-review/SKILL.md +6 -6
- package/templates/workflow/agents/final-audit.toml +0 -13
- package/templates/workflow/agents/proposal-auditor.toml +0 -13
- package/templates/workflow/prompts/final-audit.md +0 -32
- package/templates/workflow/prompts/proposal-auditor.md +0 -34
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ superspec.cmd install
|
|
|
135
135
|
|
|
136
136
|
你日常主要记住这五个入口就够了。
|
|
137
137
|
|
|
138
|
-
CLI 不带 `--risk`
|
|
138
|
+
CLI 不带 `--risk` 时默认走完整审查路径;需要轻量路径时显式传 `--risk normal` 或 `--risk minimal`。探索阶段会创建 `critic` 工作项审查需求澄清记录;计划阶段会创建 `critic`、`architect` 和 `test-engineer` 工作项后再进入实现准备。
|
|
139
139
|
|
|
140
140
|
## 它会多保存哪些记录
|
|
141
141
|
|
|
@@ -206,6 +206,26 @@ superspec install
|
|
|
206
206
|
|
|
207
207
|
`superspec init --scope project` 是兼容别名,也会执行同一套安装逻辑。
|
|
208
208
|
|
|
209
|
+
### OpenSpec 中文输出
|
|
210
|
+
|
|
211
|
+
OpenSpec 生成文档的语言应通过官方项目配置控制。在 `openspec/config.yaml` 中使用 `context`:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
schema: spec-driven
|
|
215
|
+
|
|
216
|
+
context: |
|
|
217
|
+
语言:中文(简体)
|
|
218
|
+
所有产出物必须用简体中文撰写。
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`superspec install` 会创建缺失的 `openspec/config.yaml`,或在没有顶层 `context` 时追加这段官方中文 context。如果文件已经有顶层 `context`,SuperSpec 不会覆盖它。
|
|
222
|
+
|
|
223
|
+
可以用下面的命令检查生成的 instructions 是否包含语言上下文:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
openspec instructions proposal --change <change>
|
|
227
|
+
```
|
|
228
|
+
|
|
209
229
|
检查当前项目的 OpenSpec 探测结果:
|
|
210
230
|
|
|
211
231
|
```bash
|
|
@@ -236,8 +256,6 @@ superspec update
|
|
|
236
256
|
prompts/critic.md
|
|
237
257
|
prompts/executor.md
|
|
238
258
|
prompts/explore.md
|
|
239
|
-
prompts/final-audit.md
|
|
240
|
-
prompts/proposal-auditor.md
|
|
241
259
|
prompts/test-engineer.md
|
|
242
260
|
prompts/test-runner.md
|
|
243
261
|
prompts/verifier.md
|
|
@@ -246,8 +264,6 @@ superspec update
|
|
|
246
264
|
agents/critic.toml
|
|
247
265
|
agents/executor.toml
|
|
248
266
|
agents/explore.toml
|
|
249
|
-
agents/final-audit.toml
|
|
250
|
-
agents/proposal-auditor.toml
|
|
251
267
|
agents/test-engineer.toml
|
|
252
268
|
agents/test-runner.toml
|
|
253
269
|
agents/verifier.toml
|
package/dist/cli.js
CHANGED
|
@@ -134,12 +134,20 @@ jobs 子命令:
|
|
|
134
134
|
const cr = changeRoot(projectRoot, change);
|
|
135
135
|
const snapshot = rebuildSnapshot(projectRoot, change, cr);
|
|
136
136
|
const jobs = jobsList(projectRoot, change);
|
|
137
|
+
const staleAcceptedJobs = Math.max(0, jobs.accepted.length - snapshot.accepted_jobs.length);
|
|
137
138
|
console.log(JSON.stringify({
|
|
138
139
|
change,
|
|
139
140
|
state: snapshot.state,
|
|
140
|
-
open_jobs:
|
|
141
|
-
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,
|
|
142
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
|
+
})),
|
|
143
151
|
document_digests: snapshot.document_digests,
|
|
144
152
|
last_transition: snapshot.last_transition,
|
|
145
153
|
}, null, 2));
|
|
@@ -153,7 +161,7 @@ jobs 子命令:
|
|
|
153
161
|
return 0;
|
|
154
162
|
case "explore":
|
|
155
163
|
{
|
|
156
|
-
const risk = opts.risk ?? "
|
|
164
|
+
const risk = opts.risk ?? "strict";
|
|
157
165
|
console.log(JSON.stringify(transitionExplore(projectRoot, change, cr, risk), null, 2));
|
|
158
166
|
}
|
|
159
167
|
return 0;
|
|
@@ -169,13 +177,13 @@ jobs 子命令:
|
|
|
169
177
|
return 0;
|
|
170
178
|
}
|
|
171
179
|
case "next": {
|
|
172
|
-
const risk = opts.risk ?? "
|
|
180
|
+
const risk = opts.risk ?? "strict";
|
|
173
181
|
const result = nextCmd(projectRoot, change, cr, risk);
|
|
174
182
|
console.log(JSON.stringify(result, null, 2));
|
|
175
183
|
return 0;
|
|
176
184
|
}
|
|
177
185
|
case "propose-ready": {
|
|
178
|
-
const risk = opts.risk ?? "
|
|
186
|
+
const risk = opts.risk ?? "strict";
|
|
179
187
|
const result = proposeReady(projectRoot, change, cr, risk);
|
|
180
188
|
console.log(JSON.stringify(result, null, 2));
|
|
181
189
|
return result.events_written === 0 && result.message.includes("不能") ? 1 : 0;
|
|
@@ -206,7 +214,7 @@ jobs 子命令:
|
|
|
206
214
|
return result.events_written === 0 ? 1 : 0;
|
|
207
215
|
}
|
|
208
216
|
case "review-ready": {
|
|
209
|
-
const risk = opts.risk ?? "
|
|
217
|
+
const risk = opts.risk ?? "strict";
|
|
210
218
|
const result = reviewReady(projectRoot, change, cr, risk);
|
|
211
219
|
console.log(JSON.stringify(result, null, 2));
|
|
212
220
|
return result.events_written === 0 ? 1 : 0;
|
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) {
|
package/dist/install.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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", "
|
|
3
|
-
export declare const WORKFLOW_AGENTS: readonly ["architect.toml", "code-reviewer.toml", "critic.toml", "executor.toml", "explore.toml", "
|
|
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
4
|
export interface InstallResult {
|
|
5
5
|
ok: boolean;
|
|
6
6
|
message: string;
|
|
@@ -10,6 +10,7 @@ export interface InstallResult {
|
|
|
10
10
|
prompts: string[];
|
|
11
11
|
agents: string[];
|
|
12
12
|
config: string;
|
|
13
|
+
openspec_config: string;
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
export interface InstallOptions {
|
package/dist/install.js
CHANGED
|
@@ -13,8 +13,6 @@ export const WORKFLOW_PROMPTS = [
|
|
|
13
13
|
"critic.md",
|
|
14
14
|
"executor.md",
|
|
15
15
|
"explore.md",
|
|
16
|
-
"final-audit.md",
|
|
17
|
-
"proposal-auditor.md",
|
|
18
16
|
"test-engineer.md",
|
|
19
17
|
"test-runner.md",
|
|
20
18
|
"verifier.md",
|
|
@@ -25,8 +23,6 @@ export const WORKFLOW_AGENTS = [
|
|
|
25
23
|
"critic.toml",
|
|
26
24
|
"executor.toml",
|
|
27
25
|
"explore.toml",
|
|
28
|
-
"final-audit.toml",
|
|
29
|
-
"proposal-auditor.toml",
|
|
30
26
|
"test-engineer.toml",
|
|
31
27
|
"test-runner.toml",
|
|
32
28
|
"verifier.toml",
|
|
@@ -133,6 +129,33 @@ function ensureCodexConfig(projectRoot) {
|
|
|
133
129
|
writeFileSync(configPath, next);
|
|
134
130
|
return ".codex/config.toml";
|
|
135
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
|
+
}
|
|
136
159
|
export function installProject(projectRoot, options = {}) {
|
|
137
160
|
if (legacyStateFound(projectRoot)) {
|
|
138
161
|
throw new Error("检测到老版 SuperSpec (0.x) 的状态文件。\n" +
|
|
@@ -156,6 +179,7 @@ export function installProject(projectRoot, options = {}) {
|
|
|
156
179
|
prompts: copyPrompts(templateRoot, projectRoot),
|
|
157
180
|
agents: copyAgents(templateRoot, projectRoot),
|
|
158
181
|
config: ensureCodexConfig(projectRoot),
|
|
182
|
+
openspec_config: ensureOpenSpecChineseContext(projectRoot),
|
|
159
183
|
},
|
|
160
184
|
};
|
|
161
185
|
}
|
package/dist/next.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — next:返回可执行路径
|
|
2
2
|
import { rebuildSnapshot } from "./sync.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { validateDiscovery, countDiscoveryOpenQuestions } from "./format.js";
|
|
5
|
+
import { validateDiscovery, countDiscoveryOpenQuestions, collectProposeOpenQuestions } from "./format.js";
|
|
6
|
+
const ACTIVE_PROPOSAL_REVIEW_ROLES = new Set(["critic", "architect", "test-engineer"]);
|
|
7
|
+
function isActiveProposalReviewJob(job) {
|
|
8
|
+
return job.created_from_transition === "propose-ready" && ACTIVE_PROPOSAL_REVIEW_ROLES.has(job.role);
|
|
9
|
+
}
|
|
6
10
|
function packetCommand(change, jobId) {
|
|
7
11
|
return `superspec jobs packet --change "${change}" --job "${jobId}"`;
|
|
8
12
|
}
|
|
9
13
|
function transitionCommand(change, name, extra = "") {
|
|
10
14
|
return `superspec transition ${name} --change "${change}"${extra ? " " + extra : ""}`;
|
|
11
15
|
}
|
|
16
|
+
function riskFlag(risk) {
|
|
17
|
+
return risk === "strict" ? "" : `--risk ${risk}`;
|
|
18
|
+
}
|
|
12
19
|
/** next 命令:读 snapshot,返回唯一可执行路径 */
|
|
13
|
-
export function next(projectRoot, change, changeRoot, defaultRisk = "
|
|
20
|
+
export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
14
21
|
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
15
22
|
switch (snapshot.state) {
|
|
16
23
|
case "init":
|
|
@@ -46,14 +53,25 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
46
53
|
return {
|
|
47
54
|
state: "explore",
|
|
48
55
|
path: "next_command",
|
|
49
|
-
next_command: transitionCommand(change, "explore",
|
|
56
|
+
next_command: transitionCommand(change, "explore", riskFlag(defaultRisk)),
|
|
50
57
|
reason: "探索完成,推进到计划阶段",
|
|
51
58
|
missing_inputs: [],
|
|
52
59
|
};
|
|
53
60
|
}
|
|
54
61
|
case "propose": {
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
63
|
+
if (openQuestions.openCount > 0) {
|
|
64
|
+
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
65
|
+
const ask = {
|
|
66
|
+
question: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}。请确认并更新计划文档后继续`,
|
|
67
|
+
allowed_answers: ["所有问题已确认"],
|
|
68
|
+
scope: "propose_open_questions",
|
|
69
|
+
};
|
|
70
|
+
return { state: "propose", path: "ask_user", ask_user: ask, reason: `有 ${openQuestions.openCount} 个 propose 未确认问题` };
|
|
71
|
+
}
|
|
72
|
+
const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
|
|
73
|
+
if (proposalReviewJobs.length > 0) {
|
|
74
|
+
const jobs = proposalReviewJobs.map(j => ({
|
|
57
75
|
job_id: j.job_id,
|
|
58
76
|
role: j.role,
|
|
59
77
|
packet_command: packetCommand(change, j.job_id),
|
|
@@ -65,16 +83,28 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
65
83
|
reason: `有 ${jobs.length} 个待完成工作项`,
|
|
66
84
|
};
|
|
67
85
|
}
|
|
68
|
-
const riskFlag = `--risk ${defaultRisk}`;
|
|
69
86
|
return {
|
|
70
87
|
state: "propose",
|
|
71
88
|
path: "next_command",
|
|
72
|
-
next_command: transitionCommand(change, "propose-ready", riskFlag),
|
|
89
|
+
next_command: transitionCommand(change, "propose-ready", riskFlag(defaultRisk)),
|
|
73
90
|
reason: "计划文档就绪,提交 propose-ready",
|
|
74
91
|
missing_inputs: [],
|
|
75
92
|
};
|
|
76
93
|
}
|
|
77
|
-
case "propose_ready":
|
|
94
|
+
case "propose_ready": {
|
|
95
|
+
const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
|
|
96
|
+
if (proposalReviewJobs.length > 0) {
|
|
97
|
+
return {
|
|
98
|
+
state: "propose_ready",
|
|
99
|
+
path: "required_job",
|
|
100
|
+
required_jobs: proposalReviewJobs.map(j => ({
|
|
101
|
+
job_id: j.job_id,
|
|
102
|
+
role: j.role,
|
|
103
|
+
packet_command: packetCommand(change, j.job_id),
|
|
104
|
+
})),
|
|
105
|
+
reason: `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
78
108
|
return {
|
|
79
109
|
state: "propose_ready",
|
|
80
110
|
path: "next_command",
|
|
@@ -82,6 +112,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
82
112
|
reason: "计划就绪,开始执行",
|
|
83
113
|
missing_inputs: [],
|
|
84
114
|
};
|
|
115
|
+
}
|
|
85
116
|
case "apply": {
|
|
86
117
|
// 有 open job → 做
|
|
87
118
|
if (snapshot.open_jobs.length > 0) {
|
|
@@ -99,7 +130,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
99
130
|
return {
|
|
100
131
|
state: "apply",
|
|
101
132
|
path: "next_command",
|
|
102
|
-
next_command: transitionCommand(change, "review-ready"),
|
|
133
|
+
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
103
134
|
reason: "所有任务完成,进入审查",
|
|
104
135
|
missing_inputs: [],
|
|
105
136
|
};
|
|
@@ -124,7 +155,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
124
155
|
return {
|
|
125
156
|
state: "apply_done",
|
|
126
157
|
path: "next_command",
|
|
127
|
-
next_command: transitionCommand(change, "review-ready"),
|
|
158
|
+
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
128
159
|
reason: "所有任务完成,进入审查",
|
|
129
160
|
missing_inputs: [],
|
|
130
161
|
};
|
package/dist/record.js
CHANGED
|
@@ -4,29 +4,30 @@ import { join } from "node:path";
|
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, } from "./store.js";
|
|
5
5
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
6
6
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
7
|
+
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
8
|
+
function requiresReviewer(role) {
|
|
9
|
+
return role === "critic" || role === "architect" || role === "test-engineer";
|
|
10
|
+
}
|
|
7
11
|
function recommendedAgentForRole(role) {
|
|
8
12
|
switch (role) {
|
|
9
|
-
case "proposal-auditor": return "proposal-auditor";
|
|
10
13
|
case "critic": return "critic";
|
|
11
14
|
case "architect": return "architect";
|
|
12
15
|
case "test-engineer": return "test-engineer";
|
|
13
|
-
case "
|
|
16
|
+
case "verifier": return "verifier";
|
|
14
17
|
case "executor": return "executor";
|
|
15
18
|
case "test-run": return "test-runner";
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
function roleDescription(role) {
|
|
19
22
|
switch (role) {
|
|
20
|
-
case "proposal-auditor":
|
|
21
|
-
return "审查 proposal/tasks/design/discovery/business-invariants/test-contract 是否足够进入实现计划门";
|
|
22
23
|
case "critic":
|
|
23
24
|
return "从反方角度审查需求澄清或计划材料中的隐藏假设、范围漂移、验收漏洞和证据缺口";
|
|
24
25
|
case "architect":
|
|
25
26
|
return "审查架构边界、接口契约、长期维护风险和设计取舍";
|
|
26
27
|
case "test-engineer":
|
|
27
28
|
return "审查测试契约、覆盖策略、RED/GREEN 可信度和验收场景映射";
|
|
28
|
-
case "
|
|
29
|
-
return "
|
|
29
|
+
case "verifier":
|
|
30
|
+
return "验证 proposal、实现状态、任务完成、测试契约和 SuperSpec 证据是否足以支撑完成结论";
|
|
30
31
|
case "executor":
|
|
31
32
|
return "执行受限实现工作项";
|
|
32
33
|
case "test-run":
|
|
@@ -120,6 +121,22 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
120
121
|
if (obj.verdict === "fail") {
|
|
121
122
|
checks.push("报告 verdict=fail,工作项未通过");
|
|
122
123
|
}
|
|
124
|
+
if (requiresReviewer(job.role)) {
|
|
125
|
+
if (!("reviewer" in obj))
|
|
126
|
+
checks.push("报告缺少必填字段 reviewer");
|
|
127
|
+
const reviewer = obj.reviewer;
|
|
128
|
+
if (!reviewer || typeof reviewer !== "object" || Array.isArray(reviewer)) {
|
|
129
|
+
checks.push("报告 reviewer 必须是包含 kind/id 的对象");
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (typeof reviewer.kind !== "string" || !REVIEWER_KINDS.has(reviewer.kind)) {
|
|
133
|
+
checks.push(`报告 reviewer.kind 必须是 ${[...REVIEWER_KINDS].join("|")} 之一`);
|
|
134
|
+
}
|
|
135
|
+
if (typeof reviewer.id !== "string" || reviewer.id.trim() === "") {
|
|
136
|
+
checks.push("报告 reviewer.id 必须是非空字符串");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
142
|
catch {
|
|
@@ -253,11 +270,14 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
253
270
|
boundFiles: job.boundFiles,
|
|
254
271
|
packet_digest: job.packet_digest,
|
|
255
272
|
required_output_kind: "job_report_json",
|
|
256
|
-
output_contract_fields: [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
273
|
+
output_contract_fields: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
257
274
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
258
275
|
output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
|
|
276
|
+
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} reviewer 执行并在 reviewer.kind/id 中记录来源,` : "") +
|
|
259
277
|
`产出 JSON 报告文件并通过 superspec record job-submit 登记。` +
|
|
260
|
-
|
|
278
|
+
(requiresReviewer(job.role)
|
|
279
|
+
? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
|
|
280
|
+
: `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]}`),
|
|
261
281
|
stop_conditions: ["审查完成后提交报告,不要修改文档"],
|
|
262
282
|
created_from_transition: job.created_from_transition,
|
|
263
283
|
},
|
package/dist/transition.d.ts
CHANGED
package/dist/transition.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, stagingDir, sha256File, sha256Text, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
|
-
import { validateDiscovery, findTaskInLines, parseTasksMd, tasksStructureDigest } from "./format.js";
|
|
6
|
+
import { validateDiscovery, collectProposeOpenQuestions, findTaskInLines, parseTasksMd, tasksStructureDigest } from "./format.js";
|
|
7
7
|
let transitionSeq = 0;
|
|
8
8
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
9
9
|
let jobSeq = 0;
|
|
@@ -11,8 +11,8 @@ function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice
|
|
|
11
11
|
const TRANSITION_REQUIREMENTS = {
|
|
12
12
|
"propose-ready": {
|
|
13
13
|
minimal: [],
|
|
14
|
-
normal: ["
|
|
15
|
-
strict: ["
|
|
14
|
+
normal: ["critic"],
|
|
15
|
+
strict: ["critic", "architect", "test-engineer"],
|
|
16
16
|
},
|
|
17
17
|
"explore": {
|
|
18
18
|
minimal: [],
|
|
@@ -28,10 +28,10 @@ const TRANSITION_REQUIREMENTS = {
|
|
|
28
28
|
function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, transitionName, bindDocPaths) {
|
|
29
29
|
const staleRoles = [];
|
|
30
30
|
for (const role of requiredRoles) {
|
|
31
|
-
const openForRole = snapshot.open_jobs.find(j => j.role === role);
|
|
31
|
+
const openForRole = snapshot.open_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
32
32
|
if (openForRole)
|
|
33
33
|
return { fromState: snapshot.state, toState: snapshot.state, outcome: "advanced", reason: `工作项 ${role} 已存在(${openForRole.job_id}),请先完成它` };
|
|
34
|
-
const fresh = snapshot.accepted_jobs.find(j => j.role === role);
|
|
34
|
+
const fresh = snapshot.accepted_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
35
35
|
if (!fresh) {
|
|
36
36
|
staleRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
|
|
37
37
|
}
|
|
@@ -57,6 +57,20 @@ function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, tr
|
|
|
57
57
|
}
|
|
58
58
|
return null; // 全部满足
|
|
59
59
|
}
|
|
60
|
+
function historicalProposeReadyRoles(projectRoot, change) {
|
|
61
|
+
const roles = new Set();
|
|
62
|
+
for (const ev of readEvents(projectRoot, change)) {
|
|
63
|
+
if (ev.event_type !== "transition_commit")
|
|
64
|
+
continue;
|
|
65
|
+
const newJobs = ev.payload.new_jobs ?? [];
|
|
66
|
+
for (const job of newJobs) {
|
|
67
|
+
if (job.created_from_transition === "propose-ready" &&
|
|
68
|
+
(job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
|
|
69
|
+
roles.add(job.role);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...roles];
|
|
73
|
+
}
|
|
60
74
|
/**
|
|
61
75
|
* 已迁移到 format.ts:findTaskInLines / parseTasksMd / tasksStructureDigest
|
|
62
76
|
* 以下保留 findTaskLine 作为兼容 wrapper(内部调用 format.ts)
|
|
@@ -92,7 +106,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
92
106
|
created_jobs: [], message: decision.message, events_written: 0,
|
|
93
107
|
};
|
|
94
108
|
}
|
|
95
|
-
const { fromState, toState, outcome, newJobs = [], reason, extraEvents = [] } = decision;
|
|
109
|
+
const { fromState, toState, outcome, newJobs = [], reason, extraEvents = [], details } = decision;
|
|
96
110
|
if (fromState !== snapshot.state) {
|
|
97
111
|
return {
|
|
98
112
|
transition: name, outcome: "advanced",
|
|
@@ -127,11 +141,12 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
127
141
|
created_jobs: newJobs.map(j => j.job_id),
|
|
128
142
|
message: outcome === "advanced" ? `状态推进:${fromState} → ${toState}` : `状态不变(${fromState}),创建了 ${newJobs.length} 个工作项`,
|
|
129
143
|
events_written: 1 + extraEvents.length,
|
|
144
|
+
...(details ? { details } : {}),
|
|
130
145
|
};
|
|
131
146
|
});
|
|
132
147
|
}
|
|
133
148
|
// ===== propose-ready =====
|
|
134
|
-
export function proposeReady(projectRoot, change, changeRoot, risk = "
|
|
149
|
+
export function proposeReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
135
150
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
136
151
|
name: "propose-ready", idempotencyInputs: { risk },
|
|
137
152
|
decide: (snapshot) => {
|
|
@@ -143,6 +158,11 @@ export function proposeReady(projectRoot, change, changeRoot, risk = "normal") {
|
|
|
143
158
|
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
144
159
|
if (!tasksContent.includes("# Tasks") && !tasksContent.includes("- [ ]"))
|
|
145
160
|
return { skip: true, message: "tasks.md 内容不像任务计划文档" };
|
|
161
|
+
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
162
|
+
if (openQuestions.openCount > 0) {
|
|
163
|
+
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
164
|
+
return { skip: true, message: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}` };
|
|
165
|
+
}
|
|
146
166
|
if (risk !== "minimal") {
|
|
147
167
|
const artifactsDir = join(changeRoot, ".superspec", "artifacts");
|
|
148
168
|
for (const doc of ["discovery.md", "business-invariants.md", "test-contract.md"]) {
|
|
@@ -172,7 +192,7 @@ export function transitionInit(projectRoot, change, changeRoot) {
|
|
|
172
192
|
});
|
|
173
193
|
}
|
|
174
194
|
// ===== explore =====
|
|
175
|
-
export function transitionExplore(projectRoot, change, changeRoot, risk = "
|
|
195
|
+
export function transitionExplore(projectRoot, change, changeRoot, risk = "strict") {
|
|
176
196
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
177
197
|
name: "explore", idempotencyInputs: { phase: "explore", risk },
|
|
178
198
|
decide: (snapshot) => {
|
|
@@ -204,6 +224,16 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
204
224
|
decide: (snapshot) => {
|
|
205
225
|
if (snapshot.state !== "propose_ready")
|
|
206
226
|
return { skip: true, message: `当前状态 ${snapshot.state},需要 propose_ready` };
|
|
227
|
+
const reviewedRoles = historicalProposeReadyRoles(projectRoot, change);
|
|
228
|
+
if (reviewedRoles.length > 0) {
|
|
229
|
+
const reviewResult = checkOrCreateReviewJobs(snapshot, reviewedRoles, changeRoot, change, "propose-ready", ["proposal.md", "tasks.md", "design.md", ".superspec/artifacts/discovery.md", ".superspec/artifacts/business-invariants.md", ".superspec/artifacts/test-contract.md"]);
|
|
230
|
+
if (reviewResult) {
|
|
231
|
+
return {
|
|
232
|
+
...reviewResult,
|
|
233
|
+
reason: `进入 apply 前需要 fresh proposal 审查:${reviewResult.reason}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
207
237
|
return { fromState: "propose_ready", toState: "apply", outcome: "advanced", reason: "进入执行阶段" };
|
|
208
238
|
},
|
|
209
239
|
});
|
|
@@ -240,12 +270,13 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
240
270
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
241
271
|
reason: `创建任务 ${taskId} 执行尝试`,
|
|
242
272
|
extraEvents: [{ type: "task_started", payload: attempt }],
|
|
273
|
+
details: { attempt_id: attempt.attempt_id },
|
|
243
274
|
};
|
|
244
275
|
},
|
|
245
276
|
});
|
|
246
277
|
}
|
|
247
278
|
// ===== review-ready =====
|
|
248
|
-
export function reviewReady(projectRoot, change, changeRoot, risk = "
|
|
279
|
+
export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
249
280
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
250
281
|
name: "review-ready", idempotencyInputs: { phase: "review-ready", risk },
|
|
251
282
|
decide: (snapshot) => {
|
|
@@ -259,27 +290,26 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "normal") {
|
|
|
259
290
|
return { fromState: "apply", toState: "apply_done", outcome: "advanced", reason: "所有任务完成" };
|
|
260
291
|
}
|
|
261
292
|
if (snapshot.state === "apply_done") {
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
return { skip: true, message:
|
|
266
|
-
const
|
|
267
|
-
// minimal 直接推进;normal/strict 需要
|
|
268
|
-
if (risk !== "minimal" && !
|
|
269
|
-
// 创建 final-audit job
|
|
293
|
+
const isReviewReadyVerifier = (job) => job.role === "verifier" && job.created_from_transition === "review-ready";
|
|
294
|
+
const verifierOpen = snapshot.open_jobs.find(isReviewReadyVerifier);
|
|
295
|
+
if (verifierOpen)
|
|
296
|
+
return { skip: true, message: `有待完成的最终验证工作项 ${verifierOpen.job_id}` };
|
|
297
|
+
const verifierAccepted = snapshot.accepted_jobs.find(isReviewReadyVerifier);
|
|
298
|
+
// minimal 直接推进;normal/strict 需要 review-ready verifier
|
|
299
|
+
if (risk !== "minimal" && !verifierAccepted) {
|
|
270
300
|
const docPaths = ["proposal.md", "tasks.md", "design.md", ".superspec/artifacts/discovery.md", ".superspec/artifacts/business-invariants.md", ".superspec/artifacts/test-contract.md"];
|
|
271
301
|
const boundFiles = docPaths.filter(p => existsSync(join(changeRoot, p))).map(p => ({ path: p, sha: sha256File(join(changeRoot, p)) ?? "sha256:missing" }));
|
|
272
302
|
const job = {
|
|
273
|
-
job_id: newJobId(change, "
|
|
274
|
-
boundFiles, packet_digest: sha256Text(JSON.stringify({ role: "
|
|
303
|
+
job_id: newJobId(change, "verifier"), role: "verifier", state: "requested",
|
|
304
|
+
boundFiles, packet_digest: sha256Text(JSON.stringify({ role: "verifier", boundFiles, created_from_transition: "review-ready" })),
|
|
275
305
|
created_from_transition: "review-ready", created_at: new Date().toISOString(),
|
|
276
306
|
};
|
|
277
307
|
return {
|
|
278
308
|
fromState: "apply_done", toState: "apply_done", outcome: "job_created",
|
|
279
|
-
newJobs: [job], reason: "
|
|
309
|
+
newJobs: [job], reason: "创建最终验证工作项",
|
|
280
310
|
};
|
|
281
311
|
}
|
|
282
|
-
return { fromState: "apply_done", toState: "review", outcome: "advanced", reason: "
|
|
312
|
+
return { fromState: "apply_done", toState: "review", outcome: "advanced", reason: "最终验证已接受,进入审查阶段" };
|
|
283
313
|
}
|
|
284
314
|
return { skip: true, message: `当前状态 ${snapshot.state},review-ready 不适用` };
|
|
285
315
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type Ref = {
|
|
|
5
5
|
sha: string;
|
|
6
6
|
};
|
|
7
7
|
export type JobState = "requested" | "accepted" | "rejected";
|
|
8
|
-
export type JobRole = "
|
|
8
|
+
export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier";
|
|
9
9
|
export interface Job {
|
|
10
10
|
job_id: string;
|
|
11
11
|
role: JobRole;
|
|
@@ -132,6 +132,7 @@ export interface TransitionResult {
|
|
|
132
132
|
created_jobs: string[];
|
|
133
133
|
message: string;
|
|
134
134
|
events_written: number;
|
|
135
|
+
details?: Record<string, unknown>;
|
|
135
136
|
}
|
|
136
137
|
export interface RecordResult {
|
|
137
138
|
event_type: EventType;
|