@peterxiaoyang/superspec 0.1.46 → 0.1.48

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 (39) hide show
  1. package/dist/cli.js +54 -9
  2. package/dist/code_review.d.ts +11 -1
  3. package/dist/code_review.js +40 -0
  4. package/dist/explore_round.d.ts +25 -0
  5. package/dist/explore_round.js +139 -0
  6. package/dist/format.d.ts +78 -2
  7. package/dist/format.js +227 -16
  8. package/dist/next.d.ts +1 -1
  9. package/dist/next.js +97 -12
  10. package/dist/openspec.js +26 -5
  11. package/dist/phase_confirmation.js +73 -2
  12. package/dist/phase_plan.d.ts +24 -1
  13. package/dist/phase_plan.js +194 -34
  14. package/dist/propose_round.d.ts +15 -0
  15. package/dist/propose_round.js +137 -0
  16. package/dist/record.js +313 -57
  17. package/dist/review.js +2 -0
  18. package/dist/review_job_gates.d.ts +1 -1
  19. package/dist/review_job_gates.js +12 -4
  20. package/dist/skill_loop.js +20 -0
  21. package/dist/task_evidence.js +5 -3
  22. package/dist/transition.d.ts +1 -0
  23. package/dist/transition.js +248 -31
  24. package/dist/types.d.ts +77 -1
  25. package/dist/workflow_profile.js +1 -1
  26. package/package.json +7 -1
  27. package/templates/workflow/AGENTS.md +17 -5
  28. package/templates/workflow/prompts/architect.md +4 -2
  29. package/templates/workflow/prompts/code-reviewer.md +3 -3
  30. package/templates/workflow/prompts/critic.md +13 -4
  31. package/templates/workflow/prompts/executor.md +5 -5
  32. package/templates/workflow/prompts/explore.md +2 -2
  33. package/templates/workflow/prompts/test-engineer.md +6 -5
  34. package/templates/workflow/prompts/test-runner.md +5 -5
  35. package/templates/workflow/prompts/verifier.md +4 -4
  36. package/templates/workflow/skills/superspec-apply/SKILL.md +26 -12
  37. package/templates/workflow/skills/superspec-explore/SKILL.md +25 -7
  38. package/templates/workflow/skills/superspec-propose/SKILL.md +33 -16
  39. package/templates/workflow/skills/superspec-review/SKILL.md +9 -9
@@ -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, planningValidationProfileForNewRound, 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, proposeAnswerRegistrationPayloadForChange, 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";
@@ -17,9 +17,9 @@ let transitionSeq = 0;
17
17
  function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
18
18
  let jobSeq = 0;
