@peterxiaoyang/superspec 0.1.43 → 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 (46) 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 -3
  5. package/dist/format.js +53 -29
  6. package/dist/git_state.d.ts +12 -1
  7. package/dist/git_state.js +46 -1
  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 +51 -7
  14. package/dist/phase_plan.d.ts +9 -2
  15. package/dist/phase_plan.js +178 -45
  16. package/dist/record.d.ts +1 -1
  17. package/dist/record.js +102 -10
  18. package/dist/review.d.ts +48 -1
  19. package/dist/review.js +108 -4
  20. package/dist/review_job_gates.d.ts +5 -0
  21. package/dist/review_job_gates.js +52 -1
  22. package/dist/sync.js +13 -5
  23. package/dist/task.js +15 -2
  24. package/dist/task_evidence.d.ts +1 -1
  25. package/dist/task_evidence.js +85 -10
  26. package/dist/transition.d.ts +4 -3
  27. package/dist/transition.js +172 -38
  28. package/dist/types.d.ts +25 -1
  29. package/dist/types.js +1 -0
  30. package/dist/workflow_config.d.ts +24 -0
  31. package/dist/workflow_config.js +127 -0
  32. package/package.json +1 -1
  33. package/templates/workflow/AGENTS.md +1 -1
  34. package/templates/workflow/agents/executor.toml +1 -1
  35. package/templates/workflow/agents/test-runner.toml +1 -1
  36. package/templates/workflow/prompts/architect.md +1 -1
  37. package/templates/workflow/prompts/code-reviewer.md +2 -2
  38. package/templates/workflow/prompts/critic.md +4 -6
  39. package/templates/workflow/prompts/executor.md +2 -2
  40. package/templates/workflow/prompts/test-engineer.md +5 -6
  41. package/templates/workflow/prompts/test-runner.md +1 -1
  42. package/templates/workflow/prompts/verifier.md +1 -1
  43. package/templates/workflow/skills/superspec-apply/SKILL.md +13 -71
  44. package/templates/workflow/skills/superspec-explore/SKILL.md +7 -9
  45. package/templates/workflow/skills/superspec-propose/SKILL.md +36 -48
  46. package/templates/workflow/skills/superspec-review/SKILL.md +21 -50
@@ -1,6 +1,9 @@
1
1
  import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
2
- import { reviewEvidenceDigest } from "./review.js";
2
+ import { historicalProposeReadyRoles, reviewEvidenceDigest, reviewGateRoleResolution } from "./review.js";
3
+ import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
4
+ import { changeRoot as openspecChangeRoot } from "./openspec.js";
3
5
  import { findLatestEvent, sha256Text } from "./store.js";
6
+ import { hasFrozenWorkflowModeForProposeRound, workflowRiskForProject, workflowRiskForState, } from "./workflow_config.js";
4
7
  export const PHASE_CONFIRMATION_SCOPE_PREFIX = "phase_confirmation:";
5
8
  function latestTransition(events, predicate) {
6
9
  return findLatestEvent(events, "transition_commit", event => predicate(event.payload));
@@ -23,7 +26,7 @@ const SPECS = {
23
26
  },
24
27
  ],
25
28
  scopePrefix: `${PHASE_CONFIRMATION_SCOPE_PREFIX}explore_to_propose`,
26
- epoch: events => latestTransition(events, payload => payload.from_state === "init" && payload.to_state === "explore"),
29
+ epoch: events => latestTransition(events, payload => payload.to_state === "explore" && payload.from_state !== "explore"),
27
30
  },
28
31
  propose_to_apply: {
29
32
  state: "propose_ready",
@@ -63,7 +66,21 @@ function boundaryForState(state) {
63
66
  default: return null;
64
67
  }
65
68
  }
