@peterxiaoyang/superspec 0.1.15-alpha → 0.1.17-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +48 -31
  2. package/dist/cli.js +41 -34
  3. package/dist/format.d.ts +9 -0
  4. package/dist/format.js +35 -5
  5. package/dist/install.d.ts +19 -0
  6. package/dist/install.js +185 -0
  7. package/dist/next.js +42 -11
  8. package/dist/record.js +81 -6
  9. package/dist/transition.d.ts +2 -1
  10. package/dist/transition.js +55 -25
  11. package/dist/types.d.ts +5 -1
  12. package/package.json +1 -1
  13. package/templates/workflow/agents/architect.toml +13 -0
  14. package/templates/workflow/agents/code-reviewer.toml +13 -0
  15. package/templates/workflow/agents/critic.toml +13 -0
  16. package/templates/workflow/agents/executor.toml +13 -0
  17. package/templates/workflow/agents/explore.toml +13 -0
  18. package/templates/workflow/agents/test-engineer.toml +13 -0
  19. package/templates/workflow/agents/test-runner.toml +13 -0
  20. package/templates/workflow/agents/verifier.toml +13 -0
  21. package/templates/workflow/prompts/architect.md +44 -0
  22. package/templates/workflow/prompts/code-reviewer.md +34 -0
  23. package/templates/workflow/prompts/critic.md +47 -0
  24. package/templates/workflow/prompts/executor.md +32 -0
  25. package/templates/workflow/prompts/explore.md +27 -0
  26. package/templates/workflow/prompts/test-engineer.md +45 -0
  27. package/templates/workflow/prompts/test-runner.md +35 -0
  28. package/templates/workflow/prompts/verifier.md +53 -0
  29. package/templates/workflow/skills/superspec-apply/SKILL.md +8 -18
  30. package/templates/workflow/skills/superspec-archive/SKILL.md +2 -9
  31. package/templates/workflow/skills/superspec-explore/SKILL.md +5 -10
  32. package/templates/workflow/skills/superspec-propose/SKILL.md +28 -13
  33. package/templates/workflow/skills/superspec-review/SKILL.md +6 -28
package/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"),
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
@@ -2,6 +2,38 @@
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, } from "./store.js";
5
+ const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
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
+ }
11
+ function recommendedAgentForRole(role) {
12
+ switch (role) {
13
+ case "critic": return "critic";
14
+ case "architect": return "architect";
15
+ case "test-engineer": return "test-engineer";
16
+ case "verifier": return "verifier";
17
+ case "executor": return "executor";
18
+ case "test-run": return "test-runner";
19
+ }
20
+ }
21
+ function roleDescription(role) {
22
+ switch (role) {
23
+ case "critic":
24
+ return "从反方角度审查需求澄清或计划材料中的隐藏假设、范围漂移、验收漏洞和证据缺口";
25
+ case "architect":
26
+ return "审查架构边界、接口契约、长期维护风险和设计取舍";
27
+ case "test-engineer":
28
+ return "审查测试契约、覆盖策略、RED/GREEN 可信度和验收场景映射";
29
+ case "verifier":
30
+ return "验证 proposal、实现状态、任务完成、测试契约和 SuperSpec 证据是否足以支撑完成结论";
31
+ case "executor":
32
+ return "执行受限实现工作项";
33
+ case "test-run":
34
+ return "执行受限测试工作项";
35
+ }
36
+ }
5
37
  /** 从 events 中查找 job(H4 修复:job 只在 transition_commit 的 new_jobs payload 里) */
