@peterxiaoyang/superspec 0.1.45 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/code_review.d.ts +11 -1
  3. package/dist/code_review.js +40 -0
  4. package/dist/explore_round.d.ts +23 -0
  5. package/dist/explore_round.js +94 -0
  6. package/dist/format.d.ts +67 -2
  7. package/dist/format.js +273 -22
  8. package/dist/openspec.d.ts +13 -0
  9. package/dist/openspec.js +53 -4
  10. package/dist/phase_confirmation.js +71 -2
  11. package/dist/phase_plan.d.ts +6 -1
  12. package/dist/phase_plan.js +180 -32
  13. package/dist/record.js +191 -57
  14. package/dist/review.js +2 -0
  15. package/dist/task_evidence.js +5 -3
  16. package/dist/transition.d.ts +1 -0
  17. package/dist/transition.js +222 -28
  18. package/dist/types.d.ts +42 -0
  19. package/package.json +1 -1
  20. package/templates/workflow/AGENTS.md +15 -5
  21. package/templates/workflow/agents/architect.toml +1 -1
  22. package/templates/workflow/agents/code-reviewer.toml +1 -1
  23. package/templates/workflow/agents/critic.toml +1 -1
  24. package/templates/workflow/agents/executor.toml +1 -1
  25. package/templates/workflow/agents/explore.toml +1 -1
  26. package/templates/workflow/agents/test-engineer.toml +1 -1
  27. package/templates/workflow/agents/test-runner.toml +1 -1
  28. package/templates/workflow/agents/verifier.toml +1 -1
  29. package/templates/workflow/prompts/architect.md +25 -33
  30. package/templates/workflow/prompts/code-reviewer.md +19 -67
  31. package/templates/workflow/prompts/critic.md +36 -86
  32. package/templates/workflow/prompts/executor.md +17 -19
  33. package/templates/workflow/prompts/explore.md +12 -46
  34. package/templates/workflow/prompts/test-engineer.md +22 -34
  35. package/templates/workflow/prompts/test-runner.md +11 -21
  36. package/templates/workflow/prompts/verifier.md +13 -37
  37. package/templates/workflow/skills/superspec-apply/SKILL.md +26 -26
  38. package/templates/workflow/skills/superspec-explore/SKILL.md +69 -60
  39. package/templates/workflow/skills/superspec-propose/SKILL.md +85 -133
  40. package/templates/workflow/skills/superspec-review/SKILL.md +14 -44
@@ -5,11 +5,11 @@ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot,
5
5
  import { rebuildSnapshot } from "./sync.js";
6
6
  import { requiredJobActions } from "./job_action.js";
7
7
  import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, latestReviewHistoryForGateRole, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
8
- import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeForGateRole, } from "./review_job_gates.js";
9
- import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
8
+ import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeForGateRole, } from "./review_job_gates.js";
9
+ import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, effectiveCoverageExemptionRefsFromEvents, latestCodeReviewGateEvidence, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, taskExecutionIndexForReview, } from "./code_review.js";
10
10
  import { taskEvidenceReadiness } from "./task_evidence.js";
11
- import { adoptedContractForTask, findTaskInLines, isReviewFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
12
- import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planTransition, discoveryDocsBaseline, proposalDocsBaseline, } from "./phase_plan.js";
11
+ import { adoptedContractForTask, findTaskInLines, isFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
12
+ import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, exploreAnswerRegistrationPayloadForChange, proposalDocsBaseline, } from "./phase_plan.js";
13
13
  import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
14
14
  import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
15
15
  import { workflowRiskForProject } from "./workflow_config.js";
