@peterxiaoyang/superspec 0.1.44 → 0.1.45

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/README.md +13 -1
  2. package/dist/cli.js +23 -24
  3. package/dist/code_review.js +7 -2
  4. package/dist/format.d.ts +4 -2
  5. package/dist/format.js +50 -16
  6. package/dist/git_state.d.ts +12 -1
  7. package/dist/git_state.js +45 -0
  8. package/dist/install.d.ts +1 -0
  9. package/dist/install.js +12 -0
  10. package/dist/next.d.ts +1 -1
  11. package/dist/next.js +3 -8
  12. package/dist/phase_confirmation.d.ts +6 -0
  13. package/dist/phase_confirmation.js +22 -6
  14. package/dist/phase_plan.d.ts +8 -1
  15. package/dist/phase_plan.js +130 -21
  16. package/dist/record.d.ts +1 -1
  17. package/dist/record.js +38 -30
  18. package/dist/review.js +18 -2
  19. package/dist/sync.js +13 -4
  20. package/dist/task.js +15 -2
  21. package/dist/task_evidence.d.ts +1 -1
  22. package/dist/task_evidence.js +85 -10
  23. package/dist/transition.d.ts +4 -3
  24. package/dist/transition.js +162 -29
  25. package/dist/types.d.ts +24 -1
  26. package/dist/types.js +1 -0
  27. package/dist/workflow_config.d.ts +24 -0
  28. package/dist/workflow_config.js +127 -0
  29. package/package.json +1 -1
  30. package/templates/workflow/AGENTS.md +1 -1
  31. package/templates/workflow/prompts/architect.md +1 -1
  32. package/templates/workflow/prompts/code-reviewer.md +2 -2
  33. package/templates/workflow/prompts/critic.md +4 -5
  34. package/templates/workflow/prompts/executor.md +2 -2
  35. package/templates/workflow/prompts/test-engineer.md +3 -4
  36. package/templates/workflow/prompts/verifier.md +1 -1
  37. package/templates/workflow/skills/superspec-apply/SKILL.md +13 -71
  38. package/templates/workflow/skills/superspec-explore/SKILL.md +5 -9
  39. package/templates/workflow/skills/superspec-propose/SKILL.md +26 -26
  40. package/templates/workflow/skills/superspec-review/SKILL.md +21 -50
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseTasksMd } from "./format.js";
4
4
  import { readEvents, sha256Text } from "./store.js";
