@peterxiaoyang/superspec 0.1.23 → 0.1.25-beta.0

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/dist/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  // SuperSpec 流程引擎 — CLI 入口
3
- import { join } from "node:path";
4
3
  import { execFileSync } from "node:child_process";
5
4
  import { createInterface } from "node:readline/promises";
5
+ import { readFileSync } from "node:fs";
6
6
  import { installProject } from "./install.js";
7
7
  import { writeSnapshot } from "./store.js";
8
8
  import { rebuildSnapshot } from "./sync.js";
9
9
  import { next as nextCmd } from "./next.js";
10
10
  import { proposeReady, commitTransition, transitionInit, transitionExplore, startApply, taskStart, taskComplete, reopen, reviewReady, accept, archive } from "./transition.js";
11
- import { recordJobSubmit, recordUserDecision, jobsList, jobsPacket } from "./record.js";
12
- import { recordTestRun } from "./task.js";
11
+ import { recordJobSubmit, recordJobSubmitContent, recordUserDecision, recordUserDecisionContent, jobsList, jobsPacket } from "./record.js";
12
+ import { recordTestRun, recordTestRunContent } from "./task.js";
13
13
  import { probeOpenSpec, openspecStatus, changeRoot } from "./openspec.js";
14
14
  import { SUPERSPEC_VERSION } from "./version.js";
15
15
  const PACKAGE_NAME = "@peterxiaoyang/superspec";
@@ -42,6 +42,18 @@ function parseFlags(args) {
42
42
  }
43
43
  return opts;
44
44
  }
45
+ class StdinRecordInputError extends Error {
46
+ constructor(flag) {
47
+ super(`${flag} - 需要通过 pipe 或重定向提供 JSON;不方便时请使用文件路径。`);
48
+ this.name = "StdinRecordInputError";
49
+ }
50
+ }
51
+ function readStdinRecordContent(flag) {
52
+ if (process.stdin.isTTY === true) {
53
+ throw new StdinRecordInputError(flag);
54
+ }
55
+ return readFileSync(0, "utf8");
56
+ }
45
57
  function parseVersion(version) {
46
58
  const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
47
59
  if (!match)
@@ -342,9 +354,9 @@ transition 子命令:
342
354
  review-ready / accept / archive
343
355
 
344
356
  record 子命令:
345
- job-submit --job <J> --report <F>
346
- user-decision --input <F>
347
- test-run --input <F>
357
+ job-submit --job <J> --report <F|->
358
+ user-decision --input <F|->
359
+ test-run --input <F|->
348
360
 
349
361
  jobs 子命令:
350
362
  list / packet --job <J>
@@ -555,7 +567,19 @@ jobs 子命令:
555
567
  console.error("record job-submit 需要 --job 和 --report");
556
568
  return 1;
557
569
  }
558
- const result = recordJobSubmit(projectRoot, change, cr, jobId, report);
570
+ let result;
571
+ try {
572
+ result = report === "-"
573
+ ? recordJobSubmitContent(projectRoot, change, cr, jobId, readStdinRecordContent("--report"))
574
+ : recordJobSubmit(projectRoot, change, cr, jobId, report);
575
+ }
576
+ catch (err) {
577
+ if (err instanceof StdinRecordInputError) {
578
+ console.error(err.message);
579
+ return 1;
580
+ }
581
+ throw err;
582
+ }
559
583
  console.log(JSON.stringify(result, null, 2));
560
584
  return result.accepted ? 0 : 1;
561
585
  }
@@ -565,7 +589,19 @@ jobs 子命令:
565
589
  console.error("record user-decision 需要 --input");
566
590
  return 1;
567
591
  }
568
- const result = recordUserDecision(projectRoot, change, inputFile);
592
+ let result;
593
+ try {
594
+ result = inputFile === "-"
595
+ ? recordUserDecisionContent(projectRoot, change, readStdinRecordContent("--input"))
596
+ : recordUserDecision(projectRoot, change, inputFile);
597
+ }
598
+ catch (err) {
599
+ if (err instanceof StdinRecordInputError) {
600
+ console.error(err.message);
601
+ return 1;
602
+ }
603
+ throw err;
604
+ }
569
605
  console.log(JSON.stringify(result, null, 2));
570
606
  return result.accepted ? 0 : 1;
571
607
  }
@@ -575,7 +611,19 @@ jobs 子命令:
575
611
  console.error("record test-run 需要 --input");
576
612
  return 1;
577
613
  }
578
- const result = recordTestRun(projectRoot, change, inputFile);
614
+ let result;
615
+ try {
616
+ result = inputFile === "-"
617
+ ? recordTestRunContent(projectRoot, change, readStdinRecordContent("--input"))
618
+ : recordTestRun(projectRoot, change, inputFile);
619
+ }
620
+ catch (err) {
621
+ if (err instanceof StdinRecordInputError) {
622
+ console.error(err.message);
623
+ return 1;
624
+ }
625
+ throw err;
626
+ }
579
627
  console.log(JSON.stringify(result, null, 2));
580
628
  return result.accepted ? 0 : 1;
581
629
  }
package/dist/record.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import type { RecordResult, Job } from "./types.ts";
2
2
  /** record job-submit:登记工作项结果 */
3
3
  export declare function recordJobSubmit(projectRoot: string, change: string, changeRoot: string, jobId: string, reportFile: string): RecordResult;
4
+ /** record job-submit:从 JSON 内容登记工作项结果 */
5
+ export declare function recordJobSubmitContent(projectRoot: string, change: string, changeRoot: string, jobId: string, reportContent: string): RecordResult;
4
6
  /** record user-decision:登记用户决策 */
5
7
  export declare function recordUserDecision(projectRoot: string, change: string, inputFile: string): RecordResult;