19
19
  function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice(0, 4)}-${Date.now()}-${++jobSeq}`; }
20
- function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason, events) {
20
+ function createReviewJobsForGate(state, gate, roles, requiredRoles, changeRoot, change, reason, events) {
21
21
  const newJobs = roles.map(role => {
22
- const scope = reviewScopeForGateRole(gate, role);
22
+ const scope = reviewScopeForGateRole(gate, role, requiredRoles);
23
23
  // 角色职责目标和显式 freshness 路径绑定时点指纹:单文件缺失使用 sha256:missing,目录缺失使用稳定空指纹。
24
24
  const boundPaths = [...new Set(scope.boundPaths)];
25
25
  const boundFiles = boundPaths
@@ -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 {
@@ -209,6 +209,28 @@ function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationW
209
209
  accepted_green_statuses: ["expected_success"],
210
210
  };
211
211
  }
212
+ function evidenceActionsForAttempt(change, attemptId, required) {
213
+ const testIds = required.test_ids.length > 0 ? required.test_ids : [undefined];
214
+ const statuses = [];
215
+ if (required.red_required)
216
+ statuses.push("expected_failure");
217
+ if (required.green_required)
218
+ statuses.push(required.accepted_green_statuses[0] ?? "expected_success");
219
+ return testIds.flatMap(testId => statuses.map(semanticStatus => ({
220
+ kind: "test_run",
221
+ ...(testId ? { test_id: testId } : {}),
222
+ record_argv: ["superspec", "record", "test-run", "--change", change, "--input", "-"],
223
+ record_input: {
224
+ ...(testId ? { test_id: testId } : {}),
225
+ attempt_id: attemptId,
226
+ command: null,
227
+ cwd: null,
228
+ exit_code: null,
229
+ semantic_status: semanticStatus,
230
+ },
231
+ required_fields: ["command", "cwd", "exit_code"],
232
+ })));
233
+ }
212
234
  function hasRejectedReviewReadyVerifier(events) {
213
235
  const reviewReadyVerifierIds = new Set();
214
236
  for (const ev of events) {
@@ -279,26 +301,133 @@ function findReviewFailedFinding(events, ref) {
279
301
  return null;
280
302
  return { event: status.terminal.event, finding };
281
303
  }
282
- function reviewFixMarker(ref) {
283
- return `review_fix_of:${ref.jobId}#${ref.findingId}`;
304
+ function reviewFixDescriptor(ref, finding) {
305
+ const reason = typeof finding.description === "string" && finding.description.trim()
306
+ ? finding.description.trim().replace(/\s+/g, " ")
307
+ : `修复代码审查问题 ${ref.findingId}`;
308
+ return {
309
+ fix_id: `REVIEW-FIX-${ref.jobId}#${ref.findingId}`,
310
+ source: "code_review",
311
+ parent_task_id: null,
312
+ reason,
313
+ review_finding: { job_id: ref.jobId, finding_id: ref.findingId },
314
+ };
315
+ }
316
+ function selfTestFixBaseId(parentTaskId, reason) {
317
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
318
+ const normalizedTaskId = parentTaskId.replace(/[^A-Za-z0-9_-]/g, "-");
319
+ const digest = sha256Text(`${parentTaskId}\n${normalizedReason}`).replace(/^sha256:/, "").slice(0, 12);
320
+ return `FIX-SELFTEST-${normalizedTaskId}-${digest}`;
321
+ }
322
+ function selfTestFixDescriptor(parentTaskId, reason, occurrence) {
323
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
324
+ const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
325
+ return {
326
+ fix_id: occurrence === 1 ? baseId : `${baseId}-${occurrence}`,
327
+ source: "self_test",
328
+ parent_task_id: parentTaskId,
329
+ reason: normalizedReason,
330
+ };
284
331
  }
