@peterxiaoyang/superspec 0.1.24 → 0.1.26

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
@@ -81,7 +81,7 @@ superspec install
81
81
  ```
82
82
 
83
83
  这条命令的意思是:把 SuperSpec 当前可用的工作流入口安装到项目里。
84
- 当前 beta 会安装 `.superspec/` 引擎目录、`.codex/skills/superspec-*` 阶段入口、`.codex/prompts/*.md` 角色 prompt、`.codex/agents/*.toml` 子智能体配置,并补齐 `.codex/config.toml` 的多 agent 开关。`superspec init --scope project` 仍作为兼容别名可用。
84
+ 当前 beta 会安装 `.superspec/` 引擎目录、`.codex/skills/superspec-*` 阶段入口、`.codex/prompts/*.md` 角色 prompt、`.codex/agents/*.toml` 子智能体配置,补齐 `.codex/config.toml` 的多 agent 开关,并在项目根 `AGENTS.md` 中维护 SuperSpec 轻量门禁片段。`superspec init --scope project` 仍作为兼容别名可用。
85
85
 
86
86
  Windows PowerShell 如果拦截 npm 的 `.ps1` 脚本,请改用:
87
87
 
@@ -160,6 +160,14 @@ openspec/changes/<变更ID>/.superspec/
160
160
  `.superspec/` 要不要提交到 git,由你的团队决定。
161
161
  如果不提交,删掉后就没有 git 历史可以恢复。
162
162
 
163
+ ## 流程门禁
164
+
165
+ SuperSpec 的阶段入口由 `superspec transition next --change <变更ID>` 驱动。
166
+
167
+ 当当前阶段还有未完成的用户确认、审查、验证或工作项时,`next` 会先返回这些事项,不会把下一阶段命令作为推荐路径。重复运行会创建审查工作项的 transition 时,如果同阶段工作项已经存在,CLI 会返回正常的门禁结果,不会写入新事件,也不会把它当作程序错误。
168
+
169
+ 这仍然是轻量流程控制,不是写入拦截。它约束按 SuperSpec 正常入口执行时的下一步建议和状态提交结果,不承诺阻止绕过流程的手动编辑。
170
+
163
171
  ## Hook 会做什么
164
172
 
165
173
  SuperSpec 默认安装的 hook 只在子智能体启动和停止时运行:
@@ -238,7 +246,7 @@ superspec status
238
246
  superspec update
239
247
  ```
240
248
 
241
- 这条命令会先检查 npm 上的 latest 版本;如果有新版,会自动执行全局升级并用新版 CLI 重新同步项目入口。同步内容包括补齐 `.superspec/changes` 运行时目录,并把当前 CLI 内置的 `.codex/skills/superspec-*`、`.codex/prompts/*.md`、`.codex/agents/*.toml` 同步到项目里。
249
+ 这条命令会先检查 npm 上的 latest 版本;如果有新版,会自动执行全局升级并用新版 CLI 重新同步项目入口。同步内容包括补齐 `.superspec/changes` 运行时目录,把当前 CLI 内置的 `.codex/skills/superspec-*`、`.codex/prompts/*.md`、`.codex/agents/*.toml` 同步到项目里,并更新 `AGENTS.md` 中 marker 包裹的 SuperSpec 轻量门禁片段。
242
250
 
243
251
  ## 进阶信息
244
252
 
package/dist/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  // SuperSpec 流程引擎 — CLI 入口
3
- import { join } from "node:path";
4
3
  import { execFileSync } from "node:child_process";
5
4
  import { createInterface } from "node:readline/promises";
5
+ import { readFileSync } from "node:fs";
6
6
  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
10
  import { proposeReady, commitTransition, transitionInit, transitionExplore, startApply, taskStart, taskComplete, reopen, reviewReady, accept, archive } from "./transition.js";
11
- import { recordJobSubmit, recordUserDecision, jobsList, jobsPacket } from "./record.js";
12
- import { recordTestRun } from "./task.js";
11
+ import { recordJobSubmit, recordJobSubmitContent, recordUserDecision, recordUserDecisionContent, jobsList, jobsPacket } from "./record.js";
12
+ import { recordTestRun, recordTestRunContent } from "./task.js";
13
13
  import { probeOpenSpec, openspecStatus, changeRoot } from "./openspec.js";
14
14
  import { SUPERSPEC_VERSION } from "./version.js";
15
15
  const PACKAGE_NAME = "@peterxiaoyang/superspec";
@@ -42,6 +42,28 @@ function parseFlags(args) {
42
42
  }
43
43
  return opts;
44
44
  }
45
+ class StdinRecordInputError extends Error {
46
+ constructor(flag) {
47
+ super(`${flag} - 需要通过 pipe 或重定向提供 JSON;不方便时请使用文件路径。`);
48
+ this.name = "StdinRecordInputError";
49
+ }
50
+ }
51
+ function readStdinRecordContent(flag) {
52
+ if (process.stdin.isTTY === true) {
53
+ throw new StdinRecordInputError(flag);
54
+ }
55
+ return readFileSync(0, "utf8");
56
+ }
57
+ function transitionExitCode(result) {
58
+ if (result.outcome === "blocked")
59
+ return 0;
60
+ return result.events_written === 0 ? 1 : 0;
61
+ }
62
+ function proposeReadyExitCode(result) {
63
+ if (result.outcome === "blocked")
64
+ return 0;
65
+ return result.events_written === 0 && result.message.includes("不能") ? 1 : 0;
66
+ }
45
67
  function parseVersion(version) {
46
68
  const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
47
69
  if (!match)
@@ -342,9 +364,9 @@ transition 子命令:
342
364
  review-ready / accept / archive
343
365
 
344
366
  record 子命令:
345
- job-submit --job <J> --report <F>
346
- user-decision --input <F>
347
- test-run --input <F>
367
+ job-submit --job <J> --report <F|->
368
+ user-decision --input <F|->
369
+ test-run --input <F|->
348
370
 
349
371
  jobs 子命令:
350
372
  list / packet --job <J>
@@ -482,12 +504,12 @@ jobs 子命令:
482
504
  const risk = opts.risk ?? "strict";
483
505
  const result = proposeReady(projectRoot, change, cr, risk);
484
506
  console.log(JSON.stringify(result, null, 2));
485
- return result.events_written === 0 && result.message.includes("不能") ? 1 : 0;
507
+ return proposeReadyExitCode(result);
486
508
  }
487
509
  case "start-apply": {
488
510
  const result = startApply(projectRoot, change, cr);
489
511
  console.log(JSON.stringify(result, null, 2));
490
- return result.events_written === 0 ? 1 : 0;
512
+ return transitionExitCode(result);
491
513
  }
492
514
  case "task-start": {
493
515
  const taskId = opts.task;
@@ -497,7 +519,7 @@ jobs 子命令:
497
519
  }
498
520
  const result = taskStart(projectRoot, change, cr, taskId);
499
521
  console.log(JSON.stringify(result, null, 2));
500
- return result.events_written === 0 ? 1 : 0;
522
+ return transitionExitCode(result);
501
523
  }
502
524
  case "task-complete": {
503
525
  const taskId = opts.task;
@@ -507,7 +529,7 @@ jobs 子命令:
507
529
  }
508
530
  const result = taskComplete(projectRoot, change, cr, taskId);
509
531
  console.log(JSON.stringify(result, null, 2));
510
- return result.events_written === 0 ? 1 : 0;
532
+ return transitionExitCode(result);
511
533
  }
512
534
  case "reopen": {
513
535
  const to = opts.to;
@@ -522,23 +544,23 @@ jobs 子命令:
522
544
  }
523
545
  const result = reopen(projectRoot, change, cr, to, reason);
524
546
  console.log(JSON.stringify(result, null, 2));
525
- return result.events_written === 0 ? 1 : 0;
547
+ return transitionExitCode(result);
526
548
  }
527
549
  case "review-ready": {
528
550
  const risk = opts.risk ?? "strict";
529
551
  const result = reviewReady(projectRoot, change, cr, risk);
530
552
  console.log(JSON.stringify(result, null, 2));
531
- return result.events_written === 0 ? 1 : 0;
553
+ return transitionExitCode(result);
532
554
  }
533
555
  case "accept": {
534
556
  const result = accept(projectRoot, change, cr);
535
557
  console.log(JSON.stringify(result, null, 2));
536
- return result.events_written === 0 ? 1 : 0;
558
+ return transitionExitCode(result);
537
559
  }
538
560
  case "archive": {
539
561
  const result = archive(projectRoot, change, cr);
540
562
  console.log(JSON.stringify(result, null, 2));
541
- return result.events_written === 0 ? 1 : 0;
563
+ return transitionExitCode(result);
542
564
  }
543
565
  default:
544
566
  console.error(`未知的 transition 子命令:${subcommand}`);
@@ -555,7 +577,19 @@ jobs 子命令:
555
577
  console.error("record job-submit 需要 --job 和 --report");
556
578
  return 1;
557
579
  }
558
- const result = recordJobSubmit(projectRoot, change, cr, jobId, report);
580
+ let result;
581
+ try {
582
+ result = report === "-"
583
+ ? recordJobSubmitContent(projectRoot, change, cr, jobId, readStdinRecordContent("--report"))
584
+ : recordJobSubmit(projectRoot, change, cr, jobId, report);
585
+ }
586
+ catch (err) {
587
+ if (err instanceof StdinRecordInputError) {
588
+ console.error(err.message);
589
+ return 1;
590
+ }
591
+ throw err;
592
+ }
559
593
  console.log(JSON.stringify(result, null, 2));
560
594
  return result.accepted ? 0 : 1;
561
595
  }
@@ -565,7 +599,19 @@ jobs 子命令:
565
599
  console.error("record user-decision 需要 --input");
566
600
  return 1;
567
601
  }
568
- const result = recordUserDecision(projectRoot, change, inputFile);
602
+ let result;
603
+ try {
604
+ result = inputFile === "-"
605
+ ? recordUserDecisionContent(projectRoot, change, readStdinRecordContent("--input"))
606
+ : recordUserDecision(projectRoot, change, inputFile);
607
+ }
608
+ catch (err) {
609
+ if (err instanceof StdinRecordInputError) {
610
+ console.error(err.message);
611
+ return 1;
612
+ }
613
+ throw err;
614
+ }
569
615
  console.log(JSON.stringify(result, null, 2));
570
616
  return result.accepted ? 0 : 1;
571
617
  }
@@ -575,7 +621,19 @@ jobs 子命令:
575
621
  console.error("record test-run 需要 --input");
576
622
  return 1;
577
623
  }
578
- const result = recordTestRun(projectRoot, change, inputFile);
624
+ let result;
625
+ try {
626
+ result = inputFile === "-"
627
+ ? recordTestRunContent(projectRoot, change, readStdinRecordContent("--input"))
628
+ : recordTestRun(projectRoot, change, inputFile);
629
+ }
630
+ catch (err) {
631
+ if (err instanceof StdinRecordInputError) {
632
+ console.error(err.message);
633
+ return 1;
634
+ }
635
+ throw err;
636
+ }
579
637
  console.log(JSON.stringify(result, null, 2));
580
638
  return result.accepted ? 0 : 1;
581
639
  }
package/dist/install.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface InstallResult {
10
10
  prompts: string[];
11
11
  agents: string[];
12
12
  config: string;
13
+ agents_md: string;
13
14
  openspec_config: string;
14
15
  };
15
16
  }
package/dist/install.js CHANGED
@@ -55,6 +55,9 @@ function assertWorkflowTemplates(templateRoot) {
55
55
  if (!existsSync(file))
56
56
  missing.push(file);
57
57
  }
58
+ const agentsMdTemplate = join(templateRoot, "AGENTS.md");
59
+ if (!existsSync(agentsMdTemplate))
60
+ missing.push(agentsMdTemplate);
58
61
  if (missing.length > 0) {
59
62
  throw new Error(`workflow templates missing: ${missing.join(", ")}`);
60
63
  }
@@ -66,7 +69,6 @@ function writeBundledFile(src, dest) {
66
69
  const current = readFileSync(dest, "utf8");
67
70
  if (current === next)
68
71
  return;
69
- writeFileSync(`${dest}.bak`, current);
70
72
  }
71
73
  writeFileSync(dest, next);
72
74
  }
@@ -242,6 +244,38 @@ function ensureOpenSpecChineseContext(projectRoot) {
242
244
  writeFileSync(configPath, next);
243
245
  return OPENSPEC_CONFIG_PATH;
244
246
  }
247
+ const AGENTS_MD_PATH = "AGENTS.md";
248
+ const SUPERSPEC_AGENTS_START = "<!-- SUPERSPEC:AGENTS:START -->";
249
+ const SUPERSPEC_AGENTS_END = "<!-- SUPERSPEC:AGENTS:END -->";
250
+ function readAgentsMdTemplate(templateRoot) {
251
+ const template = readFileSync(join(templateRoot, AGENTS_MD_PATH), "utf8").trimEnd();
252
+ if (!template.includes(SUPERSPEC_AGENTS_START) || !template.includes(SUPERSPEC_AGENTS_END)) {
253
+ throw new Error(`workflow AGENTS.md template missing SuperSpec markers: ${join(templateRoot, AGENTS_MD_PATH)}`);
254
+ }
255
+ return template;
256
+ }
257
+ function replaceMarkerBlock(content, block) {
258
+ const start = content.indexOf(SUPERSPEC_AGENTS_START);
259
+ const end = content.indexOf(SUPERSPEC_AGENTS_END);
260
+ if (start < 0 || end < 0 || end < start)
261
+ return null;
262
+ const afterEnd = end + SUPERSPEC_AGENTS_END.length;
263
+ return `${content.slice(0, start)}${block}${content.slice(afterEnd)}`;
264
+ }
265
+ function ensureAgentsMd(projectRoot, block) {
266
+ const agentsPath = join(projectRoot, AGENTS_MD_PATH);
267
+ if (!existsSync(agentsPath)) {
268
+ writeFileSync(agentsPath, `${block}\n`);
269
+ return AGENTS_MD_PATH;
270
+ }
271
+ const current = readFileSync(agentsPath, "utf8");
272
+ const replaced = replaceMarkerBlock(current, block);
273
+ const next = replaced ?? `${current.trimEnd()}\n\n${block}\n`;
274
+ if (next === current)
275
+ return AGENTS_MD_PATH;
276
+ writeFileSync(agentsPath, next);
277
+ return AGENTS_MD_PATH;
278
+ }
245
279
  export function installProject(projectRoot, options = {}) {
246
280
  if (!options.allowLegacyState && legacyStateFound(projectRoot)) {
247
281
  throw new Error("检测到老版 SuperSpec (0.x) 的状态文件。\n" +
@@ -251,6 +285,7 @@ export function installProject(projectRoot, options = {}) {
251
285
  }
252
286
  const templateRoot = options.templateRoot ?? defaultTemplateRoot();
253
287
  assertWorkflowTemplates(templateRoot);
288
+ const agentsMdTemplate = readAgentsMdTemplate(templateRoot);
254
289
  const engineDir = join(projectRoot, ".superspec");
255
290
  mkdirSync(join(engineDir, "changes"), { recursive: true });
256
291
  const gitignorePath = join(engineDir, ".gitignore");
@@ -266,6 +301,7 @@ export function installProject(projectRoot, options = {}) {
266
301
  prompts: copyPrompts(templateRoot, projectRoot),
267
302
  agents: copyAgents(templateRoot, projectRoot),
268
303
  config: ensureCodexConfig(projectRoot),
304
+ agents_md: ensureAgentsMd(projectRoot, agentsMdTemplate),
269
305
  openspec_config: ensureOpenSpecChineseContext(projectRoot),
270
306
  },
271
307
  };
@@ -0,0 +1,6 @@
1
+ import type { Job, RequiredJobAction } from "./types.ts";
2
+ export declare function jobPacketCommand(change: string, jobId: string): string;
3
+ export declare function jobPacketArgv(change: string, jobId: string): string[];
4
+ export declare function requiredJobAction(change: string, job: Job): RequiredJobAction;
5
+ export declare function requiredJobActions(change: string, jobs: Job[]): RequiredJobAction[];
6
+ export declare function jobSubmitArgv(change: string, jobId: string): string[];
@@ -0,0 +1,21 @@
1
+ // SuperSpec workflow engine - shared job action protocol helpers.
2
+ export function jobPacketCommand(change, jobId) {
3
+ return `superspec jobs packet --change "${change}" --job "${jobId}"`;
4
+ }
5
+ export function jobPacketArgv(change, jobId) {
6
+ return ["superspec", "jobs", "packet", "--change", change, "--job", jobId];
7
+ }
8
+ export function requiredJobAction(change, job) {
9
+ return {
10
+ job_id: job.job_id,
11
+ role: job.role,
12
+ packet_command: jobPacketCommand(change, job.job_id),
13
+ packet_argv: jobPacketArgv(change, job.job_id),
14
+ };
15
+ }
16
+ export function requiredJobActions(change, jobs) {
17
+ return jobs.map(job => requiredJobAction(change, job));
18
+ }
19
+ export function jobSubmitArgv(change, jobId) {
20
+ return ["superspec", "record", "job-submit", "--change", change, "--job", jobId, "--report", "-"];
21
+ }
package/dist/next.js CHANGED
@@ -4,13 +4,22 @@ import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { readEvents, sha256Text } from "./store.js";
6
6
  import { isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest } from "./review.js";
7
+ import { requiredJobActions } from "./job_action.js";
7
8
  import { validateDiscovery, countDiscoveryOpenQuestions, collectProposeOpenQuestions, parseTasksMd, pendingTasksInContent } from "./format.js";
8
9
  const ACTIVE_PROPOSAL_REVIEW_ROLES = new Set(["critic", "architect", "test-engineer"]);
9
10
  function isActiveProposalReviewJob(job) {
10
11
  return job.created_from_transition === "propose-ready" && ACTIVE_PROPOSAL_REVIEW_ROLES.has(job.role);
11
12
  }
12
- function packetCommand(change, jobId) {
13
- return `superspec jobs packet --change "${change}" --job "${jobId}"`;
13
+ function isExploreReviewJob(job) {
14
+ return job.created_from_transition === "explore" && job.role === "critic";
15
+ }
16
+ function requiredJobsOutput(state, change, jobs, reason) {
17
+ return {
18
+ state,
19
+ path: "required_job",
20
+ required_jobs: requiredJobActions(change, jobs),
21
+ reason,
22
+ };
14
23
  }
15
24
  function transitionCommand(change, name, extra = "") {
16
25
  return `superspec transition ${name} --change "${change}"${extra ? " " + extra : ""}`;
@@ -67,6 +76,9 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
67
76
  const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
68
77
  switch (snapshot.state) {
69
78
  case "init":
79
+ if (snapshot.open_jobs.length > 0) {
80
+ return requiredJobsOutput("init", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
81
+ }
70
82
  return {
71
83
  state: "init",
72
84
  path: "next_command",
@@ -75,6 +87,10 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
75
87
  missing_inputs: [],
76
88
  };
77
89
  case "explore": {
90
+ const exploreReviewJobs = snapshot.open_jobs.filter(isExploreReviewJob);
91
+ if (exploreReviewJobs.length > 0) {
92
+ return requiredJobsOutput("explore", change, exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
93
+ }
78
94
  // Phase 2:检查 discovery.md
79
95
  const discoveryCheck = validateDiscovery(changeRoot);
80
96
  if (!discoveryCheck.ok) {
@@ -117,17 +133,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
117
133
  }
118
134
  const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
119
135
  if (proposalReviewJobs.length > 0) {
120
- const jobs = proposalReviewJobs.map(j => ({
121
- job_id: j.job_id,
122
- role: j.role,
123
- packet_command: packetCommand(change, j.job_id),
124
- }));
125
- return {
126
- state: "propose",
127
- path: "required_job",
128
- required_jobs: jobs,
129
- reason: `有 ${jobs.length} 个待完成工作项`,
130
- };
136
+ return requiredJobsOutput("propose", change, proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
131
137
  }
132
138
  return {
133
139
  state: "propose",
@@ -140,16 +146,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
140
146
  case "propose_ready": {
141
147
  const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
142
148
  if (proposalReviewJobs.length > 0) {
143
- return {
144
- state: "propose_ready",
145
- path: "required_job",
146
- required_jobs: proposalReviewJobs.map(j => ({
147
- job_id: j.job_id,
148
- role: j.role,
149
- packet_command: packetCommand(change, j.job_id),
150
- })),
151
- reason: `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`,
152
- };
149
+ return requiredJobsOutput("propose_ready", change, proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
153
150
  }
154
151
  return {
155
152
  state: "propose_ready",
@@ -160,6 +157,9 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
160
157
  };
161
158
  }
162
159
  case "apply": {
160
+ if (snapshot.open_jobs.length > 0) {
161
+ return requiredJobsOutput("apply", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
162
+ }
163
163
  // 检查是否所有任务已完成
164
164
  const pending = pendingTaskIds(changeRoot);
165
165
  if (pending.length > 0) {
@@ -190,15 +190,6 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
190
190
  missing_inputs: [],
191
191
  };
192
192
  }
193
- // 有 open job → 做
194
- if (snapshot.open_jobs.length > 0) {
195
- return {
196
- state: "apply",
197
- path: "required_job",
198
- required_jobs: snapshot.open_jobs.map(j => ({ job_id: j.job_id, role: j.role, packet_command: packetCommand(change, j.job_id) })),
199
- reason: `有 ${snapshot.open_jobs.length} 个待完成工作项`,
200
- };
201
- }
202
193
  return {
203
194
  state: "apply",
204
195
  path: "next_command",
@@ -219,12 +210,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
219
210
  };
220
211
  }
221
212
  if (snapshot.open_jobs.length > 0) {
222
- return {
223
- state: "apply_done",
224
- path: "required_job",
225
- required_jobs: snapshot.open_jobs.map(j => ({ job_id: j.job_id, role: j.role, packet_command: packetCommand(change, j.job_id) })),
226
- reason: `有 ${snapshot.open_jobs.length} 个待完成工作项`,
227
- };
213
+ return requiredJobsOutput("apply_done", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
228
214
  }
229
215
  return {
230
216
  state: "apply_done",
@@ -247,16 +233,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
247
233
  }
248
234
  const reviewVerifierJobs = snapshot.open_jobs.filter(isReviewReadyVerifier);
249
235
  if (reviewVerifierJobs.length > 0) {
250
- return {
251
- state: "review",
252
- path: "required_job",
253
- required_jobs: reviewVerifierJobs.map(j => ({
254
- job_id: j.job_id,
255
- role: j.role,
256
- packet_command: packetCommand(change, j.job_id),
257
- })),
258
- reason: `有 ${reviewVerifierJobs.length} 个待完成最终验证工作项`,
259
- };
236
+ return requiredJobsOutput("review", change, reviewVerifierJobs, `有 ${reviewVerifierJobs.length} 个待完成最终验证工作项`);
260
237
  }
261
238
  const events = readEvents(projectRoot, change);
262
239
  const policy = readReviewPolicyFromEvents(events);
@@ -291,6 +268,9 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
291
268
  };
292
269
  }
293
270
  case "accepted":
271
+ if (snapshot.open_jobs.length > 0) {
272
+ return requiredJobsOutput("accepted", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不归档`);
273
+ }
294
274
  return {
295
275
  state: "accepted",
296
276
  path: "next_command",
@@ -299,11 +279,20 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
299
279
  missing_inputs: [],
300
280
  };
301
281
  case "archive":
282
+ if (snapshot.open_jobs.length > 0) {
283
+ return requiredJobsOutput("archive", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不结束`);
284
+ }
302
285
  return {
303
286
  state: "archive",
304
287
  path: "done",
305
288
  reason: "已归档,流程完成。",
306
289
  };
290
+ case "abandoned":
291
+ return {
292
+ state: "abandoned",
293
+ path: "done",
294
+ reason: "变更已放弃,流程终止。",
295
+ };
307
296
  default:
308
297
  return {
309
298
  state: snapshot.state,
package/dist/record.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import type { RecordResult, Job } from "./types.ts";
2
2
  /** record job-submit:登记工作项结果 */
3
3
  export declare function recordJobSubmit(projectRoot: string, change: string, changeRoot: string, jobId: string, reportFile: string): RecordResult;
4
+ /** record job-submit:从 JSON 内容登记工作项结果 */
5
+ export declare function recordJobSubmitContent(projectRoot: string, change: string, changeRoot: string, jobId: string, reportContent: string): RecordResult;
4
6
  /** record user-decision:登记用户决策 */
5
7
  export declare function recordUserDecision(projectRoot: string, change: string, inputFile: string): RecordResult;
8
+ /** record user-decision:从 JSON 内容登记用户决策 */
9
+ export declare function recordUserDecisionContent(projectRoot: string, change: string, content: string): RecordResult;
6
10
  /** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
7
11
  export declare function jobsList(projectRoot: string, change: string): {
8
12
  open: Job[];