8
+ /** record user-decision:从 JSON 内容登记用户决策 */
9
+ export declare function recordUserDecisionContent(projectRoot: string, change: string, content: string): RecordResult;
6
10
  /** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
7
11
  export declare function jobsList(projectRoot: string, change: string): {
8
12
  open: Job[];
package/dist/record.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // SuperSpec 流程引擎 — record:工作项结果登记
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, appendRawRecord, } from "./store.js";
4
+ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha256Text, withLock, appendRawRecord, } from "./store.js";
5
5
  import { reviewEvidenceDigest, reviewVerifierStaleReason } from "./review.js";
6
6
  const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
7
7
  const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
@@ -57,142 +57,194 @@ function jobTerminalState(events, jobId) {
57
57
  }
58
58
  return null;
59
59
  }
60
+ function terminalJobSubmitResult(events, jobId, terminal, reportDigest) {
61
+ const existing = events.find(e => (e.event_type === "job_accepted" || e.event_type === "job_rejected")
62
+ && e.payload.job_id === jobId
63
+ && e.payload.report_digest === reportDigest);
64
+ if (existing) {
65
+ return {
66
+ event_type: existing.event_type,
67
+ accepted: existing.event_type === "job_accepted",
68
+ message: "幂等返回:同 report 已提交",
69
+ job_state: existing.event_type === "job_accepted" ? "accepted" : "rejected",
70
+ };
71
+ }
72
+ return {
73
+ event_type: "job_rejected",
74
+ accepted: false,
75
+ message: `工作项 ${jobId} 已终态(${terminal}),不接受新报告。需要新工作项请重跑 transition。`,
76
+ };
77
+ }
78
+ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, events, reportContent, reportDigest) {
79
+ const checks = [];
80
+ let parsedReport = null;
81
+ try {
82
+ const report = JSON.parse(reportContent);
83
+ if (!report || typeof report !== "object" || Array.isArray(report)) {
84
+ checks.push("报告必须是 JSON object");
85
+ }
86
+ else {
87
+ parsedReport = report;
88
+ const obj = parsedReport;
89
+ for (const field of REVIEW_REPORT_REQUIRED_FIELDS) {
90
+ if (!(field in obj))
91
+ checks.push(`报告缺少必填字段 ${field}`);
92
+ }
93
+ if (obj.role !== job.role) {
94
+ checks.push(`报告角色 ${String(obj.role)} 与工作项角色 ${job.role} 不匹配`);
95
+ }
96
+ if (obj.verdict !== "pass" && obj.verdict !== "fail") {
97
+ checks.push("报告 verdict 必须是 pass 或 fail");
98
+ }
99
+ if (!Array.isArray(obj.findings)) {
100
+ checks.push("报告 findings 必须是数组");
101
+ }
102
+ if (obj.verdict === "fail") {
103
+ checks.push("报告 verdict=fail,工作项未通过");
104
+ }
105
+ if (requiresReviewer(job.role)) {
106
+ if (!("reviewer" in obj))
107
+ checks.push("报告缺少必填字段 reviewer");
108
+ const reviewer = obj.reviewer;
109
+ if (!reviewer || typeof reviewer !== "object" || Array.isArray(reviewer)) {
110
+ checks.push("报告 reviewer 必须是包含 kind/id 的对象");
111
+ }
112
+ else {
113
+ if (typeof reviewer.kind !== "string" || !REVIEWER_KINDS.has(reviewer.kind)) {
114
+ checks.push(`报告 reviewer.kind 必须是 ${[...REVIEWER_KINDS].join("|")} 之一`);
115
+ }
116
+ if (typeof reviewer.id !== "string" || reviewer.id.trim() === "") {
117
+ checks.push("报告 reviewer.id 必须是非空字符串");
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ checks.push("报告必须是有效 JSON");
125
+ }
126
+ for (const bf of job.boundFiles) {
127
+ const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
128
+ if (currentSha !== bf.sha) {
129
+ checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
130
+ }
131
+ }
132
+ const reviewStaleReason = reviewVerifierStaleReason(job, changeRoot, reviewEvidenceDigest(events));
133
+ if (reviewStaleReason && !checks.includes(reviewStaleReason)) {
134
+ checks.push(reviewStaleReason);
135
+ }
136
+ if (!reportContent.trim()) {
137
+ checks.push("报告内容为空");
138
+ }
139
+ if (checks.length > 0) {
140
+ const rejectEvent = makeEvent(change, "job_rejected", {
141
+ job_id: jobId,
142
+ role: job.role,
143
+ report_digest: reportDigest,
144
+ reason: checks.join("; "),
145
+ });
146
+ appendEvent(projectRoot, change, rejectEvent);
147
+ return {
148
+ event_type: "job_rejected",
149
+ accepted: false,
150
+ message: `工作项 ${jobId} 被拒绝:${checks.join("; ")}`,
151
+ job_state: "rejected",
152
+ };
153
+ }
154
+ const rawRef = appendRawRecord(projectRoot, change, "review-reports", parsedReport);
155
+ const acceptEvent = makeEvent(change, "job_accepted", {
156
+ job_id: jobId,
157
+ role: job.role,
158
+ report_digest: reportDigest,
159
+ accepted_at: new Date().toISOString(),
160
+ ...rawRef,
161
+ });
162
+ appendEvent(projectRoot, change, acceptEvent);
163
+ return {
164
+ event_type: "job_accepted",
165
+ accepted: true,
166
+ message: `工作项 ${jobId}(${job.role})已接受`,
167
+ job_state: "accepted",
168
+ };
169
+ }
60
170
  /** record job-submit:登记工作项结果 */
