@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
@@ -8,11 +8,14 @@ import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRol
8
8
  import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
9
9
  import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
10
10
  import { taskEvidenceReadiness } from "./task_evidence.js";
11
+ import { workflowRiskForProposeRound, workflowRiskForState } from "./workflow_config.js";
11
12
  function requiredJobs(state, jobs, reason) {
12
13
  return { kind: "required_jobs", state, jobs, reason };
13
14
  }
14
15
  function phaseConfirmationStep(context, boundary, reason) {
15
- const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, boundary, context.mode.risk);
16
+ // 进入 propose_ready / apply 后,mode 来自本轮快照而非当前配置。
17
+ const risk = workflowRiskForState(context.events, context.snapshot.state, context.mode.risk);
18
+ const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, boundary, risk);
16
19
  if (!confirmation || isPhaseAdvanceAuthorized(context.events, confirmation))
17
20
  return null;
18
21
  return {
@@ -85,14 +88,26 @@ function validateTasksPlan(changeRoot) {
85
88
  return "tasks.md 内容不像任务计划文档";
86
89
  return null;
87
90
  }
88
- function validateExecutionRequirementPlan(changeRoot) {
91
+ export function executionPolicyForRisk(risk) {
92
+ return risk === "strict" ? "tdd" : "green_only";
93
+ }
94
+ export function executionPolicyForCurrentRound(events) {
95
+ const index = latestStartApplyIndex(events);
96
+ if (index < 0)
97
+ return "tdd";
98
+ const payload = events[index].payload;
99
+ return payload.execution_policy === "green_only" || payload.execution_policy === "tdd"
100
+ ? payload.execution_policy
101
+ : "tdd";
102
+ }
103
+ function validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion = 2) {
89
104
  const tasksPath = join(changeRoot, "tasks.md");
90
105
  if (!existsSync(tasksPath))
91
106
  return { ok: true, mode: false };
92
107
  const tasksContent = readFileSync(tasksPath, "utf8");
93
108
  const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
94
109
  const testContractContent = existsSync(testContractPath) ? readFileSync(testContractPath, "utf8") : null;
95
- const validation = validateExecutionRequirements(tasksContent, testContractContent);
110
+ const validation = validateExecutionRequirements(tasksContent, testContractContent, executionPolicy, executionRequirementVersion);
96
111
  return validation.ok
97
112
  ? { ok: true, mode: validation.mode }
98
113
  : { ok: false, mode: validation.mode, message: validation.errors.join(";") };
@@ -116,6 +131,15 @@ export function proposalDocsBaseline(changeRoot) {
116
131
  }
117
132
  return baseline;
118
133
  }
134
+ export function discoveryDocsBaseline(changeRoot) {
135
+ // 与 Explore gate 使用同一组审查目标。回退到 explore 后,至少要更新一项
136
+ // discovery 材料,才允许重新进入 propose,避免把一次纯状态回退误当作新探索轮次。
137
+ const baseline = {};
138
+ for (const doc of EXPLORE_DISCOVERY_REVIEW_GATE.reviewTargets) {
139
+ baseline[doc] = docRef(changeRoot, doc).sha;
140
+ }
141
+ return baseline;
142
+ }
119
143
  function isDigestMap(value) {
120
144
  if (!value || typeof value !== "object" || Array.isArray(value))
121
145
  return false;
@@ -152,10 +176,28 @@ export function latestReopenProposeBaseline(events) {
152
176
  }
153
177
  return null;
154
178
  }
179
+ export function latestReopenExploreBaseline(events) {
180
+ for (let i = events.length - 1; i >= 0; i--) {
181
+ const ev = events[i];
182
+ if (ev.event_type !== "transition_commit")
183
+ continue;
184
+ const payload = ev.payload;
185
+ if (payload.transition !== "reopen" || payload.reopen_target !== "explore")
186
+ continue;
187
+ if (!payload.baseline_docs || typeof payload.baseline_docs !== "object" || Array.isArray(payload.baseline_docs))
188
+ return null;
189
+ return payload.baseline_docs;
190
+ }
191
+ return null;
192
+ }
155
193
  export function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
156
194
  const current = proposalDocsBaseline(changeRoot);
157
195
  return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
158
196
  }