5
+ import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
5
6
  export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt) {
6
7
  if (attempt.contract_mode === true) {
7
8
  return contractTaskEvidenceReadiness(projectRoot, change, attempt);
@@ -18,6 +19,9 @@ export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt)
18
19
  if (currentDigest !== attempt.task_structure_digest)
19
20
  missing.push("任务结构指纹");
20
21
  if (!taskInfo.tddRequired) {
22
+ if (taskInfo.noTddReason === GREEN_ONLY_NO_TDD_REASON) {
23
+ missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务必须使用执行依据模式`);
24
+ }
21
25
  if (!taskInfo.noTddReason)
22
26
  missing.push("no_tdd_reason");
23
27
  return missing.length === 0
@@ -48,7 +52,76 @@ export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt)
48
52
  : { ready: false, missing, reason: missing.join("、") };
49
53
  }
50
54
  function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
55
+ if (attempt.required_evidence) {
56
+ return effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt);
57
+ }
58
+ return legacyContractTaskEvidenceReadiness(projectRoot, change, attempt);
59
+ }
60
+ /** 新执行依据模式:只消费 task-start 冻结的有效证据计划。 */
61
+ function effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt) {
51
62
  const missing = [];
63
+ const required = attempt.required_evidence;
64
+ const declaredTests = required.test_ids;
65
+ if (declaredTests.length === 0) {
66
+ const evidence = attemptLevelEvidence(projectRoot, change, attempt.attempt_id, new Set(required.accepted_green_statuses));
67
+ if (required.red_required && !evidence.hasRed)
68
+ missing.push("RED 证据");
69
+ if (required.green_required && !evidence.hasGreen)
70
+ missing.push("GREEN 证据");
71
+ return missing.length === 0
72
+ ? { ready: true, missing: [] }
73
+ : { ready: false, missing, reason: missing.join("、") };
74
+ }
75
+ const perTest = new Map();
76
+ for (const testId of declaredTests) {
77
+ perTest.set(testId, { sawRed: false, sawGreen: false, paired: false });
78
+ }
79
+ for (const ev of readEvents(projectRoot, change)) {
80
+ if (ev.event_type !== "test_run_recorded")
81
+ continue;
82
+ const tr = ev.payload;
83
+ if (tr.attempt_id !== attempt.attempt_id)
84
+ continue;
85
+ if (typeof tr.test_id !== "string")
86
+ continue;
87
+ const state = perTest.get(tr.test_id);
88
+ if (!state)
89
+ continue;
90
+ if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
91
+ state.sawRed = true;
92
+ }
93
+ const isAcceptedGreen = typeof tr.semantic_status === "string" &&
94
+ required.accepted_green_statuses.includes(tr.semantic_status) &&
95
+ tr.exit_code === 0;
96
+ if (isAcceptedGreen) {
97
+ if (state.sawRed)
98
+ state.paired = true;
99
+ state.sawGreen = true;
100
+ }
101
+ }
102
+ if (required.green_required) {
103
+ for (const [testId, state] of perTest) {
104
+ if (!state.sawGreen)
105
+ missing.push(`${testId} GREEN 证据`);
106
+ }
107
+ }
108
+ if (required.red_required) {
109
+ const hasPairedTest = [...perTest.values()].some(state => state.paired);
110
+ if (!hasPairedTest)
111
+ missing.push("同一 TEST 的 RED/GREEN 配对");
112
+ }
113
+ return missing.length === 0
114
+ ? { ready: true, missing: [] }
115
+ : { ready: false, missing, reason: missing.join("、") };
116
+ }
117
+ /** 历史执行依据模式仍按当时写入 attempt 的标记回放。 */
118
+ function legacyContractTaskEvidenceReadiness(projectRoot, change, attempt) {
119
+ const missing = [];
120
+ const executionPolicy = attempt.execution_policy ?? "tdd";
121
+ const greenOnly = executionPolicy === "green_only" && attempt.no_tdd_reason === GREEN_ONLY_NO_TDD_REASON;
122
+ if (attempt.no_tdd_reason === GREEN_ONLY_NO_TDD_REASON && !greenOnly) {
123
+ missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务不适用于当前 TDD apply`);
124
+ }
52
125
  const tddRequired = attempt.tdd_required !== false;
53
126
  const characterization = attempt.tdd_required === false && attempt.no_tdd_reason === "characterization";
54
127
  const declaredTests = attempt.contract?.tests ?? [];
@@ -56,8 +129,11 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
56
129
  missing.push("no_tdd_reason");
57
130
  }