285
- function reviewFixTaskId(ref) {
286
- return `REVIEW-FIX-${ref.jobId}#${ref.findingId}`;
332
+ function selfTestFixOccurrence(taskId, baseId) {
333
+ if (taskId === baseId)
334
+ return 1;
335
+ const suffix = taskId.slice(baseId.length + 1);
336
+ if (!taskId.startsWith(`${baseId}-`) || !/^\d+$/.test(suffix))
337
+ return null;
338
+ const occurrence = Number(suffix);
339
+ return Number.isSafeInteger(occurrence) && occurrence >= 2 ? occurrence : null;
287
340
  }
288
- function appendReviewFixTask(changeRoot, ref, finding) {
341
+ function nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason) {
342
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
343
+ const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
344
+ let maxOccurrence = 0;
345
+ const completion = pendingTaskStatusForApply(changeRoot, events);
346
+ const pendingTaskIds = new Set(completion.pending);
347
+ const taskInfos = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"));
348
+ for (const task of taskInfos) {
349
+ const occurrence = selfTestFixOccurrence(task.taskId, baseId);
350
+ if (occurrence == null)
351
+ continue;
352
+ maxOccurrence = Math.max(maxOccurrence, occurrence);
353
+ if (pendingTaskIds.has(task.taskId))
354
+ return { activeFixTaskId: task.taskId };
355
+ }
356
+ for (const event of events) {
357
+ if (event.event_type !== "transition_commit")
358
+ continue;
359
+ const fix = event.payload.fix;
360
+ if (!fix || typeof fix !== "object" || Array.isArray(fix))
361
+ continue;
362
+ const candidate = fix;
363
+ if (candidate.source !== "self_test" || candidate.parent_task_id !== parentTaskId || candidate.reason !== normalizedReason)
364
+ continue;
365
+ if (typeof candidate.fix_id !== "string")
366
+ continue;
367
+ const occurrence = selfTestFixOccurrence(candidate.fix_id, baseId);
368
+ if (occurrence != null)
369
+ maxOccurrence = Math.max(maxOccurrence, occurrence);
370
+ }
371
+ return { fix: selfTestFixDescriptor(parentTaskId, normalizedReason, maxOccurrence + 1) };
372
+ }
373
+ function fixMarker(fix) {
374
+ if (fix.source === "code_review" && fix.review_finding) {
375
+ return `review_fix_of:${fix.review_finding.job_id}#${fix.review_finding.finding_id}`;
376
+ }
377
+ return `self_test_fix_of:${fix.parent_task_id ?? "unknown"}:${fix.fix_id}`;
378
+ }
379
+ function appendFixTask(changeRoot, fix) {
289
380
  const tasksPath = join(changeRoot, "tasks.md");
290
381
  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}`;
382
+ const marker = fixMarker(fix);
383
+ if (content.includes(marker)) {
384
+ const matchingTask = content.split("\n").some(line => line.includes(marker) && line.startsWith(`- [ ] ${fix.fix_id} `));
385
+ if (matchingTask)
386
+ return "exists";
387
+ throw new Error(`修复标记 ${marker} 已被其它 task 占用,拒绝创建 ${fix.fix_id}`);
388
+ }
389
+ if (parseTasksMd(content).some(task => task.taskId === fix.fix_id)) {
390
+ throw new Error(`修复 task ID ${fix.fix_id} 已被其它任务占用,拒绝创建重复修复`);
391
+ }
392
+ const description = fix.source === "self_test"
393
+ ? `修复自测问题(关联 ${fix.parent_task_id}):${fix.reason}`
394
+ : fix.reason;
395
+ const line = `- [ ] ${fix.fix_id} ${description} ${marker}`;
298
396
  const suffix = content.endsWith("\n") ? "" : "\n";
299
397
  writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
300
398
  return "created";
301
399
  }
400
+ function fixDescriptorForTask(events, taskId) {
401
+ for (let i = events.length - 1; i >= 0; i--) {
402
+ const event = events[i];
403
+ if (event.event_type !== "transition_commit")
404
+ continue;
405
+ const fix = event.payload.fix;
406
+ if (!fix || typeof fix !== "object" || Array.isArray(fix))
407
+ continue;
408
+ const candidate = fix;
409
+ if (candidate.fix_id !== taskId)
410
+ continue;
411
+ if (candidate.source !== "code_review" && candidate.source !== "self_test")
412
+ continue;
413
+ if (typeof candidate.parent_task_id !== "string" && candidate.parent_task_id !== null)
414
+ continue;
415
+ if (typeof candidate.reason !== "string")
416
+ continue;
417
+ return candidate;
418
+ }
419
+ // 兼容发布前已写入 tasks.md、但 transition payload 尚未保存修复描述符的记录。
420
+ const legacy = /^REVIEW-FIX-(.+)#([^#]+)$/.exec(taskId);
421
+ return legacy
422
+ ? {
423
+ fix_id: taskId,
424
+ source: "code_review",
425
+ parent_task_id: null,
426
+ reason: "历史代码审查修复任务",
427
+ review_finding: { job_id: legacy[1], finding_id: legacy[2] },
428
+ }
429
+ : null;
430
+ }
302
431
  function isFreshOpenCodeReviewerJob(job, projectRoot, currentWorkingPaths) {
303
432
  return codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null;
304
433
  }
@@ -356,6 +485,17 @@ function evaluateApplyDoneCodeReviewGate(input) {
356
485
  };
357
486
  }
358
487
  if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
488
+ const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
489
+ if (staleReason) {
490
+ const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
491
+ return {
492
+ fromState: "apply_done",
493
+ toState: "apply_done",
494
+ outcome: "job_created",
495
+ newJobs: [job],
496
+ reason: `代码审查结论已不再匹配当前代码状态,重新创建代码审查工作项;${staleReason};${scanReason}`,
497
+ };
498
+ }
359
499
  const reviewFailedStatus = latestCodeReviewFailedStatus(input.events);
360
500
  if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
361
501
  const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
@@ -400,9 +540,14 @@ function evaluateApplyDoneCodeReviewGate(input) {
400
540
  }
401
541
  function createFinalVerifierJob(change, projectRoot, changeRoot, events, currentEvidenceDigest) {
402
542
  const boundFiles = reviewBoundFiles(changeRoot);
543
+ const codeReviewGate = latestCodeReviewGateEvidence(events);
403
544
  const packetContext = {
404
545
  code_state_check: computeCodeStateCheck(projectRoot, events),
546
+ coverage_exemption_refs: effectiveCoverageExemptionRefsFromEvents(events),
547
+ task_execution_index: taskExecutionIndexForReview(projectRoot, events),
548
+ ...(codeReviewGate ? { code_review_gate: codeReviewGate } : {}),
405
549
  };
550
+ const previousRejection = latestReviewHistoryForGateRole(events, REVIEW_FINAL_VERIFIER_GATE, "verifier");
406
551
  return {
407
552
  job_id: newJobId(change, "verifier"),
408
553
  role: "verifier",
@@ -418,9 +563,11 @@ function createFinalVerifierJob(change, projectRoot, changeRoot, events, current
418
563
  review_evidence_digest: currentEvidenceDigest,
419
564
  packet_context: packetContext,
420
565
  created_from_transition: "review-ready",
566
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
421
567
  })),
422
568
  created_from_transition: "review-ready",
423
569
  created_at: new Date().toISOString(),
570
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
424
571
  };
425
572
  }
426
573
  function evaluateFinalVerifierGate(input) {
@@ -502,7 +649,7 @@ function transitionPlanToDecision(snapshot, changeRoot, change, plan, events) {
502
649
  case "blocked":
503
650
  return { blocked: true, reason: plan.reason, jobs: plan.jobs, ...(plan.details ? { details: plan.details } : {}) };
504
651
  case "create_gate_jobs":
505
- return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, changeRoot, change, plan.reason, events);
652
+ return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, plan.requiredRoles, changeRoot, change, plan.reason, events);
506
653
  case "advance":
507
654
  return {
508
655
  fromState: plan.fromState,
@@ -695,11 +842,15 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
695
842
  // 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
696
843
  // legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
697
844
  const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
698
- const reviewFix = isReviewFixTaskId(taskId);
699
- if (contractMode && executionRequirementVersion === 2 && !reviewFix && !adopted.parsed) {
845
+ const fix = fixDescriptorForTask(events, taskId);
846
+ const isFixTask = isFixTaskId(taskId);
847
+ if (isFixTask && !fix) {
848
+ return { skip: true, message: `Fix task ${taskId} 缺少状态机创建记录,不能直接手工追加` };
849
+ }
850
+ if (contractMode && executionRequirementVersion === 2 && !isFixTask && !adopted.parsed) {
700
851
  return { skip: true, message: `执行依据模式下,任务 ${taskId} 缺少执行依据` };
701
852
  }
702
- if (contractMode && executionRequirementVersion === 2 && !reviewFix && adopted.parsed) {
853
+ if (contractMode && executionRequirementVersion === 2 && !isFixTask && adopted.parsed) {
703
854
  const contractError = validateTaskStartContract(changeRoot, taskId, adopted.parsed);
704
855
  if (contractError)
705
856
  return { skip: true, message: contractError };
@@ -711,17 +862,19 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
711
862
  }
712
863
  const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
713
864
  const effectivePolicy = executionPolicy;
714
- const requiredEvidence = contractMode && executionRequirementVersion === 2
715
- ? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], reviewFix)
865
+ const requiredEvidence = isFixTask || (contractMode && executionRequirementVersion === 2)
866
+ ? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], isFixTask)
716
867
  : null;
717
868
  const attempt = {
718
869
  attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
719
870
  task_id: taskId, state: "active",
720
871
  task_structure_digest: structureDigest,
872
+ ...(fix ? { fix } : {}),
721
873
  contract_mode: contractMode,
722
874
  contract: adopted.contract,
723
875
  ...(requiredEvidence ? { required_evidence: requiredEvidence } : {
724
- // 历史模式以及 v1 契约轮继续使用 task 行标记回放;v2 只消费快照。
876
+ // Fix 的历史模式以及 v1 契约轮继续使用 task 行标记回放;新建 Fix
877
+ // 无论来自哪个版本,均只消费本轮冻结的有效证据要求。
725
878
  tdd_required: taskInfo.tddRequired,
726
879
  no_tdd_reason: taskInfo.noTddReason,
727
880
  }),
@@ -736,6 +889,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
736
889
  ...attempt,
737
890
  ...boundarySnapshotPayload(projectRoot),
738
891
  };
892
+ const evidenceActions = requiredEvidence
893
+ ? evidenceActionsForAttempt(change, attempt.attempt_id, requiredEvidence)
894
+ : null;
739
895
  return {
740
896
  fromState: "apply", toState: "apply", outcome: "advanced",
741
897
  reason: `创建任务 ${taskId} 执行尝试`,
@@ -745,8 +901,10 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
745
901
  task_id: taskId,
746
902
  execution_policy: effectivePolicy,
747
903
  contract: adopted.contract,
904
+ ...(fix ? { fix } : {}),
748
905
  ...(requiredEvidence ? { required_evidence: requiredEvidence } : {}),
749
- // legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 REVIEW-FIX)
906
+ ...(evidenceActions ? { evidence_actions: evidenceActions } : {}),
907
+ // legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 Fix task)
750
908
  ...(adopted.contract ? {} : { legacy_contract: !contractMode }),
751
909
  },
752
910
  };
@@ -792,11 +950,20 @@ function canReopenToPropose(from) {
792
950
  }
793
951
  export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
794
952
  return commitTransition(projectRoot, change, changeRoot, {
795
- name: "reopen", idempotencyInputs: { to, reason, reviewFix: opts.reviewFix ?? "", reviewFinding: opts.reviewFinding ?? "" },
953
+ name: "reopen", idempotencyInputs: {
954
+ to,
955
+ reason,
956
+ reviewFix: opts.reviewFix ?? "",
957
+ reviewFinding: opts.reviewFinding ?? "",
958
+ selfTestFix: opts.selfTestFix ?? "",
959
+ },
796
960
  decide: (snapshot) => {
797
961
  if (!reason || reason.trim() === "")
798
962
  return { skip: true, message: "reopen 需要非空 --reason" };
799
963
  const events = readEvents(projectRoot, change);
964
+ const specialFixCount = [opts.reviewFix, opts.reviewFinding, opts.selfTestFix].filter(Boolean).length;
965
+ if (specialFixCount > 1)
966
+ return { skip: true, message: "一次 reopen 只能指定一种修复或审查引用" };
800
967
  if (opts.reviewFinding) {
801
968
  if (to !== "propose")
802
969
  return { skip: true, message: "--review-finding 只能用于回到计划阶段(reopen --to propose)" };
@@ -828,6 +995,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
828
995
  baseline_docs: proposalDocsBaseline(changeRoot),
829
996
  planning_validation_version: 2,
830
997
  planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
998
+ ...proposeAnswerRegistrationPayloadForChange(changeRoot),
831
999
  },
832
1000
  };
833
1001
  }
@@ -852,6 +1020,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
852
1020
  else if (type !== "implementation") {
853
1021
  return { skip: true, message: "这个代码审查问题不能直接回到实现阶段处理" };
854
1022
  }
1023
+ const fix = reviewFixDescriptor(ref, found.finding);
855
1024
  return {
856
1025
  fromState: "apply_done",
857
1026
  toState: "apply",
@@ -861,9 +1030,56 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
861
1030
  review_fix_of: `${ref.jobId}#${ref.findingId}`,
862
1031
  source_job_id: ref.jobId,
863
1032
  finding_id: ref.findingId,
1033
+ fix,
1034
+ },
1035
+ postCommit: (_pr, _ch, cr) => {
1036
+ appendFixTask(cr, fix);
864
1037
  },