61
171
  export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFile) {
62
172
  return withLock(projectRoot, change, () => {
63
173
  ensureChangeLayout(projectRoot, change);
64
174
  const events = readEvents(projectRoot, change);
65
- // 查找 job
66
175
  const job = findJob(events, jobId);
67
176
  if (!job) {
68
177
  return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
69
178
  }
70
- // 检查终态
71
179
  const terminal = jobTerminalState(events, jobId);
72
180
  if (terminal) {
73
- // 幂等检查:同 report_digest → 返回旧结果
74
181
  const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
75
- const existing = events.find(e => (e.event_type === "job_accepted" || e.event_type === "job_rejected")
76
- && e.payload.job_id === jobId
77
- && e.payload.report_digest === reportDigest);
78
- if (existing) {
79
- return {
80
- event_type: existing.event_type,
81
- accepted: existing.event_type === "job_accepted",
82
- message: "幂等返回:同 report 已提交",
83
- job_state: existing.event_type === "job_accepted" ? "accepted" : "rejected",
84
- };
85
- }
86
- // 终态 job + 不同 report → 拒绝
87
- return {
88
- event_type: "job_rejected",
89
- accepted: false,
90
- message: `工作项 ${jobId} 已终态(${terminal}),不接受新报告。需要新工作项请重跑 transition。`,
91
- };
182
+ return terminalJobSubmitResult(events, jobId, terminal, reportDigest);
92
183
  }
93
- // 读报告
94
184
  if (!existsSync(reportFile)) {
95
185
  return { event_type: "job_rejected", accepted: false, message: `报告文件不存在:${reportFile}` };
96
186
  }
97
187
  const reportContent = readFileSync(reportFile, "utf8");
98
188
  const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
99
- // acceptance checks
100
- const checks = [];
101
- let parsedReport = null;
102
- // 0. 报告格式和角色匹配(最小 JSON contract)
103
- try {
104
- const report = JSON.parse(reportContent);
105
- if (!report || typeof report !== "object" || Array.isArray(report)) {
106
- checks.push("报告必须是 JSON object");
107
- }
108
- else {
109
- parsedReport = report;
110
- const obj = parsedReport;
111
- for (const field of REVIEW_REPORT_REQUIRED_FIELDS) {
112
- if (!(field in obj))
113
- checks.push(`报告缺少必填字段 ${field}`);
114
- }
115
- if (obj.role !== job.role) {
116
- checks.push(`报告角色 ${String(obj.role)} 与工作项角色 ${job.role} 不匹配`);
117
- }
118
- if (obj.verdict !== "pass" && obj.verdict !== "fail") {
119
- checks.push("报告 verdict 必须是 pass 或 fail");
120
- }
121
- if (!Array.isArray(obj.findings)) {
122
- checks.push("报告 findings 必须是数组");
123
- }
124
- if (obj.verdict === "fail") {
125
- checks.push("报告 verdict=fail,工作项未通过");
126
- }
127
- if (requiresReviewer(job.role)) {
128
- if (!("reviewer" in obj))
129
- checks.push("报告缺少必填字段 reviewer");
130
- const reviewer = obj.reviewer;
131
- if (!reviewer || typeof reviewer !== "object" || Array.isArray(reviewer)) {
132
- checks.push("报告 reviewer 必须是包含 kind/id 的对象");
133
- }
134
- else {
135
- if (typeof reviewer.kind !== "string" || !REVIEWER_KINDS.has(reviewer.kind)) {
136
- checks.push(`报告 reviewer.kind 必须是 ${[...REVIEWER_KINDS].join("|")} 之一`);
137
- }
138
- if (typeof reviewer.id !== "string" || reviewer.id.trim() === "") {
139
- checks.push("报告 reviewer.id 必须是非空字符串");
140
- }
141
- }
142
- }
143
- }
144
- }
145
- catch {
146
- checks.push("报告必须是有效 JSON");
147
- }
148
- // 1. boundFiles 仍匹配当前文档(missing 也算不匹配)
149
- for (const bf of job.boundFiles) {
150
- const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
151
- if (currentSha !== bf.sha) {
152
- checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
153
- }
154
- }
155
- const reviewStaleReason = reviewVerifierStaleReason(job, changeRoot, reviewEvidenceDigest(events));
156
- if (reviewStaleReason && !checks.includes(reviewStaleReason)) {
157
- checks.push(reviewStaleReason);
158
- }
159
- // 2. 报告格式基本校验(非空 JSON 或文本)
160
- if (!reportContent.trim()) {
161
- checks.push("报告内容为空");
189
+ return recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, events, reportContent, reportDigest);
190
+ });
191
+ }
192
+ /** record job-submit:从 JSON 内容登记工作项结果 */
193
+ export function recordJobSubmitContent(projectRoot, change, changeRoot, jobId, reportContent) {
194
+ return withLock(projectRoot, change, () => {
195
+ ensureChangeLayout(projectRoot, change);
196
+ const events = readEvents(projectRoot, change);
197
+ const job = findJob(events, jobId);
198
+ if (!job) {
199
+ return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
162
200
  }
163
- if (checks.length > 0) {
164
- // 拒绝
165
- const rejectEvent = makeEvent(change, "job_rejected", {
166
- job_id: jobId,
167
- role: job.role,
168
- report_digest: reportDigest,
169
- reason: checks.join("; "),
170
- });
171
- appendEvent(projectRoot, change, rejectEvent);
172
- return {
173
- event_type: "job_rejected",
174
- accepted: false,
175
- message: `工作项 ${jobId} 被拒绝:${checks.join("; ")}`,
176
- job_state: "rejected",
177
- };
201
+ const reportDigest = sha256Text(reportContent);
202
+ const terminal = jobTerminalState(events, jobId);
203
+ if (terminal) {
204
+ return terminalJobSubmitResult(events, jobId, terminal, reportDigest);
178
205
  }
179
- // 接受
180
- const rawRef = appendRawRecord(projectRoot, change, "review-reports", parsedReport);
181
- const acceptEvent = makeEvent(change, "job_accepted", {
182
- job_id: jobId,
183
- role: job.role,
184
- report_digest: reportDigest,
185
- accepted_at: new Date().toISOString(),
186
- ...rawRef,
187
- });
188
- appendEvent(projectRoot, change, acceptEvent);
206
+ return recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, events, reportContent, reportDigest);
207
+ });
208
+ }
209
+ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDigest) {
210
+ let decision;
211
+ try {
212
+ decision = JSON.parse(content);
213
+ }
214
+ catch {
215
+ appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "invalid_json" }));
216
+ return { event_type: "user_decision_recorded", accepted: false, message: "决策文件不是有效 JSON" };
217
+ }
218
+ if (!decision.scope || !decision.answer) {
219
+ appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "missing_scope_or_answer" }));
220
+ return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少 scope 或 answer" };
221
+ }
222
+ const existing = events.find(e => e.event_type === "user_decision_recorded"
223
+ && e.payload.input_digest === inputDigest);
224
+ if (existing) {
189
225
  return {
190
- event_type: "job_accepted",
226
+ event_type: "user_decision_recorded",
191
227
  accepted: true,
192
- message: `工作项 ${jobId}(${job.role})已接受`,
193
- job_state: "accepted",
228
+ message: "幂等返回:同 user decision 已登记",
194
229
  };
230
+ }
231
+ const normalizedDecision = {
232
+ scope: decision.scope,
233
+ question: decision.question ?? "",
234
+ answer: decision.answer,
235
+ };
236
+ const rawRef = appendRawRecord(projectRoot, change, "user-decisions", normalizedDecision);
237
+ const event = makeEvent(change, "user_decision_recorded", {
238
+ ...normalizedDecision,
239
+ input_digest: inputDigest,
240
+ ...rawRef,
195
241
  });
242
+ appendEvent(projectRoot, change, event);
243
+ return {
244
+ event_type: "user_decision_recorded",
245
+ accepted: true,
246
+ message: `用户决策已登记:scope=${decision.scope}`,
247
+ };
196
248
  }
197
249
  /** record user-decision:登记用户决策 */
198
250
  export function recordUserDecision(projectRoot, change, inputFile) {
@@ -204,45 +256,16 @@ export function recordUserDecision(projectRoot, change, inputFile) {
204
256
  return { event_type: "user_decision_recorded", accepted: false, message: `决策文件不存在:${inputFile}` };
205
257
  }
206
258
  const content = readFileSync(inputFile, "utf8");
207
- let decision;
208
- try {
209
- decision = JSON.parse(content);
210
- }
211
- catch {
212
- appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "invalid_json" }));
213
- return { event_type: "user_decision_recorded", accepted: false, message: "决策文件不是有效 JSON" };
214
- }
215
- if (!decision.scope || !decision.answer) {
216
- appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "missing_scope_or_answer" }));
217
- return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少 scope 或 answer" };
218
- }
219
259
  const inputDigest = sha256File(inputFile) ?? "sha256:unknown";
220
- const existing = events.find(e => e.event_type === "user_decision_recorded"
221
- && e.payload.input_digest === inputDigest);
222
- if (existing) {
223
- return {
224
- event_type: "user_decision_recorded",
225
- accepted: true,
226
- message: "幂等返回:同 user decision 已登记",
227
- };
228
- }
229
- const normalizedDecision = {
230
- scope: decision.scope,
231
- question: decision.question ?? "",
232
- answer: decision.answer,
233
- };
234
- const rawRef = appendRawRecord(projectRoot, change, "user-decisions", normalizedDecision);
235
- const event = makeEvent(change, "user_decision_recorded", {
236
- ...normalizedDecision,
237
- input_digest: inputDigest,
238
- ...rawRef,
239
- });
240
- appendEvent(projectRoot, change, event);
241
- return {
242
- event_type: "user_decision_recorded",
243
- accepted: true,
244
- message: `用户决策已登记:scope=${decision.scope}`,
245
- };
260
+ return recordUserDecisionLoaded(projectRoot, change, events, content, inputDigest);
261
+ });
262
+ }
263
+ /** record user-decision:从 JSON 内容登记用户决策 */
264
+ export function recordUserDecisionContent(projectRoot, change, content) {
265
+ return withLock(projectRoot, change, () => {
266
+ ensureChangeLayout(projectRoot, change);
267
+ const events = readEvents(projectRoot, change);
268
+ return recordUserDecisionLoaded(projectRoot, change, events, content, sha256Text(content));
246
269
  });
247
270
  }
