@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
package/dist/record.js CHANGED
@@ -1,26 +1,32 @@
1
1
  // SuperSpec 流程引擎 — record:工作项结果登记
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  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
7
  import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, workflowRiskForPhaseConfirmation, } from "./phase_confirmation.js";
8
- import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
8
+ import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewJobStaleReason, codeReviewDecisionAnswerLabel, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, normalizeCodeReviewDecisionAnswer, parseCodeReviewDecisionScope, } from "./code_review.js";
9
9
  import { invalidReasonForSubmittedReport } from "./job_validity.js";
10
10
  import { jobSubmitArgv } from "./job_action.js";
11
11
  import { REVIEW_DOC_PATHS, REVIEW_REJECTION_OVERRIDE_SCOPE_PREFIX, REVIEW_REJECTION_OVERRIDE_ANSWER, parseReviewRejectionOverrideScope, reviewGateRoleResolution, reviewRejectionOverrideScope, } from "./review.js";
12
12
  import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
13
13
  import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
14
+ import { currentExploreRoundId } from "./explore_round.js";
15
+ import { currentProposeOpenQuestion, currentProposeQuestionContent, currentProposeRoundId, } from "./propose_round.js";
16
+ import { discoveryOpenQuestionDisplayText, discoveryQuestionContextFingerprint, discoveryQuestionDecisionBasisDigest, discoveryOpenQuestionScope, legacyDiscoveryOpenQuestionScope, EXPLORE_OPEN_QUESTION_SCOPE_PREFIX, parseDiscoveryOpenQuestions, proposeOpenQuestionDisplayText, proposeOpenQuestionScope, proposeQuestionContextFingerprint, proposeQuestionDecisionBasisDigest, legacyProposeOpenQuestionScope, PROPOSE_OPEN_QUESTION_SCOPE_PREFIX, } from "./format.js";
14
17
  const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
15
18
  const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
16
19
  const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
17
20
  const CODE_REVIEW_FINDING_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
21
+ function isReviewRole(role) {
22
+ return role === "critic" || role === "architect" || role === "test-engineer" || role === "code-reviewer" || role === "verifier";
23
+ }
18
24
  function requiresReviewer(role) {
19
25
  return role === "critic" || role === "architect" || role === "test-engineer" || role === "code-reviewer";
20
26
  }
21
27
  /** All review reports with bound material acknowledge full file coverage. */
22
28
  function requiresReviewScope(job) {
23
- return job.boundFiles.length > 0 && job.role !== "executor" && job.role !== "test-run";
29
+ return job.boundFiles.length > 0 && isReviewRole(job.role);
24
30
  }