1038
+ };
1039
+ }
1040
+ if (opts.selfTestFix) {
1041
+ if (to !== "apply")
1042
+ return { skip: true, message: "--self-test-fix 只能用于回到实现阶段(reopen --to apply)" };
1043
+ const allowedStates = ["apply", "apply_done", "review", "accepted"];
1044
+ if (!allowedStates.includes(snapshot.state)) {
1045
+ return { skip: true, message: `当前状态 ${snapshot.state},不能通过自测问题回到实现阶段` };
1046
+ }
1047
+ const parentTaskId = opts.selfTestFix.trim();
1048
+ const parentTask = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"))
1049
+ .find(task => task.taskId === parentTaskId);
1050
+ if (!parentTask)
1051
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 不存在` };
1052
+ const nextFix = nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason);
1053
+ if ("activeFixTaskId" in nextFix) {
1054
+ return { skip: true, message: `同一自测问题的修复 ${nextFix.activeFixTaskId} 尚未完成,不能重复创建` };
1055
+ }
1056
+ const fix = nextFix.fix;
1057
+ const pendingStatus = pendingTaskStatusForApply(changeRoot, events);
1058
+ if (pendingStatus.pending.length > 0 || snapshot.active_task_attempts.some(attempt => attempt.state === "active")) {
1059
+ return { skip: true, message: "自测修复只允许在当前 task 全部完成且没有活跃执行尝试后创建" };
1060
+ }
1061
+ if (pendingStatus.mode === "contract" && !pendingStatus.completedByEvent.includes(parentTaskId)) {
1062
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 缺少完成事件,不能只依赖 checkbox` };
1063
+ }
1064
+ if (pendingStatus.mode === "legacy" && !parentTask.done) {
1065
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 尚未完成` };
1066
+ }
1067
+ const failedReview = latestCodeReviewFailedStatus(events);
1068
+ if (failedReview?.unresolved.length) {
1069
+ return {
1070
+ skip: true,
1071
+ message: `已有未处理的代码审查问题 ${failedReview.unresolved[0].id};请先按 next 返回的 --review-fix 处理`,
1072
+ };
1073
+ }
1074
+ return {
1075
+ fromState: snapshot.state,
1076
+ toState: "apply",
1077
+ outcome: "advanced",
1078
+ reason: reason.trim(),
1079
+ commitPayload: { fix },
1080
+ extraEvents: invalidateOpenJobs(snapshot, "apply", reason.trim()),
865
1081
  postCommit: (_pr, _ch, cr) => {
866
- appendReviewFixTask(cr, ref, found.finding);
1082
+ appendFixTask(cr, fix);
867
1083
  },
868
1084
  };
869
1085
  }
@@ -880,6 +1096,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
880
1096
  reopen_target: "explore",
881
1097
  reopen_source: snapshot.state,
882
1098
  baseline_docs: discoveryDocsBaseline(changeRoot),
1099
+ ...exploreAnswerRegistrationPayloadForChange(changeRoot),
883
1100
  },
884
1101
  extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
885
1102
  };
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[];
@@ -141,7 +168,7 @@ export interface RequiredJobAction {
141
168
  packet_command: string;
142
169
  packet_argv: string[];
143
170
  }
144
- export type EventType = "transition_prepare" | "transition_commit" | "job_requested" | "job_invalidated" | "reopen" | "abandon" | "task_started" | "task_completed" | "task_abandoned" | "job_accepted" | "job_rejected" | "user_decision_recorded" | "test_run_recorded" | "task_activation_recorded" | "artifact_recorded";
171
+ export type EventType = "transition_prepare" | "transition_commit" | "job_requested" | "job_invalidated" | "reopen" | "abandon" | "task_started" | "task_completed" | "task_abandoned" | "job_accepted" | "job_rejected" | "user_question_presented" | "user_decision_recorded" | "test_run_recorded" | "task_activation_recorded" | "artifact_recorded";
145
172
  export interface Event {
146
173
  event_id: string;
147
174
  event_type: EventType;
@@ -165,6 +192,9 @@ export type OpenSpecValidationProfile = {
165
192
  export interface PlanningValidationProfile {
166
193
  version: 2;
167
194
  openspec: OpenSpecValidationProfile;
195
+ design?: {
196
+ schema_version: 1;
197
+ };
168
198
  }
169
199
  export interface TransitionCommitPayload {
170
200
  transition: string;
@@ -227,6 +257,7 @@ export interface TaskAttempt {
227
257
  task_id: string;
228
258
  state: AttemptState;
229
259
  task_structure_digest: string;
260
+ fix?: FixDescriptor | null;
230
261
  contract?: ExecutionContract | null;
231
262
  contract_mode?: boolean;
232
263
  /** task-start 编译出的有效执行要求;缺失表示历史 attempt,按旧字段回放。 */
@@ -290,6 +321,14 @@ export interface AskUser {
290
321
  allowed_answers: string[];
291
322
  scope: string;
292
323
  actions?: AskUserAction[];
324
+ /** 自由文本问题的直接登记入口;固定选项继续使用 actions。 */
325
+ record_argv?: string[];
326
+ record_input?: {
327
+ scope: string;
328
+ question: string;
329
+ answer: null;
330
+ };
331
+ required_fields?: Array<"answer">;
293
332
  }
294
333
  export interface AcceptedMaterialFollowupContinuation {
295
334
  kind: "accepted_material_followup";
@@ -303,6 +342,33 @@ export interface AcceptedMaterialFollowupContinuation {
303
342
  };
304
343
  plan_docs_changed_since_accept: boolean | null;
305
344
  }
345
+ export type WorkflowArtifactKind = "discovery" | "test_contract";
346
+ export interface RequiredWorkflowArtifact {
347
+ kind: WorkflowArtifactKind;
348
+ /** Repository-relative canonical path owned by the workflow engine. */
349
+ path: string;
350
+ operation: "create_or_update";
351
+ }
352
+ export interface ArtifactRequiredResume {
353
+ argv: string[];
354
+ }
355
+ export interface MaterialUpdateRequiredResume {
356
+ argv: string[];
357
+ }
358
+ export interface TestEvidenceAction {
359
+ kind: "test_run";
360
+ test_id?: string;
361
+ record_argv: string[];
362
+ record_input: {
363
+ test_id?: string;
364
+ attempt_id: string;
365
+ command: null;
366
+ cwd: null;
367
+ exit_code: null;
368
+ semantic_status: "expected_failure" | "expected_success" | "characterization_pass";
369
+ };
370
+ required_fields: Array<"command" | "cwd" | "exit_code">;
371
+ }
306
372
  export type NextOutput = {
307
373
  state: State;
308
374
  } & ({
@@ -314,6 +380,16 @@ export type NextOutput = {
314
380
  path: "required_job";
315
381
  required_jobs: RequiredJobAction[];
316
382
  reason: string;
383
+ } | {
384
+ path: "artifact_required";
385
+ artifact: RequiredWorkflowArtifact;
386
+ resume: ArtifactRequiredResume;
387
+ reason: string;
388
+ } | {
389
+ path: "material_update_required";
390
+ errors: string[];
391
+ resume: MaterialUpdateRequiredResume;
392
+ reason: string;
317
393
  } | {
318
394
  path: "ask_user";
319
395
  ask_user: AskUser;
@@ -6,7 +6,7 @@ const CURRENT_GATE_ROLES_BY_RISK = {
6
6
  "review.final_verifier": ["verifier"],
7
7
  },
8
8
  normal: {
9
- "explore.discovery_review": [],
9
+ "explore.discovery_review": ["critic"],
10
10
  "propose.final_review": ["critic"],
11
11
  "review.code_review": ["code-reviewer"],
12
12
  "review.final_verifier": ["verifier"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": {
@@ -24,6 +24,12 @@
24
24
  "build": "node build.js",
25
25
  "typecheck": "tsc --noEmit",
26
26
  "test": "node --test tests/*.ts",
27
+ "eval:probe": "node evals/probe.mjs",
28
+ "eval:dynamic": "node evals/probe.mjs --scenario evals/scenarios/probe-dynamic-accepted.json",
29
+ "eval:m2": "node evals/m2.mjs",
30
+ "eval:m2:validate": "node evals/m2.mjs --validate-faults",
31
+ "eval:arena": "node evals/arena.mjs",
32
+ "eval:delegation": "node evals/delegation-probe.mjs",
27
33
  "prepack": "npm run build",
28
34
  "prepublishOnly": "npm run build"
29
35
  },