197
+ export function discoveryDocsChangedSinceBaseline(changeRoot, baseline) {
198
+ const current = discoveryDocsBaseline(changeRoot);
199
+ return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
200
+ }
159
201
  export function pendingTaskIds(changeRoot) {
160
202
  const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
161
203
  return pendingTasksInContent(tasksContent).map(task => task.taskId);
@@ -171,6 +213,27 @@ function latestStartApplyIndex(events) {
171
213
  }
172
214
  return -1;
173
215
  }
216
+ function executionRequirementVersionFromPayload(payload) {
217
+ return payload.execution_requirement_version === 2 ? 2 : 1;
218
+ }
219
+ /** 当前 Apply round 的规则版本;缺失版本的历史 event 保持 v1 回放。 */
220
+ export function executionRequirementVersionForCurrentRound(events) {
221
+ const index = latestStartApplyIndex(events);
222
+ return index < 0 ? 1 : executionRequirementVersionFromPayload(events[index].payload);
223
+ }
224
+ /** Propose-ready 的规则版本决定随后 start-apply 是否采用新的全任务声明要求。 */
225
+ function executionRequirementVersionForProposeRound(events) {
226
+ for (let i = events.length - 1; i >= 0; i--) {
227
+ const event = events[i];
228
+ if (event.event_type !== "transition_commit")
229
+ continue;
230
+ const payload = event.payload;
231
+ if (payload.transition === "propose-ready" && payload.to_state === "propose_ready") {
232
+ return executionRequirementVersionFromPayload(event.payload);
233
+ }
234
+ }
235
+ return 1;
236
+ }
174
237
  export function applyRequirementModeForCurrentRound(events) {
175
238
  const index = latestStartApplyIndex(events);
176
239
  if (index < 0)
@@ -238,14 +301,13 @@ export function pendingTaskStatusForApply(changeRoot, events) {
238
301
  export function formatPendingTaskMessage(ids, action) {
239
302
  return `尚有未完成任务:${ids.join(", ")};${action}`;
240
303
  }
241
- function nextArgv(change, risk) {
304
+ function nextArgv(change, _risk) {
242
305
  return [
243
306
  "superspec",
244
307
  "transition",
245
308
  "next",
246
309
  "--change",
247
310
  change,
248
- ...(risk === "strict" ? [] : ["--risk", risk]),
249
311
  ];
250
312
  }
251
313
  function acceptedMaterialFollowup(change, risk, planDocsChangedSinceAccept) {
@@ -281,6 +343,15 @@ export function planNextStep(context) {
281
343
  }
282
344
  return { kind: "run_transition", state: "init", transition: "explore", reason: "初始化完成,开始探索" };
283
345
  case "explore": {
346
+ const reopenBaseline = latestReopenExploreBaseline(events);
347
+ if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
348
+ return {
349
+ kind: "run_transition",
350
+ state: "explore",
351
+ transition: "explore",
352
+ reason: "回到 explore 后至少一个 discovery 材料必须变化",
353
+ };
354
+ }
284
355
  const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
285
356
  if (exploreReviewJobs.length > 0) {
286
357
  return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
@@ -352,6 +423,16 @@ export function planNextStep(context) {
352
423
  if (confirmation)
353
424
  return confirmation;
354
425
  }
426
+ // 尚未获得用户确认时,按本次 next 的候选风险预检失败不能退回默认 strict
427
+ // 的 start-apply 命令;否则 normal/minimal 的策略不匹配会被静默改写为 strict。
428
+ if (startApplyPlan.kind === "skip") {
429
+ const ask = {
430
+ question: `${startApplyPlan.message}。请更新计划材料后重新执行 next。`,
431
+ allowed_answers: ["计划已更新"],
432
+ scope: "propose_apply_preflight",
433
+ };
434
+ return { kind: "ask_user", state: "propose_ready", ask, reason: startApplyPlan.message };
435
+ }
355
436
  return { kind: "run_transition", state: "propose_ready", transition: "start-apply", reason: "计划就绪,开始执行" };
356
437
  }
357
438
  case "apply":
@@ -628,6 +709,10 @@ function planExploreTransition(context) {
628
709
  if (snapshot.state !== "explore") {
629
710
  return { kind: "skip", message: `当前状态 ${snapshot.state},explore 不适用` };
630
711
  }
712
+ const reopenBaseline = latestReopenExploreBaseline(events);
713
+ if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
714
+ return { kind: "skip", message: "回到 explore 后至少一个 discovery 材料必须变化" };
715
+ }
631
716
  const discoveryCheck = validateDiscovery(changeRoot);
632
717
  if (!discoveryCheck.ok)
633
718
  return { kind: "skip", message: discoveryCheck.message };
@@ -656,7 +741,7 @@ function planProposeReadyTransition(context) {
656
741
  const tasksPlanError = validateTasksPlan(changeRoot);
657
742
  if (tasksPlanError)
658
743
  return { kind: "skip", message: tasksPlanError };
659
- const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
744
+ const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicyForRisk(risk), 2);
660
745
  if (!executionRequirementPlan.ok)
661
746
  return { kind: "skip", message: executionRequirementPlan.message };
662
747
  const openQuestions = collectProposeOpenQuestions(changeRoot);
@@ -676,8 +761,17 @@ function planProposeReadyTransition(context) {
676
761
  fromState: "propose",
677
762
  toState: "propose_ready",
678
763
  reason: `risk=${risk},所有需求已满足`,
764
+ payload: {
765
+ workflow_mode: risk,
766
+ execution_requirement_version: 2,
767
+ },
679
768
  };
680
769
  }
770
+ function acceptedProposeToApplyConfirmation(context, risk) {
771
+ const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, "propose_to_apply", risk);
772
+ const decision = confirmation ? latestAcceptedPhaseDecision(context.events, confirmation) : null;
773
+ return confirmation && decision?.decision === "advance" ? { confirmation, decision } : null;
774
+ }
681
775
  function planStartApplyTransition(context, enforceConfirmation = true) {
682
776
  const { changeRoot, events, projectRoot, snapshot } = context;
683
777
  if (snapshot.state !== "propose_ready")
@@ -687,23 +781,31 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
687
781
  // 按基线实际键名提示:升级前留下的旧基线可能不含 specs/,静态清单会误导
688
782
  return { kind: "skip", message: `回到 propose 后至少一个计划文档必须变化(基线绑定:${Object.keys(reopenBaseline).join("、")})` };
689
783
  }
690
- const reviewedRoles = historicalProposeReadyRoles(events);
691
- if (reviewedRoles.length > 0) {
692
- const gatePlan = reviewGatePlan(snapshot, events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, reviewedRoles);
693
- if (gatePlan) {
694
- return {
695
- ...gatePlan,
696
- reason: `进入执行阶段前需要重新完成计划文档审查:${gatePlan.reason}`,
697
- };
698
- }
784
+ const risk = workflowRiskForProposeRound(events, context.mode.risk);
785
+ const executionRequirementVersion = executionRequirementVersionForProposeRound(events);
786
+ const requiredRoles = executionRequirementVersion === 2
787
+ ? PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk)
788
+ : historicalProposeReadyRoles(events);
789
+ const gatePlan = reviewGatePlan(snapshot, events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
790
+ if (gatePlan) {
791
+ return {
792
+ ...gatePlan,
793
+ reason: `进入执行阶段前需要重新完成计划文档审查:${gatePlan.reason}`,
794
+ };
699
795
  }
700
- const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
796
+ const acceptedConfirmation = enforceConfirmation
797
+ ? acceptedProposeToApplyConfirmation(context, risk)
798
+ : null;
799
+ const executionPolicy = executionPolicyForRisk(risk);
800
+ const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion);
701
801
  if (!executionRequirementPlan.ok)
702
802
  return { kind: "skip", message: executionRequirementPlan.message };
703
- const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply");
704
- const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
705
- if (enforceConfirmation && (!confirmation || decision?.decision !== "advance")) {
706
- return { kind: "skip", message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Propose 阶段确认范围" };
803
+ if (enforceConfirmation && !acceptedConfirmation) {
804
+ const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply", risk);
805
+ return {
806
+ kind: "skip",
807
+ message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Propose 阶段确认范围",
808
+ };
707
809
  }
708
810
  const gitHead = currentGitHead(projectRoot);
709
811
  return {
@@ -715,7 +817,14 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
715
817
  apply_start_head: gitHead.head,
716
818
  apply_start_head_reason: gitHead.reason,
717
819
  apply_contract_mode: executionRequirementPlan.mode,
718
- ...(confirmation && decision?.decision === "advance" ? phaseConfirmationCommitPayload(confirmation, decision) : {}),
820
+ ...(executionRequirementVersion === 2 ? { execution_requirement_version: 2 } : {}),
821
+ execution_policy: executionPolicy,
822
+ workflow_mode: risk,
823
+ review_policy: {
824
+ review_risk: risk,
825
+ requires_verifier: risk !== "minimal",
826
+ },
827
+ ...(acceptedConfirmation ? phaseConfirmationCommitPayload(acceptedConfirmation.confirmation, acceptedConfirmation.decision) : {}),
719
828
  },
720
829
  };
721
830
  }
package/dist/record.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare function recordJobSubmitContent(projectRoot: string, change: stri
7
7
  export declare function recordUserDecision(projectRoot: string, change: string, inputFile: string): RecordResult;
8
8
  /** record user-decision:从 JSON 内容登记用户决策 */
9
9
  export declare function recordUserDecisionContent(projectRoot: string, change: string, content: string): RecordResult;
10
- /** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
10
+ /** jobs list(job transition_commit.new_jobs 提取;reopen 可用 job_invalidated 关闭未完成工作项) */
11
11
  export declare function jobsList(projectRoot: string, change: string): {
12
12
  open: Job[];
13
13
  accepted: Job[];
package/dist/record.js CHANGED
@@ -4,7 +4,7 @@ import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha256Text, withLock, appendRawRecord, } from "./store.js";
5
5
  import { rebuildSnapshot } from "./sync.js";
6
6
  import { changeRoot as openspecChangeRoot } from "./openspec.js";
7
- import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, } from "./phase_confirmation.js";
7
+ import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, workflowRiskForPhaseConfirmation, } from "./phase_confirmation.js";
8
8
  import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
9
9
  import { invalidReasonForSubmittedReport } from "./job_validity.js";
10
10
  import { jobSubmitArgv } from "./job_action.js";
@@ -346,6 +346,10 @@ function jobTerminalState(events, jobId) {
346
346
  }
347
347
  return null;
348
348
  }
