@peterxiaoyang/superspec 0.1.44 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +13 -1
  2. package/dist/cli.js +23 -24
  3. package/dist/code_review.js +7 -2
  4. package/dist/format.d.ts +4 -2
  5. package/dist/format.js +50 -16
  6. package/dist/git_state.d.ts +12 -1
  7. package/dist/git_state.js +45 -0
  8. package/dist/install.d.ts +1 -0
  9. package/dist/install.js +12 -0
  10. package/dist/next.d.ts +1 -1
  11. package/dist/next.js +3 -8
  12. package/dist/phase_confirmation.d.ts +6 -0
  13. package/dist/phase_confirmation.js +22 -6
  14. package/dist/phase_plan.d.ts +8 -1
  15. package/dist/phase_plan.js +130 -21
  16. package/dist/record.d.ts +1 -1
  17. package/dist/record.js +38 -30
  18. package/dist/review.js +18 -2
  19. package/dist/sync.js +13 -4
  20. package/dist/task.js +15 -2
  21. package/dist/task_evidence.d.ts +1 -1
  22. package/dist/task_evidence.js +85 -10
  23. package/dist/transition.d.ts +4 -3
  24. package/dist/transition.js +162 -29
  25. package/dist/types.d.ts +24 -1
  26. package/dist/types.js +1 -0
  27. package/dist/workflow_config.d.ts +24 -0
  28. package/dist/workflow_config.js +127 -0
  29. package/package.json +1 -1
  30. package/templates/workflow/AGENTS.md +1 -1
  31. package/templates/workflow/prompts/architect.md +1 -1
  32. package/templates/workflow/prompts/code-reviewer.md +2 -2
  33. package/templates/workflow/prompts/critic.md +4 -5
  34. package/templates/workflow/prompts/executor.md +2 -2
  35. package/templates/workflow/prompts/test-engineer.md +3 -4
  36. package/templates/workflow/prompts/verifier.md +1 -1
  37. package/templates/workflow/skills/superspec-apply/SKILL.md +13 -71
  38. package/templates/workflow/skills/superspec-explore/SKILL.md +5 -9
  39. package/templates/workflow/skills/superspec-propose/SKILL.md +26 -26
  40. package/templates/workflow/skills/superspec-review/SKILL.md +21 -50
package/README.md CHANGED
@@ -131,7 +131,7 @@ superspec.cmd install
131
131
 
132
132
  你日常主要记住这四个入口就够了。
133
133
 
134
- CLI 不带 `--risk` 时默认走完整审查路径;需要轻量路径时显式传 `--risk normal` 或 `--risk minimal`。探索阶段会创建 `critic` 工作项审查需求澄清记录;计划阶段会创建 `critic`、`architect` 和 `test-engineer` 工作项后再进入实现准备。
134
+ 工作流档位由项目配置统一控制,不通过命令行临时指定。探索阶段会按配置创建相应审查工作项;计划阶段按同一档位创建 `critic`、`architect`、`test-engineer` 等必要审查后再进入实现准备。
135
135
 
136
136
  ## 它会多保存哪些记录
137
137
 
@@ -191,6 +191,18 @@ superspec install
191
191
 
192
192
  `superspec install` 会创建缺失的 `openspec/config.yaml`,或在没有顶层 `context` 时追加这段官方中文 context。如果文件已经有顶层 `context`,SuperSpec 不会覆盖它。
193
193
 
194
+ 它还会创建项目级 `.superspec/config.json`,用于统一设置整个工作流的默认档位:
195
+
196
+ ```json
197
+ {
198
+ "workflow": {
199
+ "mode": "normal"
200
+ }
201
+ }
202
+ ```
203
+
204
+ 可选值为 `minimal`、`normal`、`strict`。`next`、Explore、Propose、进入 Apply 和 Review 都读取此配置;`--risk` 不再是用户可用的工作流入口。已启动的 task attempt 和已经建立的审查策略仍按其事件快照执行,不会被中途改配置追溯改写。
205
+
194
206
  可以用下面的命令检查生成的 instructions 是否包含语言上下文:
195
207
 