58
131
  if (declaredTests.length === 0) {
59
- if (tddRequired) {
60
- const hasAttemptEvidence = attemptLevelRedGreenEvidence(projectRoot, change, attempt.attempt_id);
132
+ if (greenOnly) {
133
+ missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务缺少声明 TEST`);
134
+ }
135
+ else if (tddRequired) {
136
+ const hasAttemptEvidence = attemptLevelEvidence(projectRoot, change, attempt.attempt_id, new Set(["expected_success"]));
61
137
  if (!hasAttemptEvidence.hasRed)
62
138
  missing.push("RED 证据");
63
139
  if (!hasAttemptEvidence.hasGreen)
@@ -75,16 +151,13 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
75
151
  if (ev.event_type !== "test_run_recorded")
76
152
  continue;
77
153
  const tr = ev.payload;
78
- if (tr.attempt_id !== attempt.attempt_id)
79
- continue;
80
- if (typeof tr.test_id !== "string")
154
+ if (tr.attempt_id !== attempt.attempt_id || typeof tr.test_id !== "string")
81
155
  continue;
82
156
  const state = perTest.get(tr.test_id);
83
157
  if (!state)
84
158
  continue;
85
- if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
159
+ if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0)
86
160
  state.sawRed = true;
87
- }
88
161
  const isExpectedSuccess = tr.semantic_status === "expected_success" && tr.exit_code === 0;
89
162
  const isCharacterizationPass = characterization && tr.semantic_status === "characterization_pass" && tr.exit_code === 0;
90
163
  if (isExpectedSuccess || isCharacterizationPass) {
@@ -97,7 +170,7 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
97
170
  if (!state.sawGreen)
98
171
  missing.push(`${testId} GREEN 证据`);
99
172
  }
100
- if (tddRequired) {
173
+ if (tddRequired && !greenOnly) {
101
174
  const hasPairedTest = [...perTest.values()].some(state => state.paired);
102
175
  if (!hasPairedTest)
103
176
  missing.push("同一 TEST 的 RED/GREEN 配对");
@@ -106,7 +179,7 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
106
179
  ? { ready: true, missing: [] }
107
180
  : { ready: false, missing, reason: missing.join("、") };
108
181
  }
109
- function attemptLevelRedGreenEvidence(projectRoot, change, attemptId) {
182
+ function attemptLevelEvidence(projectRoot, change, attemptId, acceptedGreenStatuses) {
110
183
  let hasRed = false;
111
184
  let hasGreen = false;
112
185
  for (const ev of readEvents(projectRoot, change)) {
@@ -118,7 +191,9 @@ function attemptLevelRedGreenEvidence(projectRoot, change, attemptId) {
118
191
  if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
119
192
  hasRed = true;
120
193
  }
121
- if (tr.semantic_status === "expected_success" && tr.exit_code === 0) {
194
+ if (typeof tr.semantic_status === "string" &&
195
+ acceptedGreenStatuses.has(tr.semantic_status) &&
196
+ tr.exit_code === 0) {
122
197
  hasGreen = true;
123
198
  }
124
199
  }
@@ -1,3 +1,4 @@
1
+ import { type ReviewRisk } from "./review.ts";
1
2
  import type { Snapshot, State, Job, TransitionResult } from "./types.ts";
2
3
  interface Decision {
3
4
  fromState: State;
@@ -31,16 +32,16 @@ export declare function commitTransition(projectRoot: string, change: string, ch
31
32
  decide: (snapshot: Snapshot) => Decision | SkipDecision | BlockedDecision;
32
33
  idempotencyInputs?: Record<string, unknown>;
33
34
  }): TransitionResult;
34
- export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
35
+ export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
35
36
  export declare function transitionInit(projectRoot: string, change: string, changeRoot: string): TransitionResult;
36
- export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
37
+ export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
37
38
  export declare function startApply(projectRoot: string, change: string, changeRoot: string): TransitionResult;
38
39
  export declare function taskStart(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
39
40
  export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string, opts?: {
40
41
  reviewFix?: string;
41
42
  reviewFinding?: string;
42
43
  }): TransitionResult;
43
- export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
44
+ export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
44
45
  export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
45
46
  export declare function taskComplete(projectRoot: string, change: string, changeRoot: string, taskId: string, inputContent?: string | null): TransitionResult;
46
47
  export {};
@@ -9,9 +9,10 @@ import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeF
9
9
  import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
10
10
  import { taskEvidenceReadiness } from "./task_evidence.js";
11
11
  import { adoptedContractForTask, findTaskInLines, isReviewFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
12
- import { applyRequirementModeForCurrentRound, blockingJobsForApplyDone, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planTransition, proposalDocsBaseline, } from "./phase_plan.js";
12
+ import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planTransition, discoveryDocsBaseline, proposalDocsBaseline, } from "./phase_plan.js";
13
13
  import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
14
- import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
14
+ import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
15
+ import { workflowRiskForProject } from "./workflow_config.js";
15
16
  let transitionSeq = 0;
16
17
  function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
17
18
  let jobSeq = 0;
@@ -81,6 +82,17 @@ function boundarySnapshotPayload(projectRoot) {
81
82
  ...(result.reason ? { boundary_snapshot_reason: result.reason } : {}),
82
83
  };
83
84
  }
85
+ function boundarySnapshotForTaskAttempt(events, attemptId) {
86
+ const start = events.findLast(event => event.event_type === "task_started" &&
87
+ event.payload.attempt_id === attemptId);
88
+ const boundary = start?.payload?.boundary_snapshot;
89
+ if (!boundary || typeof boundary !== "object" || Array.isArray(boundary))
90
+ return null;
91
+ const value = boundary;
92
+ if (!Array.isArray(value.dirty_files))
93
+ return null;
94
+ return value;
95
+ }
84
96
  function parseScopeNoteInput(inputContent) {
85
97
  if (inputContent == null)
86
98
  return { ok: true, value: null, digest: null };
@@ -130,9 +142,41 @@ function parseScopeNoteInput(inputContent) {
130
142
  },
131
143
  };
132
144
  }
133
- function validateTaskStartContract(changeRoot, taskId, tddRequired, parsedContract) {
134
- if (!parsedContract)
145
+ function validateTaskStartContract(changeRoot, taskId, parsedContract) {
146
+ if (parsedContract.errors.length > 0)
147
+ return parsedContract.errors.join(";");
148
+ if (!parsedContract.declaredFields.includes("tests"))
149
+ return `${taskId} 的执行依据缺少测试字段`;
150
+ if (!parsedContract.contract.design)
151
+ return `${taskId} 的执行依据缺少设计`;
152
+ if (parsedContract.contract.source.length === 0)
153
+ return `${taskId} 的执行依据缺少来源`;
154
+ if (!parsedContract.contract.acceptance)
155
+ return `${taskId} 的执行依据缺少验收目标`;
156
+ if (!parsedContract.contract.guard)
157
+ return `${taskId} 的执行依据缺少边界`;
158
+ if (parsedContract.contract.tests.length === 0)
135
159
  return null;
160
+ const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
161
+ if (!existsSync(testContractPath))
162
+ return "test-contract.md 不存在,无法校验执行依据测试引用";
163
+ const parsed = parseTestContractEntries(readFileSync(testContractPath, "utf8"));
164
+ if (!parsed.ok)
165
+ return parsed.message;
166
+ const known = new Set(parsed.entries.map(entry => entry.test_id));
167
+ const missing = parsedContract.contract.tests.filter(testId => !known.has(testId));
168
+ return missing.length > 0 ? `执行依据引用了不存在的 TEST ID:${missing.join(", ")}` : null;
169
+ }
170
+ /**
171
+ * v1 只回放旧 contract 约束:普通 TDD task 必须保有执行依据和 TEST 引用,
172
+ * 但不要求 v2 新增的五字段,避免阻断历史 documentation-only task。
173
+ */
174
+ function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsedContract) {
175
+ if (!parsedContract) {
176
+ return tddRequired && !isReviewFixTaskId(taskId)
177
+ ? `执行依据模式下,普通 TDD 任务 ${taskId} 缺少执行依据`
178
+ : null;
179
+ }
136
180
  if (parsedContract.errors.length > 0)
137
181
  return parsedContract.errors.join(";");
138
182
  if (tddRequired && !isReviewFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
@@ -150,6 +194,21 @@ function validateTaskStartContract(changeRoot, taskId, tddRequired, parsedContra
150
194
  const missing = parsedContract.contract.tests.filter(testId => !known.has(testId));
151
195
  return missing.length > 0 ? `执行依据引用了不存在的 TEST ID:${missing.join(", ")}` : null;
152
196
  }
197
+ /**
198
+ * 只在 task-start 编译新模式 task 的有效证据要求。Propose 只声明 TEST,
199
+ * RED 是否要求由这里读取已冻结的 execution_policy 决定,之后不再依赖 tasks.md。
200
+ */
201
+ function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationWithoutDeclaredTest) {
202
+ // REVIEW-FIX 没有计划阶段声明的 TEST,但仍必须登记一次真实回归验证。
203
+ // 是否需要 RED 始终由已冻结的 execution_policy 决定。
204
+ const requiresVerification = testIds.length > 0 || requiresVerificationWithoutDeclaredTest;
205
+ return {
206
+ test_ids: testIds,
207
+ red_required: executionPolicy === "tdd" && requiresVerification,
208
+ green_required: requiresVerification,
209
+ accepted_green_statuses: ["expected_success"],
210
+ };
211
+ }
153
212
  function hasRejectedReviewReadyVerifier(events) {
154
213
  const reviewReadyVerifierIds = new Set();
155
214
  for (const ev of events) {
@@ -235,7 +294,7 @@ function appendReviewFixTask(changeRoot, ref, finding) {
235
294
  const description = typeof finding.description === "string" && finding.description.trim()
236
295
  ? finding.description.trim().replace(/\s+/g, " ")
237
296
  : `修复代码审查问题 ${ref.findingId}`;
238
- const line = `- [ ] ${reviewFixTaskId(ref)} ${description} tdd_required:true ${marker}`;
297
+ const line = `- [ ] ${reviewFixTaskId(ref)} ${description} ${marker}`;
239
298
  const suffix = content.endsWith("\n") ? "" : "\n";
240
299
  writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
241
300
  return "created";
@@ -416,7 +475,7 @@ function evaluateFinalVerifierGate(input) {
416
475
  return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
417
476
  }
418
477
  function authorizePhaseAdvance(input) {
419
- const confirmation = phaseConfirmationForBoundary(input.projectRoot, input.events, input.snapshot, input.boundary);
478
+ const confirmation = phaseConfirmationForBoundary(input.projectRoot, input.events, input.snapshot, input.boundary, input.risk);
420
479
  const phaseDecision = confirmation
421
480
  ? latestAcceptedPhaseDecision(input.events, confirmation)
422
481
  : null;
@@ -540,7 +599,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
540
599
  });
541
600
  }
542
601
  // ===== propose-ready =====
543
- export function proposeReady(projectRoot, change, changeRoot, risk = "strict") {
602
+ export function proposeReady(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
544
603
  return commitTransition(projectRoot, change, changeRoot, {
545
604
  name: "propose-ready", idempotencyInputs: { risk },
546
605
  decide: (snapshot) => {
@@ -570,7 +629,7 @@ export function transitionInit(projectRoot, change, changeRoot) {
570
629
  });
571
630
  }
572
631
  // ===== explore =====
573
- export function transitionExplore(projectRoot, change, changeRoot, risk = "strict") {
632
+ export function transitionExplore(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
574
633
  return commitTransition(projectRoot, change, changeRoot, {
575
634
  name: "explore", idempotencyInputs: { phase: "explore", risk },
576
635
  decide: (snapshot) => {
@@ -599,7 +658,7 @@ export function startApply(projectRoot, change, changeRoot) {
599
658
  changeRoot,
600
659
  events,
601
660
  snapshot,
602
- mode: { kind: "risk", risk: "strict" },
661
+ mode: { kind: "risk", risk: workflowRiskForProject(projectRoot) },
603
662
  });
604
663
  return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
605
664
  },
@@ -620,6 +679,8 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
620
679
  if (taskLineIdx < 0)
621
680
  return { skip: true, message: `任务 ${taskId} 不存在` };
622
681
  const contractMode = applyRequirementModeForCurrentRound(events);
682
+ const executionRequirementVersion = executionRequirementVersionForCurrentRound(events);
683
+ const executionPolicy = executionPolicyForCurrentRound(events);
623
684
  if (contractMode && pendingTaskStatusForApply(changeRoot, events).completedByEvent.includes(taskId)) {
624
685
  return { skip: true, message: `任务 ${taskId} 已通过完成事件完成` };
625
686
  }
@@ -634,23 +695,38 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
634
695
  // 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
635
696
  // legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
636
697
  const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
637
- if (contractMode && taskInfo.tddRequired && !isReviewFixTaskId(taskId) && !adopted.parsed) {
638
- return { skip: true, message: `执行依据模式下,普通 TDD 任务 ${taskId} 缺少执行依据` };
698
+ const reviewFix = isReviewFixTaskId(taskId);
699
+ if (contractMode && executionRequirementVersion === 2 && !reviewFix && !adopted.parsed) {
700
+ return { skip: true, message: `执行依据模式下,任务 ${taskId} 缺少执行依据` };
701
+ }
702
+ if (contractMode && executionRequirementVersion === 2 && !reviewFix && adopted.parsed) {
703
+ const contractError = validateTaskStartContract(changeRoot, taskId, adopted.parsed);
704
+ if (contractError)
705
+ return { skip: true, message: contractError };
639
706
  }
640
- if (contractMode) {
641
- const contractError = validateTaskStartContract(changeRoot, taskId, taskInfo.tddRequired, adopted.parsed);
707
+ if (contractMode && executionRequirementVersion === 1) {
708
+ const contractError = validateLegacyTaskStartContract(changeRoot, taskId, taskInfo.tddRequired, adopted.parsed);
642
709
  if (contractError)
643
710
  return { skip: true, message: contractError };
644
711
  }
645
712
  const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
713
+ const effectivePolicy = executionPolicy;
714
+ const requiredEvidence = contractMode && executionRequirementVersion === 2
715
+ ? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], reviewFix)
716
+ : null;
646
717
  const attempt = {
647
718
  attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
648
719
  task_id: taskId, state: "active",
649
720
  task_structure_digest: structureDigest,
650
721
  contract_mode: contractMode,
651
722
  contract: adopted.contract,
652
- tdd_required: taskInfo.tddRequired,
653
- no_tdd_reason: taskInfo.noTddReason,
723
+ ...(requiredEvidence ? { required_evidence: requiredEvidence } : {
724
+ // 历史模式以及 v1 契约轮继续使用 task 行标记回放;v2 只消费快照。
725
+ tdd_required: taskInfo.tddRequired,
726
+ no_tdd_reason: taskInfo.noTddReason,
727
+ }),
728
+ // REVIEW-FIX 没有计划阶段测试契约,但有效证据要求仍由本轮冻结策略编译。
729
+ execution_policy: effectivePolicy,
654
730
  declared_write_scope: [], pre_edit_source_fingerprint: null,
655
731
  pre_edit_red_ref: null, executor_packet_digest: null,
656
732
  executor_result_ref: null, post_edit_green_ref: null,
@@ -667,7 +743,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
667
743
  details: {
668
744
  attempt_id: attempt.attempt_id,
669
745
  task_id: taskId,
746
+ execution_policy: effectivePolicy,
670
747
  contract: adopted.contract,
748
+ ...(requiredEvidence ? { required_evidence: requiredEvidence } : {}),
671
749
  // legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 REVIEW-FIX)
672
750
  ...(adopted.contract ? {} : { legacy_contract: !contractMode }),
673
751
  },
@@ -676,6 +754,42 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
676
754
  });
677
755
  }
678
756
  // ===== reopen =====
757
+ function abandonActiveTaskAttempts(snapshot, to, reason) {
758
+ return snapshot.active_task_attempts
759
+ .filter(attempt => attempt.state === "active")
760
+ .map(attempt => ({
761
+ type: "task_abandoned",
762
+ payload: {
763
+ attempt_id: attempt.attempt_id,
764
+ task_id: attempt.task_id,
765
+ reason: `reopen --to ${to}:${reason}`,
766
+ },
767
+ }));
768
+ }
769
+ function invalidateOpenJobs(snapshot, to, reason) {
770
+ return snapshot.open_jobs.map(job => ({
771
+ type: "job_invalidated",
772
+ payload: {
773
+ job_id: job.job_id,
774
+ role: job.role,
775
+ reason: `reopen --to ${to}:${reason}`,
776
+ },
777
+ }));
778
+ }
779
+ function planningReopenExtraEvents(snapshot, to, reason) {
780
+ return [
781
+ ...invalidateOpenJobs(snapshot, to, reason),
782
+ ...abandonActiveTaskAttempts(snapshot, to, reason),
783
+ ];
784
+ }
785
+ function canReopenToExplore(from) {
786
+ return from === "propose" || from === "propose_ready" || from === "apply" ||
787
+ from === "apply_done" || from === "review" || from === "accepted";
788
+ }
789
+ function canReopenToPropose(from) {
790
+ return from === "propose_ready" || from === "apply" || from === "apply_done" ||
791
+ from === "review" || from === "accepted";
792
+ }
679
793
  export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
680
794
  return commitTransition(projectRoot, change, changeRoot, {
681
795
  name: "reopen", idempotencyInputs: { to, reason, reviewFix: opts.reviewFix ?? "", reviewFinding: opts.reviewFinding ?? "" },
@@ -751,22 +865,36 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
751
865
  },
752
866
  };
753
867
  }
868
+ if (to === "explore") {
869
+ if (!canReopenToExplore(snapshot.state)) {
870
+ return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 explore` };
871
+ }
872
+ return {
873
+ fromState: snapshot.state,
874
+ toState: "explore",
875
+ outcome: "advanced",
876
+ reason: reason.trim(),
877
+ commitPayload: {
878
+ reopen_target: "explore",
879
+ reopen_source: snapshot.state,
880
+ baseline_docs: discoveryDocsBaseline(changeRoot),
881
+ },
882
+ extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
883
+ };
884
+ }
754
885
  if (to === "propose") {
755
- if (snapshot.state !== "accepted") {
756
- return {
757
- skip: true,
758
- message: `当前状态 ${snapshot.state},主动 reopen --to propose 只允许从 accepted 发起;apply_done 的代码审查问题请使用 --review-finding`,
759
- };
886
+ if (!canReopenToPropose(snapshot.state)) {
887
+ return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 propose` };
760
888
  }
761
- if (snapshot.open_jobs.length > 0) {
889
+ if (snapshot.state === "accepted" && snapshot.open_jobs.length > 0) {
762
890
  return {
763
891
  blocked: true,
764
- reason: `状态未推进;accepted 仍有 ${snapshot.open_jobs.length} 个待完成工作项`,
892
+ reason: `状态未推进;accepted 状态仍有 ${snapshot.open_jobs.length} 个待完成工作项`,
765
893
  jobs: snapshot.open_jobs,
766
894
  };
767
895
  }
768
- const acceptedBaseline = latestAcceptedProposalBaseline(events);
769
896
  const currentBaseline = proposalDocsBaseline(changeRoot);
897
+ const acceptedBaseline = snapshot.state === "accepted" ? latestAcceptedProposalBaseline(events) : null;
770
898
  // 旧版 accepted 事件的基线可能缺少后来纳入 Propose gate 的材料。保留其已冻结
771
899
  // 的摘要,并用 reopen 当刻的摘要补齐缺项,确保本轮之后对任一审查目标的修改都能被检测。
772
900
  const baselineNeedsBackfill = acceptedBaseline !== null && Object.keys(currentBaseline)
@@ -778,20 +906,21 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
778
906
  ]))
779
907
  : currentBaseline;
780
908
  return {
781
- fromState: "accepted",
909
+ fromState: snapshot.state,
782
910
  toState: "propose",
783
911
  outcome: "advanced",
784
912
  reason: reason.trim(),
785
913
  commitPayload: {
786
914
  reopen_target: "propose",
787
- reopen_source: "accepted",
915
+ reopen_source: snapshot.state,
788
916
  baseline_source: acceptedBaseline ? (baselineNeedsBackfill ? "accepted_backfill" : "accepted") : "reopen_fallback",
789
917
  baseline_docs: baselineDocs,
790
918
  },
919
+ extraEvents: planningReopenExtraEvents(snapshot, "propose", reason.trim()),
791
920
  };
792
921
  }
793
922
  if (to !== "apply")
794
- return { skip: true, message: `reopen 当前只支持 --to apply 或 --to propose,不支持 ${to}` };
923
+ return { skip: true, message: `reopen 当前只支持 --to explore、--to propose 或 --to apply,不支持 ${to}` };
795
924
  if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
796
925
  return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 apply` };
797
926
  }
@@ -808,7 +937,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
808
937
  });