248
271
  /** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
@@ -296,12 +319,15 @@ export function jobsPacket(projectRoot, change, jobId) {
296
319
  ...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
297
320
  packet_digest: job.packet_digest,
298
321
  required_output_kind: "job_report_json",
322
+ preferred_input_mode: "stdin",
323
+ submission_command: `superspec record job-submit --change "${change}" --job "${job.job_id}" --report -`,
324
+ file_fallback: true,
299
325
  output_contract_fields: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
300
326
  output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
301
327
  output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
302
328
  (job.review_evidence_digest ? `本工作项绑定的执行证据版本为 ${job.review_evidence_digest},` : "") +
303
329
  (requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} reviewer 执行并在 reviewer.kind/id 中记录来源,` : "") +
304
- `产出 JSON 报告文件并通过 superspec record job-submit 登记。` +
330
+ `产出 JSON 报告内容并优先通过 --report - stdin 登记;文件路径模式仍可作为 fallback。` +
305
331
  (requiresReviewer(job.role)
306
332
  ? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
307
333
  : `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]}`),
package/dist/task.d.ts CHANGED
@@ -5,3 +5,8 @@ export declare function recordTestRun(projectRoot: string, change: string, input
5
5
  accepted: boolean;
6
6
  message: string;
7
7
  };
8
+ /** record test-run:从 JSON 内容登记测试运行记录 */
9
+ export declare function recordTestRunContent(projectRoot: string, change: string, content: string): {
10
+ accepted: boolean;
11
+ message: string;
12
+ };
package/dist/task.js CHANGED
@@ -11,39 +11,49 @@ export function tasksStructureDigestOf(changeRoot) {
11
11
  const content = readFileSync(p, "utf8");
12
12
  return formatDigest(content, sha256Text);
13
13
  }
14
+ function recordTestRunLoaded(projectRoot, change, content) {
15
+ let tr;
16
+ try {
17
+ tr = JSON.parse(content);
18
+ }
19
+ catch {
20
+ return { accepted: false, message: "无效 JSON" };
21
+ }
22
+ if (!tr.test_id || !tr.task_structure_digest) {
23
+ return { accepted: false, message: "缺少 test_id 或 task_structure_digest" };
24
+ }
25
+ const normalizedTestRun = {
26
+ test_id: tr.test_id,
27
+ task_structure_digest: tr.task_structure_digest,
28
+ attempt_id: tr.attempt_id ?? null,
29
+ command: tr.command ?? "",
30
+ cwd: tr.cwd ?? "",
31
+ exit_code: tr.exit_code ?? -1,
32
+ semantic_status: tr.semantic_status ?? "unknown",
33
+ target_fingerprint: tr.target_fingerprint ?? null,
34
+ raw_log_ref: tr.raw_log_ref ?? null,
35
+ };
36
+ const rawRef = appendRawRecord(projectRoot, change, "test-runs", normalizedTestRun);
37
+ const event = makeEvent(change, "test_run_recorded", {
38
+ ...normalizedTestRun,
39
+ ...rawRef,
40
+ });
41
+ appendEvent(projectRoot, change, event);
42
+ return { accepted: true, message: `测试运行已登记:test_id=${tr.test_id}` };
43
+ }
14
44
  /** record test-run:登记测试运行记录 */
15
45
  export function recordTestRun(projectRoot, change, inputFile) {
16
46
  return withLock(projectRoot, change, () => {
17
47
  ensureChangeLayout(projectRoot, change);
18
48
  if (!existsSync(inputFile))
19
49
  return { accepted: false, message: `文件不存在:${inputFile}` };
20
- let tr;
21
- try {
22
- tr = JSON.parse(readFileSync(inputFile, "utf8"));
23
- }
24
- catch {
25
- return { accepted: false, message: "无效 JSON" };
26
- }
27
- if (!tr.test_id || !tr.task_structure_digest) {
28
- return { accepted: false, message: "缺少 test_id 或 task_structure_digest" };
29
- }
30
- const normalizedTestRun = {
31
- test_id: tr.test_id,
32
- task_structure_digest: tr.task_structure_digest,
33
- attempt_id: tr.attempt_id ?? null,
34
- command: tr.command ?? "",
35
- cwd: tr.cwd ?? "",
36
- exit_code: tr.exit_code ?? -1,
37
- semantic_status: tr.semantic_status ?? "unknown",
38
- target_fingerprint: tr.target_fingerprint ?? null,
39
- raw_log_ref: tr.raw_log_ref ?? null,
40
- };
41
- const rawRef = appendRawRecord(projectRoot, change, "test-runs", normalizedTestRun);
42
- const event = makeEvent(change, "test_run_recorded", {
43
- ...normalizedTestRun,
44
- ...rawRef,
45
- });
46
- appendEvent(projectRoot, change, event);
47
- return { accepted: true, message: `测试运行已登记:test_id=${tr.test_id}` };
50
+ return recordTestRunLoaded(projectRoot, change, readFileSync(inputFile, "utf8"));
51
+ });
52
+ }
53
+ /** record test-run:从 JSON 内容登记测试运行记录 */
54
+ export function recordTestRunContent(projectRoot, change, content) {
55
+ return withLock(projectRoot, change, () => {
56
+ ensureChangeLayout(projectRoot, change);
57
+ return recordTestRunLoaded(projectRoot, change, content);
48
58
  });
49
59
  }
