@peterxiaoyang/superspec 0.1.45 → 0.1.47

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/dist/cli.js +2 -1
  2. package/dist/code_review.d.ts +11 -1
  3. package/dist/code_review.js +40 -0
  4. package/dist/explore_round.d.ts +23 -0
  5. package/dist/explore_round.js +94 -0
  6. package/dist/format.d.ts +67 -2
  7. package/dist/format.js +273 -22
  8. package/dist/openspec.d.ts +13 -0
  9. package/dist/openspec.js +53 -4
  10. package/dist/phase_confirmation.js +71 -2
  11. package/dist/phase_plan.d.ts +6 -1
  12. package/dist/phase_plan.js +180 -32
  13. package/dist/record.js +191 -57
  14. package/dist/review.js +2 -0
  15. package/dist/task_evidence.js +5 -3
  16. package/dist/transition.d.ts +1 -0
  17. package/dist/transition.js +222 -28
  18. package/dist/types.d.ts +42 -0
  19. package/package.json +1 -1
  20. package/templates/workflow/AGENTS.md +15 -5
  21. package/templates/workflow/agents/architect.toml +1 -1
  22. package/templates/workflow/agents/code-reviewer.toml +1 -1
  23. package/templates/workflow/agents/critic.toml +1 -1
  24. package/templates/workflow/agents/executor.toml +1 -1
  25. package/templates/workflow/agents/explore.toml +1 -1
  26. package/templates/workflow/agents/test-engineer.toml +1 -1
  27. package/templates/workflow/agents/test-runner.toml +1 -1
  28. package/templates/workflow/agents/verifier.toml +1 -1
  29. package/templates/workflow/prompts/architect.md +25 -33
  30. package/templates/workflow/prompts/code-reviewer.md +19 -67
  31. package/templates/workflow/prompts/critic.md +36 -86
  32. package/templates/workflow/prompts/executor.md +17 -19
  33. package/templates/workflow/prompts/explore.md +12 -46
  34. package/templates/workflow/prompts/test-engineer.md +22 -34
  35. package/templates/workflow/prompts/test-runner.md +11 -21
  36. package/templates/workflow/prompts/verifier.md +13 -37
  37. package/templates/workflow/skills/superspec-apply/SKILL.md +26 -26
  38. package/templates/workflow/skills/superspec-explore/SKILL.md +69 -60
  39. package/templates/workflow/skills/superspec-propose/SKILL.md +85 -133
  40. package/templates/workflow/skills/superspec-review/SKILL.md +14 -44
@@ -1,8 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
4
- import { collectProposeOpenQuestions, countDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, } from "./format.js";
4
+ import { currentExploreRoundId, exploreAnswerRegistrationPayload, unregisteredClosedExploreQuestions, } from "./explore_round.js";
5
+ import { collectProposeOpenQuestions, discoveryOpenQuestionDisplayText, discoveryOpenQuestionScope, parseExecutionRequirements, parseDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, validateExecutionRequirementDocumentReferences, validateProposalImpact, validateTasksDocument, } from "./format.js";
5
6
  import { currentGitHead } from "./git_state.js";
7
+ import { validateOpenSpecChange } from "./openspec.js";
6
8
  import { docRef, sha256File } from "./store.js";
7
9
  import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRoles, readReviewPolicyFromEvents, reviewGateRoleResolution, reviewRejectionOverrideScope, reviewEvidenceDigest, } from "./review.js";
8
10
  import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
@@ -79,13 +81,19 @@ function reviewGatePlan(snapshot, events, changeRoot, gate, requiredRoles) {
79
81
  }
80
82
  return null;
81
83
  }