25
31
  function projectLocalInvalidReportMustTerminate(job) {
26
32
  return job.role === "code-reviewer" || job.packet_context?.code_state_check !== undefined;
@@ -62,14 +68,13 @@ function previousRejectionInstruction(job) {
62
68
  if (!previous)
63
69
  return "";
64
70
  const reason = `上一次同角色审查没有形成可推进结论,原因:${previous.reason}。`;
65
- if (!previous.findings || previous.findings.length === 0)
66
- return `${reason}本轮是修复复核;完整读取材料只用于核对当前修正和直接一致性,不得借复核重新审计与修正无关的历史设计,`;
67
- return `${reason}本轮是修复复核:逐项判断本工作项附带的上一次同角色 finding 是否仍成立。Finding 中的 recommendation 只是非绑定建议,不是需求或验收标准;先独立核对 underlying problem、直接证据和本次验收,不得因原建议指定了某种架构就要求照做。同一问题仍存在时复用原 finding ID;legacy finding 没有 ID 时沿用其原始语义并补一个稳定 ID;已解决或已由等价证据闭环的问题不要重复报告,不得通过更换 ID、标题或措辞重复同一问题。默认只复核历史 finding;新 blocker 仅允许是本次修正直接引入的回归,并必须说明“修正动作 → 新问题”的因果链,不得展开无关的故障模型、消费者或架构议题。`;
68
- }
69
- function ordinaryReviewerFindingInstruction(job) {
70
- if (!isOrdinaryReviewer(job.role))
71
- return "";
72
- return "问题列表中的每个新 finding 必须分配稳定 ID,后续同一问题沿用该 ID,";
71
+ if (!previous.findings || previous.findings.length === 0) {
72
+ return `${reason}上一轮没有可复核的历史 finding;请按当前 gate 的完整范围独立审查,不要把拒绝原因当作需求或验收标准,`;
73
+ }
74
+ const identityRule = job.role === "code-reviewer"
75
+ ? "同一问题仍存在时复用原 finding ID;legacy finding 没有 ID 时沿用原始语义并补一个稳定 ID;"
76
+ : "已解决或已由等价证据闭环的问题不要重复报告,不得通过更换标题或措辞重复同一问题;";
77
+ return `${reason}本轮是修复复核:逐项判断本工作项附带的上一次同角色 finding 是否仍成立。Finding 中的 recommendation 只是非绑定建议,不是需求或验收标准;先独立核对 underlying problem、直接证据和本次验收,不得因原建议指定了某种架构就要求照做。修正不得通过缩小已确认范围、改写用户决定或删除验收来让 finding 字面消失;这类偏离属于本次修正直接引入的回归。${identityRule}默认只复核历史 finding;新 blocker 仅允许是本次修正直接引入的回归,并必须说明“修正动作 → 新问题”的因果链,不得展开无关的故障模型、消费者或架构议题。`;
73
78
  }
74
79
  function reviewScopeForJob(job) {
75
80
  if (job.review_targets !== undefined || job.read_only_refs !== undefined) {
@@ -94,10 +99,15 @@ function reviewScopeInstruction(job, reviewTargets, readOnlyRefs) {
94
99
  : "";
95
100
  return targets + refs;
96
101
  }
97
- function reviewCoverageInstruction(job) {
102
+ function genericReviewCoverageInstruction(job) {
98
103
  if (!requiresReviewScope(job))
99
104
  return "";
100
- return `审查顺序固定为:先建立覆盖索引,按文件和标题/行段完整浏览全部 boundFiles 至文件末尾;长文档必须分段读取,不能在发现第一个 blocker 时停止覆盖。read_only_refs 按本次问题需要读取,用于核对目标与上下游一致性,不要求机械全文遍历或写入 checked_paths。完整读取只用于核对本次 change 的目标、直接修改及跨文档一致性,不等于允许重新审计全部历史设计。若 proposal.md 存在“需求变化”,以其中记录的受影响能力、直接修改章节和保持不变范围作为本轮增量审查的权威锚点;本轮新 finding 必须由该需求变化、为接入变化所做的直接修改,或这些修改造成的跨文档矛盾引起,并说明因果链。此前已通过且被明确记录为保持不变的设计不得重新打开为 blocker;不要凭通用风险类别猜测变化范围。注意事项和故障类别是条件式检查项,不是必须穷举的清单。Recommendation 只能描述需要补足的结果、契约或证据,不得把未经 proposal、design 或用户决定选定的新基础设施写成 required fix。报告中的 review_scope.checked_paths 必须列出全部已浏览的 boundFiles;它只是覆盖回执,不能代替语义审查,也不扩大可报告问题的范围。`;
105
+ return "完整审查全部 boundFiles,不因发现第一个 blocker 停止;read_only_refs 只在核对本次问题与上下游一致性时读取。review_scope.checked_paths 必须回执已浏览的全部 boundFiles,但该回执不能代替语义审查,也不扩大可报告问题的范围。";
106
+ }
107
+ function proposalIncrementalReviewInstruction(job) {
108
+ if (!PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job))
109
+ return "";
110
+ return "若 proposal.md 有“需求变化”,以其中记录的受影响能力、直接修改章节和保持不变范围作为增量审查锚点;新 finding 必须由该变化、其直接修改或由此造成的跨文档矛盾引起,并说明因果链。此前已通过且明确保持不变的设计不得重新打开为 blocker;Recommendation 只描述需要补足的结果、契约或证据,不得把未经 proposal、design 或用户决定选定的新基础设施写成 required fix。";
101
111
  }
102
112
  function migrationEvidenceInstruction(job) {
103
113
  const isProposeReview = PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job);
@@ -117,6 +127,121 @@ function asObject(value) {
117
127
  function nonEmptyString(value) {
118
128
  return typeof value === "string" && value.trim() !== "";
119
129
  }
130
+ function currentDiscoveryOpenQuestion(projectRoot, change) {
131
+ const discoveryPath = join(openspecChangeRoot(projectRoot, change), ".superspec", "artifacts", "discovery.md");
132
+ if (!existsSync(discoveryPath))
133
+ return null;
134
+ return parseDiscoveryOpenQuestions(readFileSync(discoveryPath, "utf8"))[0] ?? null;
135
+ }
136
+ function latestAcceptedExploreOpenQuestionDecision(events, scopes, identity) {
137
+ return [...events].reverse().find(event => {
138
+ if (event.event_type !== "user_decision_recorded")
139
+ return false;
140
+ const payload = event.payload;
141
+ if (payload.accepted === false)
142
+ return false;
143
+ const recorded = payload.explore_open_question;
144
+ if (recorded && typeof recorded.decision_basis_digest === "string") {
145
+ return recorded.round_id === identity.roundId &&
146
+ recorded.question_id === identity.questionId &&
147
+ (!identity.questionId.startsWith("item-") || recorded.question_ordinal === identity.questionOrdinal) &&
148
+ recorded.decision_basis_digest === identity.decisionBasisDigest;
149
+ }
150
+ return typeof payload.scope === "string" && scopes.includes(payload.scope);
151
+ }) ?? null;
152
+ }
153
+ function isExploreOpenQuestionScope(scope) {
154
+ return scope.startsWith(EXPLORE_OPEN_QUESTION_SCOPE_PREFIX);
155
+ }
156
+ function isWellFormedExploreOpenQuestionScope(scope) {
157
+ return /^explore_open_question:sha256:[a-f0-9]{64}:(?:Q-[A-Za-z0-9][A-Za-z0-9_-]*|item-[1-9]\d*)$/.test(scope);
158
+ }
159
+ function invalidExploreOpenQuestionResult(projectRoot, change, inputDigest, existing, decision, reason) {
160
+ const existingPayload = existing?.payload;
161
+ if (existingPayload?.accepted === false && existingPayload.reason === reason) {
162
+ return {
163
+ event_type: "user_decision_recorded",
164
+ accepted: false,
165
+ message: "幂等返回:同一无效待确认问题答复已登记",
166
+ };
167
+ }
168
+ appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
169
+ accepted: false,
170
+ scope: decision.scope,
171
+ answer: decision.answer,
172
+ reason,
173
+ input_digest: inputDigest,
174
+ }));
175
+ return {
176
+ event_type: "user_decision_recorded",
177
+ accepted: false,
178
+ message: reason === "invalid_explore_open_question_scope"
179
+ ? "确认事项的内部标识无效,请重新执行 next 获取当前事项"
180
+ : reason === "explore_open_question_already_recorded"
181
+ ? "这件事已有已登记答复,请先回写 discovery.md 后重新执行 next"
182
+ : "需要确认的事项已变化或已完成,请重新执行 next 获取当前事项",
183
+ };
184
+ }
185
+ function latestAcceptedProposeOpenQuestionDecision(events, scopes, identity) {
186
+ return [...events].reverse().find(event => {
187
+ if (event.event_type !== "user_decision_recorded")
188
+ return false;
189
+ const payload = event.payload;
190
+ if (payload.accepted === false)
191
+ return false;
192
+ const recorded = payload.propose_open_question;
193
+ if (recorded && typeof recorded.decision_basis_digest === "string") {
194
+ return recorded.round_id === identity.roundId &&
195
+ recorded.path === identity.path &&
196
+ recorded.question_id === identity.questionId &&
197
+ (!identity.questionId.startsWith("item-") || recorded.question_ordinal === identity.questionOrdinal) &&
198
+ recorded.decision_basis_digest === identity.decisionBasisDigest;
199
+ }
200
+ return typeof payload.scope === "string" && scopes.includes(payload.scope);
201
+ }) ?? null;
202
+ }
203
+ function isProposeOpenQuestionScope(scope) {
204
+ return scope.startsWith(PROPOSE_OPEN_QUESTION_SCOPE_PREFIX);
205
+ }
206
+ function isWellFormedProposeOpenQuestionScope(scope) {
207
+ return /^propose_open_question:sha256:[a-f0-9]{64}:(?:DEC-[A-Za-z0-9][A-Za-z0-9_-]*|item-[1-9]\d*)$/.test(scope);
208
+ }
209
+ function invalidProposeOpenQuestionResult(projectRoot, change, inputDigest, existing, decision, reason) {
210
+ const existingPayload = existing?.payload;
211
+ if (existingPayload?.accepted === false && existingPayload.reason === reason) {
212
+ return { event_type: "user_decision_recorded", accepted: false, message: "幂等返回:同一无效设计决定答复已登记" };
213
+ }
214
+ appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
215
+ accepted: false,
216
+ scope: decision.scope,
217
+ answer: decision.answer,
218
+ reason,
219
+ input_digest: inputDigest,
220
+ }));
221
+ return {
222
+ event_type: "user_decision_recorded",
223
+ accepted: false,
224
+ message: reason === "invalid_propose_open_question_scope"
225
+ ? "设计决定的内部标识无效,请重新执行 next 获取当前事项"
226
+ : reason === "propose_open_question_already_recorded"
227
+ ? "这项设计决定已有答复,请先回写计划材料后重新执行 next"
228
+ : "需要确认的设计决定已变化或已完成,请重新执行 next 获取当前事项",
229
+ };
230
+ }
231
+ function invalidCodeReviewDecisionResult(projectRoot, change, inputDigest, existing, decision, reason, message) {
232
+ const existingPayload = existing?.payload;
233
+ if (existingPayload?.accepted === false && existingPayload.reason === reason) {
234
+ return { event_type: "user_decision_recorded", accepted: false, message: "幂等返回:同一无效代码审查决策已登记" };
235
+ }
236
+ appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
237
+ accepted: false,
238
+ scope: decision.scope,
239
+ answer: decision.answer,
240
+ reason,
241
+ input_digest: inputDigest,
242
+ }));
243
+ return { event_type: "user_decision_recorded", accepted: false, message };
244
+ }
120
245
  function stringArray(value) {
121
246
  return Array.isArray(value) && value.every(item => typeof item === "string");
122
247
  }