349
+ function jobInvalidated(events, jobId) {
350
+ return events.some(ev => ev.event_type === "job_invalidated" &&
351
+ ev.payload.job_id === jobId);
352
+ }
349
353
  function terminalJobSubmitResult(events, jobId, terminal, reportDigest) {
350
354
  const existing = events.find(e => (e.event_type === "job_accepted" || e.event_type === "job_rejected")
351
355
  && e.payload.job_id === jobId
@@ -547,6 +551,9 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
547
551
  if (!job) {
548
552
  return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
549
553
  }
554
+ if (jobInvalidated(events, jobId)) {
555
+ return { accepted: false, message: `工作项 ${jobId} 已因回退到更早阶段失效,不接受新报告。` };
556
+ }
550
557
  const terminal = jobTerminalState(events, jobId);
551
558
  if (terminal) {
552
559
  const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
@@ -584,6 +591,9 @@ export function recordJobSubmitContent(projectRoot, change, changeRoot, jobId, r
584
591
  if (!job) {
585
592
  return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
586
593
  }
594
+ if (jobInvalidated(events, jobId)) {
595
+ return { accepted: false, message: `工作项 ${jobId} 已因回退到更早阶段失效,不接受新报告。` };
596
+ }
587
597
  const reportDigest = sha256Text(reportContent);
588
598
  const terminal = jobTerminalState(events, jobId);
589
599
  if (terminal) {
@@ -639,14 +649,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
639
649
  });
640
650
  const changeRoot = openspecChangeRoot(projectRoot, change);
641
651
  const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
642
- const decisionRisk = decision.review_risk === undefined
643
- ? "strict"
644
- : decision.review_risk === "minimal" || decision.review_risk === "normal" || decision.review_risk === "strict"
645
- ? decision.review_risk
646
- : null;
647
- const current = decisionRisk
648
- ? phaseConfirmationForCurrentState(projectRoot, events, snapshot, decisionRisk)
649
- : null;
652
+ // mode 是状态机从配置/round 快照推导的输入,不接受 user-decision JSON 注入。
653
+ const decisionRisk = workflowRiskForPhaseConfirmation(projectRoot, events, snapshot);
654
+ const current = phaseConfirmationForCurrentState(projectRoot, events, snapshot, decisionRisk);
650
655
  if (existingAccepted &&
651
656
  current?.scope === decision.scope &&
652
657
  latestAcceptedForScope?.event_id === existing.event_id) {
@@ -657,17 +662,15 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
657
662
  };
658
663
  }
659
664
  const action = current ? phaseActionForAnswer(current, decision.answer) : null;
660
- const rejectionReason = !decisionRisk
661
- ? "invalid_phase_confirmation_risk"
662
- : !current
663
- ? "phase_confirmation_not_pending"
664
- : decision.scope !== current.scope
665
- ? "stale_phase_confirmation_scope"
666
- : !action
667
- ? "invalid_phase_confirmation_answer"
668
- : action.reason === "required" && !nonEmptyString(decision.reason)
669
- ? "missing_phase_confirmation_reason"
670
- : null;
665
+ const rejectionReason = !current
666
+ ? "phase_confirmation_not_pending"
667
+ : decision.scope !== current.scope
668
+ ? "stale_phase_confirmation_scope"
669
+ : !action
670
+ ? "invalid_phase_confirmation_answer"
671
+ : action.reason === "required" && !nonEmptyString(decision.reason)
672
+ ? "missing_phase_confirmation_reason"
673
+ : null;
671
674
  if (rejectionReason) {
672
675
  if (existing &&
673
676
  existing.payload.accepted === false &&
@@ -687,11 +690,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
687
690
  }));
688
691
  const message = rejectionReason === "invalid_phase_confirmation_answer" && current
689
692
  ? `阶段确认答复必须精确为:${current.ask.allowed_answers.join("、")}`
690
- : rejectionReason === "invalid_phase_confirmation_risk"
691
- ? "阶段确认 review_risk 必须是 minimal、normal 或 strict"
692
- : rejectionReason === "missing_phase_confirmation_reason" && action
693
- ? action.reason_prompt ?? "当前选择必须写明原因"
694
- : "阶段确认已失效或当前没有待确认的阶段边界,请重新执行 next";
693
+ : rejectionReason === "missing_phase_confirmation_reason" && action
694
+ ? action.reason_prompt ?? "当前选择必须写明原因"
695
+ : "阶段确认已失效或当前没有待确认的阶段边界,请重新执行 next";
695
696
  return { event_type: "user_decision_recorded", accepted: false, message };
696
697
  }