82
- function validateTasksPlan(changeRoot) {
84
+ function validateTasksPlan(changeRoot, executionRequirementVersion) {
83
85
  const tasksPath = join(changeRoot, "tasks.md");
84
86
  if (!existsSync(tasksPath))
85
87
  return "tasks.md 不存在";
86
88
  const tasksContent = readFileSync(tasksPath, "utf8");
87
- if (!tasksContent.includes("# Tasks") && !tasksContent.includes("- [ ]"))
88
- return "tasks.md 内容不像任务计划文档";
89
+ if (executionRequirementVersion === 1) {
90
+ return tasksContent.includes("# Tasks") || tasksContent.includes("- [ ]")
91
+ ? null
92
+ : "tasks.md 内容不像任务计划文档";
93
+ }
94
+ const errors = validateTasksDocument(tasksContent);
95
+ if (errors.length > 0)
96
+ return errors.join(";");
89
97
  return null;
90
98
  }
91
99
  export function executionPolicyForRisk(risk) {
@@ -122,6 +130,43 @@ function missingBaseArtifact(changeRoot, risk) {
122
130
  }
123
131
  return null;
124
132
  }
133
+ /** OpenSpec strict gate 只在当前 planning round 冻结为 strict 时执行。 */
134
+ function validateOpenSpecPlanningDocuments(projectRoot, change, changeRoot, profile) {
135
+ if (profile == null || profile.openspec.mode !== "strict")
136
+ return null;
137
+ const currentConfigDigest = sha256File(join(projectRoot, "openspec", "config.yaml"));
138
+ if (currentConfigDigest !== profile.openspec.config_digest) {
139
+ return "OpenSpec 配置自本 planning round 起已变化;请恢复原配置或 reopen --to propose 创建新的计划轮";
140
+ }
141
+ const tasksPath = join(changeRoot, "tasks.md");
142
+ const referenceErrors = validateExecutionRequirementDocumentReferences(changeRoot, parseExecutionRequirements(readFileSync(tasksPath, "utf8")));
143
+ if (referenceErrors.length > 0)
144
+ return referenceErrors.join(";");
145
+ const proposalPath = join(changeRoot, "proposal.md");
146
+ if (!existsSync(proposalPath))
147
+ return "proposal.md 不存在";
148
+ const impact = validateProposalImpact(readFileSync(proposalPath, "utf8"));
149
+ if (!impact.ok)
150
+ return impact.message;
151
+ const native = validateOpenSpecChange(projectRoot, change);
152
+ return native.ok ? null : native.message;
153
+ }
154
+ function validatePlanningPreflight(projectRoot, change, changeRoot, risk, executionPolicy, profile) {
155
+ const executionRequirementVersion = profile?.version ?? 1;
156
+ const tasksPlanError = validateTasksPlan(changeRoot, executionRequirementVersion);
157
+ if (tasksPlanError)
158
+ return { error: tasksPlanError, contractMode: false };
159
+ const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion);
160
+ if (!executionRequirementPlan.ok)
161
+ return { error: executionRequirementPlan.message, contractMode: false };
162
+ const missingArtifact = missingBaseArtifact(changeRoot, risk);
163
+ if (missingArtifact)
164
+ return { error: missingArtifact, contractMode: false };
165
+ return {
166
+ error: validateOpenSpecPlanningDocuments(projectRoot, change, changeRoot, profile),
167
+ contractMode: executionRequirementPlan.mode,
168
+ };
169
+ }
125
170
  export function proposalDocsBaseline(changeRoot) {
126
171
  // 与 Propose gate 的可修改审查目标保持同一来源:specs/ 用目录聚合指纹,避免 reopen 基线漏掉任一个可修改的计划材料。
127
172
  const docs = PROPOSE_FINAL_REVIEW_GATE.reviewTargets;
@@ -140,6 +185,11 @@ export function discoveryDocsBaseline(changeRoot) {
140
185
  }
141
186
  return baseline;
142
187
  }
188
+ /** 新 Explore 轮次冻结已有已确认事项,避免把历史答复当作本轮遗漏。 */
189
+ export function exploreAnswerRegistrationPayloadForChange(changeRoot) {
190
+ const discoveryPath = join(changeRoot, ".superspec", "artifacts", "discovery.md");
191
+ return exploreAnswerRegistrationPayload(existsSync(discoveryPath) ? readFileSync(discoveryPath, "utf8") : null);
192
+ }
143
193
  function isDigestMap(value) {
144
194
  if (!value || typeof value !== "object" || Array.isArray(value))
145
195
  return false;
@@ -234,6 +284,62 @@ function executionRequirementVersionForProposeRound(events) {
234
284
  }
235
285
  return 1;
236
286
  }