@@ -421,8 +546,8 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
421
546
  if (!Array.isArray(obj.findings)) {
422
547
  checks.push("报告问题列表 findings 必须是数组");
423
548
  }
424
- else if (isOrdinaryReviewer(job.role) && obj.verdict === "fail" && obj.findings.length === 0) {
425
- checks.push("普通审查报告结论为 fail 时 findings 至少包含一个问题");
549
+ else if ((isOrdinaryReviewer(job.role) || job.role === "verifier") && obj.verdict === "fail" && obj.findings.length === 0) {
550
+ checks.push("审查报告结论为 fail 时 findings 至少包含一个问题");
426
551
  }
427
552
  if (requiresReviewer(job.role)) {
428
553
  validateReviewer(obj, checks);
@@ -633,6 +758,92 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
633
758
  }));
634
759
  return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少决策范围(scope)或答复内容(answer)" };
635
760
  }
761
+ // Explore 的用户答复只能绑定当前文档顺序中的第一项。这里不判断答案是否
762
+ // “正确”,只机械校验当前项、Discovery 决策上下文和 Explore 轮次仍与 next
763
+ // 返回时一致。决定身份绑定问题项中明确写出的 basis;其它文档内容不参与当前
764
+ // scope,避免无关材料编辑触发重复提问。
765
+ let exploreOpenQuestion = null;
766
+ let exploreOpenQuestionRoundId = null;
767
+ let exploreOpenQuestionContextFingerprint = null;
768
+ let exploreOpenQuestionBasisDigest = null;
769
+ if (isExploreOpenQuestionScope(decision.scope)) {
770
+ if (!isWellFormedExploreOpenQuestionScope(decision.scope)) {
771
+ return invalidExploreOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "invalid_explore_open_question_scope");
772
+ }
773
+ const current = currentDiscoveryOpenQuestion(projectRoot, change);
774
+ const exploreRoundId = currentExploreRoundId(events);
775
+ const expectedScopes = current
776
+ ? [discoveryOpenQuestionScope(current, exploreRoundId), legacyDiscoveryOpenQuestionScope(current, exploreRoundId)]
777
+ : [];
778
+ if (!expectedScopes.includes(decision.scope)) {
779
+ return invalidExploreOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "stale_explore_open_question_scope");
780
+ }
781
+ const currentBasisDigest = current ? discoveryQuestionDecisionBasisDigest(current) : "";
782
+ const acceptedForCurrentScope = latestAcceptedExploreOpenQuestionDecision(events, expectedScopes, {
783
+ roundId: exploreRoundId,
784
+ questionId: current.id,
785
+ questionOrdinal: current.ordinal,
786
+ decisionBasisDigest: currentBasisDigest,
787
+ });
788
+ if (acceptedForCurrentScope) {
789
+ const previousAnswer = acceptedForCurrentScope.payload.answer;
790
+ if (previousAnswer === decision.answer) {
791
+ return {
792
+ event_type: "user_decision_recorded",
793
+ accepted: true,
794
+ message: "幂等返回:同一用户决策已登记",
795
+ };
796
+ }
797
+ return invalidExploreOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "explore_open_question_already_recorded");
798
+ }
799
+ exploreOpenQuestion = current;
800
+ exploreOpenQuestionRoundId = exploreRoundId;
801
+ const discoveryPath = join(openspecChangeRoot(projectRoot, change), ".superspec", "artifacts", "discovery.md");
802
+ exploreOpenQuestionContextFingerprint = current
803
+ ? discoveryQuestionContextFingerprint(readFileSync(discoveryPath, "utf8"), current)
804
+ : null;
805
+ exploreOpenQuestionBasisDigest = currentBasisDigest;
806
+ }
807
+ let proposeOpenQuestion = null;
808
+ let proposeOpenQuestionRoundId = null;
809
+ let proposeOpenQuestionContextFingerprint = null;
810
+ let proposeOpenQuestionBasisDigest = null;
811
+ if (isProposeOpenQuestionScope(decision.scope)) {
812
+ if (!isWellFormedProposeOpenQuestionScope(decision.scope)) {
813
+ return invalidProposeOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "invalid_propose_open_question_scope");
814
+ }
815
+ const changeRoot = openspecChangeRoot(projectRoot, change);
816
+ const current = currentProposeOpenQuestion(changeRoot);
817
+ const proposeRoundId = currentProposeRoundId(events);
818
+ const expectedScopes = current
819
+ ? [proposeOpenQuestionScope(current, proposeRoundId), legacyProposeOpenQuestionScope(current, proposeRoundId)]
820
+ : [];
821
+ if (!expectedScopes.includes(decision.scope)) {
822
+ return invalidProposeOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "stale_propose_open_question_scope");
823
+ }
824
+ const currentBasisDigest = current ? proposeQuestionDecisionBasisDigest(current) : "";
825
+ const acceptedForCurrentScope = latestAcceptedProposeOpenQuestionDecision(events, expectedScopes, {
826
+ roundId: proposeRoundId,
827
+ path: current.path,
828
+ questionId: current.id,
829
+ questionOrdinal: current.ordinal,
830
+ decisionBasisDigest: currentBasisDigest,
831
+ });
832
+ if (acceptedForCurrentScope) {
833
+ const previousAnswer = acceptedForCurrentScope.payload.answer;
834
+ if (previousAnswer === decision.answer) {
835
+ return { event_type: "user_decision_recorded", accepted: true, message: "幂等返回:同一用户决策已登记" };
836
+ }
837
+ return invalidProposeOpenQuestionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "propose_open_question_already_recorded");
838
+ }
839
+ const content = current ? currentProposeQuestionContent(changeRoot, current) : null;
840
+ proposeOpenQuestion = current;
841
+ proposeOpenQuestionRoundId = proposeRoundId;
842
+ proposeOpenQuestionContextFingerprint = current && content
843
+ ? proposeQuestionContextFingerprint(content, current)
844
+ : null;
845
+ proposeOpenQuestionBasisDigest = currentBasisDigest;
846
+ }
636
847
  let phaseConfirmation = null;