697
698
  phaseConfirmation = current;
@@ -857,7 +858,7 @@ export function recordUserDecisionContent(projectRoot, change, content) {
857
858
  return recordUserDecisionLoaded(projectRoot, change, events, content, sha256Text(content));
858
859
  });
859
860
  }
860
- /** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
861
+ /** jobs list(job transition_commit.new_jobs 提取;reopen 可用 job_invalidated 关闭未完成工作项) */
861
862
  export function jobsList(projectRoot, change) {
862
863
  const events = readEvents(projectRoot, change);
863
864
  const open = [];
@@ -888,6 +889,12 @@ export function jobsList(projectRoot, change) {
888
889
  open.splice(idx, 1)[0];
889
890
  rejected.push({ job_id, role });
890
891
  }
892
+ else if (ev.event_type === "job_invalidated") {
893
+ const { job_id } = ev.payload;
894
+ const idx = open.findIndex(j => j.job_id === job_id);
895
+ if (idx >= 0)
896
+ open.splice(idx, 1);
897
+ }
891
898
  }
892
899
  return { open, accepted, rejected };
893
900
  }
@@ -898,8 +905,9 @@ function packetFieldDescriptions() {
898
905
  boundFiles: "本工作项绑定的文件清单;审查报告必须说明这些文件是否都看过。",
899
906
  review_scope: "报告中的审查覆盖范围;普通 reviewer/verifier 用 checked_paths 回执全部绑定文件,code-reviewer 还需按专用协议说明未检查项。",
900
907
  code_review_scope: "代码审查范围:从已审基点到当前 HEAD 的提交改动、工作区改动和未跟踪代码文件。",
901
- task_execution_index: "按任务汇总的执行证据:每个任务(task)的执行依据、声明测试、测试证据和改动文件。",
902
- contract: "任务启动时的执行依据快照:tests/design/source/reason/guard 分别对应 测试/设计/来源/原因/边界;null 表示历史任务没有执行依据。",
908
+ task_execution_index: "按任务汇总的执行证据:每个任务(task)的执行依据、有效证据要求、声明测试、测试证据和改动文件。",
909
+ contract: "任务启动时的执行依据快照:tests/design/source/acceptance/guard 分别对应 测试/设计/来源/验收/边界;null 表示历史任务没有执行依据。",
910
+ required_evidence: "task-start 结合冻结策略编译并写入 attempt 的有效证据要求:test_ids、red_required、green_required 和允许的 GREEN 语义状态;null 表示历史任务按旧记录回放。",
903
911
  changed_paths: "与某个任务(task)或代码状态检查相关的改动文件。",
904
912
  changed_paths_partial_reason: "该任务(task)的提交段 diff 失败原因;存在时 changed_paths 只包含工作区对比结果,归属可能不完整。",
905
913
  unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
@@ -974,10 +982,10 @@ export function jobsPacket(projectRoot, change, jobId) {
974
982
  ? `最小格式:{"role":"code-reviewer","verdict":"pass|fail","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":${JSON.stringify(job.boundFiles.map(f => f.path))},"checked_docs":${JSON.stringify(REVIEW_DOC_PATHS)},"unchecked":[]},"findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}};审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"}。`
975
983
  + `报告结论为 fail 时,问题列表(findings)至少包含一个可处理、可追溯的阻塞问题,字段为 {"id":"<stable-id>","blocking":true,"type":"implementation|spec|mixed","description":"<what>","evidence":"<why>","source_refs":["<path:line>"],"impact":"<impact>","suggested_action":"apply|propose"}。问题类型(type)中 implementation 表示纯代码实现问题,spec 表示方案/需求文档问题,mixed 表示需要使用者判断的混合问题。`
976
984
  + (packetContext?.task_execution_index
977
- ? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
985
+ ? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
978
986
  : "")
979
987
  : job.role === "verifier"
980
- ? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对代码审查问题闭环:实现修复任务必须带审查修复引用(review_fix_of:<job_id>#<problem_id>),方案/混合问题必须有用户决策或后续修复证据。核对 RED/GREEN:证据须在同一已完成任务的任务尝试 ID(task_completed.attempt_id)下闭环——普通 TDD 任务至少一个同 TEST RED(expected_failure)后 GREEN(expected_success)配对且每个声明 TEST 都有 GREEN;特征化任务(no_tdd_reason:characterization)可用 characterization_pass 作为通过证据,不要求 RED;测试运行证据应包含测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)、语义状态(semantic_status);审查修复的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
988
+ ? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对代码审查问题闭环:实现修复任务必须带审查修复引用(review_fix_of:<job_id>#<problem_id>),方案/混合问题必须有用户决策或后续修复证据。按 task_execution_index required_evidence 核对测试证据:red_required 时需要同一 TEST RED(expected_failure)后 GREEN;green_required 时每个声明 TEST 都需要允许的 GREEN 语义状态;测试运行证据应包含测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)、语义状态(semantic_status)。审查修复的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
981
989
  (packetContext?.code_state_check
982
990
  ? `本工作项带代码状态检查(code_state_check):head_matches 为 false 或 changed_paths 非空表示代码审查后代码又发生变化,须在报告中列出差异并交主流程与用户裁决,不自行判定无害,也不据此自动否定已接受的代码审查。`
983
991
  : "")
package/dist/review.js CHANGED
@@ -42,11 +42,27 @@ function isReviewPolicy(value) {
42
42
  typeof obj.requires_verifier === "boolean");
43
43
  }
44
44
  export function readReviewPolicyFromEvents(events) {
45
- for (const ev of events) {
45
+ // 新格式在 start-apply 写入,并且只能读取最新 Apply round 的策略。
46
+ // 回退到该 round 内的 review-ready,是为了回放升级前的历史 event。
47
+ let latestStartApplyIndex = -1;
48
+ for (let i = events.length - 1; i >= 0; i--) {
49
+ const event = events[i];
50
+ if (event.event_type !== "transition_commit")
51
+ continue;
52
+ const payload = event.payload;
53
+ if (payload.transition === "start-apply" && payload.to_state === "apply") {
54
+ latestStartApplyIndex = i;
55
+ break;
56
+ }
57
+ }
58
+ if (latestStartApplyIndex < 0)
59
+ return null;
60
+ for (let i = events.length - 1; i >= latestStartApplyIndex; i--) {
61
+ const ev = events[i];
46
62
  if (ev.event_type !== "transition_commit")
47
63
  continue;
48
64
  const payload = ev.payload;
49
- if (payload.transition !== "review-ready")
65
+ if (payload.transition !== "start-apply" && payload.transition !== "review-ready")
50
66
  continue;
51
67
  if (isReviewPolicy(payload.review_policy))
52
68
  return payload.review_policy;
package/dist/sync.js CHANGED
@@ -34,7 +34,8 @@ function replayEvents(events) {
34
34
  }
35
35
  break;
36
36
  }
37
- // H4 修复:删除 job_requested / job_invalidated 死分支——job 只通过 transition_commit 的 new_jobs payload 创建,staleness 只在 sync 内存计算(不写事件)
37
+ // job 只通过 transition_commit 的 new_jobs payload 创建。reopen 到更早阶段时,
38
+ // 已绑定旧材料的待完成工作项会被 job_invalidated 关闭,避免阻塞新一轮 gate。
38
39
  case "job_accepted": {
39
40
  const { job_id } = ev.payload;
40
41
  const idx = openJobs.findIndex(j => j.job_id === job_id);
@@ -53,7 +54,13 @@ function replayEvents(events) {
53
54
  }
54
55
  break;
55
56
  }
56
- // H4:job_invalidated 事件从不发射(删除死分支)
57
+ case "job_invalidated": {
58
+ const { job_id } = ev.payload;
59
+ const idx = openJobs.findIndex(j => j.job_id === job_id);
60
+ if (idx >= 0)
61
+ openJobs.splice(idx, 1);
62
+ break;
63
+ }
57
64
  case "task_started": {
58
65
  const attempt = ev.payload;
59
66
  activeAttempts.push(attempt);
@@ -71,8 +78,10 @@ function replayEvents(events) {
71
78
  case "task_abandoned": {
72
79
  const { attempt_id } = ev.payload;
73
80
  const idx = activeAttempts.findIndex(a => a.attempt_id === attempt_id);
74
- if (idx >= 0)
75
- activeAttempts.splice(idx, 1);
81
+ if (idx >= 0) {
82
+ const [attempt] = activeAttempts.splice(idx, 1);
83
+ taskStatuses[attempt.task_id] = "todo";
84
+ }
76
85
  break;
77
86
  }
78
87
  }
package/dist/task.js CHANGED
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord, readEvents } from "./store.js";
5
5
  import { tasksStructureDigest as formatDigest } from "./format.js";
6
6
  import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
7
+ import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
7
8
  /** tasks.md 结构指纹(委托给 format.ts 统一实现) */
8
9
  export function tasksStructureDigestOf(changeRoot) {
9
10
  const p = join(changeRoot, "tasks.md");
@@ -124,16 +125,28 @@ function validateContractTestRunInput(tr, attempt) {
124
125
  if (!["expected_failure", "expected_success", "characterization_pass"].includes(tr.semantic_status)) {
125
126
  return { ok: false, message: "语义状态(semantic_status)必须是 expected_failure、expected_success 或 characterization_pass" };
126
127
  }
128
+ const requiredEvidence = attempt.required_evidence;
129
+ if (tr.semantic_status === "expected_failure" && requiredEvidence && !requiredEvidence.red_required) {
130
+ return { ok: false, message: "当前任务执行快照不要求 RED(expected_failure);请登记声明 TEST 的 GREEN" };
131
+ }
132
+ if (tr.semantic_status === "expected_failure" && !requiredEvidence &&
133
+ attempt.execution_policy === "green_only" &&
134
+ attempt.no_tdd_reason === GREEN_ONLY_NO_TDD_REASON) {
135
+ return { ok: false, message: "GREEN-only 任务不登记 RED(expected_failure);请在实现后登记声明 TEST 的 GREEN" };
136
+ }
127
137
  if (tr.semantic_status === "expected_failure" && tr.exit_code === 0) {
128
138
  return { ok: false, message: "RED 预期失败(expected_failure)要求退出码(exit_code)非 0" };
129
139
  }
130
140
  if ((tr.semantic_status === "expected_success" || tr.semantic_status === "characterization_pass") && tr.exit_code !== 0) {
131
141
  return { ok: false, message: `语义状态(semantic_status=${tr.semantic_status})要求退出码(exit_code)为 0` };
132
142
  }
133
- if (tr.semantic_status === "characterization_pass" && !(attempt.tdd_required === false && attempt.no_tdd_reason === "characterization")) {
143
+ if (tr.semantic_status === "characterization_pass" && requiredEvidence && !requiredEvidence.accepted_green_statuses.includes("characterization_pass")) {
144
+ return { ok: false, message: "当前任务执行快照不接受特征化通过(characterization_pass)" };
145
+ }
146
+ if (tr.semantic_status === "characterization_pass" && !requiredEvidence && !(attempt.tdd_required === false && attempt.no_tdd_reason === "characterization")) {
134
147
  return { ok: false, message: "特征化通过(characterization_pass)只适用于无需 TDD 的特征化任务(tdd_required:false,no_tdd_reason:characterization)" };
135
148
  }
136
- const declaredTests = attempt.contract?.tests ?? [];
149
+ const declaredTests = requiredEvidence?.test_ids ?? attempt.contract?.tests ?? [];
137
150
  if (declaredTests.length > 0 && !declaredTests.includes(tr.test_id)) {
138
151
  return { ok: false, message: `测试 ID(test_id=${tr.test_id})不属于当前任务契约声明的测试列表` };
139
152
  }
@@ -1,4 +1,4 @@
1
- import type { TaskAttempt } from "./types.ts";
1
+ import { type TaskAttempt } from "./types.ts";
2
2
  export type TaskEvidenceReadiness = {
3
3
  ready: true;
4
4
  missing: [];