287
+ function isPlanningValidationProfile(value) {
288
+ if (!value || typeof value !== "object" || Array.isArray(value))
289
+ return false;
290
+ const profile = value;
291
+ if (profile.version !== 2 || !profile.openspec || typeof profile.openspec !== "object")
292
+ return false;
293
+ return profile.openspec.mode === "disabled" ||
294
+ profile.openspec.mode === "strict" && typeof profile.openspec.config_digest === "string";
295
+ }
296
+ /** 新 planning round 在进入 propose 时冻结当前 OpenSpec 校验契约。 */
297
+ export function planningValidationProfileForNewRound(projectRoot) {
298
+ const configDigest = sha256File(join(projectRoot, "openspec", "config.yaml"));
299
+ return configDigest == null
300
+ ? { version: 2, openspec: { mode: "disabled" } }
301
+ : { version: 2, openspec: { mode: "strict", config_digest: configDigest } };
302
+ }
303
+ /** propose 状态尚未 ready 时,从进入本 planning round 的事件读取冻结 profile。 */
304
+ function planningValidationProfileForPendingProposeRound(events) {
305
+ for (let i = events.length - 1; i >= 0; i--) {
306
+ const event = events[i];
307
+ if (event.event_type !== "transition_commit")
308
+ continue;
309
+ const payload = event.payload;
310
+ // 只读取“进入 propose”的边界事件。propose-ready 创建审查 job 时也会
311
+ // 保持在 propose;若把它误当作新的 planning round,便会覆盖此前冻结的
312
+ // profile 并把当前轮错误降级为 v1。
313
+ const entersPropose = payload.to_state === "propose" && (payload.transition === "explore" ||
314
+ payload.transition === "propose" ||
315
+ payload.transition === "reopen" && payload.reopen_target === "propose");
316
+ if (!entersPropose)
317
+ continue;
318
+ return isPlanningValidationProfile(payload.planning_validation_profile)
319
+ ? payload.planning_validation_profile
320
+ : null;
321
+ }
322
+ return null;
323
+ }
324
+ /** 已完成 propose-ready 的 round 只回放当时冻结的 profile。 */
325
+ function planningValidationProfileForReadyProposeRound(events) {
326
+ for (let i = events.length - 1; i >= 0; i--) {
327
+ const event = events[i];
328
+ if (event.event_type !== "transition_commit")
329
+ continue;
330
+ const payload = event.payload;
331
+ if (payload.transition !== "propose-ready" || payload.to_state !== "propose_ready")
332
+ continue;
333
+ if (isPlanningValidationProfile(payload.planning_validation_profile))
334
+ return payload.planning_validation_profile;
335
+ // 过渡期已写 v2 执行依据、但尚未带 profile 的事件保持 v2 tasks 契约,
336
+ // 但不在 start-apply 追溯新增 strict gate。
337
+ return executionRequirementVersionFromPayload(event.payload) === 2
338
+ ? { version: 2, openspec: { mode: "disabled" } }
339
+ : null;
340
+ }
341
+ return null;
342
+ }
237
343
  export function applyRequirementModeForCurrentRound(events) {
238
344
  const index = latestStartApplyIndex(events);
239
345
  if (index < 0)
@@ -352,10 +458,6 @@ export function planNextStep(context) {
352
458
  reason: "回到 explore 后至少一个 discovery 材料必须变化",
353
459
  };
354
460
  }
355
- const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
356
- if (exploreReviewJobs.length > 0) {
357
- return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
358
- }
359
461
  const discoveryCheck = validateDiscovery(changeRoot);