637
848
  let phaseAction = null;
638
849
  let phaseReviewRisk = null;
@@ -699,7 +910,10 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
699
910
  phaseAction = action;
700
911
  phaseReviewRisk = decisionRisk;
701
912
  }
702
- if (existing && !phaseAction) {
913
+ // Explore scope 已在上面按当前问题和同 scope 的已接受答复完成校验。此前同一
914
+ // input 曾因 stale 被拒绝、但材料后来回到完全相同的当前项时,不能让旧拒绝
915
+ // 记录永久吞掉一次现在有效的登记。
916
+ if (existing && !phaseAction && !exploreOpenQuestion) {
703
917
  const accepted = existing.payload.accepted !== false;
704
918
  return {
705
919
  event_type: "user_decision_recorded",
@@ -760,36 +974,45 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
760
974
  };
761
975
  reviewRejectionDecisionSource = decision.decision_source;
762
976
  }
977
+ let codeReviewDecisionReference = null;
763
978
  if (decision.scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX)) {
764
979
  const normalizedAnswer = normalizeCodeReviewDecisionAnswer(decision.answer);
765
980
  if (!normalizedAnswer) {
766
- appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
767
- accepted: false,
768
- scope: decision.scope,
769
- answer: decision.answer,
770
- reason: "invalid_code_review_decision_answer",
771
- input_digest: inputDigest,
772
- }));
773
- return {
774
- event_type: "user_decision_recorded",
775
- accepted: false,
776
- message: `代码审查决策必须是 ${CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_propose}、${CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_apply} 或 ${CODE_REVIEW_DECISION_ANSWER_LABELS.dismiss}`,
777
- };
981
+ return invalidCodeReviewDecisionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "invalid_code_review_decision_answer", `代码审查决策必须是 ${CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_propose}、${CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_apply} 或 ${CODE_REVIEW_DECISION_ANSWER_LABELS.dismiss}`);
778
982
  }