196
208
  ```bash
package/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { recordTestRun, recordTestRunContent } from "./task.js";
13
13
  import { RecordInputDecodingError, decodeRecordInput } from "./record_input.js";
14
14
  import { probeOpenSpec, openspecStatus, changeRoot } from "./openspec.js";
15
15
  import { SUPERSPEC_VERSION } from "./version.js";
16
+ import { WorkflowConfigError, workflowRiskForProject } from "./workflow_config.js";
16
17
  const PACKAGE_NAME = "@peterxiaoyang/superspec";
17
18
  const OPENSPEC_PACKAGE_NAME = "@fission-ai/openspec";
18
19
  const OPENSPEC_REQUIRED_VERSION = "1.4.1";
@@ -67,19 +68,15 @@ function proposeReadyExitCode(result) {
67
68
  return 0;
68
69
  return result.events_written === 0 && result.message.includes("不能") ? 1 : 0;
69
70
  }
70
- function parseReviewRisk(value) {
71
- if (value == null)
72
- return "strict";
73
- if (value === "minimal" || value === "normal" || value === "strict")
74
- return value;
75
- return null;
76
- }
77
- function readReviewRisk(opts) {
78
- const risk = parseReviewRisk(opts.risk);
79
- if (!risk) {
80
- console.error("--risk 只能是 minimal、normal 或 strict");
71
+ function ensureWorkflowMode(projectRoot, _opts) {
72
+ try {
73
+ workflowRiskForProject(projectRoot);
74
+ return true;
75
+ }
76
+ catch (err) {
77
+ console.error(err instanceof WorkflowConfigError ? err.message : String(err));
78
+ return false;
81
79
  }
82
- return risk;
83
80
  }
84
81
  function parseVersion(version) {
85
82
  const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
@@ -516,6 +513,7 @@ async function main(argv) {
516
513
  transition 子命令:
517
514
  init / explore / sync / next / propose-ready / start-apply
518
515
  task-start --task <T> / task-complete --task <T> [--input -]
516
+ reopen --to explore|propose|apply --reason <TEXT>
519
517
  reopen --to apply --reason <TEXT> [--review-fix <JOB#FINDING>]
520
518
  reopen --to propose --reason <TEXT> [--review-finding <JOB#FINDING>]
521
519
  review-ready / accept
@@ -640,16 +638,20 @@ jobs 子命令:
640
638
  }
641
639
  case "transition": {
642
640
  const cr = changeRoot(projectRoot, change);
641
+ // mode 只能由项目配置和已冻结 round 快照决定;任何 transition 都不接受 --risk。
642
+ if (opts.risk !== undefined) {
643
+ console.error("工作流模式由 .superspec/config.json 的 workflow.mode 控制,不支持 --risk");
644
+ return 1;
645
+ }
643
646
  switch (subcommand) {
644
647
  case "init":
645
648
  console.log(JSON.stringify(transitionInit(projectRoot, change, cr), null, 2));
646
649
  return 0;
647
650
  case "explore":
648
651
  {
649
- const risk = readReviewRisk(opts);
650
- if (!risk)
652
+ if (!ensureWorkflowMode(projectRoot, opts))
651
653
  return 1;
652
- console.log(JSON.stringify(transitionExplore(projectRoot, change, cr, risk), null, 2));
654
+ console.log(JSON.stringify(transitionExplore(projectRoot, change, cr), null, 2));
653
655
  }
654
656
  return 0;
655
657
  case "sync": {
@@ -664,18 +666,16 @@ jobs 子命令:
664
666
  return 0;
665
667
  }
666
668
  case "next": {
667
- const risk = readReviewRisk(opts);
668
- if (!risk)
669
+ if (!ensureWorkflowMode(projectRoot, opts))
669
670
  return 1;
670
- const result = nextCmd(projectRoot, change, cr, risk);
671
+ const result = nextCmd(projectRoot, change, cr);
671
672
  console.log(JSON.stringify(result, null, 2));
672
673
  return 0;
673
674
  }
674
675
  case "propose-ready": {
675
- const risk = readReviewRisk(opts);
676
- if (!risk)
676
+ if (!ensureWorkflowMode(projectRoot, opts))
677
677
  return 1;
678
- const result = proposeReady(projectRoot, change, cr, risk);
678
+ const result = proposeReady(projectRoot, change, cr);
679
679
  console.log(JSON.stringify(result, null, 2));
680
680
  return proposeReadyExitCode(result);
681
681
  }
@@ -740,10 +740,9 @@ jobs 子命令:
740
740
  return transitionExitCode(result);
741
741
  }
742
742
  case "review-ready": {
743
- const risk = readReviewRisk(opts);
744
- if (!risk)
743
+ if (!ensureWorkflowMode(projectRoot, opts))
745
744
  return 1;
746
- const result = reviewReady(projectRoot, change, cr, risk);
745
+ const result = reviewReady(projectRoot, change, cr);
747
746
  console.log(JSON.stringify(result, null, 2));
748
747
  return transitionExitCode(result);
749
748
  }
@@ -348,7 +348,9 @@ function changedPathsBetweenSnapshots(projectRoot, start, completed) {
348
348
  return { paths: [...changed].sort(), partial_reason: partialReason };
349
349
  }
350
350
  function testEvidenceForAttempt(events, attempt) {
351
- const declaredTests = attempt.contract_mode === true ? attempt.contract?.tests ?? [] : [];
351
+ const declaredTests = attempt.contract_mode === true
352
+ ? attempt.required_evidence?.test_ids ?? attempt.contract?.tests ?? []
353
+ : [];
352
354
  const eventsByTest = new Map();
353
355
  for (const ev of events) {
354
356
  if (ev.event_type !== "test_run_recorded")
@@ -411,14 +413,17 @@ function taskExecutionIndexFromEvents(projectRoot, events) {
411
413
  const started = attempts.get(payload.attempt_id);
412
414
  const attempt = started?.attempt;
413
415
  const effectiveContract = attempt?.contract_mode === true ? attempt.contract ?? null : null;
416
+ const requiredEvidence = attempt?.required_evidence ?? null;
414
417
  const changedResult = changedPathsBetweenSnapshots(projectRoot, started?.boundary ?? null, boundaryFromPayload(ev.payload));
415
418
  entries.push({
416
419
  task_id: payload.task_id,
417
420
  attempt_id: payload.attempt_id,
421
+ execution_policy: attempt?.execution_policy ?? "tdd",
418
422
  changed_paths: changedResult ? changedResult.paths : null,
419
423
  ...(changedResult?.partial_reason ? { changed_paths_partial_reason: changedResult.partial_reason } : {}),
420
424
  contract: effectiveContract,
421
- declared_tests: effectiveContract?.tests ?? [],
425
+ required_evidence: requiredEvidence,
426
+ declared_tests: requiredEvidence?.test_ids ?? effectiveContract?.tests ?? [],
422
427
  scope_note: payload.scope_note && typeof payload.scope_note === "object" && !Array.isArray(payload.scope_note)
423
428
  ? payload.scope_note
424
429
  : null,
package/dist/format.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExecutionContract } from "./types.ts";
1
+ import { type ExecutionContract, type ExecutionPolicy } from "./types.ts";
2
2
  /** 从 discovery.md 提取"待确认问题"段内的未确认项数量 */
3
3
  export declare function countDiscoveryOpenQuestions(content: string): number;
4
4
  export interface DiscoveryChainCoverageCheck {
@@ -35,6 +35,8 @@ export interface ParsedExecutionRequirement {
35
35
  taskId: string;
36
36
  lineIdx: number;
37
37
  contract: ExecutionContract;
38
+ /** 执行依据中实际出现的字段;用于区分“测试为空”和“遗漏测试字段”。 */
39
+ declaredFields: Array<keyof ExecutionContract>;
38
40
  errors: string[];
39
41
  }
40
42
  export interface TestContractEntry {
@@ -68,7 +70,7 @@ export interface ExecutionRequirementValidation {
68
70
  contracts: ParsedExecutionRequirement[];
69
71
  errors: string[];
70
72
  }
71
- export declare function validateExecutionRequirements(content: string, testContractContent: string | null): ExecutionRequirementValidation;
73
+ export declare function validateExecutionRequirements(content: string, testContractContent: string | null, executionPolicy?: ExecutionPolicy, executionRequirementVersion?: 1 | 2): ExecutionRequirementValidation;
72
74
  /** 返回未完成任务 */
73
75
  export declare function pendingTasksInContent(content: string): ParsedTask[];
74
76
  /** 在 tasks.md 中按 taskId 精确查找任务(词边界,不误判子串) */
package/dist/format.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // 修改格式 = 修改这里 + 对应 skill。禁止在其它地方重复解析。
5
5
  import { readFileSync, existsSync } from "node:fs";
6
6
  import { join } from "node:path";
7
+ import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
7
8
  // ===== discovery.md =====
8
9
  //
9
10
  // 格式(explore skill 定义):
@@ -151,8 +152,11 @@ const CONTRACT_FIELD_ALIASES = {
151
152
  "Design": "design",
152
153
  "来源": "source",
153
154
  "Source": "source",
154
- "原因": "reason",
155
- "Reason": "reason",
155
+ // 原因是历史文档字段;新文档用验收,二者都归一为 acceptance。
156
+ "验收": "acceptance",
157
+ "Acceptance": "acceptance",
158
+ "原因": "acceptance",
159
+ "Reason": "acceptance",
156
160
  "边界": "guard",
157
161
  "Guard": "guard",
158
162
  };
@@ -197,7 +201,7 @@ function emptyContract() {
197
201
  tests: [],
198
202
  design: null,
199
203
  source: [],
200
- reason: null,
204
+ acceptance: null,
201
205
  guard: null,
202
206
  };
203
207
  }
@@ -224,7 +228,7 @@ function parseExecutionRequirementBlock(lines, task, headerIdx) {
224
228
  if (/^\s*-\s+\[[ xX]\]/.test(line)) {
225
229
  errors.push(`${task.taskId} 的执行依据内不能包含复选框`);
226
230
  }
227
- const fieldMatch = line.match(/^\s*-\s*(测试|Tests|设计|Design|来源|Source|原因|Reason|边界|Guard)\s*[::]\s*(.*)$/);
231
+ const fieldMatch = line.match(/^\s*-\s*(测试|Tests|设计|Design|来源|Source|验收|Acceptance|原因|Reason|边界|Guard)\s*[::]\s*(.*)$/);
228
232
  if (fieldMatch) {
229
233
  const key = CONTRACT_FIELD_ALIASES[fieldMatch[1]];
230
234
  const value = fieldMatch[2].trim();
@@ -247,7 +251,7 @@ function parseExecutionRequirementBlock(lines, task, headerIdx) {
247
251
  }
248
252
  index += 1;
249
253
  }
250
- return { taskId: task.taskId, lineIdx: task.lineIdx, contract, errors };
254
+ return { taskId: task.taskId, lineIdx: task.lineIdx, contract, declaredFields: [...seen], errors };
251
255
  }
252
256
  export function hasTaskBoundExecutionRequirements(content) {
253
257
  const lines = content.split("\n");
@@ -343,28 +347,58 @@ export function parseTestContractEntries(content) {
343
347
  return { ok: false, entries: [], message: "test-contract.md 没有 TEST-* 行" };
344
348
  return { ok: true, entries };
345
349
  }
346
- export function validateExecutionRequirements(content, testContractContent) {
347
- const mode = hasTaskBoundExecutionRequirements(content);
350
+ export function validateExecutionRequirements(content, testContractContent, executionPolicy = "tdd", executionRequirementVersion = 2) {
351
+ const tasks = parseTasksMd(content);
352
+ // 历史 green-only task 在旧版本中用标记强制进入契约模式。保留该入口,
353
+ // 防止已生成但尚未执行的 change 在升级后静默退回无验证的 legacy 模式;
354
+ // 新 Propose 不再产生此标记。
355
+ const hasLegacyGreenOnlyTask = tasks.some(task => task.noTddReason === GREEN_ONLY_NO_TDD_REASON);
356
+ // v2 不允许“所有任务都没有执行依据”这一静默回退:新 Propose 的每个普通
357
+ // task 都必须显式声明五字段。v1 的缺失版本仍保留旧的按需契约语义。
358
+ const hasV2OrdinaryTask = executionRequirementVersion === 2 && tasks.some(task => !isReviewFixTaskId(task.taskId));
359
+ const mode = hasTaskBoundExecutionRequirements(content) || hasLegacyGreenOnlyTask || hasV2OrdinaryTask;
348
360
  const contracts = parseExecutionRequirements(content);
349
361
  // 孤儿检测必须在 mode=false 的 early return 之前:全部块都悬空时 mode=false,
350
362
  // 恰恰是最需要报错的场景(否则契约模式静默失效)
351
363
  const errors = [...orphanExecutionRequirementErrors(content), ...contracts.flatMap(item => item.errors)];
352
364
  if (!mode)
353
365
  return { ok: errors.length === 0, mode, contracts, errors };
354
- const tasks = parseTasksMd(content);
355
366
  const contractsByTask = new Map(contracts.map(item => [item.taskId, item]));
356
367
  const declaredTestIds = new Set();
357
368
  let needsTestContract = false;
358
369
  for (const task of tasks) {
359
370
  const contract = contractsByTask.get(task.taskId);
360
- if (task.tddRequired && !isReviewFixTaskId(task.taskId)) {
361
- if (!contract) {
362
- errors.push(`${task.taskId} 缺少执行依据`);
363
- continue;
364
- }
365
- if (contract.contract.tests.length === 0) {
366
- errors.push(`${task.taskId} 是普通 TDD 任务,执行依据缺少测试`);
367
- }
371
+ const legacyGreenOnly = task.noTddReason === GREEN_ONLY_NO_TDD_REASON;
372
+ if (legacyGreenOnly && task.tddRequired) {
373
+ errors.push(`${task.taskId} 的 no_tdd_reason=${GREEN_ONLY_NO_TDD_REASON} 必须同时声明 tdd_required:false`);
374
+ }
375
+ if (legacyGreenOnly && executionPolicy !== "green_only") {
376
+ errors.push(`${task.taskId} no_tdd_reason=${GREEN_ONLY_NO_TDD_REASON} 只允许用于 GREEN-only apply`);
377
+ }
378
+ // v2 是本次改造后的新计划:每个普通 task 必须声明完整五字段,
379
+ // `测试:` 可显式为空以表达非行为任务。旧计划只保持原 TDD 契约要求。
380
+ if (executionRequirementVersion === 2 && !isReviewFixTaskId(task.taskId) && !contract) {
381
+ errors.push(`${task.taskId} 缺少执行依据`);
382
+ continue;
383
+ }
384
+ if (executionRequirementVersion === 1 && task.tddRequired && !isReviewFixTaskId(task.taskId) && !contract) {
385
+ errors.push(`${task.taskId} 缺少执行依据`);
386
+ continue;
387
+ }
388
+ if (contract && executionRequirementVersion === 2) {
389
+ if (!contract.declaredFields.includes("tests"))
390
+ errors.push(`${task.taskId} 的执行依据缺少测试字段`);
391
+ if (!contract.contract.design)
392
+ errors.push(`${task.taskId} 的执行依据缺少设计`);
393
+ if (contract.contract.source.length === 0)
394
+ errors.push(`${task.taskId} 的执行依据缺少来源`);
395
+ if (!contract.contract.acceptance)
396
+ errors.push(`${task.taskId} 的执行依据缺少验收目标`);
397
+ if (!contract.contract.guard)
398
+ errors.push(`${task.taskId} 的执行依据缺少边界`);
399
+ }
400
+ if (contract && (executionRequirementVersion === 1 && task.tddRequired || legacyGreenOnly) && contract.contract.tests.length === 0) {
401
+ errors.push(`${task.taskId} 的执行依据缺少测试`);
368
402
  }
369
403
  if (contract && contract.contract.tests.length > 0) {
370
404
  needsTestContract = true;
@@ -1,8 +1,13 @@
1
- import type { DirtyFileFingerprint } from "./types.ts";
1
+ import type { BoundarySnapshot, DirtyFileFingerprint } from "./types.ts";
2
2
  export interface GitHeadResult {
3
3
  head: string | null;
4
4
  reason: string;
5
5
  }
6
+ export interface JavaAutoStageResult {
7
+ status: "staged" | "skipped" | "failed";
8
+ files: string[];
9
+ reason?: string;
10
+ }
6
11
  export declare function currentGitHead(projectRoot: string): GitHeadResult;
7
12
  export declare function normalizeGitPath(rawPath: string): string;
8
13
  export declare function gitLines(projectRoot: string, args: string[]): {
@@ -33,4 +38,10 @@ export declare function dirtyCodePaths(projectRoot: string): {
33
38
  paths: string[];
34
39
  reason: string;
35
40
  };
41
+ /**
42
+ * 仅暂存当前 task 启动后新生成的生产 Java 文件。
43
+ * 不能安全归因的既有文件改动绝不自动 git add:它们可能包含用户或并行任务的未提交内容。
44
+ * 测试源码和常见测试类命名会被排除;没有可用的启动边界或 Git 状态异常时,不影响任务完成。
45
+ */
46
+ export declare function stageProductionJavaFilesSince(projectRoot: string, before: BoundarySnapshot | null): JavaAutoStageResult;
36
47
  export declare function projectHasReadableDirectory(projectRoot: string): boolean;
package/dist/git_state.js CHANGED
@@ -169,6 +169,51 @@ export function dirtyCodePaths(projectRoot) {
169
169
  return { ok: false, paths: [], reason: dirty.reason };
170
170
  return { ok: true, paths: [...new Set(dirty.files.map(file => file.path))].sort() };
171
171
  }
172
+ function isJavaTestPath(path) {
173
+ const normalized = path.replace(/\\/g, "/");
174
+ const base = normalized.split("/").pop() ?? normalized;
175
+ return /(^|\/)src\/test\//i.test(normalized) ||
176
+ /(?:Test|Tests|TestCase|IT|ITCase)\.java$/i.test(base);
177
+ }
178
+ /**
179
+ * 仅暂存当前 task 启动后新生成的生产 Java 文件。
180
+ * 不能安全归因的既有文件改动绝不自动 git add:它们可能包含用户或并行任务的未提交内容。
181
+ * 测试源码和常见测试类命名会被排除;没有可用的启动边界或 Git 状态异常时,不影响任务完成。
182
+ */
183
+ export function stageProductionJavaFilesSince(projectRoot, before) {
184
+ if (!before) {
185
+ return { status: "skipped", files: [], reason: "missing_task_start_boundary" };
186
+ }
187
+ const current = dirtyCodeFiles(projectRoot);
188
+ if (!current.ok) {
189
+ return { status: "failed", files: [], reason: current.reason };
190
+ }
191
+ const currentByPath = new Map(current.files.map(file => [file.path, file]));
192
+ const beforeByPath = new Map(before.dirty_files.map(file => [file.path, file]));
193
+ const files = diffFingerprints(before.dirty_files, current.files)
194
+ .filter(path => path.toLowerCase().endsWith(".java"))
195
+ .filter(path => !isJavaTestPath(path))
196
+ // "added" 且 task-start 边界不存在,才是可归因于本任务的新生成文件。
197
+ // 已有未跟踪文件或已有源码的任何修改一律不碰,避免把用户工作带入 index。
198
+ .filter(path => !beforeByPath.has(path) && currentByPath.get(path)?.status === "added")
199
+ .filter(path => currentByPath.get(path)?.status !== "deleted")
200
+ .sort();
201
+ if (files.length === 0)
202
+ return { status: "skipped", files: [], reason: "no_changed_production_java" };
203
+ try {
204
+ execFileSync("git", ["-C", projectRoot, "add", "--", ...files], {
205
+ stdio: ["ignore", "ignore", "pipe"],
206
+ });
207
+ return { status: "staged", files };
208
+ }
209
+ catch (err) {
210
+ return {
211
+ status: "failed",
212
+ files,
213
+ reason: err instanceof Error ? err.message : "git add failed",
214
+ };
215
+ }
216
+ }
172
217
  export function projectHasReadableDirectory(projectRoot) {
173
218
  return existsSync(projectRoot) && statSync(projectRoot).isDirectory();
174
219
  }
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
+ workflow_config: string;
13
14
  agents_md: string;
14
15
  openspec_config: string;
15
16
  };
package/dist/install.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { SUPERSPEC_VERSION } from "./version.js";
4
+ import { DEFAULT_WORKFLOW_RISK, WORKFLOW_CONFIG_PATH } from "./workflow_config.js";
4
5
  export const WORKFLOW_SKILLS = [
5
6
  "superspec-explore",
6
7
  "superspec-propose",
@@ -247,6 +248,16 @@ function ensureOpenSpecChineseContext(projectRoot) {
247
248
  writeFileSync(configPath, next);
248
249
  return OPENSPEC_CONFIG_PATH;
249
250
  }
251
+ function ensureWorkflowConfig(projectRoot) {
252
+ const configPath = join(projectRoot, WORKFLOW_CONFIG_PATH);
253
+ mkdirSync(dirname(configPath), { recursive: true });
254
+ if (!existsSync(configPath)) {
255
+ // install/update 是显式迁移动作:为以后各轮写入 normal 默认值。
256
+ // 未执行安装的旧项目仍由 workflowRiskForProject 保守回放 strict。
257
+ writeFileSync(configPath, JSON.stringify({ workflow: { mode: DEFAULT_WORKFLOW_RISK } }, null, 2) + "\n");
258
+ }
259
+ return WORKFLOW_CONFIG_PATH;
260
+ }
250
261
  const AGENTS_MD_PATH = "AGENTS.md";
251
262
  const SUPERSPEC_AGENTS_START = "<!-- SUPERSPEC:AGENTS:START -->";
252
263
  const SUPERSPEC_AGENTS_END = "<!-- SUPERSPEC:AGENTS:END -->";
@@ -304,6 +315,7 @@ export function installProject(projectRoot, options = {}) {
304
315
  prompts: copyPrompts(templateRoot, projectRoot),
305
316
  agents: copyAgents(templateRoot, projectRoot),
306
317
  config: ensureCodexConfig(projectRoot),
318
+ workflow_config: ensureWorkflowConfig(projectRoot),
307
319
  agents_md: ensureAgentsMd(projectRoot, agentsMdTemplate),
308
320
  openspec_config: ensureOpenSpecChineseContext(projectRoot),
309
321
  },
package/dist/next.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { NextOutput } from "./types.ts";
2
2
  /** next 命令:读 snapshot,返回唯一可执行路径 */
3
- export declare function next(projectRoot: string, change: string, changeRoot: string, defaultRisk?: "minimal" | "normal" | "strict"): NextOutput;
3
+ export declare function next(projectRoot: string, change: string, changeRoot: string, defaultRisk?: import("./review.ts").ReviewRisk): NextOutput;
package/dist/next.js CHANGED
@@ -3,6 +3,7 @@ import { rebuildSnapshot } from "./sync.js";
3
3
  import { readEvents } from "./store.js";
4
4
  import { requiredJobActions } from "./job_action.js";
5
5
  import { planNextStep } from "./phase_plan.js";
6
+ import { workflowRiskForProject } from "./workflow_config.js";
6
7
  function requiredJobsOutput(state, change, jobs, reason) {
7
8
  return {
8
9
  state,
@@ -14,12 +15,6 @@ function requiredJobsOutput(state, change, jobs, reason) {
14
15
  function transitionCommand(change, name, extra = "") {
15
16
  return `superspec transition ${name} --change "${change}"${extra ? " " + extra : ""}`;
16
17
  }
17
- function riskFlag(risk) {
18
- return risk === "strict" ? "" : `--risk ${risk}`;
19
- }
20
- function riskArg(risk) {
21
- return risk ? riskFlag(risk) : "";
22
- }
23
18
  function formatTransitionArgs(plan) {
24
19
  if (plan.taskId)
25
20
  return `--task ${plan.taskId}`;
@@ -32,7 +27,7 @@ function formatTransitionArgs(plan) {
32
27
  if (plan.reopen?.reason === "review_finding") {
33
28
  return `--to propose --review-finding ${plan.reopen.jobId}#${plan.reopen.findingId} --reason "根据代码审查问题 ${plan.reopen.findingId} 回到计划阶段"`;