809
938
  }
810
939
  // ===== review-ready =====
811
- export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
940
+ export function reviewReady(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
812
941
  return commitTransition(projectRoot, change, changeRoot, {
813
942
  name: "review-ready", idempotencyInputs: { phase: "review-ready", risk },
814
943
  decide: (snapshot) => {
@@ -863,6 +992,7 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
863
992
  events,
864
993
  snapshot,
865
994
  boundary: "apply_to_review",
995
+ risk: policy.review_risk,
866
996
  decision: codeReviewDecision,
867
997
  });
868
998
  }
@@ -897,7 +1027,7 @@ export function accept(projectRoot, change, changeRoot) {
897
1027
  changeRoot,
898
1028
  events,
899
1029
  snapshot,
900
- mode: { kind: "risk", risk: "strict" },
1030
+ mode: { kind: "risk", risk: workflowRiskForProject(projectRoot) },
901
1031
  });
902
1032
  return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
903
1033
  },
@@ -919,9 +1049,11 @@ export function taskComplete(projectRoot, change, changeRoot, taskId, inputConte
919
1049
  const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, attempt);
920
1050
  if (!readiness.ready)
921
1051
  return { skip: true, message: `任务 ${taskId} 无法完成:${readiness.reason}` };
1052
+ const taskStartBoundary = boundarySnapshotForTaskAttempt(readEvents(projectRoot, change), attempt.attempt_id);
922
1053
  const completedPayload = {
923
1054
  task_id: taskId,
924
1055
  attempt_id: attempt.attempt_id,
1056
+ execution_policy: attempt.execution_policy ?? "tdd",
925
1057
  ...boundarySnapshotPayload(projectRoot),
926
1058
  checkbox_update: { status: "pending" },
927
1059
  ...(scopeInput.value ? { scope_note: scopeInput.value } : {}),
@@ -930,7 +1062,8 @@ export function taskComplete(projectRoot, change, changeRoot, taskId, inputConte
930
1062
  fromState: "apply", toState: "apply", outcome: "advanced",
931
1063
  reason: `任务 ${taskId} 完成`,
932
1064
  extraEvents: [{ type: "task_completed", payload: completedPayload }],
933
- postCommit: (_pr, _ch, cr) => {
1065
+ postCommit: (pr, _ch, cr) => {
1066
+ completedPayload.java_staging = stageProductionJavaFilesSince(pr, taskStartBoundary);
934
1067
  const lines = readFileSync(join(cr, "tasks.md"), "utf8").split("\n");
935
1068
  const idx = findTaskLine(lines, taskId);
936
1069
  if (idx < 0) {
package/dist/types.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export type State = "init" | "explore" | "propose" | "propose_ready" | "apply" | "apply_done" | "review" | "accepted" | "archive" | "abandoned";
2
+ export type ExecutionPolicy = "tdd" | "green_only";
3
+ export declare const GREEN_ONLY_NO_TDD_REASON = "green-only";
2
4
  export declare const PHASE1_STATES: ReadonlySet<State>;
3
5
  export type Ref = {
4
6
  path: string;
@@ -35,9 +37,19 @@ export interface ExecutionContract {
35
37
  tests: string[];
36
38
  design: string | null;
37
39
  source: string[];
38
- reason: string | null;
40
+ acceptance: string | null;
39
41
  guard: string | null;
40
42
  }
43
+ /**
44
+ * task-start 将计划中的声明和本轮已冻结策略编译出的有效证据要求。
45
+ * 这是 attempt 的不可变快照;Apply 与 task-complete 只消费它,不再解释任务行的 TDD 标记。
46
+ */
47
+ export interface EffectiveEvidencePlan {
48
+ test_ids: string[];
49
+ red_required: boolean;
50
+ green_required: boolean;
51
+ accepted_green_statuses: Array<"expected_success" | "characterization_pass">;
52
+ }
41
53
  export interface DirtyFileFingerprint {
42
54
  path: string;
43
55
  status: "added" | "modified" | "deleted";
@@ -69,9 +81,11 @@ export interface CodeStateCheck {
69
81
  export interface TaskExecutionIndexEntry {
70
82
  task_id: string;
71
83
  attempt_id: string;
84
+ execution_policy: ExecutionPolicy;
72
85
  changed_paths: string[] | null;
73
86
  changed_paths_partial_reason?: string;
74
87
  contract: ExecutionContract | null;
88
+ required_evidence: EffectiveEvidencePlan | null;
75
89
  declared_tests: string[];
76
90
  scope_note: Record<string, unknown> | null;
77
91
  test_evidence: Record<string, unknown>[];
@@ -169,8 +183,14 @@ export interface TransitionCommitPayload {
169
183
  material_digest: string;
170
184
  scope: string;
171
185
  decision_event_id: string;
186
+ review_risk?: "minimal" | "normal" | "strict";
172
187
  };
173
188
  accepted_baseline_docs?: Record<string, string>;
189
+ /** Propose-ready / start-apply 写入的本轮 workflow mode,后续阶段只读该快照。 */
190
+ workflow_mode?: "minimal" | "normal" | "strict";
191
+ /** v2 起所有普通任务必须有五字段执行依据;缺失表示旧 change,沿用旧规则回放。 */
192
+ execution_requirement_version?: 2;
193
+ execution_policy?: ExecutionPolicy;
174
194
  }
175
195
  export interface Snapshot {
176
196
  change_id: string;
@@ -195,8 +215,11 @@ export interface TaskAttempt {
195
215
  task_structure_digest: string;
196
216
  contract?: ExecutionContract | null;
197
217
  contract_mode?: boolean;
218
+ /** task-start 编译出的有效执行要求;缺失表示历史 attempt,按旧字段回放。 */
219
+ required_evidence?: EffectiveEvidencePlan | null;
198
220
  tdd_required?: boolean;
199
221
  no_tdd_reason?: string | null;
222
+ execution_policy?: ExecutionPolicy;
200
223
  declared_write_scope: string[];
201
224
  pre_edit_source_fingerprint: string | null;
202
225
  pre_edit_red_ref: string | null;
package/dist/types.js CHANGED
@@ -1,3 +1,4 @@
1
1
  // SuperSpec 流程引擎 — 核心类型定义
2
+ export const GREEN_ONLY_NO_TDD_REASON = "green-only";
2
3
  // Phase 1 只实现前 4 个
3
4
  export const PHASE1_STATES = new Set(["init", "explore", "propose", "propose_ready"]);
@@ -0,0 +1,24 @@
1
+ import type { ReviewRisk } from "./review.ts";
2
+ import type { Event, State } from "./types.ts";
3
+ export declare const WORKFLOW_CONFIG_PATH = ".superspec/config.json";
4
+ /** 新安装项目写入配置时采用的默认档位。 */
5
+ export declare const DEFAULT_WORKFLOW_RISK: ReviewRisk;
6
+ export declare class WorkflowConfigError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ /**
10
+ * Propose-ready 是一个 planning round 的冻结点。配置只影响尚未冻结的计划;
11
+ * 已就绪计划必须沿用当时的 mode,直到 reopen 回到 propose 后创建新 round。
12
+ */
13
+ export declare function workflowRiskForProposeRound(events: Event[], fallback: ReviewRisk): ReviewRisk;
14
+ /** 是否已经由当前引擎在 propose-ready 冻结 mode;缺失时是历史 planning round。 */
15
+ export declare function hasFrozenWorkflowModeForProposeRound(events: Event[]): boolean;
16
+ /** Apply round 从 start-apply 起冻结;兼容首批事件中只写 review_policy 的格式。 */
17
+ export declare function workflowRiskForApplyRound(events: Event[], fallback: ReviewRisk): ReviewRisk;
18
+ /** 供阶段确认和登记共用,避免任何调用者从 JSON/CLI 注入本轮 mode。 */
19
+ export declare function workflowRiskForState(events: Event[], state: State, fallback: ReviewRisk): ReviewRisk;
20
+ /**
21
+ * 读取项目默认模式。新安装项目由配置提供 normal;缺少配置的旧项目保留 strict。
22
+ * 配置格式:{ "workflow": { "mode": "normal" } }
23
+ */
24
+ export declare function workflowRiskForProject(projectRoot: string): ReviewRisk;