package/dist/types.d.ts CHANGED
@@ -24,6 +24,9 @@ export interface JobPacket {
24
24
  review_evidence_digest?: string;
25
25
  packet_digest: string;
26
26
  required_output_kind: string;
27
+ preferred_input_mode?: "stdin" | "file";
28
+ submission_command?: string;
29
+ file_fallback?: boolean;
27
30
  output_contract_fields?: string[];
28
31
  output_contract_optional_fields?: string[];
29
32
  stop_conditions: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.23",
3
+ "version": "0.1.25-beta.0",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": {
@@ -19,7 +19,7 @@ argument-hint: "本次架构审查说明"
19
19
 
20
20
  在 `superspec-review` 或 disclosure review 中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
21
21
 
22
- 当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告文件必须是 JSON
22
+ 当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告内容必须是 JSON,并优先通过 `--report -` 从 stdin 登记:
23
23
 
24
24
  ```json
25
25
  {
@@ -36,6 +36,17 @@ argument-hint: "本次架构审查说明"
36
36
 
37
37
  `role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。发现阻塞架构问题时必须使用 `verdict:"fail"`。
38
38
 
39
+ ## 计划 / 设计审查口径
40
+
41
+ 审查计划文档时,重点判断影响范围、技术决策和任务拆分是否能支撑后续实现,不要把文档格式本身当成目标。
42
+
43
+ - `proposal.md` 的 `## Impact` 应通过 `Area` / `Reason` 说明受影响区域和原因;如果只有泛目录、没有原因或把 `Area` 当路径白名单,应提出阻塞或风险
44
+ - `design.md` 应记录关键决策、替代方案和风险取舍;如果只是复制影响范围、任务清单或实现步骤,说明设计边界不清
45
+ - `tasks.md` 可以用 Markdown 标题分组,但可执行边界必须落到顶格 checkbox 叶子 task
46
+ - 任务分组应贴合系统边界;高风险模块、跨入口行为或难以 review 的大改动,应要求拆成可独立验证的 task
47
+ - 如果分组标题、task id 或任务文本会让执行者容易启动错任务,应使用 `verdict:"fail"`
48
+ - 不要为了弥补拆分不清而要求新增父子任务状态、额外设计字段或 tasks 反向引用 design;先要求更清楚的分组和叶子 task
49
+
39
50
  ## 输出风格
40
51
 
41
52
  - 所有用户可见输出必须使用简体中文。
@@ -20,7 +20,7 @@ argument-hint: "本次反方审查说明"
20
20
 
21
21
  在 `superspec-review` 或 disclosure review 中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
22
22
 
23
- 当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告文件必须是 JSON
23
+ 当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告内容必须是 JSON,并优先通过 `--report -` 从 stdin 登记:
24
24
 
25
25
  ```json
26
26
  {
@@ -37,7 +37,43 @@ argument-hint: "本次反方审查说明"
37
37
 
38
38
  `role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。发现阻塞问题时必须使用 `verdict:"fail"`,并在 `findings` 中给出证据和修复建议。
39
39
 
40
- 当你在 `review_complete` 中承担 verification lane 时,必须确认本次任务说明要求输出验证意见;否则只输出 source guidance。
40
+ 当你在 `review_complete` 中承担验证职责时,必须确认本次任务说明要求输出验证意见;否则只输出 source guidance。
41
+
42
+ ## Discovery 审查口径
43
+
44
+ 当审查 explore 阶段的 discovery 时,判断它是否足以支撑进入 propose。不要接管设计,不要替主流程选方案。
45
+
46
+ 最小通过条件:
47
+
48
+ - 代码影响型需求必须包含 repo source anchors;纯文档、配置或新文件任务没有代码锚点时,必须说明 `N/A` 理由并引用相关文档、配置或需求来源。
49
+ - `当前代码事实` 必须能说明当前实现怎么工作,而不是泛泛复述需求。
50
+ - `需求理解` 必须说明用户目标和当前实现之间的差异。
51
+ - `影响范围候选` 中每个主要候选应有至少一个 `path:line` 或等价文档锚点;无法验证时必须标明不确定性。
52
+ - 风险必须绑定具体代码、行为、数据或文档事实。
53
+ - 未验证假设、会影响范围或验收的问题必须进入 `## 待确认问题`,或明确说明为什么非阻塞。
54
+
55
+ 代码影响型 discovery 缺少事实锚点、需求理解与当前实现脱节、或把未验证假设当成事实时,使用 `verdict:"fail"`。
56
+
57
+ ## Propose 审查口径
58
+
59
+ 审查 propose 阶段计划时,重点挑战影响范围、原因和任务计划是否会让 apply 跑偏。
60
+
61
+ 阻塞条件:
62
+
63
+ - `proposal.md` 缺少 `## Impact`
64
+ - `## Impact` 没有说明 `Area` / `Reason`
65
+ - `Area` 只有泛目录,且没有原因或不确定性说明
66
+ - `Reason` 只写“要改这里”,没有解释为什么受影响
67
+ - `## Impact` 写成任务清单或路径白名单
68
+ - `design.md` 把影响范围表、任务拆分或实现清单复制进去,导致技术决策不清
69
+ - `tasks.md` 的任务拆分过粗,把多个独立行为放进同一个执行单元,导致 apply 难以用一组清晰的 RED/GREEN 证据验收
70
+ - task id 重复、不稳定,或分组标题混入 task id,导致后续执行命令容易指错任务
71
+ - 普通说明或缩进 checkbox 承载了实际未完成工作,导致工作流无法自然推进
72
+ - task 中写入 RED/GREEN 命令、断言或预期输出,导致任务计划和实际执行证据混在一起
73
+
74
+ 发现这些问题时使用 `verdict:"fail"`,并给出最小拆分或补充建议。
75
+
76
+ 负例:一个 task 同时要求修改运行时行为、发布流程和文档,并且这些改动不能由同一组测试证据验收,应要求拆分;普通说明里出现 `TODO` / `follow-up` / “后续补”,但没有对应顶格 task,应使用 `verdict:"fail"`。
41
77
 
42
78
  ## 输出风格
43
79
 
@@ -15,11 +15,24 @@ argument-hint: "本次探索说明"
15
15
  - 优先使用 repo search 和文件读取验证事实,结论必须绑定可读源码或文档锚点。
16
16
  - 不要写 `proposal.md`/`design.md`/`tasks.md`/`specs/**`/`.superspec/**`。
17
17
  - 不能作为 `explore_complete` 的 role evidence;strict 风险模式需要门禁审查时交给 `critic`。
18
+ - 当作为 explore subagent 深扫时,只输出事实、文件行号锚点、隐性约束、影响范围候选、风险和需要主流程确认的问题;不要输出实现方案,不要替主流程做取舍。
19
+ - “影响范围候选”只描述现有代码表面、相邻模块和潜在风险,不写具体实现步骤。
18
20
 
19
21
  ## 本次任务说明
20
22
 
21
23
  如果主流程提供本次任务说明,先读取其中指向的 refs。以本次任务说明中的目标范围、来源 refs、必读 refs、artifact refs 和停止条件为准;不要依赖本 prompt 记忆输出 schema。
22
24
 
25
+ ## 深扫输出要求
26
+
27
+ 代码影响型需求必须尽量提供 `path:line` 形式的 repo source anchors。纯文档、配置或新文件任务没有代码锚点时,明确写出 `N/A` 理由,并引用相关文档、配置或需求来源。
28
+
29
+ 输出至少区分:
30
+
31
+ - 已确认事实
32
+ - 影响范围候选
33
+ - 风险和隐性约束
34
+ - 需要主流程确认的问题
35
+
23
36
  ## 输出风格
24
37
 
25
38
  - 所有用户可见输出必须使用简体中文。
@@ -7,20 +7,20 @@ argument-hint: "本次测试审查说明"
7
7
 
8
8
  ## 角色身份
9
9
 
10
- 你是 Test Engineer。你审查测试策略、覆盖充分性、RED/GREEN 可信度、脆弱测试风险和验收场景映射。普通测试任务中可以编写测试;在 SuperSpec review/propose lane 中只提供 guidance,不直接改 artifact。
10
+ 你是 Test Engineer。你审查测试策略、覆盖充分性、RED/GREEN 可信度、脆弱测试风险和验收场景映射。普通测试任务中可以编写测试;在 SuperSpec review/propose 阶段中只提供 guidance,不直接改 artifact。
11
11
 
12
12
  ## 读写边界
13
13
 
14
- - SuperSpec review/propose lane 默认只读;不要修改方案、测试契约或实现。
14
+ - SuperSpec review/propose 阶段默认只读;不要修改方案、测试契约或实现。
15
15
  - 普通测试实现任务中,只写测试,不写业务实现;需要实现改动时向主流程说明。
16
- - Apply 阶段如需新增或修改 RED/characterization 测试文件,只在主流程明确交付的 bounded native lane 内写测试;正式 RED/characterization/GREEN 运行证据仍由 test-runner 的本次测试说明生成。
16
+ - Apply 阶段如需新增或修改 RED/characterization 测试文件,只在主流程明确交付的有界测试任务内写测试;正式 RED/characterization/GREEN 运行证据仍由 test-runner 的本次测试说明生成。
17
17
  - 必须核对现有测试模式和目标 acceptance,不用臆测替代证据。
18
18
 
19
19
  ## 本次任务说明
20
20
 
21
- 在 SuperSpec review/propose lane 中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
21
+ 在 SuperSpec review/propose 阶段中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
22
22
 
23
- 当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告文件必须是 JSON
23
+ 当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告内容必须是 JSON,并优先通过 `--report -` 从 stdin 登记:
24
24
 
25
25
  ```json
26
26
  {
@@ -37,6 +37,16 @@ argument-hint: "本次测试审查说明"
37
37
 
38
38
  `role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。测试契约、覆盖策略或验证路径不足时必须使用 `verdict:"fail"`。
39
39
 
40
+ ## 任务拆分与 RED/GREEN 审查口径
41
+
42
+ 在 propose 或 review 阶段审查 `tasks.md` 时:
43
+
44
+ - TDD task 应能形成清晰 RED/GREEN 闭环,但 RED/GREEN 命令、断言或预期输出不应写进 `tasks.md`
45
+ - `tasks.md` 只声明任务边界和 `tdd_required:true/false`;实际 RED/GREEN 细节属于 apply 阶段的 `record test-run` 证据
46
+ - 无法定义目标测试身份、RED 失败信号、GREEN 覆盖映射,或只靠退出码/笼统命令证明的测试方案,应使用 `verdict:"fail"`
47
+ - `tdd_required:false` 必须有明确 `no_tdd_reason`
48
+ - 不要求建立新的 test-contract 关联,也不要求把 RED/GREEN 细节塞回 task 行
49
+
40
50
  ## 输出风格
41
51
 
42
52
  - 所有用户可见输出必须使用简体中文。
@@ -35,7 +35,7 @@ argument-hint: "本次验证说明"
35
35
 
36
36
  `role`、`verdict`、`findings` 是必填字段。`verdict` 只能是 `pass` 或 `fail`。任务未完成、测试证据缺失、文档与实现状态不一致、绑定文件无法核对时输出 `verdict:"fail"`。
37
37
 
38
- `superspec-review` verification lane 先读主流程提供的本次验证说明;以本次任务说明中的引用范围、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
38
+ `superspec-review` 验证环节先读主流程提供的本次验证说明;以本次任务说明中的引用范围、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
39
39
 
40
40
  确认本次任务说明要求输出 verification review 后,再输出 verification review。
41
41
 
@@ -45,6 +45,18 @@ apply worker report 字段以本次任务说明中的 `verifier_report_required_
45
45
 
46
46
  遵守本次任务说明中的报告策略:长日志、完整 diff、编译输出和大段生成内容用 artifact refs,不内联。
47
47
 
48
+ ## 计划 / 设计验证口径
49
+
50
+ 核对最终实现和计划文档时:
51
+
52
+ - 实际代码改动应能从 `proposal.md` 的 `## Impact`、`design.md` 的关键决策或已完成 task 找到合理解释;无法解释的用户可见行为、新能力或大范围改动应使用 `verdict:"fail"`
53
+ - `tasks.md` 在执行期间不应被改写计划内容;除目标 checkbox 被完成命令勾选外,新增任务、改任务含义或把未完成工作藏进普通说明,都应视为证明缺口
54
+ - 已完成 TDD task 的 RED/GREEN 以 `record test-run` 证据为准,不以 `tasks.md` 的文字描述为准
55
+ - 对每个已完成 TDD task,核对同一个 `task_completed.attempt_id` 下是否同时存在 RED/characterization 和 GREEN;新证据必须带同一 `attempt_id`
56
+ - 缺少 `attempt_id`、只靠 `task_structure_digest` 匹配的 test-run 只能视为旧数据兼容,不作为新流程“确实跑了红绿验证”的强证明
57
+ - test-run 证据应说明目标测试身份、`test_id`、`command`、`cwd`、`exit_code` 和 `semantic_status`;退出码本身不等于证明,环境错误 / 构建错误不算 RED/GREEN
58
+ - 可追溯性以引擎记录的 test-run 事件、`raw_index` 和 `raw_digest` 为准;额外日志或 test-runner report 只作为补充引用
59
+
48
60
  ## 输出风格
49
61
 
50
62
  - 所有用户可见输出必须使用简体中文。
@@ -23,15 +23,19 @@ metadata:
23
23
 
24
24
  每个任务的循环:
25
25
 
26
- 1. **任务开始**:`superspec transition task-start --change "<change>" --task TASK-XXX`
26
+ 1. **任务开始**:`superspec transition task-start --change "<change>" --task <task_id>`
27
27
  2. **拿到执行尝试 ID**:从 task-start 的返回结果或 `superspec status` 中读取当前活跃 attempt 的 `attempt_id`
28
- 3. **红灯验证**:写测试,跑测试确认失败,`superspec record test-run --change "<change>" --input <FILE>`
28
+ 3. **红灯验证**:写测试,跑测试确认失败,优先用 `superspec record test-run --change "<change>" --input -` 从 stdin 登记 JSON 内容;文件路径模式仍可作为 fallback
29
29
  4. **代码实现**:根据任务写代码实现,保证代码不出现过渡设计以及代码质量
30
- 5. **绿灯验证**:跑测试确认通过,`superspec record test-run --change "<change>" --input <FILE>`
31
- 6. **任务结束标记完成**:`superspec transition task-complete --change "<change>" --task TASK-XXX`
30
+ 5. **绿灯验证**:跑测试确认通过,优先用 `superspec record test-run --change "<change>" --input -` 从 stdin 登记 JSON 内容;文件路径模式仍可作为 fallback
31
+ 6. **任务结束标记完成**:`superspec transition task-complete --change "<change>" --task <task_id>`
32
32
 
33
33
  no-TDD 任务(tdd_required:false + no_tdd_reason)跳过 RED/GREEN。
34
34
 
35
+ 只执行 `tasks.md` 中顶格 checkbox 行里的 `<task_id>`,例如 `1.1` 或 `TASK-001.1`。Markdown 标题只是分组,不传给 `task-start` / `task-complete`;普通 bullet 只是说明,不单独成为工作流执行单元。
36
+
37
+ `tasks.md` 不写 RED/GREEN 命令、断言或预期输出。RED/GREEN 的真实证明来自 apply 阶段实际执行后登记的 `record test-run`。
38
+
35
39
  ## test-run 输入格式
36
40
 
37
41
  ```json
@@ -50,10 +54,18 @@ no-TDD 任务(tdd_required:false + no_tdd_reason)跳过 RED/GREEN。
50
54
  - `attempt_id`:从 task-start 结果获取,确保 RED/GREEN 绑定到正确的执行尝试
51
55
  - `semantic_status`:`expected_failure`(RED)/ `expected_success`(GREEN)/ `characterization_pass`
52
56
  - `task_structure_digest`:tasks.md 复选框归一化后的 sha256(引擎计算,你不需要手动算)
57
+ - 新产生的 TDD 证据必须带当前 `attempt_id`;缺少 `attempt_id`、只靠 `task_structure_digest` 匹配的 test-run 仅用于旧数据兼容,不作为新流程强证明
58
+ - `test_id`、`command`、`cwd`、`exit_code`、`semantic_status` 和目标测试身份必须能说明目标测试确实运行;退出码本身不等于证明
59
+ - 可追溯证据以引擎记录的 test-run 事件为准;如有额外日志或 test-runner report,可作为补充引用,不作为必填字段
53
60
 
54
61
  ## Guardrails
55
62
 
56
63
  - 只改 tasks.md 里本任务范围相关的文件
64
+ - 需要判断影响范围或改动原因不自明时,参考 `proposal.md` 的 `## Impact`,但不要把它当作路径白名单
65
+ - 编码时发现未列入影响范围的文件,如果从 diff 或引用链能直接解释为同一任务下的局部引用、测试辅助或机械连带改动,可以继续
66
+ - 如果发现新增能力、用户可见行为、明显新增影响范围或原因不自明,停止扩大实现并报告给主流程;不要在 apply 阶段补改 `proposal.md`
67
+ - 不修改 `proposal.md`、`design.md`、`specs/**` 或 `.superspec/**`
68
+ - active attempt 期间不要修改 `tasks.md` 中除 `task-complete` 自动勾选目标 checkbox 外的内容
57
69
  - 不跳过 RED 直接写 GREEN
58
70
  - 退出码 0 ≠ 测试通过——semantic_status 才是证据
59
71
  - 环境错误 / 构建失败不算 RED 或 GREEN
@@ -19,31 +19,55 @@ metadata:
19
19
  3. 登记结果
20
20
  4. 回到 1
21
21
 
22
- next 返回 `ask_user` 说明 discovery 不完整或有未确认问题——向用户提问,收到回答后 `superspec record user-decision --change "<change>" --input <FILE>`。
22
+ next 返回 `ask_user` 说明 discovery 不完整或有未确认问题。若 scope 是 `explore_discovery`,先检查并填写 discovery 草稿,不要把草稿占位内容直接转问用户;只有真实阻塞问题才向用户提问,收到回答后优先用 `superspec record user-decision --change "<change>" --input -` 从 stdin 登记 JSON 内容;文件路径模式仍可作为 fallback。
23
23
 
24
- 本技能默认走完整审查路径。探索完成后,`explore → propose` 会先创建 `critic` 工作项,由 Critic 角色审查需求澄清记录。审查完成后通过 `superspec record job-submit --change "<change>" --job <JOB> --report <FILE>` 登记报告。
24
+ 本技能默认走完整审查路径。探索完成后,`explore → propose` 会先创建 `critic` 工作项,由 Critic 角色审查需求澄清记录。审查完成后优先通过 `superspec record job-submit --change "<change>" --job <JOB> --report -` 从 stdin 登记 JSON 报告内容;文件路径模式仍可作为 fallback。
25
25
 
26
26
  ## 本阶段做什么
27
27
 
28
28
  1. **建立事实基线**:读代码、查架构、理解当前系统行为(只读)
29
- 2. **写 discovery.md**:
29
+ 2. **写 discovery.md**:首次进入 explore 时引擎可能已创建草稿;必须用真实事实替换草稿标记和占位内容
30
30
  3. **澄清歧义**:有阻塞歧义时向用户提问
31
31
 
32
+ ## 探索分工
33
+
34
+ 主会话负责广度:理解用户需求、提出探索问题、汇总 discovery、判断哪些问题必须问用户。
35
+
36
+ 涉及多个文件、模块、入口或文件类型时,使用 `explore` subagent 做只读深扫。以下情况也应使用:
37
+
38
+ - 当前行为不清楚
39
+ - 涉及状态机、公共 API、数据格式、测试策略、权限、迁移或发布流程
40
+ - 影响范围可能大于用户表述
41
+
42
+ 可跳过 subagent 的场景:
43
+
44
+ - 纯文档
45
+ - 明显 typo
46
+ - 单文件机械小修
47
+ - 明确无代码影响的需求
48
+
49
+ 跳过时在 discovery 中说明原因。`explore` subagent 只输出代码/文档事实、文件行号锚点、隐性约束、影响范围候选、风险和需要主流程确认的问题;不写方案、不写业务代码、不替主流程做决策。
50
+
32
51
  ## discovery.md 格式
33
52
 
34
53
  写入 `openspec/changes/<change>/.superspec/artifacts/discovery.md`:
35
54
 
55
+ 如果文件已经存在并包含 `<!-- superspec:discovery-draft -->` 或“待探索后...”占位文本,说明它是引擎生成的草稿。完成探索后必须删除草稿标记并替换所有占位内容,否则引擎会继续阻止推进。
56
+
36
57
  ```markdown
37
58
  # Discovery
38
59
 
39
- ## 现状
40
- (当前系统怎么工作)
60
+ ## 当前代码事实
61
+ - src/path.ts:10 当前系统怎么工作
41
62
 
42
- ## 需要改什么
43
- (要实现的需求)
63
+ ## 需求理解
64
+ (用户目标和当前实现之间的差异)
65
+
66
+ ## 影响范围候选
67
+ - src/path.ts:10 可能受影响的代码表面和相邻风险
44
68
 
45
69
  ## 风险和边界
46
- (技术风险、依赖、兼容性)
70
+ (技术风险、依赖、兼容性;尽量绑定代码或文档锚点)
47
71
 
48
72
  ## 待确认问题
49
73
  - [ ] 问题1的描述
@@ -51,6 +75,9 @@ next 返回 `ask_user` 说明 discovery 不完整或有未确认问题——向
51
75
  ```
52
76
 
53
77
  **重要**:`- [ ]` 标记的待确认问题必须全部解决(用户确认后改为 `- [x]` 或删除),否则工作流引擎会阻止推进到 propose。
78
+ 只有 `## 待确认问题` 段落内的 `- [ ]` 表示阻塞确认项。其他段落列事实、风险或影响范围时使用普通 bullet,不要用 checklist。
79
+
80
+ 代码影响型需求的 `当前代码事实`、`影响范围候选`、`风险和边界` 应尽量包含 `path:line` 锚点。纯文档、配置或新文件任务没有代码锚点时,写明 `N/A` 理由并引用相关文档、配置或需求来源。
54
81
 
55
82
  ## Guardrails
56
83
 
@@ -19,7 +19,7 @@ metadata:
19
19
  3. 登记结果
20
20
  4. 回到 1
21
21
 
22
- next 返回需要审查时,先按返回的审查说明完成对应审查,再用 `superspec record job-submit --change "<change>" --job <JOB> --report <FILE>` 提交审查报告。
22
+ next 返回需要审查时,先按返回的审查说明完成对应审查,再优先用 `superspec record job-submit --change "<change>" --job <JOB> --report -` 从 stdin 提交 JSON 审查报告内容;文件路径模式仍可作为 fallback。
23
23
 
24
24
  人类可读正文默认使用简体中文;OpenSpec 结构标题、规范关键字、命令、路径、JSON 字段、代码标识符保留原文。
25
25
  如果 OpenSpec 生成文档语言不符合预期,先检查 `openspec/config.yaml` 的官方 `context` 设置;不要在变更文档里添加自定义 `language` 字段。
@@ -31,28 +31,63 @@ next 返回需要审查时,先按返回的审查说明完成对应审查,再
31
31
  ## 本阶段做什么
32
32
 
33
33
  ### proposal.md
34
- 需求陈述、方案概述、影响范围。
34
+ 使用 OpenSpec proposal 原生结构。正文使用简体中文。
35
+
36
+ SuperSpec 只增加一个轻量要求:在 OpenSpec 原生 `## Impact` 段落中,必须能看出受影响范围和原因。推荐写成:
37
+
38
+ ```markdown
39
+ | Area | Reason |
40
+ |---|---|
41
+ | src/review.ts | 需要核对 review verifier 如何绑定文档和执行证据 |
42
+ ```
43
+
44
+ 规则:
45
+ - `proposal.md` 说明为什么要做、做什么、能力变化和影响范围
46
+ - `Area` 可以写代码区域、API、依赖、系统、配置或文档
47
+ - `Reason` 只解释为什么该范围受影响,不写详细实现方案
48
+ - `Area` 不作为路径白名单
49
+ - 不写任务拆分
50
+ - 只有存在阻塞确认项时才增加 `## 待用户确认`
35
51
 
36
52
  ### specs/
37
53
  OpenSpec 能力规范增量(`openspec instructions specs` 格式)。
38
54
 
39
55
  ### design.md
40
- 技术方案、关键决策、替代方案。
56
+ 使用 OpenSpec design 原生结构。正文使用简体中文。
57
+
58
+ 规则:
59
+ - `design.md` 写技术方案、关键决策、替代方案和风险取舍
60
+ - 不复制 `proposal.md` 的影响范围表
61
+ - 不写任务拆分
62
+ - 只有存在阻塞确认项时才增加 `## 待用户确认`
41
63
 
42
64
  ### tasks.md
43
- 任务列表,格式:
65
+ 使用 OpenSpec tasks 原生分组结构。每个顶格 checkbox 行是一个 SuperSpec 可执行 task,Markdown 标题只用于分组。
44
66
 
45
67
  ```markdown
46
68
  # Tasks
47
69
 
48
- - [ ] TASK-001 实现登录功能 tdd_required:true
49
- - [ ] TASK-002 更新文档 tdd_required:false no_tdd_reason:documentation-only
50
- - [ ] TASK-003 配置变更 tdd_required:false no_tdd_reason:configuration-only
70
+ ## Review verifier
71
+
72
+ - [ ] 1.1 检查 verifier 绑定文档 tdd_required:true
73
+ - [ ] 1.2 检查 verifier 绑定执行证据 tdd_required:true
74
+
75
+ ## Documentation
76
+
77
+ - [ ] 2.1 更新文档 tdd_required:false no_tdd_reason:documentation-only
51
78
  ```
52
79
 
53
80
  规则:
81
+ - 标题只分组,不是可执行 task;标题不要包含可执行 task id token,例如不要写 `## 1.1 Review verifier`
82
+ - 顶格 `- [ ] <task_id> ...` 才是可执行 task,`<task_id>` 可以是 `1.1` 或 `TASK-001.1`
83
+ - 每个可执行 task id 必须唯一、稳定
84
+ - 不展示、不推荐缩进 checkbox;task 内部步骤用普通 bullet,不用 checkbox
54
85
  - `tdd_required:true`(默认)——改运行时代码/业务逻辑/数据迁移/权限/外部接口
55
86
  - `tdd_required:false` + `no_tdd_reason:xxx`——纯文档/配置/机械改名/生成物
87
+ - task 行只标记是否需要 TDD,不写 RED/GREEN 命令、断言或预期输出;实际 RED/GREEN 由 apply 阶段执行,并通过 `record test-run` 绑定到 attempt
88
+ - 一个 task 对应一个可独立验证的行为变化,或一个明确的非行为改动
89
+ - 多个行为变化、多个入口、多个运行时模块混在一起,且不能形成同一个 RED/GREEN 闭环时,应拆开
90
+ - 如果一个 task 需要“顺便”改很多不相邻模块,应在 propose 阶段重新拆分或补充任务,不留到 apply 阶段扩大范围
56
91
 
57
92
  ### business-invariants.md
58
93
  格式:
@@ -85,12 +120,14 @@ OpenSpec 能力规范增量(`openspec instructions specs` 格式)。
85
120
  - [ ] DEC-001 是否需要兼容历史行为?
86
121
  ```
87
122
 
88
- `next` 会在 propose 阶段检查 `proposal.md`、`design.md` 和 `test-contract.md` 的该段落。存在未确认项时,先向用户提问;收到回答后写入 JSON 文件并执行:
123
+ `next` 会在 propose 阶段检查 `proposal.md`、`design.md` 和 `test-contract.md` 的该段落。存在未确认项时,先向用户提问;收到回答后将 JSON 内容通过 stdin 登记:
89
124
 
90
125
  ```bash
91
- superspec record user-decision --change "<change>" --input <FILE>
126
+ superspec record user-decision --change "<change>" --input -
92
127
  ```
93
128
 
129
+ 文件路径模式仍可作为 fallback。
130
+
94
131
  然后把用户决定反映到 proposal/design/test-contract,并将对应确认项改为 `[x]` 或移出未确认列表。局部实现细节、命名、普通文件组织和不影响需求/验收/风险的技术微调不要升级为用户确认。
95
132
 
96
133
  ## 完成条件
@@ -19,7 +19,7 @@ metadata:
19
19
  3. 登记结果
20
20
  4. 回到 1
21
21
 
22
- next 返回需要 verifier 工作项时,先按返回的验证说明执行核对,再用 `superspec record job-submit --change "<change>" --job <JOB> --report <FILE>` 提交验证报告。
22
+ next 返回需要 verifier 工作项时,先按返回的验证说明执行核对,再优先用 `superspec record job-submit --change "<change>" --job <JOB> --report -` 从 stdin 提交 JSON 验证报告内容;文件路径模式仍可作为 fallback。
23
23
 
24
24
  `record job-submit` 沿用现有 raw 归档:报告追加到 `raw/review-reports.jsonl`,不会为 review gate 新增 raw 文件类型。
25
25