@@ -173,13 +173,13 @@ function validateTaskStartContract(changeRoot, taskId, parsedContract) {
173
173
  */
174
174
  function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsedContract) {
175
175
  if (!parsedContract) {
176
- return tddRequired && !isReviewFixTaskId(taskId)
176
+ return tddRequired && !isFixTaskId(taskId)
177
177
  ? `执行依据模式下,普通 TDD 任务 ${taskId} 缺少执行依据`
178
178
  : null;
179
179
  }
180
180
  if (parsedContract.errors.length > 0)
181
181
  return parsedContract.errors.join(";");
182
- if (tddRequired && !isReviewFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
182
+ if (tddRequired && !isFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
183
183
  return `${taskId} 是普通 TDD 任务,执行依据缺少测试`;
184
184
  }
185
185
  if (parsedContract.contract.tests.length === 0)
@@ -199,7 +199,7 @@ function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsed
199
199
  * RED 是否要求由这里读取已冻结的 execution_policy 决定,之后不再依赖 tasks.md。
200
200
  */
201
201
  function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationWithoutDeclaredTest) {
202
- // REVIEW-FIX 没有计划阶段声明的 TEST,但仍必须登记一次真实回归验证。
202
+ // Fix task 没有计划阶段声明的 TEST,但仍必须登记一次真实回归验证。
203
203
  // 是否需要 RED 始终由已冻结的 execution_policy 决定。
204
204
  const requiresVerification = testIds.length > 0 || requiresVerificationWithoutDeclaredTest;
205
205
  return {
@@ -279,26 +279,133 @@ function findReviewFailedFinding(events, ref) {
279
279
  return null;
280
280
  return { event: status.terminal.event, finding };
281
281
  }
282
- function reviewFixMarker(ref) {
283
- return `review_fix_of:${ref.jobId}#${ref.findingId}`;
282
+ function reviewFixDescriptor(ref, finding) {
283
+ const reason = typeof finding.description === "string" && finding.description.trim()
284
+ ? finding.description.trim().replace(/\s+/g, " ")
285
+ : `修复代码审查问题 ${ref.findingId}`;
286
+ return {
287
+ fix_id: `REVIEW-FIX-${ref.jobId}#${ref.findingId}`,
288
+ source: "code_review",
289
+ parent_task_id: null,
290
+ reason,
291
+ review_finding: { job_id: ref.jobId, finding_id: ref.findingId },
292
+ };
293
+ }
294
+ function selfTestFixBaseId(parentTaskId, reason) {
295
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
296
+ const normalizedTaskId = parentTaskId.replace(/[^A-Za-z0-9_-]/g, "-");
297
+ const digest = sha256Text(`${parentTaskId}\n${normalizedReason}`).replace(/^sha256:/, "").slice(0, 12);
298
+ return `FIX-SELFTEST-${normalizedTaskId}-${digest}`;
299
+ }
300
+ function selfTestFixDescriptor(parentTaskId, reason, occurrence) {
301
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
302
+ const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
303
+ return {
304
+ fix_id: occurrence === 1 ? baseId : `${baseId}-${occurrence}`,
305
+ source: "self_test",
306
+ parent_task_id: parentTaskId,
307
+ reason: normalizedReason,
308
+ };
309
+ }
310
+ function selfTestFixOccurrence(taskId, baseId) {
311
+ if (taskId === baseId)
312
+ return 1;
313
+ const suffix = taskId.slice(baseId.length + 1);
314
+ if (!taskId.startsWith(`${baseId}-`) || !/^\d+$/.test(suffix))
315
+ return null;
316
+ const occurrence = Number(suffix);
317
+ return Number.isSafeInteger(occurrence) && occurrence >= 2 ? occurrence : null;
318
+ }
319
+ function nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason) {
320
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
321
+ const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
322
+ let maxOccurrence = 0;
323
+ const completion = pendingTaskStatusForApply(changeRoot, events);
324
+ const pendingTaskIds = new Set(completion.pending);
325
+ const taskInfos = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"));
326
+ for (const task of taskInfos) {
327
+ const occurrence = selfTestFixOccurrence(task.taskId, baseId);
328
+ if (occurrence == null)
329
+ continue;
330
+ maxOccurrence = Math.max(maxOccurrence, occurrence);
331
+ if (pendingTaskIds.has(task.taskId))
332
+ return { activeFixTaskId: task.taskId };
333
+ }
334
+ for (const event of events) {
335
+ if (event.event_type !== "transition_commit")
336
+ continue;
337
+ const fix = event.payload.fix;
338
+ if (!fix || typeof fix !== "object" || Array.isArray(fix))
339
+ continue;
340
+ const candidate = fix;
341
+ if (candidate.source !== "self_test" || candidate.parent_task_id !== parentTaskId || candidate.reason !== normalizedReason)
342
+ continue;
343
+ if (typeof candidate.fix_id !== "string")
344
+ continue;
345
+ const occurrence = selfTestFixOccurrence(candidate.fix_id, baseId);
346
+ if (occurrence != null)
347
+ maxOccurrence = Math.max(maxOccurrence, occurrence);
348
+ }
349
+ return { fix: selfTestFixDescriptor(parentTaskId, normalizedReason, maxOccurrence + 1) };
284
350
  }
285
- function reviewFixTaskId(ref) {
286
- return `REVIEW-FIX-${ref.jobId}#${ref.findingId}`;
351
+ function fixMarker(fix) {
352
+ if (fix.source === "code_review" && fix.review_finding) {
353
+ return `review_fix_of:${fix.review_finding.job_id}#${fix.review_finding.finding_id}`;
354
+ }
355
+ return `self_test_fix_of:${fix.parent_task_id ?? "unknown"}:${fix.fix_id}`;
287
356
  }
288
- function appendReviewFixTask(changeRoot, ref, finding) {
357
+ function appendFixTask(changeRoot, fix) {
289
358
  const tasksPath = join(changeRoot, "tasks.md");
290
359
  const content = readFileSync(tasksPath, "utf8");
291
- const marker = reviewFixMarker(ref);
292
- if (content.includes(marker))
293
- return "exists";
294
- const description = typeof finding.description === "string" && finding.description.trim()
295
- ? finding.description.trim().replace(/\s+/g, " ")
296
- : `修复代码审查问题 ${ref.findingId}`;
297
- const line = `- [ ] ${reviewFixTaskId(ref)} ${description} ${marker}`;
360
+ const marker = fixMarker(fix);
361
+ if (content.includes(marker)) {
362
+ const matchingTask = content.split("\n").some(line => line.includes(marker) && line.startsWith(`- [ ] ${fix.fix_id} `));
363
+ if (matchingTask)
364
+ return "exists";
365
+ throw new Error(`修复标记 ${marker} 已被其它 task 占用,拒绝创建 ${fix.fix_id}`);
366
+ }
367
+ if (parseTasksMd(content).some(task => task.taskId === fix.fix_id)) {
368
+ throw new Error(`修复 task ID ${fix.fix_id} 已被其它任务占用,拒绝创建重复修复`);
369
+ }
370
+ const description = fix.source === "self_test"
371
+ ? `修复自测问题(关联 ${fix.parent_task_id}):${fix.reason}`
372
+ : fix.reason;
373
+ const line = `- [ ] ${fix.fix_id} ${description} ${marker}`;
298
374
  const suffix = content.endsWith("\n") ? "" : "\n";
299
375
  writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
300
376
  return "created";
301
377
  }
378
+ function fixDescriptorForTask(events, taskId) {
379
+ for (let i = events.length - 1; i >= 0; i--) {
380
+ const event = events[i];
381
+ if (event.event_type !== "transition_commit")
382
+ continue;
383
+ const fix = event.payload.fix;
384
+ if (!fix || typeof fix !== "object" || Array.isArray(fix))
385
+ continue;
386
+ const candidate = fix;
387
+ if (candidate.fix_id !== taskId)
388
+ continue;
389
+ if (candidate.source !== "code_review" && candidate.source !== "self_test")
390
+ continue;
391
+ if (typeof candidate.parent_task_id !== "string" && candidate.parent_task_id !== null)
392
+ continue;
393
+ if (typeof candidate.reason !== "string")
394
+ continue;
395
+ return candidate;
396
+ }
397
+ // 兼容发布前已写入 tasks.md、但 transition payload 尚未保存修复描述符的记录。
398
+ const legacy = /^REVIEW-FIX-(.+)#([^#]+)$/.exec(taskId);
399
+ return legacy
400
+ ? {
401
+ fix_id: taskId,
402
+ source: "code_review",
403
+ parent_task_id: null,
404
+ reason: "历史代码审查修复任务",
405
+ review_finding: { job_id: legacy[1], finding_id: legacy[2] },
406
+ }
407
+ : null;
408
+ }
302
409
  function isFreshOpenCodeReviewerJob(job, projectRoot, currentWorkingPaths) {
303
410
  return codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null;
304
411
  }
@@ -356,6 +463,17 @@ function evaluateApplyDoneCodeReviewGate(input) {
356
463
  };
357
464
  }
358
465
  if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
466
+ const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
467
+ if (staleReason) {
468
+ const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
469
+ return {
470
+ fromState: "apply_done",
471
+ toState: "apply_done",
472
+ outcome: "job_created",
473
+ newJobs: [job],
474
+ reason: `代码审查结论已不再匹配当前代码状态,重新创建代码审查工作项;${staleReason};${scanReason}`,
475
+ };
476
+ }
359
477
  const reviewFailedStatus = latestCodeReviewFailedStatus(input.events);
360
478
  if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
361
479
  const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
@@ -400,9 +518,14 @@ function evaluateApplyDoneCodeReviewGate(input) {
400
518
  }
401
519
  function createFinalVerifierJob(change, projectRoot, changeRoot, events, currentEvidenceDigest) {
402
520
  const boundFiles = reviewBoundFiles(changeRoot);
521
+ const codeReviewGate = latestCodeReviewGateEvidence(events);
403
522
  const packetContext = {
404
523
  code_state_check: computeCodeStateCheck(projectRoot, events),
524
+ coverage_exemption_refs: effectiveCoverageExemptionRefsFromEvents(events),
525
+ task_execution_index: taskExecutionIndexForReview(projectRoot, events),
526
+ ...(codeReviewGate ? { code_review_gate: codeReviewGate } : {}),
405
527
  };
528
+ const previousRejection = latestReviewHistoryForGateRole(events, REVIEW_FINAL_VERIFIER_GATE, "verifier");
406
529
  return {
407
530
  job_id: newJobId(change, "verifier"),
408
531
  role: "verifier",
@@ -418,9 +541,11 @@ function createFinalVerifierJob(change, projectRoot, changeRoot, events, current
418
541
  review_evidence_digest: currentEvidenceDigest,
419
542
  packet_context: packetContext,
420
543
  created_from_transition: "review-ready",
544
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
421
545
  })),
422
546
  created_from_transition: "review-ready",
423
547
  created_at: new Date().toISOString(),
548
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
424
549
  };
425
550
  }
426
551
  function evaluateFinalVerifierGate(input) {
@@ -695,11 +820,15 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
695
820
  // 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
696
821
  // legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
697
822
  const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
698
- const reviewFix = isReviewFixTaskId(taskId);
699
- if (contractMode && executionRequirementVersion === 2 && !reviewFix && !adopted.parsed) {
823
+ const fix = fixDescriptorForTask(events, taskId);
824
+ const isFixTask = isFixTaskId(taskId);
825
+ if (isFixTask && !fix) {
826
+ return { skip: true, message: `Fix task ${taskId} 缺少状态机创建记录,不能直接手工追加` };
827
+ }
828
+ if (contractMode && executionRequirementVersion === 2 && !isFixTask && !adopted.parsed) {
700
829
  return { skip: true, message: `执行依据模式下,任务 ${taskId} 缺少执行依据` };
701
830
  }
702
- if (contractMode && executionRequirementVersion === 2 && !reviewFix && adopted.parsed) {
831
+ if (contractMode && executionRequirementVersion === 2 && !isFixTask && adopted.parsed) {
703
832
  const contractError = validateTaskStartContract(changeRoot, taskId, adopted.parsed);
704
833
  if (contractError)
705
834
  return { skip: true, message: contractError };
@@ -711,17 +840,19 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
711
840
  }
712
841
  const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
713
842
  const effectivePolicy = executionPolicy;
714
- const requiredEvidence = contractMode && executionRequirementVersion === 2
715
- ? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], reviewFix)
843
+ const requiredEvidence = isFixTask || (contractMode && executionRequirementVersion === 2)
844
+ ? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], isFixTask)
716
845
  : null;
717
846
  const attempt = {
718
847
  attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
719
848
  task_id: taskId, state: "active",
720
849
  task_structure_digest: structureDigest,
850
+ ...(fix ? { fix } : {}),
721
851
  contract_mode: contractMode,
722
852
  contract: adopted.contract,
723
853
  ...(requiredEvidence ? { required_evidence: requiredEvidence } : {
724
- // 历史模式以及 v1 契约轮继续使用 task 行标记回放;v2 只消费快照。
854
+ // Fix 的历史模式以及 v1 契约轮继续使用 task 行标记回放;新建 Fix
855
+ // 无论来自哪个版本,均只消费本轮冻结的有效证据要求。
725
856
  tdd_required: taskInfo.tddRequired,
726
857
  no_tdd_reason: taskInfo.noTddReason,
727
858
  }),
@@ -745,8 +876,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
745
876
  task_id: taskId,
746
877
  execution_policy: effectivePolicy,
747
878
  contract: adopted.contract,
879
+ ...(fix ? { fix } : {}),
748
880
  ...(requiredEvidence ? { required_evidence: requiredEvidence } : {}),
749
- // legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 REVIEW-FIX
881
+ // legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 Fix task
750
882
  ...(adopted.contract ? {} : { legacy_contract: !contractMode }),
751
883
  },
752
884
  };
