@peterxiaoyang/superspec 0.1.33 → 0.1.35

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/sync.js CHANGED
@@ -3,6 +3,7 @@ import { readFileSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { readEvents, eventsDigest, computeDocumentDigests, sha256Text, ensureChangeLayout, } from "./store.js";
5
5
  import { reviewEvidenceDigest, reviewVerifierStaleReason } from "./review.js";
6
+ import { codeReviewJobStaleReason } from "./code_review.js";
6
7
  const TRACKED_DOCS = [
7
8
  "proposal.md", "design.md", "tasks.md",
8
9
  ".superspec/artifacts/discovery.md",
@@ -80,10 +81,12 @@ function replayEvents(events) {
80
81
  return { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition };
81
82
  }
82
83
  /** 粗粒度失效:检查 job 的 boundFiles 是否仍匹配当前文档 */
83
- function checkStaleJobs(jobs, changeRoot, currentReviewEvidenceDigest) {
84
+ function checkStaleJobs(jobs, projectRoot, changeRoot, currentReviewEvidenceDigest) {
84
85
  const stale = [];
85
86
  for (const job of jobs) {
86
- const reason = reviewVerifierStaleReason(job, changeRoot, currentReviewEvidenceDigest);
87
+ const reason = job.role === "code-reviewer"
88
+ ? codeReviewJobStaleReason(projectRoot, job)
89
+ : reviewVerifierStaleReason(job, changeRoot, currentReviewEvidenceDigest);
87
90
  if (reason) {
88
91
  stale.push({ job_id: job.job_id, reason });
89
92
  }
@@ -109,10 +112,13 @@ export function rebuildSnapshot(projectRoot, change, changeRoot, openspecStatusD
109
112
  const tsDigest = tasksStructureDigest(changeRoot);
110
113
  const { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition } = replayEvents(events);
111
114
  const currentReviewEvidenceDigest = reviewEvidenceDigest(events);
112
- // 粗粒度失效检查(只读,不写事件):snapshot 只暴露当前可执行/可复用 job
113
- const staleOpenInfo = checkStaleJobs(openJobs, changeRoot, currentReviewEvidenceDigest);
114
- const staleAcceptedInfo = checkStaleJobs(acceptedJobs, changeRoot, currentReviewEvidenceDigest);
115
- const freshOpen = openJobs.filter(j => !staleOpenInfo.some(s => s.job_id === j.job_id));
115
+ // 粗粒度失效检查(只读,不写事件):open code-reviewer job 防止提交过期报告;
116
+ // accepted code-reviewer pass 不做持续 freshness gate,避免 apply_done 循环重审。
117
+ const staleOpenInfo = checkStaleJobs(openJobs, projectRoot, changeRoot, currentReviewEvidenceDigest);
118
+ const staleAcceptedInfo = checkStaleJobs(acceptedJobs.filter(j => j.role !== "code-reviewer"), projectRoot, changeRoot, currentReviewEvidenceDigest);
119
+ const freshOpen = openJobs
120
+ .filter(j => !staleOpenInfo.some(s => s.job_id === j.job_id))
121
+ .filter(j => j.role !== "code-reviewer" || state === "apply_done");
116
122
  const freshAccepted = acceptedJobs.filter(j => !staleAcceptedInfo.some(s => s.job_id === j.job_id));
117
123
  return {
118
124
  change_id: change,
package/dist/task.js CHANGED
@@ -22,10 +22,29 @@ function recordTestRunLoaded(projectRoot, change, content) {
22
22
  if (!tr.test_id || !tr.task_structure_digest) {
23
23
  return { accepted: false, message: "缺少 test_id 或 task_structure_digest" };
24
24
  }
25
+ let coversTaskIds;
26
+ if (tr.covers_task_ids !== undefined) {
27
+ if (!Array.isArray(tr.covers_task_ids)) {
28
+ return { accepted: false, message: "covers_task_ids 必须是字符串数组" };
29
+ }
30
+ if (tr.covers_task_ids.length === 0) {
31
+ return { accepted: false, message: "covers_task_ids 不能是空数组" };
32
+ }
33
+ for (const raw of tr.covers_task_ids) {
34
+ if (typeof raw !== "string")
35
+ return { accepted: false, message: "covers_task_ids 必须是字符串数组" };
36
+ const value = raw.trim();
37
+ if (!value)
38
+ return { accepted: false, message: "covers_task_ids 不能包含空字符串" };
39
+ (coversTaskIds ??= []).push(value);
40
+ }
41
+ coversTaskIds = [...new Set(coversTaskIds)].sort();
42
+ }
25
43
  const normalizedTestRun = {
26
44
  test_id: tr.test_id,
27
45
  task_structure_digest: tr.task_structure_digest,
28
46
  attempt_id: tr.attempt_id ?? null,
47
+ ...(coversTaskIds ? { covers_task_ids: coversTaskIds } : {}),
29
48
  command: tr.command ?? "",
30
49
  cwd: tr.cwd ?? "",
31
50
  exit_code: tr.exit_code ?? -1,
@@ -36,7 +36,10 @@ export declare function transitionInit(projectRoot: string, change: string, chan
36
36
  export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
37
37
  export declare function startApply(projectRoot: string, change: string, changeRoot: string): TransitionResult;
38
38
  export declare function taskStart(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
39
- export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string): TransitionResult;
39
+ export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string, opts?: {
40
+ reviewFix?: string;
41
+ reviewFinding?: string;
42
+ }): TransitionResult;
40
43
  export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
41
44
  export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
42
45
  export declare function archive(projectRoot: string, change: string, changeRoot: string): TransitionResult;
@@ -4,7 +4,8 @@ import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
4
4
  import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, sha256File, sha256Text, } from "./store.js";
5
5
  import { rebuildSnapshot } from "./sync.js";
6
6
  import { requiredJobActions } from "./job_action.js";
7
- import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, } from "./review.js";
7
+ import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
8
+ import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketDigest, collectCodeReviewGateFacts, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChanges, } from "./code_review.js";
8
9
  import { validateDiscovery, collectProposeOpenQuestions, findTaskInLines, parseTasksMd, pendingTasksInContent, tasksStructureDigest } from "./format.js";
9
10
  let transitionSeq = 0;
10
11
  function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
@@ -101,6 +102,106 @@ function hasRejectedReviewReadyVerifier(events) {
101
102
  return events.some(ev => ev.event_type === "job_rejected" &&
102
103
  reviewReadyVerifierIds.has(ev.payload.job_id ?? ""));
103
104
  }
105
+ function createCodeReviewerJob(change, projectRoot, events) {
106
+ const scan = scanCodeChanges(projectRoot);
107
+ const boundFiles = codeReviewBoundFiles(projectRoot, scan.paths);
108
+ const facts = collectCodeReviewGateFacts(events);
109
+ const latestRejected = facts.latestRejected;
110
+ const reviewFailedStatus = latestCodeReviewFailedStatus(events);
111
+ const previousRejection = latestRejected && latestRejected.state === "rejected"
112
+ ? {
113
+ result_kind: latestRejected.result_kind ?? "invalid_report",
114
+ reason: latestRejected.result_kind === "review_failed" && reviewFailedStatus && reviewFailedStatus.unresolved.length === 0 && reviewFailedStatus.dismissed.length > 0
115
+ ? dismissedCodeReviewSummary(reviewFailedStatus)
116
+ : latestRejected.reason ?? "缺少拒绝原因",
117
+ job_id: latestRejected.job.job_id,
118
+ }
119
+ : undefined;
120
+ const packetInput = {
121
+ role: "code-reviewer",
122
+ boundFiles,
123
+ checkedDocs: REVIEW_DOC_PATHS,
124
+ created_from_transition: "review-ready",
125
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
126
+ };
127
+ return {
128
+ scanReason: scan.reason,
129
+ job: {
130
+ job_id: newJobId(change, "code-reviewer"),
131
+ role: "code-reviewer",
132
+ state: "requested",
133
+ boundFiles,
134
+ packet_digest: codeReviewPacketDigest(packetInput),
135
+ created_from_transition: "review-ready",
136
+ created_at: new Date().toISOString(),
137
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
138
+ },
139
+ };
140
+ }
141
+ function parseCodeReviewFindingRef(value) {
142
+ const idx = value.indexOf("#");
143
+ if (idx <= 0 || idx === value.length - 1)
144
+ return null;
145
+ return { jobId: value.slice(0, idx), findingId: value.slice(idx + 1) };
146
+ }
147
+ function findReviewFailedFinding(events, ref) {
148
+ const status = latestCodeReviewFailedStatus(events);
149
+ if (!status || status.terminal.job.job_id !== ref.jobId)
150
+ return null;
151
+ const finding = status.findings.find(item => item.id === ref.findingId)?.finding;
152
+ if (!finding)
153
+ return null;
154
+ return { event: status.terminal.event, finding };
155
+ }
156
+ function reviewFixMarker(ref) {
157
+ return `review_fix_of:${ref.jobId}#${ref.findingId}`;
158
+ }
159
+ function reviewFixTaskId(ref) {
160
+ return `REVIEW-FIX-${ref.jobId}#${ref.findingId}`;
161
+ }
162
+ function appendReviewFixTask(changeRoot, ref, finding) {
163
+ const tasksPath = join(changeRoot, "tasks.md");
164
+ const content = readFileSync(tasksPath, "utf8");
165
+ const marker = reviewFixMarker(ref);
166
+ if (content.includes(marker))
167
+ return "exists";
168
+ const description = typeof finding.description === "string" && finding.description.trim()
169
+ ? finding.description.trim().replace(/\s+/g, " ")
170
+ : `修复代码审查问题 ${ref.findingId}`;
171
+ const line = `- [ ] ${reviewFixTaskId(ref)} ${description} tdd_required:true ${marker}`;
172
+ const suffix = content.endsWith("\n") ? "" : "\n";
173
+ writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
174
+ return "created";
175
+ }
176
+ function isFreshOpenCodeReviewerJob(job, projectRoot) {
177
+ return codeReviewJobStaleReason(projectRoot, job) == null;
178
+ }
179
+ function documentBaseline(changeRoot) {
180
+ const docs = ["proposal.md", "design.md", "tasks.md", ".superspec/artifacts/test-contract.md"];
181
+ const baseline = {};
182
+ for (const doc of docs) {
183
+ baseline[doc] = sha256File(join(changeRoot, doc)) ?? "sha256:missing";
184
+ }
185
+ return baseline;
186
+ }
187
+ function latestReopenProposeBaseline(events) {
188
+ for (let i = events.length - 1; i >= 0; i--) {
189
+ const ev = events[i];
190
+ if (ev.event_type !== "transition_commit")
191
+ continue;
192
+ const payload = ev.payload;
193
+ if (payload.transition !== "reopen" || payload.reopen_target !== "propose")
194
+ continue;
195
+ if (!payload.baseline_docs || typeof payload.baseline_docs !== "object" || Array.isArray(payload.baseline_docs))
196
+ return null;
197
+ return payload.baseline_docs;
198
+ }
199
+ return null;
200
+ }
201
+ function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
202
+ const current = documentBaseline(changeRoot);
203
+ return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
204
+ }
104
205
  /**
105
206
  * 统一 transition 提交协议——所有校验在锁内。
106
207
  */
@@ -265,13 +366,18 @@ export function startApply(projectRoot, change, changeRoot) {
265
366
  decide: (snapshot) => {
266
367
  if (snapshot.state !== "propose_ready")
267
368
  return { skip: true, message: `当前状态 ${snapshot.state},需要 propose_ready` };
369
+ const events = readEvents(projectRoot, change);
370
+ const reopenBaseline = latestReopenProposeBaseline(events);
371
+ if (reopenBaseline && !proposalDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
372
+ return { skip: true, message: "回到 propose 后 proposal/design/tasks/test-contract 至少一个文档必须变化" };
373
+ }
268
374
  const reviewedRoles = historicalProposeReadyRoles(projectRoot, change);
269
375
  if (reviewedRoles.length > 0) {
270
376
  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"]);
271
377
  if (reviewResult) {
272
378
  return {
273
379
  ...reviewResult,
274
- reason: `进入 apply 前需要 fresh proposal 审查:${reviewResult.reason}`,
380
+ reason: `进入执行阶段前需要重新完成计划文档审查:${reviewResult.reason}`,
275
381
  };
276
382
  }
277
383
  }
@@ -317,14 +423,83 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
317
423
  });
318
424
  }
319
425
  // ===== reopen =====
320
- export function reopen(projectRoot, change, changeRoot, to, reason) {
426
+ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
321
427
  return commitTransition(projectRoot, change, changeRoot, {
322
- name: "reopen", idempotencyInputs: { to, reason },
428
+ name: "reopen", idempotencyInputs: { to, reason, reviewFix: opts.reviewFix ?? "", reviewFinding: opts.reviewFinding ?? "" },
323
429
  decide: (snapshot) => {
324
- if (to !== "apply")
325
- return { skip: true, message: `reopen 当前只支持 --to apply,不支持 ${to}` };
326
430
  if (!reason || reason.trim() === "")
327
431
  return { skip: true, message: "reopen 需要非空 --reason" };
432
+ const events = readEvents(projectRoot, change);
433
+ if (opts.reviewFinding) {
434
+ if (to !== "propose")
435
+ return { skip: true, message: "--review-finding 只能用于回到计划阶段(reopen --to propose)" };
436
+ if (snapshot.state !== "apply_done")
437
+ return { skip: true, message: `当前状态 ${snapshot.state},不能通过代码审查问题回到计划阶段` };
438
+ const ref = parseCodeReviewFindingRef(opts.reviewFinding);
439
+ if (!ref)
440
+ return { skip: true, message: "--review-finding 必须是 <job_id>#<finding_id>" };
441
+ const found = findReviewFailedFinding(events, ref);
442
+ if (!found)
443
+ return { skip: true, message: `找不到有效的代码审查问题 ${opts.reviewFinding}` };
444
+ const type = found.finding.type;
445
+ if (type !== "spec" && type !== "mixed")
446
+ return { skip: true, message: "只有方案/需求文档问题或混合问题可以回到计划阶段" };
447
+ const scope = codeReviewDecisionScope(ref.jobId, ref.findingId);
448
+ const decision = latestCodeReviewDecision(events, scope);
449
+ if (decision?.answer !== "reopen_propose")
450
+ return { skip: true, message: `缺少使用者确认:需要先确认问题 ${ref.findingId} 是否回到计划阶段` };
451
+ return {
452
+ fromState: "apply_done",
453
+ toState: "propose",
454
+ outcome: "advanced",
455
+ reason: reason.trim(),
456
+ commitPayload: {
457
+ reopen_target: "propose",
458
+ source_job_id: ref.jobId,
459
+ finding_id: ref.findingId,
460
+ decision_scope: scope,
461
+ baseline_docs: documentBaseline(changeRoot),
462
+ },
463
+ };
464
+ }
465
+ if (opts.reviewFix) {
466
+ if (to !== "apply")
467
+ return { skip: true, message: "--review-fix 只能用于回到实现阶段(reopen --to apply)" };
468
+ if (snapshot.state !== "apply_done")
469
+ return { skip: true, message: `当前状态 ${snapshot.state},不能通过代码审查修复回到实现阶段` };
470
+ const ref = parseCodeReviewFindingRef(opts.reviewFix);
471
+ if (!ref)
472
+ return { skip: true, message: "--review-fix 必须是 <job_id>#<finding_id>" };
473
+ const found = findReviewFailedFinding(events, ref);
474
+ if (!found)
475
+ return { skip: true, message: `找不到有效的代码审查问题 ${opts.reviewFix}` };
476
+ const type = found.finding.type;
477
+ if (type === "spec" || type === "mixed") {
478
+ const scope = codeReviewDecisionScope(ref.jobId, ref.findingId);
479
+ const decision = latestCodeReviewDecision(events, scope);
480
+ if (decision?.answer !== "reopen_apply")
481
+ return { skip: true, message: `缺少使用者确认:需要先确认问题 ${ref.findingId} 是否直接回到实现阶段修复` };
482
+ }
483
+ else if (type !== "implementation") {
484
+ return { skip: true, message: "这个代码审查问题不能直接回到实现阶段处理" };
485
+ }
486
+ return {
487
+ fromState: "apply_done",
488
+ toState: "apply",
489
+ outcome: "advanced",
490
+ reason: reason.trim(),
491
+ commitPayload: {
492
+ review_fix_of: `${ref.jobId}#${ref.findingId}`,
493
+ source_job_id: ref.jobId,
494
+ finding_id: ref.findingId,
495
+ },
496
+ postCommit: (_pr, _ch, cr) => {
497
+ appendReviewFixTask(cr, ref, found.finding);
498
+ },
499
+ };
500
+ }
501
+ if (to !== "apply")
502
+ return { skip: true, message: `reopen 当前只支持 --to apply 或 --to propose,不支持 ${to}` };
328
503
  if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
329
504
  return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 apply` };
330
505
  }
@@ -362,19 +537,69 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
362
537
  commitPayload: policyPayload,
363
538
  };
364
539
  }
365
- if (snapshot.state === "apply_done" || snapshot.state === "review") {
366
- const verifierOpen = snapshot.open_jobs.find(isReviewReadyVerifier);
367
- if (verifierOpen)
368
- return { blocked: true, reason: `状态未推进;已有待完成最终验证工作项 ${verifierOpen.job_id}`, jobs: [verifierOpen] };
369
- const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
370
- if (!policy.requires_verifier) {
371
- if (snapshot.state === "apply_done") {
540
+ if (snapshot.state === "apply_done") {
541
+ const scan = scanCodeChanges(projectRoot);
542
+ const facts = collectCodeReviewGateFacts(events);
543
+ if (scan.hasCodeChanges) {
544
+ const freshOpenJobs = facts.openJobs.filter(job => isFreshOpenCodeReviewerJob(job, projectRoot));
545
+ if (freshOpenJobs.length > 0) {
546
+ return {
547
+ blocked: true,
548
+ reason: `状态未推进;已有待完成代码审查工作项 ${freshOpenJobs[0].job_id}`,
549
+ jobs: [freshOpenJobs[0]],
550
+ };
551
+ }
552
+ const latest = facts.latestTerminal;
553
+ if (latest?.state === "accepted") {
372
554
  return {
373
555
  fromState: "apply_done", toState: "review", outcome: "advanced",
374
- reason: `审查策略=${policy.review_risk},无需最终验证,进入审查阶段`,
375
- commitPayload: policyPayload,
556
+ reason: "代码审查已通过,进入最终审查阶段",
557
+ commitPayload: {
558
+ ...policyPayload,
559
+ code_review_gate: { decision: "passed", job_id: latest.job.job_id },
560
+ },
376
561
  };
377
562
  }
563
+ if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
564
+ const reviewFailedStatus = latestCodeReviewFailedStatus(events);
565
+ if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
566
+ const { job, scanReason } = createCodeReviewerJob(change, projectRoot, events);
567
+ return {
568
+ fromState: "apply_done", toState: "apply_done", outcome: "job_created",
569
+ newJobs: [job],
570
+ reason: `重新创建代码审查工作项;${scanReason};上一次阻塞问题已被主流程复核驳回`,
571
+ };
572
+ }
573
+ return {
574
+ skip: true,
575
+ message: "代码审查发现需要处理的问题,请先执行 next,根据提示回到实现阶段修复或让使用者决定是否回到计划阶段",
576
+ };
577
+ }
578
+ const { job, scanReason } = createCodeReviewerJob(change, projectRoot, events);
579
+ return {
580
+ fromState: "apply_done", toState: "apply_done", outcome: "job_created",
581
+ newJobs: [job],
582
+ reason: latest?.state === "rejected"
583
+ ? `重新创建代码审查工作项;上一次报告未被接受,原因:${latest.reason ?? "报告不符合要求"}`
584
+ : `创建代码审查工作项;${scanReason}`,
585
+ };
586
+ }
587
+ return {
588
+ fromState: "apply_done", toState: "review", outcome: "advanced",
589
+ reason: "没有代码类改动,直接进入最终审查阶段",
590
+ commitPayload: {
591
+ ...policyPayload,
592
+ code_review_gate: { decision: "skipped", reason: "no_code_changes" },
593
+ },
594
+ };
595
+ }
596
+ if (snapshot.state === "review") {
597
+ const verifierOpen = snapshot.open_jobs.find(isReviewReadyVerifier);
598
+ if (verifierOpen)
599
+ return { blocked: true, reason: `状态未推进;已有待完成最终验证工作项 ${verifierOpen.job_id}`, jobs: [verifierOpen] };
600
+ const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
601
+ const finalVerifierRequired = requiresFinalVerifierForCurrentReview(events) || policy.requires_verifier;
602
+ if (!finalVerifierRequired) {
378
603
  if (!storedPolicy) {
379
604
  return {
380
605
  fromState: "review", toState: "review", outcome: "advanced",
@@ -402,21 +627,14 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
402
627
  fromState: snapshot.state, toState: snapshot.state, outcome: "job_created",
403
628
  newJobs: [job],
404
629
  reason: previousVerifierRejected
405
- ? "此前 verifier 未通过;请先根据 findings 修改任务或文档,确认无需修改时再执行新的最终验证工作项"
630
+ ? "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项"
406
631
  : "创建最终验证工作项",
407
632
  commitPayload: policyPayload,
408
633
  ...(previousVerifierRejected ? {
409
- details: { advisory: "此前 verifier 未通过;请先根据 findings 修改任务或文档,确认无需修改时再执行新的最终验证工作项" },
634
+ details: { advisory: "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项" },
410
635
  } : {}),
411
636
  };
412
637
  }
413
- if (snapshot.state === "apply_done") {
414
- return {
415
- fromState: "apply_done", toState: "review", outcome: "advanced",
416
- reason: "最终验证已接受,进入审查阶段",
417
- commitPayload: policyPayload,
418
- };
419
- }
420
638
  if (!storedPolicy) {
421
639
  return {
422
640
  fromState: "review", toState: "review", outcome: "advanced",
@@ -444,11 +662,11 @@ export function accept(projectRoot, change, changeRoot) {
444
662
  const policy = readReviewPolicyFromEvents(events);
445
663
  if (!policy)
446
664
  return { skip: true, message: "缺少审查策略,请先运行 review-ready" };
447
- if (policy.requires_verifier) {
665
+ if (requiresFinalVerifierForCurrentReview(events) || policy.requires_verifier) {
448
666
  const currentEvidenceDigest = reviewEvidenceDigest(events);
449
667
  const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
450
668
  if (!verifierAccepted)
451
- return { skip: true, message: "缺少 fresh verifier,请先运行 review-ready" };
669
+ return { skip: true, message: "缺少仍然匹配当前证据的最终验证,请先运行 review-ready" };
452
670
  }
453
671
  return { fromState: "review", toState: "accepted", outcome: "advanced", reason: "审查通过" };
454
672
  },
package/dist/types.d.ts CHANGED
@@ -5,7 +5,13 @@ export type Ref = {
5
5
  sha: string;
6
6
  };
7
7
  export type JobState = "requested" | "accepted" | "rejected";
8
- export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier";
8
+ export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier" | "code-reviewer";
9
+ export type CodeReviewResultKind = "invalid_report" | "non_actionable_report" | "review_failed";
10
+ export interface CodeReviewPreviousRejection {
11
+ result_kind: CodeReviewResultKind;
12
+ reason: string;
13
+ job_id: string;
14
+ }
9
15
  export interface Job {
10
16
  job_id: string;
11
17
  role: JobRole;
@@ -15,6 +21,7 @@ export interface Job {
15
21
  packet_digest: string;
16
22
  created_from_transition: string;
17
23
  created_at: string;
24
+ previous_rejection?: CodeReviewPreviousRejection;
18
25
  }
19
26
  export interface JobPacket {
20
27
  job_id: string;
@@ -30,6 +37,7 @@ export interface JobPacket {
30
37
  file_fallback?: boolean;
31
38
  output_contract_fields?: string[];
32
39
  output_contract_optional_fields?: string[];
40
+ output_instructions?: string;
33
41
  stop_conditions: string[];
34
42
  created_from_transition: string;
35
43
  }
@@ -66,6 +74,11 @@ export interface TransitionCommitPayload {
66
74
  review_risk: "minimal" | "normal" | "strict";
67
75
  requires_verifier: boolean;
68
76
  };
77
+ code_review_gate?: {
78
+ decision: "passed" | "skipped";
79
+ job_id?: string;
80
+ reason?: "no_code_changes";
81
+ };
69
82
  }
70
83
  export interface Snapshot {
71
84
  change_id: string;
@@ -100,6 +113,7 @@ export interface TestRun {
100
113
  test_id: string;
101
114
  attempt_id?: string | null;
102
115
  task_structure_digest: string;
116
+ covers_task_ids?: string[];
103
117
  command: string;
104
118
  cwd: string;
105
119
  exit_code: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": {
@@ -5,9 +5,9 @@ model_reasoning_effort = "high"
5
5
  developer_instructions = """
6
6
  Role: Architect. Review system boundaries, interface contracts, data flow, maintenance risk, rollback risk, and design tradeoffs.
7
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.
8
+ Task binding: load `.codex/prompts/architect.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
9
9
 
10
10
  Boundary: read-only. Do not edit files or judge materials you have not opened. Report missing context upward instead of guessing.
11
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.
12
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise put the conclusion first, cite file:line evidence, and write `无阻塞问题` when no blocking issue is found.
13
13
  """
@@ -1,13 +1,13 @@
1
1
  # SuperSpec Codex agent: code-reviewer
2
2
  name = "code-reviewer"
3
- description = "Comprehensive review across all concerns"
3
+ description = "Code-level review for spec fit, bugs, safety, and test gaps"
4
4
  model_reasoning_effort = "high"
5
5
  developer_instructions = """
6
- Role: Code Reviewer. Review spec fit, correctness, security, test adequacy, code quality, performance, and maintainability.
6
+ Role: Code Reviewer. Check spec fit, correctness, security, test adequacy, code quality, performance, and maintainability without making the workflow heavy.
7
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.
8
+ Task binding: load `.codex/prompts/code-reviewer.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
9
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.
10
+ Boundary: read-only. Do not implement fixes, write evidence, mark tasks complete, decide GREEN, reopen, accept, or replace main-thread workflow decisions. Start from packet-provided materials and report missing context upward instead of guessing.
11
11
 
12
- Output: concise Simplified Chinese. Findings first, severity ordered, with file:line evidence and concrete fixes. Write `无阻塞问题` when no blocking issue is found.
12
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Blocking issues must be traceable and actionable. Write `无阻塞问题` when no blocking issue is found.
13
13
  """
@@ -5,9 +5,9 @@ model_reasoning_effort = "high"
5
5
  developer_instructions = """
6
6
  Role: Critic. Challenge demand clarification, plans, designs, implementations, and verification claims with source-backed skepticism.
7
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.
8
+ Task binding: load `.codex/prompts/critic.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
9
9
 
10
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
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.
12
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise state pass or reject first, distinguish defects from proof gaps and residual risk, and cite concrete evidence.
13
13
  """
@@ -5,9 +5,9 @@ model_reasoning_effort = "high"
5
5
  developer_instructions = """
6
6
  Role: Test Engineer. Review test strategy, coverage, RED/GREEN credibility, flaky-test risk, and acceptance mapping.
7
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.
8
+ Task binding: load `.codex/prompts/test-engineer.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
9
9
 
10
- Boundary: SuperSpec review/propose lanes are read-only. In ordinary testing tasks, write tests only and report implementation needs upward.
10
+ Boundary: review jobs are read-only. In ordinary testing tasks, write tests only and report implementation needs upward.
11
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.
12
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise list coverage gaps, suggested tests, fresh validation commands, unverifiable items, and residual risk.
13
13
  """
@@ -3,11 +3,11 @@ name = "verifier"
3
3
  description = "Completion evidence, claim validation, test adequacy"
4
4
  model_reasoning_effort = "high"
5
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.
6
+ Role: Verifier. Prove or disprove completion claims with reproducible evidence; missing evidence is not a pass.
7
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.
8
+ Task binding: load `.codex/prompts/verifier.md` first, then read the current job packet and task instructions. The job packet is the runtime contract; follow it over static prompt memory, including any previous rejection it asks you to correct.
9
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.
10
+ Boundary: read-only. Check commands, test output, artifacts, evidence refs, acceptance criteria, code-reviewer closure, and whether the verifier job still matches the packet-provided evidence version. Use diffs only as evidence references when the packet requires them. Do not edit files, write evidence, mark tasks complete, or add an extra code-diff blocker outside the packet contract.
11
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.
12
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. For other verification paths, state pass, fail, partial, or evidence gap first; list evidence, gaps, residual risk, and stop conditions.
13
13
  """