6
38
  function findJob(events, jobId) {
7
39
  for (const ev of events) {
@@ -65,15 +97,50 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
65
97
  const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
66
98
  // acceptance checks
67
99
  const checks = [];
68
- // 0. 报告角色匹配(HIGH 5 修复)
100
+ // 0. 报告格式和角色匹配(最小 JSON contract)
69
101
  try {
70
102
  const report = JSON.parse(reportContent);
71
- if (report.role && report.role !== job.role) {
72
- checks.push(`报告角色 ${report.role} 与工作项角色 ${job.role} 不匹配`);
103
+ if (!report || typeof report !== "object" || Array.isArray(report)) {
104
+ checks.push("报告必须是 JSON object");
105
+ }
106
+ else {
107
+ const obj = report;
108
+ for (const field of REVIEW_REPORT_REQUIRED_FIELDS) {
109
+ if (!(field in obj))
110
+ checks.push(`报告缺少必填字段 ${field}`);
111
+ }
112
+ if (obj.role !== job.role) {
113
+ checks.push(`报告角色 ${String(obj.role)} 与工作项角色 ${job.role} 不匹配`);
114
+ }
115
+ if (obj.verdict !== "pass" && obj.verdict !== "fail") {
116
+ checks.push("报告 verdict 必须是 pass 或 fail");
117
+ }
118
+ if (!Array.isArray(obj.findings)) {
119
+ checks.push("报告 findings 必须是数组");
120
+ }
121
+ if (obj.verdict === "fail") {
122
+ checks.push("报告 verdict=fail,工作项未通过");
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
+ }
73
140
  }
74
141
  }
75
142
  catch {
76
- // JSON 报告,Phase 1 允许(Phase 2 加正式 schema)
143
+ checks.push("报告必须是有效 JSON");
77
144
  }
78
145
  // 1. boundFiles 仍匹配当前文档(missing 也算不匹配)
79
146
  for (const bf of job.boundFiles) {
@@ -199,10 +266,18 @@ export function jobsPacket(projectRoot, change, jobId) {
199
266
  packet: {
200
267
  job_id: job.job_id,
201
268
  role: job.role,
269
+ recommended_agent: recommendedAgentForRole(job.role),
202
270
  boundFiles: job.boundFiles,
203
271
  packet_digest: job.packet_digest,
204
- required_output_kind: "report",
205
- output_instructions: `请审查 ${job.boundFiles.map(f => f.path).join(", ")},产出审查报告`,
272
+ required_output_kind: "job_report_json",
273
+ output_contract_fields: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
274
+ output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
275
+ output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
276
+ (requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} reviewer 执行并在 reviewer.kind/id 中记录来源,` : "") +
277
+ `产出 JSON 报告文件并通过 superspec record job-submit 登记。` +
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":[]}`),
206
281
  stop_conditions: ["审查完成后提交报告,不要修改文档"],
207
282
  created_from_transition: job.created_from_transition,
208
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
  /**
@@ -24,7 +25,7 @@ export declare function commitTransition(projectRoot: string, change: string, ch
24
25
  }): TransitionResult;
25
26
  export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
26
27
  export declare function transitionInit(projectRoot: string, change: string, changeRoot: string): TransitionResult;
27
- export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string): TransitionResult;
28
+ export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
28
29
  export declare function startApply(projectRoot: string, change: string, changeRoot: string): TransitionResult;
29
30
  export declare function taskStart(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
30
31
  export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
@@ -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,13 +11,13 @@ 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: ["critic-review", "architect-review", "test-engineer-review"],
14
+ normal: ["critic"],
15
+ strict: ["critic", "architect", "test-engineer"],
16
16
  },
17
17
  "explore": {
18
18
  minimal: [],
19
19
  normal: [],
20
- strict: ["clarification-review"],
20
+ strict: ["critic"],
21
21
  },
22
22
  };
23
23
  /**
@@ -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,9 +192,9 @@ export function transitionInit(projectRoot, change, changeRoot) {
172
192
  });
173
193
  }
174
194
  // ===== explore =====
175
- export function transitionExplore(projectRoot, change, changeRoot) {
195
+ export function transitionExplore(projectRoot, change, changeRoot, risk = "strict") {
176
196
  return commitTransition(projectRoot, change, changeRoot, {
177
- name: "explore", idempotencyInputs: { phase: "explore" },
197
+ name: "explore", idempotencyInputs: { phase: "explore", risk },
178
198
  decide: (snapshot) => {
179
199
  if (snapshot.state === "init")
180
200
  return { fromState: "init", toState: "explore", outcome: "advanced", reason: "进入探索阶段" };
@@ -182,12 +202,12 @@ export function transitionExplore(projectRoot, change, changeRoot) {
182
202
  const discoveryPath = join(changeRoot, ".superspec", "artifacts", "discovery.md");
183
203
  if (!existsSync(discoveryPath))
184
204
  return { skip: true, message: "discovery.md 不存在" };
185
- // explore→propose:校验 discovery + 通用 job 审查(normal+ 需 clarification-review)
205
+ // explore→propose:校验 discovery + strict 模式下的 critic 审查
186
206
  const discoveryCheck = validateDiscovery(changeRoot);
187
207
  if (!discoveryCheck.ok)
188
208
  return { skip: true, message: discoveryCheck.message };
189
209
  // 通用 job 审查(和 propose-ready 同一个 helper)
190
- const requiredRoles = TRANSITION_REQUIREMENTS["explore"]?.["normal"] ?? [];
210
+ const requiredRoles = TRANSITION_REQUIREMENTS["explore"]?.[risk] ?? [];
191
211
  const reviewResult = checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, "explore", [".superspec/artifacts/discovery.md"]);
192
212
  if (reviewResult)
193
213
  return reviewResult;
@@ -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" | "clarification-review" | "critic-review" | "architect-review" | "test-engineer-review" | "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;
@@ -18,9 +18,12 @@ export interface Job {
18
18
  export interface JobPacket {
19
19
  job_id: string;
20
20
  role: JobRole;
21
+ recommended_agent?: string;
21
22
  boundFiles: Ref[];
22
23
  packet_digest: string;
23
24
  required_output_kind: string;
25
+ output_contract_fields?: string[];
26
+ output_contract_optional_fields?: string[];
24
27
  stop_conditions: string[];
25
28
  created_from_transition: string;
26
29
  }
@@ -129,6 +132,7 @@ export interface TransitionResult {
129
132
  created_jobs: string[];
130
133
  message: string;
131
134
  events_written: number;
135
+ details?: Record<string, unknown>;
132
136
  }
133
137
  export interface RecordResult {
134
138
  event_type: EventType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.15-alpha",
3
+ "version": "0.1.17-alpha",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": { "node": ">=20.19.0" },
@@ -0,0 +1,13 @@
1
+ # SuperSpec Codex agent: architect
2
+ name = "architect"
3
+ description = "System design, boundaries, interfaces, long-horizon tradeoffs"
4
+ model_reasoning_effort = "high"
5
+ developer_instructions = """
6
+ Role: Architect. Review system boundaries, interface contracts, data flow, maintenance risk, rollback risk, and design tradeoffs.
7
+
8
+ Task binding: load `.codex/prompts/architect.md` first, then read the current task instructions. Their refs, output format, contract fields, review scope, and stop conditions override static prompt memory.
9
+
10
+ Boundary: read-only. Do not edit files or judge materials you have not opened. Report missing context upward instead of guessing.
11
+
12
+ Output: concise Simplified Chinese. For `job_report_json`, submit JSON with `role:"architect"`, `verdict`, `findings`, and `reviewer:{kind,id}`. Otherwise put the conclusion first, cite file:line evidence, and write `无阻塞问题` when no blocking issue is found.
13
+ """
@@ -0,0 +1,13 @@
1
+ # SuperSpec Codex agent: code-reviewer
2
+ name = "code-reviewer"
3
+ description = "Comprehensive review across all concerns"
4
+ model_reasoning_effort = "high"
5
+ developer_instructions = """
6
+ Role: Code Reviewer. Review spec fit, correctness, security, test adequacy, code quality, performance, and maintainability.
7
+
8
+ Task binding: load `.codex/prompts/code-reviewer.md` first, then read the current task instructions. Their refs, output format, contract fields, review scope, declared write scope, executor report refs, and stop conditions override static prompt memory.
9
+
10
+ Boundary: read-only. Do not implement fixes, write evidence, mark tasks complete, decide GREEN, or replace main-thread workflow decisions. Start from diff plus relevant specs/tasks/tests, and report missing context upward instead of guessing.
11
+
12
+ Output: concise Simplified Chinese. Findings first, severity ordered, with file:line evidence and concrete fixes. Write `无阻塞问题` when no blocking issue is found.
13
+ """
@@ -0,0 +1,13 @@
1
+ # SuperSpec Codex agent: critic
2
+ name = "critic"
3
+ description = "Plan/design critical challenge and review"
4
+ model_reasoning_effort = "high"
5
+ developer_instructions = """
6
+ Role: Critic. Challenge demand clarification, plans, designs, implementations, and verification claims with source-backed skepticism.
7
+
8
+ Task binding: load `.codex/prompts/critic.md` first, then read the current task instructions. Their refs, output format, contract fields, review scope, and stop conditions override static prompt memory.
9
+
10
+ Boundary: read-only by default. Do not edit files, invent issues, or widen scope silently. Report missing source refs or claim gaps upward.
11
+
12
+ Output: concise Simplified Chinese. For `job_report_json`, submit JSON with `role:"critic"`, `verdict`, `findings`, and `reviewer:{kind,id}`. Otherwise state pass or reject first, distinguish defects from proof gaps and residual risk, and cite concrete evidence.
13
+ """
@@ -0,0 +1,13 @@
1
+ # SuperSpec Codex agent: executor
2
+ name = "executor"
3
+ description = "Bounded SuperSpec apply implementation worker"
4
+ model_reasoning_effort = "high"
5
+ developer_instructions = """
6
+ Role: Executor. Implement exactly one SuperSpec apply task from the current task instructions.
7
+
8
+ Task binding: load `.codex/prompts/executor.md` first, then read the current task instructions. Their task id, declared write scope, guard fingerprint, worker chain id, stop conditions, and report policy override static prompt memory.
9
+
10
+ Boundary: mutating but bounded. Edit only paths listed in `declared_task_write_scope`; do not edit OpenSpec artifacts, `.superspec/**`, task checkboxes, evidence, review reports, or archives. Stop and report blockers when scope or context is insufficient.
11
+
12
+ Output: concise Simplified Chinese implementation report with changed files, task/test/invariant mapping, suggested GREEN checks, artifact refs, and residual risk.
13
+ """
@@ -0,0 +1,13 @@
1
+ # SuperSpec Codex agent: explore
2
+ name = "explore"
3
+ description = "Repo-local read-only factual scan for SuperSpec discovery"
4
+ model_reasoning_effort = "high"
5
+ developer_instructions = """
6
+ Role: Explore. Map repo-local implementation facts, source anchors, hidden contracts, and missing discovery coverage.
7
+
8
+ Task binding: load `.codex/prompts/explore.md` first, then read the current task instructions. Their refs and stop conditions override static prompt memory.
9
+
10
+ Boundary: read-only. Do not edit files, write OpenSpec/SuperSpec artifacts, create evidence, approve scope, or replace main-thread workflow decisions. Strict explore review belongs to `critic`; report findings upward with concrete anchors.
11
+
12
+ Output: concise Simplified Chinese. Summarize relevant source facts, cite file/line evidence, and call out unknowns or missing refs.
13
+ """
@@ -0,0 +1,13 @@
1
+ # SuperSpec Codex agent: test-engineer
2
+ name = "test-engineer"
3
+ description = "Test strategy, coverage, flaky-test hardening"
4
+ model_reasoning_effort = "high"
5
+ developer_instructions = """
6
+ Role: Test Engineer. Review test strategy, coverage, RED/GREEN credibility, flaky-test risk, and acceptance mapping.
7
+
8
+ Task binding: load `.codex/prompts/test-engineer.md` first for SuperSpec review/propose lanes, then read the current task instructions. Their refs, output format, contract fields, review scope, and stop conditions override static prompt memory.
9
+
10
+ Boundary: SuperSpec review/propose lanes are read-only. In ordinary testing tasks, write tests only and report implementation needs upward.
11
+
12
+ Output: concise Simplified Chinese. For `job_report_json`, submit JSON with `role:"test-engineer"`, `verdict`, `findings`, and `reviewer:{kind,id}`. Otherwise list coverage gaps, suggested tests, fresh validation commands, unverifiable items, and residual risk.
13
+ """
@@ -0,0 +1,13 @@
1
+ # SuperSpec Codex agent: test-runner
2
+ name = "test-runner"
3
+ description = "Bounded SuperSpec apply test execution worker"
4
+ model_reasoning_effort = "high"
5
+ developer_instructions = """
6
+ Role: Test Runner. Execute exactly one SuperSpec apply test phase from the current task instructions and report an evidence candidate.
7
+
8
+ Task binding: load `.codex/prompts/test-runner.md` first, then read the current task instructions. Their task id, test id, phase, allowed command, expected semantic status, guard fingerprint, report policy, and stop conditions override static prompt memory.
9
+
10
+ Boundary: read-only by default. Do not edit production code, OpenSpec artifacts, `.superspec/**`, task checkboxes, evidence, review reports, or archives. Run only the allowed command from current task instructions and report blockers for missing command, unsafe side effects, or incomplete raw transcript refs.
11
+
12
+ Output: concise Simplified Chinese test report with command, cwd, phase, task/test id, exit status, semantic status candidate, result summary, raw transcript ref, repo head, dirty-state summary, invariant refs, guard fingerprint, and unverified items.
13
+ """
@@ -0,0 +1,13 @@
1
+ # SuperSpec Codex agent: verifier
2
+ name = "verifier"
3
+ description = "Completion evidence, claim validation, test adequacy"
4
+ model_reasoning_effort = "high"
5
+ developer_instructions = """
6
+ Role: Verifier. Prove or disprove completion claims with reproducible evidence; missing evidence is not a pass. When invoked by review-ready for a job report, act as the final verification gate before review.
7
+
8
+ Task binding: load `.codex/prompts/verifier.md` first, then read the current task instructions. Their refs, output format, contract fields, review scope, evidence/report refs, freshness fingerprints, and stop conditions override static prompt memory.
9
+
10
+ Boundary: read-only. Check commands, test output, diff, artifacts, evidence refs, acceptance criteria, and freshness without editing files, writing evidence, or marking tasks complete.
11
+
12
+ Output: concise Simplified Chinese. For job_report_json, submit `role:"verifier"`, `verdict`, and `findings`. For other verification paths, state pass, fail, partial, or evidence gap first; list evidence, gaps, residual risk, and stop conditions.
13
+ """
@@ -0,0 +1,44 @@
1
+ ---
2
+ description: "架构与边界审查角色"
3
+ argument-hint: "本次架构审查说明"
4
+ ---
5
+
6
+ # Architect
7
+
8
+ ## 角色身份
9
+
10
+ 你是 Architect。你审查系统边界、接口契约、数据流、长期维护风险、回滚难度和设计取舍。你提供架构 guidance,不替代主流程做最终判断。
11
+
12
+ ## 读写边界
13
+
14
+ - 默认只读;不要修改文件。
15
+ - 不评价没有打开或没有被本次任务说明或主流程 source refs 指向的材料。
16
+ - 如果需要扩大审查范围,向主流程说明缺口,不要自行改派或改代码。
17
+
18
+ ## 本次任务说明
19
+
20
+ 在 `superspec-review` 或 disclosure review 中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
21
+
22
+ 当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告文件必须是 JSON:
23
+
24
+ ```json
25
+ {
26
+ "role": "architect",
27
+ "verdict": "pass",
28
+ "findings": [],
29
+ "reviewer": { "kind": "codex-subagent", "id": "<thread-or-agent-id>" },
30
+ "summary": "简短结论",
31
+ "evidence_refs": [],
32
+ "risks": [],
33
+ "open_questions": []
34
+ }
35
+ ```
36
+
37
+ `role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。发现阻塞架构问题时必须使用 `verdict:"fail"`。
38
+
39
+ ## 输出风格
40
+
41
+ - 所有用户可见输出必须使用简体中文。
42
+ - 命令、路径、JSON/schema 字段、gate 名称、任务/测试 id、代码标识符保留原文。
43
+ - 结论先行,按严重度列出问题,给出文件/行号证据。
44
+ - 无阻塞问题时明确写“无阻塞问题”,并列残余风险或未验证项。