@@ -792,11 +924,20 @@ function canReopenToPropose(from) {
792
924
  }
793
925
  export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
794
926
  return commitTransition(projectRoot, change, changeRoot, {
795
- name: "reopen", idempotencyInputs: { to, reason, reviewFix: opts.reviewFix ?? "", reviewFinding: opts.reviewFinding ?? "" },
927
+ name: "reopen", idempotencyInputs: {
928
+ to,
929
+ reason,
930
+ reviewFix: opts.reviewFix ?? "",
931
+ reviewFinding: opts.reviewFinding ?? "",
932
+ selfTestFix: opts.selfTestFix ?? "",
933
+ },
796
934
  decide: (snapshot) => {
797
935
  if (!reason || reason.trim() === "")
798
936
  return { skip: true, message: "reopen 需要非空 --reason" };
799
937
  const events = readEvents(projectRoot, change);
938
+ const specialFixCount = [opts.reviewFix, opts.reviewFinding, opts.selfTestFix].filter(Boolean).length;
939
+ if (specialFixCount > 1)
940
+ return { skip: true, message: "一次 reopen 只能指定一种修复或审查引用" };
800
941
  if (opts.reviewFinding) {
801
942
  if (to !== "propose")
802
943
  return { skip: true, message: "--review-finding 只能用于回到计划阶段(reopen --to propose)" };
@@ -826,6 +967,8 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
826
967
  finding_id: ref.findingId,
827
968
  decision_scope: scope,
828
969
  baseline_docs: proposalDocsBaseline(changeRoot),
970
+ planning_validation_version: 2,
971
+ planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
829
972
  },
830
973
  };
831
974
  }
@@ -850,6 +993,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
850
993
  else if (type !== "implementation") {
851
994
  return { skip: true, message: "这个代码审查问题不能直接回到实现阶段处理" };
852
995
  }
996
+ const fix = reviewFixDescriptor(ref, found.finding);
853
997
  return {
854
998
  fromState: "apply_done",
855
999
  toState: "apply",
@@ -859,9 +1003,56 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
859
1003
  review_fix_of: `${ref.jobId}#${ref.findingId}`,
860
1004
  source_job_id: ref.jobId,
861
1005
  finding_id: ref.findingId,
1006
+ fix,
1007
+ },
1008
+ postCommit: (_pr, _ch, cr) => {
1009
+ appendFixTask(cr, fix);
862
1010
  },
1011
+ };
1012
+ }
1013
+ if (opts.selfTestFix) {
1014
+ if (to !== "apply")
1015
+ return { skip: true, message: "--self-test-fix 只能用于回到实现阶段(reopen --to apply)" };
1016
+ const allowedStates = ["apply", "apply_done", "review", "accepted"];
1017
+ if (!allowedStates.includes(snapshot.state)) {
1018
+ return { skip: true, message: `当前状态 ${snapshot.state},不能通过自测问题回到实现阶段` };
1019
+ }
1020
+ const parentTaskId = opts.selfTestFix.trim();
1021
+ const parentTask = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"))
1022
+ .find(task => task.taskId === parentTaskId);
1023
+ if (!parentTask)
1024
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 不存在` };
1025
+ const nextFix = nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason);
1026
+ if ("activeFixTaskId" in nextFix) {
1027
+ return { skip: true, message: `同一自测问题的修复 ${nextFix.activeFixTaskId} 尚未完成,不能重复创建` };
1028
+ }
1029
+ const fix = nextFix.fix;
1030
+ const pendingStatus = pendingTaskStatusForApply(changeRoot, events);
1031
+ if (pendingStatus.pending.length > 0 || snapshot.active_task_attempts.some(attempt => attempt.state === "active")) {
1032
+ return { skip: true, message: "自测修复只允许在当前 task 全部完成且没有活跃执行尝试后创建" };
1033
+ }
1034
+ if (pendingStatus.mode === "contract" && !pendingStatus.completedByEvent.includes(parentTaskId)) {
1035
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 缺少完成事件,不能只依赖 checkbox` };
1036
+ }
1037
+ if (pendingStatus.mode === "legacy" && !parentTask.done) {
1038
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 尚未完成` };
1039
+ }
1040
+ const failedReview = latestCodeReviewFailedStatus(events);
1041
+ if (failedReview?.unresolved.length) {
1042
+ return {
1043
+ skip: true,
1044
+ message: `已有未处理的代码审查问题 ${failedReview.unresolved[0].id};请先按 next 返回的 --review-fix 处理`,
1045
+ };
1046
+ }
1047
+ return {
1048
+ fromState: snapshot.state,
1049
+ toState: "apply",
1050
+ outcome: "advanced",
1051
+ reason: reason.trim(),
1052
+ commitPayload: { fix },
1053
+ extraEvents: invalidateOpenJobs(snapshot, "apply", reason.trim()),
863
1054
  postCommit: (_pr, _ch, cr) => {
864
- appendReviewFixTask(cr, ref, found.finding);
1055
+ appendFixTask(cr, fix);
865
1056
  },
866
1057
  };
867
1058
  }
@@ -878,6 +1069,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
878
1069
  reopen_target: "explore",
879
1070
  reopen_source: snapshot.state,
880
1071
  baseline_docs: discoveryDocsBaseline(changeRoot),
1072
+ ...exploreAnswerRegistrationPayloadForChange(changeRoot),
881
1073
  },
882
1074
  extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
883
1075
  };
@@ -915,6 +1107,8 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
915
1107
  reopen_source: snapshot.state,
916
1108
  baseline_source: acceptedBaseline ? (baselineNeedsBackfill ? "accepted_backfill" : "accepted") : "reopen_fallback",
917
1109
  baseline_docs: baselineDocs,
1110
+ planning_validation_version: 2,
1111
+ planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
918
1112
  },
919
1113
  extraEvents: planningReopenExtraEvents(snapshot, "propose", reason.trim()),
920
1114
  };
package/dist/types.d.ts CHANGED
@@ -50,6 +50,21 @@ export interface EffectiveEvidencePlan {
50
50
  green_required: boolean;
51
51
  accepted_green_statuses: Array<"expected_success" | "characterization_pass">;
52
52
  }
53
+ /**
54
+ * 状态机创建的实现修复工作项。它只纠正已批准 task 的实现,不产生新的计划需求。
55
+ * 旧事件没有该字段时按历史行为回放。
56
+ */
57
+ export type FixSource = "code_review" | "self_test";
58
+ export interface FixDescriptor {
59
+ fix_id: string;
60
+ source: FixSource;
61
+ parent_task_id: string | null;
62
+ reason: string;
63
+ review_finding?: {
64
+ job_id: string;
65
+ finding_id: string;
66
+ };
67
+ }
53
68
  export interface DirtyFileFingerprint {
54
69
  path: string;
55
70
  status: "added" | "modified" | "deleted";
@@ -78,9 +93,19 @@ export interface CodeStateCheck {
78
93
  changed_paths: string[];
79
94
  scope_reason: string;
80
95
  }
96
+ /** 最终验证读取的最新代码审查门禁事实。 */
97
+ export interface CodeReviewGateEvidence {
98
+ decision: "passed" | "skipped";
99
+ job_id: string | null;
100
+ packet_digest: string | null;
101
+ reason?: "no_code_changes";
102
+ event_id: string;
103
+ event_digest: string;
104
+ }
81
105
  export interface TaskExecutionIndexEntry {
82
106
  task_id: string;
83
107
  attempt_id: string;
108
+ fix?: FixDescriptor | null;
84
109
  execution_policy: ExecutionPolicy;
85
110
  changed_paths: string[] | null;
86
111
  changed_paths_partial_reason?: string;
@@ -99,6 +124,7 @@ export interface CoverageExemptionRef {
99
124
  }
100
125
  export interface JobPacketContext {
101
126
  code_review_scope?: CodeReviewScope;
127
+ code_review_gate?: CodeReviewGateEvidence;
102
128
  coverage_exemption_refs?: CoverageExemptionRef[];
103
129
  task_execution_index?: TaskExecutionIndexEntry[];
104
130
  unattributed_paths?: string[];
@@ -117,6 +143,7 @@ export interface JobPacket {
117
143
  previous_rejection?: ReviewPreviousRejection;
118
144
  packet_context?: JobPacketContext;
119
145
  code_review_scope?: CodeReviewScope;
146
+ code_review_gate?: CodeReviewGateEvidence;
120
147
  coverage_exemption_refs?: CoverageExemptionRef[];
121
148
  task_execution_index?: TaskExecutionIndexEntry[];
122
149
  unattributed_paths?: string[];
@@ -156,6 +183,16 @@ export interface Event {
156
183
  payload: Record<string, unknown>;
157
184
  event_digest: string;
158
185
  }
186
+ export type OpenSpecValidationProfile = {
187
+ mode: "disabled";
188
+ } | {
189
+ mode: "strict";
190
+ config_digest: string;
191
+ };
192
+ export interface PlanningValidationProfile {
193
+ version: 2;
194
+ openspec: OpenSpecValidationProfile;
195
+ }
159
196
  export interface TransitionCommitPayload {
160
197
  transition: string;
161
198
  from_state: State;
@@ -190,6 +227,10 @@ export interface TransitionCommitPayload {
190
227
  workflow_mode?: "minimal" | "normal" | "strict";
191
228
  /** v2 起所有普通任务必须有五字段执行依据;缺失表示旧 change,沿用旧规则回放。 */
192
229
  execution_requirement_version?: 2;
230
+ /** 新 planning round 的格式协议版本;缺失表示升级前的 v1 change。 */
231
+ planning_validation_version?: 2;
232
+ /** propose-ready 成功时冻结的 OpenSpec 校验 profile,start-apply 只回放此快照。 */
233
+ planning_validation_profile?: PlanningValidationProfile;
193
234
  execution_policy?: ExecutionPolicy;
194
235
  }
195
236
  export interface Snapshot {
@@ -213,6 +254,7 @@ export interface TaskAttempt {
213
254
  task_id: string;
214
255
  state: AttemptState;
215
256
  task_structure_digest: string;
257
+ fix?: FixDescriptor | null;
216
258
  contract?: ExecutionContract | null;
217
259
  contract_mode?: boolean;
218
260
  /** task-start 编译出的有效执行要求;缺失表示历史 attempt,按旧字段回放。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,15 +1,25 @@
1
1
  <!-- SUPERSPEC:AGENTS:START -->
2
- 本项目启用 SuperSpec。使用 `superspec-*` 工作流时,以 `superspec transition next --change "<change>"` 返回的下一步为准;流程完成前不得跳阶段、不得自称完成。
2
+ 只有当用户显式调用 `superspec-*`,或明确要求继续处理某个已有 SuperSpec change 时,才进入或续转 SuperSpec 工作流。普通开发、修复、排查、测试或审查请求,即使项目已安装 SuperSpec,也不得自行启动工作流、创建 change、执行 `transition next`,或切换到某个 `superspec-*` 阶段。
3
3
 
4
- 即使用户没有显式调用 `superspec-*`,如果新输入像是在改变业务规则、产品口径、验收标准、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新,编辑代码前先提醒并做只读确认:这是实现偏差,还是需要先回 `superspec-propose` 更新计划文档;不要直接把这类自然语言当作 apply 授权。
4
+ 一旦用户已显式启动工作流或明确指定 change,使用 `superspec-*` 工作流时一律以 `superspec transition next --change "<change>"` 返回的下一步推进;主流程执行内部命令,不要求用户手动运行工作流命令。流程完成前不得跳阶段、不得自称完成。
5
5
 
6
- 用户补充 SuperSpec 相关内容时,先确定对应 change,再按 next 返回处理;无法确定时只询问归属,不执行流转。同一 change 的方案、需求、验收或实现约束补充,按影响回到该 change `propose`,不要另建 repair change;内部命令由主流程完成,不交给用户。
6
+ 每完成 `next` 返回的当前事项(材料更新、用户答复回写、实现、验证、审查或修复时),立即再次运行 `superspec transition next --change "<change>"` 并继续处理。完成单个事项不等于完成整个 change;只有工作流明确需要用户决定、当前独立工作项尚未返回结果、遇到真实阻塞或整个 change 已完成时才暂停。
7
7
 
8
- 当用户显式调用 `$superspec-explore` 工作流时,视为已明确授权启动 `explore` subagent 做只读深扫;其他 `$superspec-*` 阶段仅在工作流引擎创建独立工作项时,视为授权启动对应 subagent。
8
+ 当用户显式调用 `$superspec-explore`,或明确要求继续处于 Explore 的已有 change 时,视为已明确授权启动 `explore` subagent 做只读深扫;其他 `$superspec-*` 阶段仅在工作流引擎创建独立工作项时,视为授权启动对应 subagent。
9
+
10
+ Explore 中需要用户决定业务、验收、范围或关键取舍时,先简要说明当前理解、影响和建议,再一次只请用户决定一件事;收到明确答复后,更新相关 discovery 结论,再继续工作流。其他阶段要求用户确认、选择处理方向或补齐材料时,按当前工作流返回的要求登记结论或更新相应材料;不得把这类答复默认写入 discovery。
11
+
12
+ 当前 change 的自测、联调或用户指出的问题若仍能由既有 task 的批准行为、边界和验收解释,就在同一 change 内处理:
13
+
14
+ - 当前 task 尚未完成时,在其范围内直接修复;不要为同一实现问题新增 task 或回 propose。
15
+ - 所有 task 已完成后,若问题仍能关联一个已完成 task、且不改变已批准行为和方案,主流程执行 `superspec transition reopen --change "<change>" --to apply --self-test-fix "<task>" --reason "<reason>"`,让工作流创建修复事项;随后继续 `next`,不得手改 tasks。
16
+ - 无法关联既有 task,或需要改变行为、验收、接口、数据语义或实现路线时,才回 propose。
17
+
18
+ 在用户已显式启动工作流或明确指定 change 后,若新增或改变业务规则、产品口径、验收、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新时,先确定对应 change;归属明确则回同一 change 的 `propose` 更新计划,归属不明才询问。不要把这类输入直接当作 apply 授权,也不要另建 repair change。
9
19
 
10
20
  SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
11
21
 
12
22
  审查/验证工作项只授权处理该工作项绑定的内容,不得扩大范围、跳过阶段或代替后续流程。若当前环境无法启动 subagent,只能使用本地或用户已授权的独立来源;没有可用独立来源时停在当前工作项并说明阻塞,主会话不得因此自批。
13
23
 
14
- 执行 CLI 返回命令时优先使用 `*_argv` 字段。用户可见回复使用自然语言;除非用户要求调试信息,不复述内部 JSON 字段。
24
+ 主流程代为执行必要的工作流操作;用户可见回复使用自然语言,除非用户要求调试信息,不展示底层命令或数据。
15
25
  <!-- SUPERSPEC:AGENTS:END -->
@@ -1,7 +1,7 @@
1
1
  # SuperSpec Codex agent: architect
2
2
  name = "architect"
3
3
  description = "System design, boundaries, interfaces, long-horizon tradeoffs"
4
- model_reasoning_effort = "high"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  Role: Architect. Review system boundaries, interface contracts, data flow, maintenance risk, rollback risk, and design tradeoffs.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # SuperSpec Codex agent: code-reviewer
2
2
  name = "code-reviewer"
3
3
  description = "Code-level review for spec fit, bugs, safety, and test gaps"
4
- model_reasoning_effort = "high"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  Role: Code Reviewer. Check spec fit, correctness, security, test adequacy, code quality, performance, and maintainability without making the workflow heavy.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # SuperSpec Codex agent: critic
2
2
  name = "critic"
3
3
  description = "Plan/design critical challenge and review"
4
- model_reasoning_effort = "high"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  Role: Critic. Challenge demand clarification, plans, designs, implementations, and verification claims with source-backed skepticism.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # SuperSpec Codex agent: executor
2
2
  name = "executor"
3
3
  description = "Bounded SuperSpec apply implementation worker"
4
- model_reasoning_effort = "high"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  Role: Executor. Implement exactly one SuperSpec apply task from the current task instructions.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # SuperSpec Codex agent: explore
2
2
  name = "explore"
3
3
  description = "Repo-local read-only factual scan for SuperSpec discovery"
4
- model_reasoning_effort = "high"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  Role: Explore. Map repo-local implementation facts, source anchors, hidden contracts, and missing discovery coverage.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # SuperSpec Codex agent: test-engineer
2
2
  name = "test-engineer"
3
3
  description = "Test strategy, coverage, flaky-test hardening"
4
- model_reasoning_effort = "high"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  Role: Test Engineer. Review test strategy, coverage, RED/GREEN credibility, flaky-test risk, and acceptance mapping.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # SuperSpec Codex agent: test-runner
2
2
  name = "test-runner"
3
3
  description = "Bounded SuperSpec apply test execution worker"
4
- model_reasoning_effort = "high"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  Role: Test Runner. Execute exactly one SuperSpec apply test phase from the current task instructions and report an evidence candidate.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # SuperSpec Codex agent: verifier
2
2
  name = "verifier"
3
3
  description = "Completion evidence, claim validation, test adequacy"
4
- model_reasoning_effort = "high"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  Role: Verifier. Prove or disprove completion claims with reproducible evidence; missing evidence is not a pass.
7
7