34
29
  }
35
- return riskArg(plan.risk);
30
+ return "";
36
31
  }
37
32
  function toNextOutput(change, plan) {
38
33
  switch (plan.kind) {
@@ -58,7 +53,7 @@ function toNextOutput(change, plan) {
58
53
  }
59
54
  }
60
55
  /** next 命令:读 snapshot,返回唯一可执行路径 */
61
- export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
56
+ export function next(projectRoot, change, changeRoot, defaultRisk = workflowRiskForProject(projectRoot)) {
62
57
  const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
63
58
  const events = readEvents(projectRoot, change);
64
59
  const plannedNextStep = planNextStep({ projectRoot, change, changeRoot, events, snapshot, mode: { kind: "risk", risk: defaultRisk } });
@@ -20,7 +20,13 @@ export interface PhaseConfirmationDecision {
20
20
  scope: string;
21
21
  answer: string;
22
22
  decision: PhaseDecision;
23
+ review_risk: ReviewRisk;
23
24
  }
25
+ /**
26
+ * 阶段确认不是 mode 的输入。它只读取当前 planning/apply round 的冻结快照;
27
+ * 仍处于 Explore/Propose 时才从项目配置获取候选 mode。
28
+ */
29
+ export declare function workflowRiskForPhaseConfirmation(projectRoot: string, events: Event[], snapshot: Snapshot): ReviewRisk;
24
30
  export declare function isPhaseConfirmationScope(value: unknown): value is string;
25
31
  export declare function phaseConfirmationForBoundary(projectRoot: string, events: Event[], snapshot: Snapshot, boundary: PhaseBoundary, risk?: ReviewRisk): PhaseConfirmation | null;
26
32
  export declare function phaseConfirmationForCurrentState(projectRoot: string, events: Event[], snapshot: Snapshot, risk?: ReviewRisk): PhaseConfirmation | null;
@@ -3,6 +3,7 @@ import { historicalProposeReadyRoles, reviewEvidenceDigest, reviewGateRoleResolu
3
3
  import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
4
4
  import { changeRoot as openspecChangeRoot } from "./openspec.js";
5
5
  import { findLatestEvent, sha256Text } from "./store.js";
6
+ import { hasFrozenWorkflowModeForProposeRound, workflowRiskForProject, workflowRiskForState, } from "./workflow_config.js";
6
7
  export const PHASE_CONFIRMATION_SCOPE_PREFIX = "phase_confirmation:";
7
8
  function latestTransition(events, predicate) {
8
9
  return findLatestEvent(events, "transition_commit", event => predicate(event.payload));
@@ -25,7 +26,7 @@ const SPECS = {
25
26
  },
26
27
  ],
27
28
  scopePrefix: `${PHASE_CONFIRMATION_SCOPE_PREFIX}explore_to_propose`,
28
- 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"),
29
30
  },
30
31
  propose_to_apply: {
31
32
  state: "propose_ready",
@@ -66,9 +67,18 @@ function boundaryForState(state) {
66
67
  }
67
68
  }
68
69
  function ordinaryReviewRolesForBoundary(events, boundary, gate, risk) {
69
- return boundary === "propose_to_apply"
70
- ? historicalProposeReadyRoles(events)
71
- : gate.requiredRolesForRisk(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));
72
82
  }
73
83
  function materialDigest(projectRoot, events, snapshot, boundary, risk) {
74
84
  const head = currentGitHead(projectRoot);
@@ -105,6 +115,7 @@ function materialDigest(projectRoot, events, snapshot, boundary, risk) {
105
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}`))
106
116
  : [];
107
117
  return sha256Text(JSON.stringify({
118
+ review_risk: risk,
108
119
  documents,
109
120
  tasks_structure_digest: snapshot.tasks_structure_digest,
110
121
  accepted_jobs: acceptedJobs,
@@ -121,14 +132,13 @@ function materialDigest(projectRoot, events, snapshot, boundary, risk) {
121
132
  function phaseRecordArgv(change) {
122
133
  return ["superspec", "record", "user-decision", "--change", change, "--input", "-"];
123
134
  }
124
- function nextArgv(change, risk) {
135
+ function nextArgv(change, _risk) {
125
136
  return [
126
137
  "superspec",
127
138
  "transition",
128
139
  "next",
129
140
  "--change",
130
141
  change,
131
- ...(risk === "strict" ? [] : ["--risk", risk]),
132
142
  ];
133
143
  }
134
144
  function buildActions(change, boundary, scope, question, specs, risk) {
@@ -220,6 +230,11 @@ export function latestAcceptedPhaseDecision(events, confirmation) {
220
230
  scope: confirmation.scope,
221
231
  answer: payload.answer,
222
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",
223
238
  };
224
239
  }
225
240
  export function isPhaseAdvanceAuthorized(events, confirmation) {
@@ -237,6 +252,7 @@ export function phaseConfirmationCommitPayload(confirmation, decision) {
237
252
  material_digest: confirmation.material_digest,
238
253
  scope: confirmation.scope,
239
254
  decision_event_id: decision.event.event_id,
255
+ review_risk: decision.review_risk,
240
256
  },
241
257
  };
242
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";
@@ -83,11 +83,18 @@ export interface ApplyPendingTaskStatus {
83
83
  needsCompletionEvent: string[];
84
84
  completedByEvent: string[];
85
85
  }
86
+ export declare function executionPolicyForRisk(risk: ReviewRisk): ExecutionPolicy;
87
+ export declare function executionPolicyForCurrentRound(events: Event[]): ExecutionPolicy;
86
88
  export declare function proposalDocsBaseline(changeRoot: string): Record<string, string>;
89
+ export declare function discoveryDocsBaseline(changeRoot: string): Record<string, string>;
87
90
  export declare function latestAcceptedProposalBaseline(events: Event[]): Record<string, string> | null;
88
91
  export declare function latestReopenProposeBaseline(events: Event[]): Record<string, string> | null;
92
+ export declare function latestReopenExploreBaseline(events: Event[]): Record<string, string> | null;
89
93
  export declare function proposalDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
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;