66
- function materialDigest(projectRoot, events, snapshot) {
69
+ function ordinaryReviewRolesForBoundary(events, boundary, gate, risk) {
70
+ // 历史 plan 没有冻结 mode,当时的 gate 只能按实际创建过的角色回放。
71
+ if (boundary === "propose_to_apply" && !hasFrozenWorkflowModeForProposeRound(events)) {
72
+ return historicalProposeReadyRoles(events);
73
+ }
74
+ return gate.requiredRolesForRisk(risk);
75
+ }
76
+ /**
77
+ * 阶段确认不是 mode 的输入。它只读取当前 planning/apply round 的冻结快照;
78
+ * 仍处于 Explore/Propose 时才从项目配置获取候选 mode。
79
+ */
80
+ export function workflowRiskForPhaseConfirmation(projectRoot, events, snapshot) {
81
+ return workflowRiskForState(events, snapshot.state, workflowRiskForProject(projectRoot));
82
+ }
83
+ function materialDigest(projectRoot, events, snapshot, boundary, risk) {
67
84
  const head = currentGitHead(projectRoot);
68
85
  const dirty = dirtyCodeFiles(projectRoot);
69
86
  const acceptedJobs = snapshot.accepted_jobs
@@ -77,10 +94,32 @@ function materialDigest(projectRoot, events, snapshot) {
77
94
  .sort((a, b) => a.job_id.localeCompare(b.job_id));
78
95
  const documents = Object.entries(snapshot.document_digests)
79
96
  .sort(([left], [right]) => left.localeCompare(right));
97
+ const ordinaryReviewGate = boundary === "explore_to_propose"
98
+ ? EXPLORE_DISCOVERY_REVIEW_GATE
99
+ : boundary === "propose_to_apply"
100
+ ? PROPOSE_FINAL_REVIEW_GATE
101
+ : null;
102
+ const currentChangeRoot = openspecChangeRoot(projectRoot, snapshot.change_id);
103
+ const overriddenReviewJobs = ordinaryReviewGate
104
+ ? ordinaryReviewRolesForBoundary(events, boundary, ordinaryReviewGate, risk)
105
+ .map(role => reviewGateRoleResolution(events, currentChangeRoot, ordinaryReviewGate, role))
106
+ .filter(resolution => resolution.kind === "overridden")
107
+ .map(resolution => ({
108
+ job_id: resolution.override.job_id,
109
+ role: resolution.override.role,
110
+ gate_id: resolution.override.gate_id,
111
+ packet_digest: resolution.override.packet_digest,
112
+ decision_event_id: resolution.override.decision_event_id,
113
+ decision_event_digest: resolution.override.decision_event_digest,
114
+ }))
115
+ .sort((left, right) => `${left.gate_id}\u0000${left.role}\u0000${left.job_id}`.localeCompare(`${right.gate_id}\u0000${right.role}\u0000${right.job_id}`))
116
+ : [];
80
117
  return sha256Text(JSON.stringify({
118
+ review_risk: risk,
81
119
  documents,
82
120
  tasks_structure_digest: snapshot.tasks_structure_digest,
83
121
  accepted_jobs: acceptedJobs,
122
+ ...(overriddenReviewJobs.length > 0 ? { overridden_review_jobs: overriddenReviewJobs } : {}),
84
123
  review_evidence_digest: reviewEvidenceDigest(events),
85
124
  code_state: {
86
125
  head: head.head,
@@ -93,14 +132,13 @@ function materialDigest(projectRoot, events, snapshot) {
93
132
  function phaseRecordArgv(change) {
94
133
  return ["superspec", "record", "user-decision", "--change", change, "--input", "-"];
95
134
  }
96
- function nextArgv(change, risk) {
135
+ function nextArgv(change, _risk) {
97
136
  return [
98
137
  "superspec",
99
138
  "transition",
100
139
  "next",
101
140
  "--change",
102
141
  change,
103
- ...(risk === "strict" ? [] : ["--risk", risk]),
104
142
  ];
105
143
  }
106
144
  function buildActions(change, boundary, scope, question, specs, risk) {
@@ -126,7 +164,7 @@ function buildActions(change, boundary, scope, question, specs, risk) {
126
164
  reason: spec.reason,
127
165
  ...(spec.reasonPrompt ? { reason_prompt: spec.reasonPrompt } : {}),
128
166
  record_argv: phaseRecordArgv(change),
129
- record_input: { scope, question, answer: spec.label },
167
+ record_input: { scope, question, answer: spec.label, review_risk: risk },
130
168
  resume: spec.resume.kind === "next"
131
169
  ? { kind: "next", argv: nextArgv(change, risk) }
132
170
  : spec.resume.kind === "continue_current_phase"
@@ -147,7 +185,7 @@ export function phaseConfirmationForBoundary(projectRoot, events, snapshot, boun
147
185
  return null;
148
186
  const epoch = spec.epoch(events);
149
187
  const epochEventId = epoch?.event_id ?? `legacy-${spec.state}`;
150
- const digest = materialDigest(projectRoot, events, snapshot);
188
+ const digest = materialDigest(projectRoot, events, snapshot, boundary, risk);
151
189
  const scope = `${spec.scopePrefix}:${epochEventId}:${digest}`;
152
190
  const actions = buildActions(snapshot.change_id, boundary, scope, spec.question, spec.actions, risk);
153
191
  return {
@@ -192,6 +230,11 @@ export function latestAcceptedPhaseDecision(events, confirmation) {
192
230
  scope: confirmation.scope,
193
231
  answer: payload.answer,
194
232
  decision: payload.phase_confirmation.decision,
233
+ review_risk: payload.phase_confirmation.review_risk === "minimal" ||
234
+ payload.phase_confirmation.review_risk === "normal" ||
235
+ payload.phase_confirmation.review_risk === "strict"
236
+ ? payload.phase_confirmation.review_risk
237
+ : "strict",
195
238
  };
196
239
  }
197
240
  export function isPhaseAdvanceAuthorized(events, confirmation) {
@@ -209,6 +252,7 @@ export function phaseConfirmationCommitPayload(confirmation, decision) {
209
252
  material_digest: confirmation.material_digest,
210
253
  scope: confirmation.scope,
211
254
  decision_event_id: decision.event.event_id,
255
+ review_risk: decision.review_risk,
212
256
  },
213
257
  };
214
258
  }
@@ -1,5 +1,5 @@
1
1
  import type { ReviewGateRule } from "./review_job_gates.ts";
2
- import type { AcceptedMaterialFollowupContinuation, AskUser, Event, Job, JobRole, State } from "./types.ts";
2
+ import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, State } from "./types.ts";
3
3
  import type { Snapshot } from "./types.ts";
4
4
  import type { ReviewRisk } from "./review.ts";
5
5
  export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept";
@@ -64,6 +64,7 @@ export type TransitionDecisionPlan = {
64
64
  kind: "blocked";
65
65
  jobs: Job[];
66
66
  reason: string;
67
+ details?: Record<string, unknown>;
67
68
  } | {
68
69
  kind: "create_gate_jobs";
69
70
  gate: ReviewGateRule;
@@ -82,12 +83,18 @@ export interface ApplyPendingTaskStatus {
82
83
  needsCompletionEvent: string[];
83
84
  completedByEvent: string[];
84
85
  }
86
+ export declare function executionPolicyForRisk(risk: ReviewRisk): ExecutionPolicy;
87
+ export declare function executionPolicyForCurrentRound(events: Event[]): ExecutionPolicy;
85
88
  export declare function proposalDocsBaseline(changeRoot: string): Record<string, string>;
89
+ export declare function discoveryDocsBaseline(changeRoot: string): Record<string, string>;
86
90
  export declare function latestAcceptedProposalBaseline(events: Event[]): Record<string, string> | null;
87
91
  export declare function latestReopenProposeBaseline(events: Event[]): Record<string, string> | null;
92
+ export declare function latestReopenExploreBaseline(events: Event[]): Record<string, string> | null;
88
93
  export declare function proposalDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
89
- export declare function historicalProposeReadyRoles(events: Event[]): JobRole[];
94
+ export declare function discoveryDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
90
95
  export declare function pendingTaskIds(changeRoot: string): string[];
96
+ /** 当前 Apply round 的规则版本;缺失版本的历史 event 保持 v1 回放。 */
97
+ export declare function executionRequirementVersionForCurrentRound(events: Event[]): 1 | 2;
91
98
  export declare function applyRequirementModeForCurrentRound(events: Event[]): boolean;
92
99
  export declare function pendingTaskStatusForApply(changeRoot: string, events: Event[]): ApplyPendingTaskStatus;
93
100
  export declare function formatPendingTaskMessage(ids: string[], action: string): string;
@@ -4,15 +4,18 @@ import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./revi
4
4
  import { collectProposeOpenQuestions, countDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, } from "./format.js";
5
5
  import { currentGitHead } from "./git_state.js";
6
6
  import { docRef, sha256File } from "./store.js";
7
- import { isReviewReadyVerifier, isFreshReviewVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest, } from "./review.js";
7
+ import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRoles, readReviewPolicyFromEvents, reviewGateRoleResolution, reviewRejectionOverrideScope, reviewEvidenceDigest, } from "./review.js";
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 {
@@ -22,16 +25,49 @@ function phaseConfirmationStep(context, boundary, reason) {
22
25
  reason,
23
26
  };
24
27
  }
25
- function reviewGatePlan(snapshot, gate, requiredRoles) {
28
+ function reviewGatePlan(snapshot, events, changeRoot, gate, requiredRoles) {
26
29
  const missingRoles = [];
27
30
  for (const role of requiredRoles) {
28
31
  const openForRole = snapshot.open_jobs.find(job => gate.isJobForGate(job) && job.role === role);
29
32
  if (openForRole) {
30
33
  return { kind: "blocked", reason: `状态未推进;已有待完成工作项 ${role}(${openForRole.job_id})`, jobs: [openForRole] };
31
34
  }
32
- const acceptedForRole = snapshot.accepted_jobs.find(job => gate.isJobForGate(job) && job.role === role);
33
- if (!acceptedForRole)
34
- missingRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
35
+ const resolution = reviewGateRoleResolution(events, changeRoot, gate, role);
36
+ if (resolution.kind === "accepted" || resolution.kind === "overridden")
37
+ continue;
38
+ if (resolution.kind === "rejected_pending") {
39
+ const terminal = resolution.terminal;
40
+ const overrideScope = reviewRejectionOverrideScope(terminal.job.job_id);
41
+ return {
42
+ kind: "blocked",
43
+ jobs: [],
44
+ reason: `状态未推进;${role} 审查工作项 ${terminal.job.job_id} 已拒绝。请修改绑定材料、在整份报告没有有效 blocker 时登记整体裁决,或在涉及业务决定时询问用户`,
45
+ details: {
46
+ review_rejection: {
47
+ job_id: terminal.job.job_id,
48
+ role,
49
+ gate_id: gate.gate_id,
50
+ packet_digest: terminal.job.packet_digest,
51
+ result_kind: terminal.result_kind,
52
+ reason: terminal.reason ?? "报告结论为 fail,工作项未通过",
53
+ override_scope: overrideScope,
54
+ allowed_actions: ["modify_materials", "record_override", "ask_user"],
55
+ record_input: {
56
+ scope: overrideScope,
57
+ answer: "do_not_block",
58
+ reason: "<说明整份报告为何没有有效 blocker>",
59
+ decision_source: "main_process",
60
+ },
61
+ },
62
+ },
63
+ };
64
+ }
65
+ const reason = resolution.kind === "stale"
66
+ ? `需求 ${role} 的最新审查工作项已过期:${resolution.stale_reason}`
67
+ : resolution.kind === "rejected_invalid"
68
+ ? `需求 ${role} 的最新审查报告无效,不能通过整体裁决绕过`
69
+ : `需求 ${role} 无已接受的工作项`;
70
+ missingRoles.push({ role, reason });
35
71
  }
36
72
  if (missingRoles.length > 0) {
37
73
  return {
@@ -52,14 +88,26 @@ function validateTasksPlan(changeRoot) {
52
88
  return "tasks.md 内容不像任务计划文档";
53
89
  return null;
54
90
  }
55
- 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) {
56
104
  const tasksPath = join(changeRoot, "tasks.md");
57
105
  if (!existsSync(tasksPath))
58
106
  return { ok: true, mode: false };
59
107
  const tasksContent = readFileSync(tasksPath, "utf8");
60
108
  const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
61
109
  const testContractContent = existsSync(testContractPath) ? readFileSync(testContractPath, "utf8") : null;
62
- const validation = validateExecutionRequirements(tasksContent, testContractContent);
110
+ const validation = validateExecutionRequirements(tasksContent, testContractContent, executionPolicy, executionRequirementVersion);
63
111
  return validation.ok
64
112
  ? { ok: true, mode: validation.mode }
65
113
  : { ok: false, mode: validation.mode, message: validation.errors.join(";") };
@@ -68,7 +116,7 @@ function missingBaseArtifact(changeRoot, risk) {
68
116
  if (risk === "minimal")
69
117
  return null;
70
118
  const artifactsDir = join(changeRoot, ".superspec", "artifacts");
71
- for (const doc of ["discovery.md", "business-invariants.md", "test-contract.md"]) {
119
+ for (const doc of ["discovery.md", "test-contract.md"]) {
72
120
  if (!existsSync(join(artifactsDir, doc)))
73
121
  return `基础职责缺失:${doc} 不存在(risk=${risk} 需要)`;
74
122
  }
@@ -83,6 +131,15 @@ export function proposalDocsBaseline(changeRoot) {
83
131
  }
84
132
  return baseline;
85
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
+ }
86
143
  function isDigestMap(value) {
87
144
  if (!value || typeof value !== "object" || Array.isArray(value))
88
145
  return false;
@@ -119,23 +176,27 @@ export function latestReopenProposeBaseline(events) {
119
176
  }
120
177
  return null;
121
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
+ }
122
193
  export function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
123
194
  const current = proposalDocsBaseline(changeRoot);
124
195
  return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
125
196
  }
126
- export function historicalProposeReadyRoles(events) {
127
- const roles = new Set();
128
- for (const ev of events) {
129
- if (ev.event_type !== "transition_commit")
130
- continue;
131
- const newJobs = ev.payload.new_jobs ?? [];
132
- for (const job of newJobs) {
133
- if (PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job) &&
134
- (job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
135
- roles.add(job.role);
136
- }
137
- }
138
- return [...roles];
197
+ export function discoveryDocsChangedSinceBaseline(changeRoot, baseline) {
198
+ const current = discoveryDocsBaseline(changeRoot);
199
+ return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
139
200
  }
140
201
  export function pendingTaskIds(changeRoot) {
141
202
  const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
@@ -152,6 +213,27 @@ function latestStartApplyIndex(events) {
152
213
  }
153
214
  return -1;
154
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
+ }
155
237
  export function applyRequirementModeForCurrentRound(events) {
156
238
  const index = latestStartApplyIndex(events);
157
239
  if (index < 0)
@@ -219,14 +301,13 @@ export function pendingTaskStatusForApply(changeRoot, events) {
219
301
  export function formatPendingTaskMessage(ids, action) {
220
302
  return `尚有未完成任务:${ids.join(", ")};${action}`;
221
303
  }
222
- function nextArgv(change, risk) {
304
+ function nextArgv(change, _risk) {
223
305
  return [
224
306
  "superspec",
225
307
  "transition",
226
308
  "next",
227
309
  "--change",
228
310
  change,
229
- ...(risk === "strict" ? [] : ["--risk", risk]),
230
311
  ];
231
312
  }
232
313
  function acceptedMaterialFollowup(change, risk, planDocsChangedSinceAccept) {
@@ -262,6 +343,15 @@ export function planNextStep(context) {
262
343
  }
263
344
  return { kind: "run_transition", state: "init", transition: "explore", reason: "初始化完成,开始探索" };
264
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
+ }
265
355
  const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
266
356
  if (exploreReviewJobs.length > 0) {
267
357
  return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
@@ -286,7 +376,7 @@ export function planNextStep(context) {
286
376
  return { kind: "ask_user", state: "explore", ask, reason: `有 ${openQs} 个未确认问题` };
287
377
  }
288
378
  const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
289
- if (!reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
379
+ if (!reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
290
380
  const confirmation = phaseConfirmationStep(context, "explore_to_propose", "探索完成,等待用户确认进入计划阶段");
291
381
  if (confirmation)
292
382
  return confirmation;
@@ -333,6 +423,16 @@ export function planNextStep(context) {
333
423
  if (confirmation)
334
424
  return confirmation;
335
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
+ }
336
436
  return { kind: "run_transition", state: "propose_ready", transition: "start-apply", reason: "计划就绪,开始执行" };
337
437
  }
338
438
  case "apply":
@@ -609,14 +709,18 @@ function planExploreTransition(context) {
609
709
  if (snapshot.state !== "explore") {
610
710
  return { kind: "skip", message: `当前状态 ${snapshot.state},explore 不适用` };
611
711
  }
712
+ const reopenBaseline = latestReopenExploreBaseline(events);
713
+ if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
714
+ return { kind: "skip", message: "回到 explore 后至少一个 discovery 材料必须变化" };
715
+ }
612
716
  const discoveryCheck = validateDiscovery(changeRoot);
613
717
  if (!discoveryCheck.ok)
614
718
  return { kind: "skip", message: discoveryCheck.message };
615
719
  const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
616
- const gatePlan = reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
720
+ const gatePlan = reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
617
721
  if (gatePlan)
618
722
  return gatePlan;
619
- const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "explore_to_propose");
723
+ const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "explore_to_propose", mode.risk);
620
724
  const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
621
725
  if (!confirmation || decision?.decision !== "advance") {
622
726
  return { kind: "skip", message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Explore 阶段确认范围" };
@@ -637,7 +741,7 @@ function planProposeReadyTransition(context) {
637
741
  const tasksPlanError = validateTasksPlan(changeRoot);
638
742
  if (tasksPlanError)
639
743
  return { kind: "skip", message: tasksPlanError };
640
- const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
744
+ const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicyForRisk(risk), 2);
641
745
  if (!executionRequirementPlan.ok)
642
746
  return { kind: "skip", message: executionRequirementPlan.message };
643
747
  const openQuestions = collectProposeOpenQuestions(changeRoot);
@@ -649,10 +753,24 @@ function planProposeReadyTransition(context) {
649
753
  if (missingArtifact)
650
754
  return { kind: "skip", message: missingArtifact };
651
755
  const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
652
- const gatePlan = reviewGatePlan(snapshot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
756
+ const gatePlan = reviewGatePlan(snapshot, context.events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
653
757
  if (gatePlan)
654
758
  return gatePlan;
655
- return { kind: "advance", fromState: "propose", toState: "propose_ready", reason: `risk=${risk},所有需求已满足` };
759
+ return {
760
+ kind: "advance",
761
+ fromState: "propose",
762
+ toState: "propose_ready",
763
+ reason: `risk=${risk},所有需求已满足`,
764
+ payload: {
765
+ workflow_mode: risk,
766
+ execution_requirement_version: 2,
767
+ },
768
+ };
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;
656
774
  }
657
775
  function planStartApplyTransition(context, enforceConfirmation = true) {
658
776
  const { changeRoot, events, projectRoot, snapshot } = context;
@@ -663,23 +781,31 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
663
781
  // 按基线实际键名提示:升级前留下的旧基线可能不含 specs/,静态清单会误导
664
782
  return { kind: "skip", message: `回到 propose 后至少一个计划文档必须变化(基线绑定:${Object.keys(reopenBaseline).join("、")})` };
665
783
  }
666
- const reviewedRoles = historicalProposeReadyRoles(events);
667
- if (reviewedRoles.length > 0) {
668
- const gatePlan = reviewGatePlan(snapshot, PROPOSE_FINAL_REVIEW_GATE, reviewedRoles);
669
- if (gatePlan) {
670
- return {
671
- ...gatePlan,
672
- reason: `进入执行阶段前需要重新完成计划文档审查:${gatePlan.reason}`,
673
- };
674
- }
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
+ };
675
795
  }
676
- 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);
677
801
  if (!executionRequirementPlan.ok)
678
802
  return { kind: "skip", message: executionRequirementPlan.message };
679
- const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply");
680
- const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
681
- if (enforceConfirmation && (!confirmation || decision?.decision !== "advance")) {
682
- 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
+ };
683
809
  }
684
810
  const gitHead = currentGitHead(projectRoot);
685
811
  return {
@@ -691,7 +817,14 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
691
817
  apply_start_head: gitHead.head,
692
818
  apply_start_head_reason: gitHead.reason,
693
819
  apply_contract_mode: executionRequirementPlan.mode,
694
- ...(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) : {}),
695
828
  },
696
829
  };
697
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[];