@peterxiaoyang/superspec 0.1.24 → 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.24",
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
  {
@@ -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
  {
@@ -20,7 +20,7 @@ argument-hint: "本次测试审查说明"
20
20
 
21
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
  {
@@ -25,9 +25,9 @@ metadata:
25
25
 
26
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>`
30
+ 5. **绿灯验证**:跑测试确认通过,优先用 `superspec record test-run --change "<change>" --input -` 从 stdin 登记 JSON 内容;文件路径模式仍可作为 fallback
31
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。
@@ -19,14 +19,14 @@ 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
32
  ## 探索分工
@@ -52,6 +52,8 @@ next 返回 `ask_user` 说明 discovery 不完整或有未确认问题——向
52
52
 
53
53
  写入 `openspec/changes/<change>/.superspec/artifacts/discovery.md`:
54
54
 
55
+ 如果文件已经存在并包含 `<!-- superspec:discovery-draft -->` 或“待探索后...”占位文本,说明它是引擎生成的草稿。完成探索后必须删除草稿标记并替换所有占位内容,否则引擎会继续阻止推进。
56
+
55
57
  ```markdown
56
58
  # Discovery
57
59
 
@@ -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` 字段。
@@ -120,12 +120,14 @@ OpenSpec 能力规范增量(`openspec instructions specs` 格式)。
120
120
  - [ ] DEC-001 是否需要兼容历史行为?
121
121
  ```
122
122
 
123
- `next` 会在 propose 阶段检查 `proposal.md`、`design.md` 和 `test-contract.md` 的该段落。存在未确认项时,先向用户提问;收到回答后写入 JSON 文件并执行:
123
+ `next` 会在 propose 阶段检查 `proposal.md`、`design.md` 和 `test-contract.md` 的该段落。存在未确认项时,先向用户提问;收到回答后将 JSON 内容通过 stdin 登记:
124
124
 
125
125
  ```bash
126
- superspec record user-decision --change "<change>" --input <FILE>
126
+ superspec record user-decision --change "<change>" --input -
127
127
  ```
128
128
 
129
+ 文件路径模式仍可作为 fallback。
130
+
129
131
  然后把用户决定反映到 proposal/design/test-contract,并将对应确认项改为 `[x]` 或移出未确认列表。局部实现细节、命名、普通文件组织和不影响需求/验收/风险的技术微调不要升级为用户确认。
130
132
 
131
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