779
983
  if (!nonEmptyString(decision.reason)) {
780
- appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
781
- accepted: false,
782
- scope: decision.scope,
783
- answer: decision.answer,
784
- reason: "missing_reason",
785
- input_digest: inputDigest,
786
- }));
787
- return {
788
- event_type: "user_decision_recorded",
789
- accepted: false,
790
- message: "代码审查决策必须写明原因",
791
- };
984
+ return invalidCodeReviewDecisionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, "missing_reason", "代码审查决策必须写明原因");
985
+ }
986
+ const ref = parseCodeReviewDecisionScope(decision.scope);
987
+ const changeRoot = openspecChangeRoot(projectRoot, change);
988
+ const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
989
+ const status = latestCodeReviewFailedStatus(events);
990
+ const finding = ref && status?.terminal.job.job_id === ref.jobId
991
+ ? status.findings.find(item => item.id === ref.findingId && (item.type === "spec" || item.type === "mixed"))
992
+ : null;
993
+ const staleReason = status && ref && status.terminal.job.job_id === ref.jobId
994
+ ? codeReviewJobStaleReason(projectRoot, status.terminal.job, currentCodeReviewWorkingPaths(projectRoot, events))
995
+ : null;
996
+ if (!ref || snapshot.state !== "apply_done" || !status || !finding || staleReason) {
997
+ const reason = !ref
998
+ ? "invalid_code_review_decision_scope"
999
+ : snapshot.state !== "apply_done"
1000
+ ? "code_review_decision_not_current"
1001
+ : staleReason
1002
+ ? "stale_code_review_decision_target"
1003
+ : "code_review_decision_target_not_found";
1004
+ const message = staleReason
1005
+ ? "代码审查材料已不再匹配当前代码,请先重新执行 review-ready 获取新的审查结论"
1006
+ : "代码审查决策只能关联当前代码审查报告中的待决定问题,请先执行 next 获取当前选择";
1007
+ return invalidCodeReviewDecisionResult(projectRoot, change, inputDigest, existing, { scope: decision.scope, answer: decision.answer }, reason, message);
792
1008
  }
1009
+ codeReviewDecisionReference = {
1010
+ job_id: status.terminal.job.job_id,
1011
+ finding_id: finding.id,
1012
+ rejection_event_id: status.terminal.event.event_id,
1013
+ rejection_event_digest: status.terminal.event.event_digest,
1014
+ packet_digest: status.terminal.job.packet_digest,
1015
+ };
793
1016
  }
794
1017
  const normalizedAnswer = decision.scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX)
795
1018
  ? normalizeCodeReviewDecisionAnswer(decision.answer)
