@peterxiaoyang/superspec 0.1.40 → 0.1.42

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.
package/README.md CHANGED
@@ -8,10 +8,10 @@
8
8
 
9
9
  它解决的是一个很常见的问题:AI 编程工具写代码很快,但有时候还没搞清楚需求、现有代码和测试边界,就已经开始改文件了。
10
10
 
11
- SuperSpec 会把一次需求变更拆成 5 步:
11
+ SuperSpec 会把一次需求变更拆成 4 个阶段:
12
12
 
13
13
  ```text
14
- 探索需求 -> 写方案 -> 做实现 -> 代码审查与最终验证 -> 归档收尾
14
+ 探索需求 -> 写方案 -> 做实现 -> 代码审查与最终验证 -> accepted
15
15
  ```
16
16
 
17
17
  这样做的目的很简单:
@@ -20,7 +20,7 @@ SuperSpec 会把一次需求变更拆成 5 步:
20
20
  - 写实现前先有方案和任务
21
21
  - 任务完成前先有测试或验证记录
22
22
  - 宣布完成前先经过审查
23
- - 归档时保留关键过程记录
23
+ - 进入 accepted 时保留关键过程记录
24
24
 
25
25
  ## 适合谁
26
26
 
@@ -53,12 +53,12 @@ SuperSpec 管“AI 应该怎样把这次改动做稳”。
53
53
  | 方案怎么写 | 提供标准的变更文档结构 | 要求方案前后有范围、风险、业务约束和测试思路 |
54
54
  | 代码怎么做 | 记录任务清单和完成状态 | 要求按任务实现,并留下测试或验证记录 |
55
55
  | 做完怎么算稳 | 可以校验规格和归档 | 增加代码审查、架构审查、反方审查和最终验证 |
56
- | 以后怎么追溯 | 保留 OpenSpec 的变更文档 | 额外保留探索、测试、审查和收尾记录 |
56
+ | 以后怎么追溯 | 保留 OpenSpec 的变更文档 | 额外保留探索、测试和审查记录 |
57
57
 
58
58
  举个例子:
59
59
 
60
60
  OpenSpec 会帮你记录“要增加登录功能、需要哪些规格、设计和任务”。
61
- SuperSpec 会进一步要求 AI 先看看现有登录/权限代码在哪里、哪些业务规则不能破坏、哪些场景必须测试、实现后要经过哪些审查,最后再归档。
61
+ SuperSpec 会进一步要求 AI 先看看现有登录/权限代码在哪里、哪些业务规则不能破坏、哪些场景必须测试、实现后要经过哪些审查,最终进入 accepted 完成本轮流程。
62
62
 
63
63
  所以 SuperSpec 不是 OpenSpec 的替代品。它更像是 OpenSpec 外面的一层执行纪律,专门约束 AI 编程工具不要跳过关键步骤。
64
64
 
@@ -118,13 +118,9 @@ superspec.cmd install
118
118
  使用 superspec-review,完成代码审查、问题处理和最终验证。
