@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.
Files changed (37) hide show
  1. package/README.md +21 -5
  2. package/dist/cli.js +14 -6
  3. package/dist/format.d.ts +9 -0
  4. package/dist/format.js +35 -5
  5. package/dist/install.d.ts +3 -2
  6. package/dist/install.js +28 -4
  7. package/dist/next.js +42 -11
  8. package/dist/record.js +28 -8
  9. package/dist/transition.d.ts +1 -0
  10. package/dist/transition.js +51 -21
  11. package/dist/types.d.ts +2 -1
  12. package/package.json +1 -1
  13. package/templates/workflow/agents/architect.toml +2 -2
  14. package/templates/workflow/agents/code-reviewer.toml +1 -1
  15. package/templates/workflow/agents/critic.toml +2 -2
  16. package/templates/workflow/agents/executor.toml +2 -2
  17. package/templates/workflow/agents/explore.toml +1 -1
  18. package/templates/workflow/agents/test-engineer.toml +2 -2
  19. package/templates/workflow/agents/test-runner.toml +3 -3
  20. package/templates/workflow/agents/verifier.toml +4 -4
  21. package/templates/workflow/prompts/architect.md +7 -6
  22. package/templates/workflow/prompts/code-reviewer.md +6 -6
  23. package/templates/workflow/prompts/critic.md +8 -7
  24. package/templates/workflow/prompts/executor.md +7 -7
  25. package/templates/workflow/prompts/explore.md +3 -3
  26. package/templates/workflow/prompts/test-engineer.md +7 -6
  27. package/templates/workflow/prompts/test-runner.md +12 -12
  28. package/templates/workflow/prompts/verifier.md +26 -8
  29. package/templates/workflow/skills/superspec-apply/SKILL.md +8 -8
  30. package/templates/workflow/skills/superspec-archive/SKILL.md +2 -2
  31. package/templates/workflow/skills/superspec-explore/SKILL.md +6 -6
  32. package/templates/workflow/skills/superspec-propose/SKILL.md +27 -11
  33. package/templates/workflow/skills/superspec-review/SKILL.md +6 -6
  34. package/templates/workflow/agents/final-audit.toml +0 -13
  35. package/templates/workflow/agents/proposal-auditor.toml +0 -13
  36. package/templates/workflow/prompts/final-audit.md +0 -32
  37. 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` 时默认是 `normal`;但内置的 `superspec-explore` `superspec-propose` 技能默认用 `--risk strict` 驱动。探索阶段会创建 `critic` 工作项审查需求澄清记录;计划阶段会创建 `proposal-auditor`、`critic`、`architect` 和 `test-engineer` 工作项后再进入实现准备。
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: jobs.open.length,
141
- 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,
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 ?? "normal";
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 ?? "normal";
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 ?? "normal";
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 ?? "normal";
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
- /** 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) {
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", "final-audit.md", "proposal-auditor.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", "final-audit.toml", "proposal-auditor.toml", "test-engineer.toml", "test-runner.toml", "verifier.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 { existsSync, readFileSync } from "node:fs";
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 = "normal") {
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", `--risk ${defaultRisk}`),
56
+ next_command: transitionCommand(change, "explore", riskFlag(defaultRisk)),
50
57
  reason: "探索完成,推进到计划阶段",
51
58
  missing_inputs: [],
52
59
  };
53
60
  }
54
61
  case "propose": {
55
- if (snapshot.open_jobs.length > 0) {
56
- const jobs = snapshot.open_jobs.map(j => ({
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 "final-audit": return "final-audit";
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 "final-audit":
29
- return "最终审查 proposal、实现状态、任务完成、测试契约和 SuperSpec 证据一致性";
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
- `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]}`,
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
  },
@@ -9,6 +9,7 @@ interface Decision {
9
9
  type: string;
10
10
  payload: Record<string, unknown>;
11
11
  }[];
12
+ details?: Record<string, unknown>;
12
13
  postCommit?: (projectRoot: string, change: string, changeRoot: string) => void;
13
14
  }
14
15
  /**
@@ -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: ["proposal-auditor"],
15
- strict: ["proposal-auditor", "critic", "architect", "test-engineer"],
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 = "normal") {
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 = "normal") {
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 = "normal") {
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
- // 检查是否有 final-audit job 需求
263
- const finalAuditOpen = snapshot.open_jobs.find(j => j.role === "final-audit");
264
- if (finalAuditOpen)
265
- return { skip: true, message: `有待完成的最终审查工作项 ${finalAuditOpen.job_id}` };
266
- const finalAuditAccepted = snapshot.accepted_jobs.find(j => j.role === "final-audit");
267
- // minimal 直接推进;normal/strict 需要 final-audit
268
- if (risk !== "minimal" && !finalAuditAccepted) {
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, "final-audit"), role: "final-audit", state: "requested",
274
- boundFiles, packet_digest: sha256Text(JSON.stringify({ role: "final-audit", boundFiles })),
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 = "proposal-auditor" | "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "final-audit";
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;