@@ -798,13 +1021,39 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
798
1021
  scope: decision.scope,
799
1022
  question: phaseConfirmation
800
1023
  ? phaseConfirmation.ask.question
801
- : typeof decision.question === "string" ? decision.question : "",
1024
+ : exploreOpenQuestion
1025
+ ? discoveryOpenQuestionDisplayText(exploreOpenQuestion)
1026
+ : proposeOpenQuestion
1027
+ ? proposeOpenQuestionDisplayText(proposeOpenQuestion)
1028
+ : typeof decision.question === "string" ? decision.question : "",
802
1029
  answer: phaseAction
803
1030
  ? phaseAction.label
804
1031
  : normalizedAnswer ? codeReviewDecisionAnswerLabel(normalizedAnswer) : decision.answer,
805
1032
  ...(typeof decision.reason === "string" ? { reason: decision.reason.trim() } : {}),
806
1033
  ...(reviewRejectionDecisionSource ? { decision_source: reviewRejectionDecisionSource } : {}),
807
1034
  ...(reviewRejectionOverride ? { review_rejection_override: reviewRejectionOverride } : {}),
1035
+ ...(codeReviewDecisionReference ? { code_review_decision: codeReviewDecisionReference } : {}),
1036
+ ...(exploreOpenQuestion && exploreOpenQuestionRoundId && exploreOpenQuestionContextFingerprint && exploreOpenQuestionBasisDigest ? {
1037
+ explore_open_question: {
1038
+ round_id: exploreOpenQuestionRoundId,
1039
+ question_id: exploreOpenQuestion.id,
1040
+ question_ordinal: exploreOpenQuestion.ordinal,
1041
+ document_fingerprint: exploreOpenQuestion.documentFingerprint,
1042
+ context_fingerprint: exploreOpenQuestionContextFingerprint,
1043
+ decision_basis_digest: exploreOpenQuestionBasisDigest,
1044
+ },
1045
+ } : {}),
1046
+ ...(proposeOpenQuestion && proposeOpenQuestionRoundId && proposeOpenQuestionContextFingerprint && proposeOpenQuestionBasisDigest ? {
1047
+ propose_open_question: {
1048
+ round_id: proposeOpenQuestionRoundId,
1049
+ path: proposeOpenQuestion.path,
1050
+ question_id: proposeOpenQuestion.id,
1051
+ question_ordinal: proposeOpenQuestion.ordinal,
1052
+ document_fingerprint: proposeOpenQuestion.documentFingerprint,
1053
+ context_fingerprint: proposeOpenQuestionContextFingerprint,
1054
+ decision_basis_digest: proposeOpenQuestionBasisDigest,
1055
+ },
1056
+ } : {}),
808
1057
  ...(phaseAction ? {
809
1058
  phase_confirmation: {
810
1059
  boundary: phaseAction.boundary,
@@ -824,7 +1073,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
824
1073
  return {
825
1074
  event_type: "user_decision_recorded",
826
1075
  accepted: true,
827
- message: `用户决策已登记:决策范围(scope)=${decision.scope}`,
1076
+ message: exploreOpenQuestion
1077
+ ? "这件事的答复已登记"
1078
+ : `用户决策已登记:决策范围(scope)=${decision.scope}`,
828
1079
  };
829
1080
  }
830
1081
  /** record user-decision:登记用户决策 */
@@ -913,6 +1164,7 @@ function packetFieldDescriptions() {
913
1164
  unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
914
1165
  unknown_attribution_tasks: "因为缺少边界快照或提交段 diff 失败而无法完整计算改动归属的任务(task)。",
915
1166
  coverage_exemption_refs: "测试覆盖豁免引用:说明某个 TEST 为什么没有绑定到任务(task)。",
1167
+ code_review_gate: "最终验证读取的代码审查门禁事实:passed 指向已接受的代码审查工作项,skipped 表示本轮没有代码类改动。",
916
1168
  code_state_check: "代码状态检查:最终验证时用于判断代码审查后代码是否又发生变化。",
917
1169
  event_id: "事件 ID,用于追溯证据来源。",
918
1170
  event_digest: "事件摘要,用于确认引用的证据事件没有被替换。",
@@ -932,6 +1184,7 @@ export function jobsPacket(projectRoot, change, jobId) {
932
1184
  return { found: false, message: `工作项 ${jobId} 不存在` };
933
1185
  }
934
1186
  const isCodeReviewer = job.role === "code-reviewer";
1187
+ const isReviewer = isReviewRole(job.role);
935
1188
  const hasReviewScope = requiresReviewScope(job);
936
1189
  const packetContext = job.packet_context;
937
1190
  const { reviewTargets, readOnlyRefs } = reviewScopeForJob(job);
@@ -943,12 +1196,13 @@ export function jobsPacket(projectRoot, change, jobId) {
943
1196
  ...(job.gate_id ? { gate_id: job.gate_id } : {}),
944
1197
  recommended_agent: recommendedAgentForRole(job.role),
945
1198
  boundFiles: job.boundFiles,
946
- ...(reviewTargets.length > 0 ? { review_targets: reviewTargets } : {}),
947
- ...(readOnlyRefs.length > 0 ? { read_only_refs: readOnlyRefs } : {}),
1199
+ ...(isReviewer && reviewTargets.length > 0 ? { review_targets: reviewTargets } : {}),
1200
+ ...(isReviewer && readOnlyRefs.length > 0 ? { read_only_refs: readOnlyRefs } : {}),
948
1201
  ...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
949
1202
  ...(job.previous_rejection ? { previous_rejection: job.previous_rejection } : {}),
950
1203
  ...(packetContext ? { packet_context: packetContext } : {}),
951
1204
  ...(packetContext?.code_review_scope ? { code_review_scope: packetContext.code_review_scope } : {}),
1205
+ ...(packetContext?.code_review_gate ? { code_review_gate: packetContext.code_review_gate } : {}),
952
1206
  ...(packetContext?.coverage_exemption_refs ? { coverage_exemption_refs: packetContext.coverage_exemption_refs } : {}),
953
1207
  ...(packetContext?.task_execution_index ? { task_execution_index: packetContext.task_execution_index } : {}),
954
1208
  ...(packetContext?.unattributed_paths ? { unattributed_paths: packetContext.unattributed_paths } : {}),
@@ -970,29 +1224,31 @@ export function jobsPacket(projectRoot, change, jobId) {
970
1224
  output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
971
1225
  字段说明: packetFieldDescriptions(),
972
1226
  output_instructions: `${roleDescription(job.role)}。` +
973
- reviewScopeInstruction(job, reviewTargets, readOnlyRefs) +
974
- migrationEvidenceInstruction(job) +
1227
+ (isReviewer ? reviewScopeInstruction(job, reviewTargets, readOnlyRefs) : "") +
1228
+ (isReviewer ? migrationEvidenceInstruction(job) : "") +
975
1229
  (job.review_evidence_digest ? `本工作项对应的执行证据版本为 ${job.review_evidence_digest},` : "") +
976
- reviewCoverageInstruction(job) +
977
- previousRejectionInstruction(job) +
1230
+ (isReviewer ? genericReviewCoverageInstruction(job) + proposalIncrementalReviewInstruction(job) + previousRejectionInstruction(job) : "") +
978
1231
  (requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
979
- ordinaryReviewerFindingInstruction(job) +
980
1232
  `产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。${recordInputInstruction(job)}协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
981
1233
  (isCodeReviewer
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>"}。`
1234
+ ? `最小格式:{"role":"code-reviewer","verdict":"pass","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>"}}。verdict 只能为 pass 或 fail;审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"}。`
983
1235
  + `报告结论为 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 表示需要使用者判断的混合问题。`
984
1236
  + (packetContext?.task_execution_index
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 豁免。`
1237
+ ? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
986
1238
  : "")
987
1239
  : job.role === "verifier"
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)的旧证据只能弱引用。` +
1240
+ ? `最小格式:{"role":"verifier","verdict":"pass","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。verdict 只能为 pass 或 fail;核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对修复闭环:task_execution_index.fix.source=code_review 时必须核对 review_finding 对应问题是否关闭;source=self_test 时必须核对 parent_task_id、记录的自测原因、本次 attempt 验证和最新代码审查是否共同闭环。方案/混合问题必须有用户决策或后续修复证据。按 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)。修复 task 的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
989
1241
  (packetContext?.code_state_check
990
- ? `本工作项带代码状态检查(code_state_check):head_matches false changed_paths 非空表示代码审查后代码又发生变化,须在报告中列出差异并交主流程与用户裁决,不自行判定无害,也不据此自动否定已接受的代码审查。`
1242
+ ? `本工作项带代码状态检查(code_state_check),它是创建 packet 时的快照:验证期间若代码状态已变化,不要提交该报告;主流程会通过 next 创建携带最新事实的验证工作项。`
991
1243
  : "")
992
- : requiresReviewer(job.role)
993
- ? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""},"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
994
- : `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}`),
995
- stop_conditions: ["审查完成后提交报告,不要修改文档"],
1244
+ : isReviewer
1245
+ ? `最小格式:{"role":"${job.role}","verdict":"pass","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""},"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}。verdict 只能为 pass 或 fail。`
1246
+ : `最小格式:{"role":"${job.role}","verdict":"pass","findings":[]}。verdict 只能为 pass fail。`),
1247
+ stop_conditions: isReviewer
1248
+ ? ["完成审查后提交报告,不要修改文档"]
1249
+ : job.role === "executor"
1250
+ ? ["完成绑定范围内的实现后提交执行报告,不要修改未绑定范围"]
1251
+ : ["完成指定验证后提交测试报告,不要修改项目文档"],
996
1252
  created_from_transition: job.created_from_transition,
997
1253
  },
998
1254
  message: `工作项 ${jobId} 的执行说明`,
package/dist/review.js CHANGED
@@ -90,6 +90,8 @@ function reviewCycleState(gate) {
90
90
  return "explore";
91
91
  if (gate.gate_id === PROPOSE_FINAL_REVIEW_GATE_ID)
92
92
  return "propose";
93
+ if (gate.gate_id === REVIEW_FINAL_VERIFIER_GATE.gate_id)
94
+ return "review";
93
95
  return null;
94
96
  }
95
97
  function currentReviewGateCycleStart(events, gate) {
@@ -15,7 +15,7 @@ export declare const EXPLORE_DISCOVERY_REVIEW_GATE_ID: "explore.discovery_review
15
15
  export declare const PROPOSE_FINAL_REVIEW_GATE_ID: "propose.final_review";
16
16
  export declare const REVIEW_CODE_REVIEW_GATE_ID: "review.code_review";
17
17
  export declare const REVIEW_FINAL_VERIFIER_GATE_ID: "review.final_verifier";
18
- export declare function reviewScopeForGateRole(gate: ReviewGateRule, role: JobRole): {
18
+ export declare function reviewScopeForGateRole(gate: ReviewGateRule, role: JobRole, requiredRoles?: readonly JobRole[]): {
19
19
  reviewTargets: string[];
20
20
  readOnlyRefs: string[];
21
21
  boundPaths: string[];
@@ -26,14 +26,22 @@ function defaultReviewScope(gate) {
26
26
  boundPaths: [...new Set([...gate.reviewTargets, ...gate.readOnlyRefs])],
27
27
  };
28
28
  }
29
- export function reviewScopeForGateRole(gate, role) {
29
+ export function reviewScopeForGateRole(gate, role, requiredRoles = gate.allowedRoles) {
30
30
  if (gate.gate_id !== PROPOSE_FINAL_REVIEW_GATE_ID)
31
31
  return defaultReviewScope(gate);
32
32
  if (role === "critic") {
33
- const reviewTargets = ["proposal.md", "specs/", "tasks.md"];
33
+ const ownsDesign = !requiredRoles.includes("architect");
34
+ const ownsTestContract = !requiredRoles.includes("test-engineer");
35
+ const reviewTargets = [
36
+ "proposal.md",
37
+ "specs/",
38
+ ...(ownsDesign ? ["design.md"] : []),
39
+ "tasks.md",
40
+ ...(ownsTestContract ? [".superspec/artifacts/test-contract.md"] : []),
41
+ ];
34
42
  const readOnlyRefs = [
35
- "design.md",
36
- ".superspec/artifacts/test-contract.md",
43
+ ...(!ownsDesign ? ["design.md"] : []),
44
+ ...(!ownsTestContract ? [".superspec/artifacts/test-contract.md"] : []),
37
45
  ".superspec/artifacts/discovery.md",
38
46
  ];
39
47
  return {
@@ -47,6 +47,26 @@ export function simulateLoop(nextFn, executeFn, maxSteps = 20) {
47
47
  return { completed: false, steps, finalState: output.state, message: `工作项执行失败` };
48
48
  }
49
49
  break;
50
+ case "artifact_required":
51
+ step.action = "artifact";
52
+ step.detail = output.artifact.path;
53
+ steps.push(step);
54
+ return {
55
+ completed: false,
56
+ steps,
57
+ finalState: output.state,
58
+ message: `需要创建或更新工作流产物:${output.artifact.path}`,
59
+ };
60
+ case "material_update_required":
61
+ step.action = "material_update";
62
+ step.detail = output.errors.join(";");
63
+ steps.push(step);
64
+ return {
65
+ completed: false,
66
+ steps,
67
+ finalState: output.state,
68
+ message: `需要修正工作流材料:${output.errors.join(";")}`,
69
+ };
50
70
  case "ask_user":
51
71
  step.action = "ask";
52
72
  step.detail = output.ask_user.question;
@@ -4,6 +4,11 @@ import { parseTasksMd } from "./format.js";
4
4
  import { readEvents, sha256Text } from "./store.js";
5
5
  import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
6
6
  export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt) {
7
+ // 有效证据计划是 task-start 在当时策略下冻结的事实。状态机新建的 Fix
8
+ // 即使来自历史/v1 Apply,也必须优先按这个快照回放。
9
+ if (attempt.required_evidence) {
10
+ return effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt);
11
+ }
7
12
  if (attempt.contract_mode === true) {
8
13
  return contractTaskEvidenceReadiness(projectRoot, change, attempt);
9
14
  }
@@ -52,9 +57,6 @@ export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt)
52
57
  : { ready: false, missing, reason: missing.join("、") };
53
58
  }
54
59
  function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
55
- if (attempt.required_evidence) {
56
- return effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt);
57
- }
58
60
  return legacyContractTaskEvidenceReadiness(projectRoot, change, attempt);
59
61
  }
60
62
  /** 新执行依据模式:只消费 task-start 冻结的有效证据计划。 */
@@ -40,6 +40,7 @@ export declare function taskStart(projectRoot: string, change: string, changeRoo
40
40
  export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string, opts?: {
41
41
  reviewFix?: string;
42
42
  reviewFinding?: string;
43
+ selfTestFix?: string;
43
44
  }): TransitionResult;
44
45
  export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
45
46
  export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;