360
462
  if (!discoveryCheck.ok) {
361
463
  const ask = {
@@ -366,14 +468,32 @@ export function planNextStep(context) {
366
468
  return { kind: "ask_user", state: "explore", ask, reason: discoveryCheck.message };
367
469
  }
368
470
  const content = readFileSync(join(changeRoot, ".superspec", "artifacts", "discovery.md"), "utf8");
369
- const openQs = countDiscoveryOpenQuestions(content);
370
- if (openQs > 0) {
471
+ const currentQuestion = parseDiscoveryOpenQuestions(content)[0];
472
+ if (currentQuestion) {
473
+ const questionText = discoveryOpenQuestionDisplayText(currentQuestion);
371
474
  const ask = {
372
- question: `discovery.md 有 ${openQs} 个未解决的待确认问题,请逐个确认`,
373
- allowed_answers: ["所有问题已确认"],
374
- scope: "explore_open_questions",
475
+ question: `现在有一件事需要你确认:${questionText}\n\n请只回答这一件事。主流程会先登记答复,再将结论回写 discovery.md;回写完成前会继续询问这一件事。`,
476
+ // 用户可以接受建议、选择其他方向或补充事实;状态机不解释答案语义。
477
+ allowed_answers: [],
478
+ scope: discoveryOpenQuestionScope(currentQuestion, currentExploreRoundId(events)),
479
+ };
480
+ return { kind: "ask_user", state: "explore", ask, reason: "等待用户确认" };
481
+ }
482
+ const unregisteredClosedQuestions = unregisteredClosedExploreQuestions(events, content);
483
+ if (unregisteredClosedQuestions.length > 0) {
484
+ const ask = {
485
+ question: "发现一项已标记为已确认的事项没有对应的答复登记。请先将该项恢复为待确认,按主流程重新登记用户答复并回写 discovery.md 后继续。",
486
+ allowed_answers: ["已处理"],
487
+ scope: "explore_answer_registration",
375
488
  };
376
- return { kind: "ask_user", state: "explore", ask, reason: `有 ${openQs} 个未确认问题` };
489
+ return { kind: "ask_user", state: "explore", ask, reason: "存在未登记答复的已确认事项" };
490
+ }
491
+ // 先让用户澄清当前 Discovery,再审查材料;否则 critic 会审查一份仍有
492
+ // 关键业务未知的文档。正常创建的 job 已绑定 discovery 指纹,材料变化后
493
+ // 会由 snapshot freshness 自动失效。
494
+ const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
495
+ if (exploreReviewJobs.length > 0) {
496
+ return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
377
497
  }
378
498
  const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
379
499
  if (!reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
@@ -542,6 +662,16 @@ function planApplyDoneNext(context) {
542
662
  }
543
663
  const latest = facts.latestTerminal;
544
664
  if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
665
+ const staleReason = codeReviewJobStaleReason(context.projectRoot, latest.job, currentWorkingPaths);
666
+ if (staleReason) {
667
+ return {
668
+ kind: "run_transition",
669
+ state: "apply_done",
670
+ transition: "review-ready",
671
+ risk: mode.risk,
672
+ reason: `代码审查结论已过期,重新发起审查:${staleReason}`,
673
+ };
674
+ }
545
675
  const status = latestCodeReviewFailedStatus(events);
546
676
  const pendingFinding = status?.unresolved[0] ?? null;
547
677
  if (status && status.findings.length > 0 && !pendingFinding) {
@@ -704,7 +834,13 @@ export function planTransition(name, context) {
704
834
  function planExploreTransition(context) {
705
835
  const { changeRoot, events, mode, projectRoot, snapshot } = context;
706
836
  if (snapshot.state === "init") {
707
- return { kind: "advance", fromState: "init", toState: "explore", reason: "进入探索阶段" };
837
+ return {
838
+ kind: "advance",
839
+ fromState: "init",
840
+ toState: "explore",
841
+ reason: "进入探索阶段",
842
+ payload: exploreAnswerRegistrationPayloadForChange(changeRoot),
843
+ };
708
844
  }
709
845
  if (snapshot.state !== "explore") {
710
846
  return { kind: "skip", message: `当前状态 ${snapshot.state},explore 不适用` };
@@ -716,6 +852,14 @@ function planExploreTransition(context) {
716
852
  const discoveryCheck = validateDiscovery(changeRoot);
717
853
  if (!discoveryCheck.ok)
718
854
  return { kind: "skip", message: discoveryCheck.message };
855
+ const discoveryContent = readFileSync(join(changeRoot, ".superspec", "artifacts", "discovery.md"), "utf8");
856
+ const currentQuestion = parseDiscoveryOpenQuestions(discoveryContent)[0];
857
+ if (currentQuestion) {
858
+ return { kind: "skip", message: "discovery.md 仍有需要确认的事项,请先完成确认并回写 discovery.md" };
859
+ }
860
+ if (unregisteredClosedExploreQuestions(events, discoveryContent).length > 0) {
861
+ return { kind: "skip", message: "discovery.md 有已确认事项缺少对应答复登记,请先恢复为待确认并按主流程登记答复" };
862
+ }
719
863
  const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
720
864
  const gatePlan = reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
721
865
  if (gatePlan)
@@ -730,7 +874,11 @@ function planExploreTransition(context) {
730
874
  fromState: "explore",
731
875
  toState: "propose",
732
876
  reason: "探索完成",
733
- payload: phaseConfirmationCommitPayload(confirmation, decision),
877
+ payload: {
878
+ ...phaseConfirmationCommitPayload(confirmation, decision),
879
+ planning_validation_version: 2,
880
+ planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
881
+ },
734
882
  };
735
883
  }
736
884
  function planProposeReadyTransition(context) {
@@ -738,20 +886,15 @@ function planProposeReadyTransition(context) {
738
886
  const risk = mode.risk;
739
887
  if (snapshot.state !== "propose")
740
888
  return { kind: "skip", message: `当前状态 ${snapshot.state},不能 propose-ready` };
741
- const tasksPlanError = validateTasksPlan(changeRoot);
742
- if (tasksPlanError)
743
- return { kind: "skip", message: tasksPlanError };
744
- const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicyForRisk(risk), 2);
745
- if (!executionRequirementPlan.ok)
746
- return { kind: "skip", message: executionRequirementPlan.message };
889
+ const planningProfile = planningValidationProfileForPendingProposeRound(context.events);
890
+ const preflight = validatePlanningPreflight(context.projectRoot, context.change, changeRoot, risk, executionPolicyForRisk(risk), planningProfile);
891
+ if (preflight.error)
892
+ return { kind: "skip", message: preflight.error };
747
893
  const openQuestions = collectProposeOpenQuestions(changeRoot);
748
894
  if (openQuestions.openCount > 0) {
749
895
  const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
750
896
  return { kind: "skip", message: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}` };
751
897
  }
752
- const missingArtifact = missingBaseArtifact(changeRoot, risk);
753
- if (missingArtifact)
754
- return { kind: "skip", message: missingArtifact };
755
898
  const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
756
899
  const gatePlan = reviewGatePlan(snapshot, context.events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
757
900
  if (gatePlan)
@@ -763,7 +906,11 @@ function planProposeReadyTransition(context) {
763
906
  reason: `risk=${risk},所有需求已满足`,
764
907
  payload: {
765
908
  workflow_mode: risk,
766
- execution_requirement_version: 2,
909
+ ...(planningProfile ? {
910
+ execution_requirement_version: 2,
911
+ planning_validation_version: 2,
912
+ planning_validation_profile: planningProfile,
913
+ } : {}),
767
914
  },
768
915
  };
769
916
  }
@@ -782,7 +929,12 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
782
929
  return { kind: "skip", message: `回到 propose 后至少一个计划文档必须变化(基线绑定:${Object.keys(reopenBaseline).join("、")})` };
783
930
  }
784
931
  const risk = workflowRiskForProposeRound(events, context.mode.risk);
785
- const executionRequirementVersion = executionRequirementVersionForProposeRound(events);
932
+ const planningProfile = planningValidationProfileForReadyProposeRound(events);
933
+ const executionRequirementVersion = planningProfile?.version ?? executionRequirementVersionForProposeRound(events);
934
+ const executionPolicy = executionPolicyForRisk(risk);
935
+ const preflight = validatePlanningPreflight(projectRoot, context.change, changeRoot, risk, executionPolicy, planningProfile);
936
+ if (preflight.error)
937
+ return { kind: "skip", message: preflight.error };
786
938
  const requiredRoles = executionRequirementVersion === 2
787
939
  ? PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk)
788
940
  : historicalProposeReadyRoles(events);
@@ -796,10 +948,6 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
796
948
  const acceptedConfirmation = enforceConfirmation
797
949
  ? acceptedProposeToApplyConfirmation(context, risk)
798
950
  : null;
799
- const executionPolicy = executionPolicyForRisk(risk);
800
- const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion);
801
- if (!executionRequirementPlan.ok)
802
- return { kind: "skip", message: executionRequirementPlan.message };
803
951
  if (enforceConfirmation && !acceptedConfirmation) {
804
952
  const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply", risk);
805
953
  return {
@@ -816,7 +964,7 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
816
964
  payload: {
817
965
  apply_start_head: gitHead.head,
818
966
  apply_start_head_reason: gitHead.reason,
819
- apply_contract_mode: executionRequirementPlan.mode,
967
+ apply_contract_mode: preflight.contractMode,
820
968
  ...(executionRequirementVersion === 2 ? { execution_requirement_version: 2 } : {}),
821
969
  execution_policy: executionPolicy,
822
970
  workflow_mode: risk,