119
119
  ```
120
120
 
121
- 审查通过后,归档:
121
+ 审查通过并进入 `accepted` 后,本轮工作流完成。如果之后需要补充或修改需求、方案或验收内容,直接用自然语言告诉 Agent;Agent 会按引擎返回的内部 continuation 自动回到计划阶段并重新完成后续审查。
122
122
 
123
- ```text
124
- 使用 superspec-archive,归档这个变更。
125
- ```
126
-
127
- ## 五个入口分别做什么
123
+ ## 四个入口分别做什么
128
124
 
129
125
  | 入口 | 什么时候用 | 它会要求做什么 |
130
126
  |---|---|---|
@@ -132,9 +128,8 @@ superspec.cmd install
132
128
  | `superspec-propose` | 需求已经清楚后 | 写正式方案、规格、设计和任务,并提前规划测试 |
133
129
  | `superspec-apply` | 方案通过后 | 按任务实现代码,记录测试或验证结果 |
134
130
  | `superspec-review` | 实现完成后 | 检查代码实现是否符合方案,处理审查问题,并完成最终验证 |
135
- | `superspec-archive` | 审查通过后 | 用 OpenSpec 完成归档,并检查关键记录是否保留 |
136
131
 
137
- 你日常主要记住这五个入口就够了。
132
+ 你日常主要记住这四个入口就够了。
138
133
 
139
134
  CLI 不带 `--risk` 时默认走完整审查路径;需要轻量路径时显式传 `--risk normal` 或 `--risk minimal`。探索阶段会创建 `critic` 工作项审查需求澄清记录;计划阶段会创建 `critic`、`architect` 和 `test-engineer` 工作项后再进入实现准备。
140
135
 
@@ -226,7 +221,6 @@ superspec update
226
221
  skills/superspec-propose/
227
222
  skills/superspec-apply/
228
223
  skills/superspec-review/
229
- skills/superspec-archive/
230
224
  prompts/architect.md
231
225
  prompts/code-reviewer.md
232
226
  prompts/critic.md
package/dist/cli.js CHANGED
@@ -7,9 +7,10 @@ import { installProject } from "./install.js";
7
7
  import { writeSnapshot } from "./store.js";
8
8
  import { rebuildSnapshot } from "./sync.js";
9
9
  import { next as nextCmd } from "./next.js";
10
- import { proposeReady, commitTransition, transitionInit, transitionExplore, startApply, taskStart, taskComplete, reopen, reviewReady, accept, archive } from "./transition.js";
10
+ import { proposeReady, commitTransition, transitionInit, transitionExplore, startApply, taskStart, taskComplete, reopen, reviewReady, accept } from "./transition.js";
11
11
  import { recordJobSubmit, recordJobSubmitContent, recordUserDecision, recordUserDecisionContent, jobsList, jobsPacket } from "./record.js";
12
12
  import { recordTestRun, recordTestRunContent } from "./task.js";
13
+ import { RecordInputDecodingError, decodeRecordInput } from "./record_input.js";
13
14
  import { probeOpenSpec, openspecStatus, changeRoot } from "./openspec.js";
14
15
  import { SUPERSPEC_VERSION } from "./version.js";
15
16
  const PACKAGE_NAME = "@peterxiaoyang/superspec";
@@ -54,7 +55,7 @@ function readStdinRecordContent(flag) {
54
55
  if (process.stdin.isTTY === true) {
55
56
  throw new StdinRecordInputError(flag);
56
57
  }
57
- return readFileSync(0, "utf8");
58
+ return decodeRecordInput(readFileSync(0));
58
59
  }
59
60
  function transitionExitCode(result) {
60
61
  if (result.outcome === "blocked")
@@ -516,8 +517,8 @@ transition 子命令:
516
517
  init / explore / sync / next / propose-ready / start-apply
517
518
  task-start --task <T> / task-complete --task <T> [--input -]
518
519
  reopen --to apply --reason <TEXT> [--review-fix <JOB#FINDING>]
519
- reopen --to propose --reason <TEXT> --review-finding <JOB#FINDING>
520
- review-ready / accept / archive
520
+ reopen --to propose --reason <TEXT> [--review-finding <JOB#FINDING>]
521
+ review-ready / accept
521
522
 
522
523
  record 子命令:
523
524
  job-submit --job <J> --report <F|->
@@ -709,7 +710,7 @@ jobs 子命令:
709
710
  inputContent = readStdinRecordContent("--input");
710
711
  }
711
712
  catch (err) {
712
- if (err instanceof StdinRecordInputError) {
713
+ if (err instanceof StdinRecordInputError || err instanceof RecordInputDecodingError) {
713
714
  console.error(err.message);
714
715
  return 1;
715
716
  }
@@ -751,11 +752,6 @@ jobs 子命令:
751
752
  console.log(JSON.stringify(result, null, 2));
752
753
  return transitionExitCode(result);
753
754
  }
754
- case "archive": {
755
- const result = archive(projectRoot, change, cr);
756
- console.log(JSON.stringify(result, null, 2));
757
- return transitionExitCode(result);
758
- }
759
755
  default:
760
756
  console.error(`未知的 transition 子命令:${subcommand}`);
761
757
  return 1;
@@ -778,7 +774,7 @@ jobs 子命令:
778
774
  : recordJobSubmit(projectRoot, change, cr, jobId, report);
779
775
  }
780
776
  catch (err) {
781
- if (err instanceof StdinRecordInputError) {
777
+ if (err instanceof StdinRecordInputError || err instanceof RecordInputDecodingError) {
782
778
  console.error(err.message);
783
779
  return 1;
784
780
  }
@@ -800,7 +796,7 @@ jobs 子命令:
800
796
  : recordUserDecision(projectRoot, change, inputFile);
801
797
  }
802
798
  catch (err) {
803
- if (err instanceof StdinRecordInputError) {
799
+ if (err instanceof StdinRecordInputError || err instanceof RecordInputDecodingError) {
804
800
  console.error(err.message);
805
801
  return 1;
806
802
  }
@@ -822,7 +818,7 @@ jobs 子命令:
822
818
  : recordTestRun(projectRoot, change, inputFile);
823
819
  }
824
820
  catch (err) {
825
- if (err instanceof StdinRecordInputError) {
821
+ if (err instanceof StdinRecordInputError || err instanceof RecordInputDecodingError) {
826
822
  console.error(err.message);
827
823
  return 1;
828
824
  }
@@ -1,4 +1,4 @@
1
- import type { CodeReviewResultKind, CodeReviewScope, CodeStateCheck, CoverageExemptionRef, Event, Job, JobPacketContext, Ref } from "./types.ts";
1
+ import type { CodeReviewResultKind, CodeReviewScope, CodeStateCheck, CoverageExemptionRef, Event, Job, JobPacketContext, Ref, ReviewPreviousRejection } from "./types.ts";
2
2
  export declare const CODE_REVIEW_REPAIR_SCOPE_PREFIX = "code_reviewer_report_repair:";
3
3
  export declare const CODE_REVIEW_DECISION_SCOPE_PREFIX = "code_review_decision:";
4
4
  export declare const TEST_COVERAGE_EXEMPTION_SCOPE_PREFIX = "test_coverage_exemption:";
@@ -63,11 +63,7 @@ export declare function codeReviewPacketDigest(input: {
63
63
  checkedDocs: string[];
64
64
  created_from_transition: string;
65
65
  packet_context?: JobPacketContext;
66
- previous_rejection?: {
67
- result_kind: CodeReviewResultKind;
68
- reason: string;
69
- job_id: string;
70
- };
66
+ previous_rejection?: ReviewPreviousRejection;
71
67
  }): string;
72
68
  export declare function codeReviewPacketContext(changeRoot: string, projectRoot: string, scope: CodeReviewScope, events: Event[]): JobPacketContext;
73
69
  export declare function effectiveCoverageExemptionRefsFromEvents(events: Event[]): CoverageExemptionRef[];
package/dist/install.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const WORKFLOW_SKILLS: readonly ["superspec-explore", "superspec-propose", "superspec-apply", "superspec-review", "superspec-archive"];
1
+ export declare const WORKFLOW_SKILLS: readonly ["superspec-explore", "superspec-propose", "superspec-apply", "superspec-review"];
2
2
  export declare const WORKFLOW_PROMPTS: readonly ["architect.md", "code-reviewer.md", "critic.md", "executor.md", "explore.md", "test-engineer.md", "test-runner.md", "verifier.md"];
3
3
  export declare const WORKFLOW_AGENTS: readonly ["architect.toml", "code-reviewer.toml", "critic.toml", "executor.toml", "explore.toml", "test-engineer.toml", "test-runner.toml", "verifier.toml"];
4
4
  export interface InstallResult {
package/dist/install.js CHANGED
@@ -6,8 +6,8 @@ export const WORKFLOW_SKILLS = [
6
6
  "superspec-propose",
7
7
  "superspec-apply",
8
8
  "superspec-review",
9
- "superspec-archive",
10
9
  ];
10
+ const DEPRECATED_WORKFLOW_SKILLS = ["superspec-archive"];
11
11
  export const WORKFLOW_PROMPTS = [
12
12
  "architect.md",
13
13
  "code-reviewer.md",
@@ -74,6 +74,9 @@ function writeBundledFile(src, dest) {
74
74
  }
75
75
  function copySkills(templateRoot, projectRoot) {
76
76
  const skillsDest = join(projectRoot, ".codex", "skills");
77
+ for (const skill of DEPRECATED_WORKFLOW_SKILLS) {
78
+ rmSync(join(skillsDest, skill), { recursive: true, force: true });
79
+ }
77
80
  const installedSkills = [];
78
81
  for (const skill of WORKFLOW_SKILLS) {
79
82
  const src = join(templateRoot, "skills", skill, "SKILL.md");
@@ -280,7 +283,7 @@ export function installProject(projectRoot, options = {}) {
280
283
  if (!options.allowLegacyState && legacyStateFound(projectRoot)) {
281
284
  throw new Error("检测到老版 SuperSpec (0.x) 的状态文件。\n" +
282
285
  `SuperSpec ${SUPERSPEC_VERSION} 是全新引擎,不兼容 0.x 的状态格式。\n` +
283
- `请先用老版(0.1.x)完成或归档现有 change,再安装 ${SUPERSPEC_VERSION}。\n` +
286
+ `请先用老版(0.1.x)完成现有 change,再安装 ${SUPERSPEC_VERSION}。\n` +
284
287
  "或在全新项目目录中安装。");
285
288
  }
286
289
  const templateRoot = options.templateRoot ?? defaultTemplateRoot();
package/dist/next.js CHANGED
@@ -40,14 +40,6 @@ function toNextOutput(change, plan) {
40
40
  return requiredJobsOutput(plan.state, change, plan.jobs, plan.reason);
41
41
  case "ask_user":
42
42
  return { state: plan.state, path: "ask_user", ask_user: plan.ask, reason: plan.reason };
43
- case "ask_archive_confirmation": {
44
- const ask = {
45
- question: `审查已通过,流程停在 accepted。确认归档时请执行 ${transitionCommand(change, "archive")}`,
46
- allowed_answers: ["确认归档"],
47
- scope: "archive_confirmation",
48
- };
49
- return { state: "accepted", path: "ask_user", ask_user: ask, reason: plan.reason };
50
- }
51
43
  case "run_transition":
52
44
  return {
53
45
  state: plan.state,
@@ -57,7 +49,12 @@ function toNextOutput(change, plan) {
57
49
  missing_inputs: [],
58
50
  };
59
51
  case "done":
60
- return { state: plan.state, path: "done", reason: plan.reason };
52
+ return {
53
+ state: plan.state,
54
+ path: "done",
55
+ reason: plan.reason,
56
+ ...(plan.continuation ? { continuation: plan.continuation } : {}),
57
+ };
61
58
  }
62
59
  }
63
60
  /** next 命令:读 snapshot,返回唯一可执行路径 */
@@ -0,0 +1,31 @@
1
+ import { type ReviewRisk } from "./review.ts";
2
+ import type { AskUser, AskUserAction, Event, Snapshot } from "./types.ts";
3
+ export declare const PHASE_CONFIRMATION_SCOPE_PREFIX = "phase_confirmation:";
4
+ export type PhaseBoundary = "explore_to_propose" | "propose_to_apply" | "apply_to_review";
5
+ export type PhaseDecision = "advance" | "stay";
6
+ export interface PhaseDecisionAction extends AskUserAction {
7
+ boundary: PhaseBoundary;
8
+ decision: PhaseDecision;
9
+ }
10
+ export interface PhaseConfirmation {
11
+ boundary: PhaseBoundary;
12
+ epoch_event_id: string;
13
+ material_digest: string;
14
+ scope: string;
15
+ actions: PhaseDecisionAction[];
16
+ ask: AskUser;
17
+ }
18
+ export interface PhaseConfirmationDecision {
19
+ event: Event;
20
+ scope: string;
21
+ answer: string;
22
+ decision: PhaseDecision;
23
+ }
24
+ export declare function isPhaseConfirmationScope(value: unknown): value is string;
25
+ export declare function phaseConfirmationForBoundary(projectRoot: string, events: Event[], snapshot: Snapshot, boundary: PhaseBoundary, risk?: ReviewRisk): PhaseConfirmation | null;
26
+ export declare function phaseConfirmationForCurrentState(projectRoot: string, events: Event[], snapshot: Snapshot, risk?: ReviewRisk): PhaseConfirmation | null;
27
+ export declare function phaseActionForAnswer(confirmation: PhaseConfirmation, answer: unknown): PhaseDecisionAction | null;
28
+ export declare function latestAcceptedPhaseDecision(events: Event[], confirmation: PhaseConfirmation): PhaseConfirmationDecision | null;
29
+ export declare function isPhaseAdvanceAuthorized(events: Event[], confirmation: PhaseConfirmation): boolean;
30
+ export declare function phaseConfirmationCommitPayload(confirmation: PhaseConfirmation, decision: PhaseConfirmationDecision): Record<string, unknown>;
31
+ export declare function phaseConfirmationMissingMessage(confirmation: PhaseConfirmation): string;
@@ -0,0 +1,218 @@
1
+ import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
2
+ import { reviewEvidenceDigest } from "./review.js";
3
+ import { findLatestEvent, sha256Text } from "./store.js";
4
+ export const PHASE_CONFIRMATION_SCOPE_PREFIX = "phase_confirmation:";
5
+ function latestTransition(events, predicate) {
6
+ return findLatestEvent(events, "transition_commit", event => predicate(event.payload));
7
+ }
8
+ const SPECS = {
9
+ explore_to_propose: {
10
+ state: "explore",
11
+ question: "探索与探索审查已完成,请选择进入计划阶段,或留在探索阶段继续完善。",
12
+ actions: [
13
+ { decision: "advance", label: "确认进入计划阶段", reason: "none", resume: { kind: "next" } },
14
+ {
15
+ decision: "stay",
16
+ label: "留在探索阶段继续完善",
17
+ reason: "required",
18
+ reasonPrompt: "请说明还需要补充或核对哪些探索信息。",
19
+ resume: {
20
+ kind: "continue_current_phase",
21
+ instruction: "根据用户说明继续完善 discovery;完成后重新执行 next。",
22
+ },
23
+ },
24
+ ],
25
+ scopePrefix: `${PHASE_CONFIRMATION_SCOPE_PREFIX}explore_to_propose`,
26
+ epoch: events => latestTransition(events, payload => payload.from_state === "init" && payload.to_state === "explore"),
27
+ },
28
+ propose_to_apply: {
29
+ state: "propose_ready",
30
+ question: "计划文档与计划审查已完成,请选择开始实现,或留在计划阶段继续完善。",
31
+ actions: [
32
+ { decision: "advance", label: "确认开始实现", reason: "none", resume: { kind: "next" } },
33
+ {
34
+ decision: "stay",
35
+ label: "留在计划阶段继续完善",
36
+ reason: "required",
37
+ reasonPrompt: "请说明还需要调整哪些计划内容。",
38
+ resume: {
39
+ kind: "continue_current_phase",
40
+ instruction: "根据用户说明继续完善绑定计划材料;完成后重新执行 next。",
41
+ },
42
+ },
43
+ ],
44
+ scopePrefix: `${PHASE_CONFIRMATION_SCOPE_PREFIX}propose_to_apply`,
45
+ epoch: events => latestTransition(events, payload => payload.to_state === "propose_ready"),
46
+ },
47
+ apply_to_review: {
48
+ state: "apply_done",
49
+ question: "实现、测试证据与代码审查已完成,请选择进入最终审查,或暂不进入最终审查。",
50
+ actions: [
51
+ { decision: "advance", label: "确认进入最终审查", reason: "none", resume: { kind: "next" } },
52
+ { decision: "stay", label: "暂不进入最终审查", reason: "optional", resume: { kind: "stop" } },
53
+ ],
54
+ scopePrefix: `${PHASE_CONFIRMATION_SCOPE_PREFIX}apply_to_review`,
55
+ epoch: events => latestTransition(events, payload => payload.from_state === "apply" && payload.to_state === "apply_done"),
56
+ },
57
+ };
58
+ function boundaryForState(state) {
59
+ switch (state) {
60
+ case "explore": return "explore_to_propose";
61
+ case "propose_ready": return "propose_to_apply";
62
+ case "apply_done": return "apply_to_review";
63
+ default: return null;
64
+ }
65
+ }
66
+ function materialDigest(projectRoot, events, snapshot) {
67
+ const head = currentGitHead(projectRoot);
68
+ const dirty = dirtyCodeFiles(projectRoot);
69
+ const acceptedJobs = snapshot.accepted_jobs
70
+ .map(job => ({
71
+ job_id: job.job_id,
72
+ role: job.role,
73
+ gate_id: job.gate_id ?? null,
74
+ packet_digest: job.packet_digest,
75
+ review_evidence_digest: job.review_evidence_digest ?? null,
76
+ }))
77
+ .sort((a, b) => a.job_id.localeCompare(b.job_id));
78
+ const documents = Object.entries(snapshot.document_digests)
79
+ .sort(([left], [right]) => left.localeCompare(right));
80
+ return sha256Text(JSON.stringify({
81
+ documents,
82
+ tasks_structure_digest: snapshot.tasks_structure_digest,
83
+ accepted_jobs: acceptedJobs,
84
+ review_evidence_digest: reviewEvidenceDigest(events),
85
+ code_state: {
86
+ head: head.head,
87
+ head_reason: head.reason,
88
+ dirty_files: dirty.files,
89
+ ...(dirty.ok ? {} : { dirty_files_reason: dirty.reason }),
90
+ },
91
+ }));
92
+ }
93
+ function phaseRecordArgv(change) {
94
+ return ["superspec", "record", "user-decision", "--change", change, "--input", "-"];
95
+ }
96
+ function nextArgv(change, risk) {
97
+ return [
98
+ "superspec",
99
+ "transition",
100
+ "next",
101
+ "--change",
102
+ change,
103
+ ...(risk === "strict" ? [] : ["--risk", risk]),
104
+ ];
105
+ }
106
+ function buildActions(change, boundary, scope, question, specs, risk) {
107
+ if (specs.length < 2)
108
+ throw new Error(`${boundary} 至少需要两个 phase actions`);
109
+ const labels = new Set();
110
+ for (const spec of specs) {
111
+ if (labels.has(spec.label))
112
+ throw new Error(`${boundary} 存在重复 action label:${spec.label}`);
113
+ labels.add(spec.label);
114
+ }
115
+ if (!specs.some(action => action.decision === "advance")) {
116
+ throw new Error(`${boundary} 缺少 advance action`);
117
+ }
118
+ if (!specs.some(action => action.decision === "stay")) {
119
+ throw new Error(`${boundary} 缺少 stay action`);
120
+ }
121
+ return specs.map(spec => ({
122
+ boundary,
123
+ decision: spec.decision,
124
+ label: spec.label,
125
+ selection: "exact_label",
126
+ reason: spec.reason,
127
+ ...(spec.reasonPrompt ? { reason_prompt: spec.reasonPrompt } : {}),
128
+ record_argv: phaseRecordArgv(change),
129
+ record_input: { scope, question, answer: spec.label },
130
+ resume: spec.resume.kind === "next"
131
+ ? { kind: "next", argv: nextArgv(change, risk) }
132
+ : spec.resume.kind === "continue_current_phase"
133
+ ? {
134
+ kind: "continue_current_phase",
135
+ instruction: spec.resume.instruction,
136
+ next_argv_after_completion: nextArgv(change, risk),
137
+ }
138
+ : { kind: "stop" },
139
+ }));
140
+ }
141
+ export function isPhaseConfirmationScope(value) {
142
+ return typeof value === "string" && value.startsWith(PHASE_CONFIRMATION_SCOPE_PREFIX);
143
+ }
144
+ export function phaseConfirmationForBoundary(projectRoot, events, snapshot, boundary, risk = "strict") {
145
+ const spec = SPECS[boundary];
146
+ if (snapshot.state !== spec.state)
147
+ return null;
148
+ const epoch = spec.epoch(events);
149
+ const epochEventId = epoch?.event_id ?? `legacy-${spec.state}`;
150
+ const digest = materialDigest(projectRoot, events, snapshot);
151
+ const scope = `${spec.scopePrefix}:${epochEventId}:${digest}`;
152
+ const actions = buildActions(snapshot.change_id, boundary, scope, spec.question, spec.actions, risk);
153
+ return {
154
+ boundary,
155
+ epoch_event_id: epochEventId,
156
+ material_digest: digest,
157
+ scope,
158
+ actions,
159
+ ask: {
160
+ question: spec.question,
161
+ allowed_answers: actions.map(action => action.label),
162
+ scope,
163
+ actions,
164
+ },
165
+ };
166
+ }
167
+ export function phaseConfirmationForCurrentState(projectRoot, events, snapshot, risk = "strict") {
168
+ const boundary = boundaryForState(snapshot.state);
169
+ return boundary
170
+ ? phaseConfirmationForBoundary(projectRoot, events, snapshot, boundary, risk)
171
+ : null;
172
+ }
173
+ export function phaseActionForAnswer(confirmation, answer) {
174
+ return typeof answer === "string"
175
+ ? confirmation.actions.find(action => action.label === answer) ?? null
176
+ : null;
177
+ }
178
+ export function latestAcceptedPhaseDecision(events, confirmation) {
179
+ const event = findLatestEvent(events, "user_decision_recorded", candidate => {
180
+ const payload = candidate.payload;
181
+ const phase = payload.phase_confirmation;
182
+ return payload.accepted !== false &&
183
+ payload.scope === confirmation.scope &&
184
+ phase?.boundary === confirmation.boundary &&
185
+ (phase.decision === "advance" || phase.decision === "stay");
186
+ });
187
+ if (!event)
188
+ return null;
189
+ const payload = event.payload;
190
+ return {
191
+ event,
192
+ scope: confirmation.scope,
193
+ answer: payload.answer,
194
+ decision: payload.phase_confirmation.decision,
195
+ };
196
+ }
197
+ export function isPhaseAdvanceAuthorized(events, confirmation) {
198
+ return latestAcceptedPhaseDecision(events, confirmation)?.decision === "advance";
199
+ }
200
+ export function phaseConfirmationCommitPayload(confirmation, decision) {
201
+ if (decision.decision !== "advance") {
202
+ throw new Error(`${confirmation.boundary} 的 stay decision 不能写入阶段推进 commit`);
203
+ }
204
+ return {
205
+ phase_confirmation: {
206
+ boundary: confirmation.boundary,
207
+ decision: decision.decision,
208
+ epoch_event_id: confirmation.epoch_event_id,
209
+ material_digest: confirmation.material_digest,
210
+ scope: confirmation.scope,
211
+ decision_event_id: decision.event.event_id,
212
+ },
213
+ };
214
+ }
215
+ export function phaseConfirmationMissingMessage(confirmation) {
216
+ const answers = confirmation.actions.map(action => action.label).join(" / ");
217
+ return `缺少当前阶段的用户确认;请先执行 next,并按返回的 scope=${confirmation.scope} 精确选择:${answers}`;
218
+ }
@@ -1,8 +1,8 @@
1
1
  import type { ReviewGateRule } from "./review_job_gates.ts";
2
- import type { AskUser, Event, Job, JobRole, State } from "./types.ts";
2
+ import type { AcceptedMaterialFollowupContinuation, AskUser, Event, Job, JobRole, State } from "./types.ts";
3
3
  import type { Snapshot } from "./types.ts";
4
4
  import type { ReviewRisk } from "./review.ts";
5
- export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept" | "archive";
5
+ export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept";
6
6
  export type WorkflowMode = {
7
7
  kind: "risk";
8
8
  risk: ReviewRisk;
@@ -43,10 +43,6 @@ export type NextStepPlan = {
43
43
  state: State;
44
44
  ask: AskUser;
45
45
  reason: string;
46
- } | {
47
- kind: "ask_archive_confirmation";
48
- state: "accepted";
49
- reason: string;
50
46
  } | {
51
47
  kind: "run_transition";
52
48
  state: State;
@@ -59,6 +55,7 @@ export type NextStepPlan = {
59
55
  kind: "done";
60
56
  state: State;
61
57
  reason: string;
58
+ continuation?: AcceptedMaterialFollowupContinuation;
62
59
  };
63
60
  export type TransitionDecisionPlan = {
64
61
  kind: "skip";
@@ -86,6 +83,7 @@ export interface ApplyPendingTaskStatus {
86
83
  completedByEvent: string[];
87
84
  }
88
85
  export declare function proposalDocsBaseline(changeRoot: string): Record<string, string>;
86
+ export declare function latestAcceptedProposalBaseline(events: Event[]): Record<string, string> | null;
89
87
  export declare function latestReopenProposeBaseline(events: Event[]): Record<string, string> | null;
90
88
  export declare function proposalDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
91
89
  export declare function historicalProposeReadyRoles(events: Event[]): JobRole[];
@@ -94,4 +92,5 @@ export declare function applyRequirementModeForCurrentRound(events: Event[]): bo
94
92
  export declare function pendingTaskStatusForApply(changeRoot: string, events: Event[]): ApplyPendingTaskStatus;
95
93
  export declare function formatPendingTaskMessage(ids: string[], action: string): string;
96
94
  export declare function planNextStep(context: PhasePlanContext): NextStepPlan | null;
95
+ export declare function blockingJobsForApplyDone(projectRoot: string, events: Event[], snapshot: Snapshot): Job[];
97
96
  export declare function planTransition(name: "explore" | "propose-ready" | "start-apply" | "accept", context: TransitionPlanContext): TransitionDecisionPlan;