@peterxiaoyang/superspec 0.1.54 → 0.1.56

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 (33) hide show
  1. package/README.md +7 -6
  2. package/dist/approved_ref.d.ts +26 -0
  3. package/dist/approved_ref.js +158 -0
  4. package/dist/cli.js +71 -10
  5. package/dist/code_review.d.ts +1 -0
  6. package/dist/code_review.js +36 -0
  7. package/dist/install.d.ts +11 -0
  8. package/dist/install.js +68 -13
  9. package/dist/next.js +4 -1
  10. package/dist/phase_plan.d.ts +2 -1
  11. package/dist/phase_plan.js +12 -0
  12. package/dist/record.js +25 -8
  13. package/dist/transition.js +19 -4
  14. package/dist/types.d.ts +11 -0
  15. package/dist/workflow_config.d.ts +10 -0
  16. package/dist/workflow_config.js +78 -20
  17. package/package.json +1 -1
  18. package/templates/workflow/AGENTS.md +3 -1
  19. package/templates/workflow/agents-md/architect.md +13 -0
  20. package/templates/workflow/agents-md/code-reviewer.md +13 -0
  21. package/templates/workflow/agents-md/critic.md +13 -0
  22. package/templates/workflow/agents-md/executor.md +13 -0
  23. package/templates/workflow/agents-md/explore.md +13 -0
  24. package/templates/workflow/agents-md/test-engineer.md +13 -0
  25. package/templates/workflow/agents-md/test-runner.md +13 -0
  26. package/templates/workflow/agents-md/verifier.md +13 -0
  27. package/templates/workflow/prompts/code-reviewer.md +3 -4
  28. package/templates/workflow/prompts/critic.md +1 -0
  29. package/templates/workflow/prompts/executor.md +1 -0
  30. package/templates/workflow/skills/superspec-apply/SKILL.md +1 -1
  31. package/templates/workflow/skills/superspec-explore/SKILL.md +1 -1
  32. package/templates/workflow/skills/superspec-propose/SKILL.md +1 -1
  33. package/templates/workflow/skills/superspec-review/SKILL.md +1 -1
package/README.md CHANGED
@@ -33,14 +33,14 @@ superspec status
33
33
 
34
34
  ## 工作流入口
35
35
 
36
- 在 Codex 中显式调用对应 Skill。新需求通常从 `$superspec-explore` 开始;已有 change 则从当前阶段继续。
36
+ 在 Codex 中显式调用对应 Skill。新需求通常从 `superspec-explore` 开始;已有 change 则从当前阶段继续。
37
37
 
38
38
  | Skill | 作用 |
39
39
  | --- | --- |
40
- | `$superspec-explore` | 调查现状,确认事实和待决策事项 |
41
- | `$superspec-propose` | 生成规格、设计和可执行任务 |
42
- | `$superspec-apply` | 按已批准任务修改代码并验证 |
43
- | `$superspec-review` | 审查实现并完成最终验证 |
40
+ | `superspec-explore` | 调查现状,确认事实和待决策事项 |
41
+ | `superspec-propose` | 生成规格、设计和可执行任务 |
42
+ | `superspec-apply` | 按已批准任务修改代码并验证 |
43
+ | `superspec-review` | 审查实现并完成最终验证 |
44
44
 
45
45
  工作流会根据问题性质留在当前阶段修复,或回到计划阶段重新确认需求、验收和技术取舍。
46
46
 
@@ -51,7 +51,8 @@ superspec status
51
51
  ```json
52
52
  {
53
53
  "workflow": {
54
- "mode": "normal"
54
+ "mode": "normal",
55
+ "hosts": ["codex"]
55
56
  }
56
57
  }
57
58
  ```
@@ -0,0 +1,26 @@
1
+ import type { CodeReviewClaimKind } from "./types.ts";
2
+ export declare const CODE_REVIEW_CLAIM_KINDS: readonly ["missing_approved", "breaks_existing", "unjustified_addition"];
3
+ export type ApprovedRefKind = "test" | "requirement" | "task" | "design" | "proposal";
4
+ export interface ResolvedApprovedRef {
5
+ raw: string;
6
+ kind: ApprovedRefKind;
7
+ short: string;
8
+ }
9
+ export declare function isCodeReviewClaimKind(value: unknown): value is CodeReviewClaimKind;
10
+ export declare function shortApprovedRef(raw: string): string;
11
+ export declare function resolveApprovedRef(changeRoot: string, raw: unknown): {
12
+ ok: true;
13
+ value: ResolvedApprovedRef;
14
+ } | {
15
+ ok: false;
16
+ reason: string;
17
+ };
18
+ export declare function resolveApprovedRefs(changeRoot: string, refs: unknown): {
19
+ ok: true;
20
+ values: ResolvedApprovedRef[];
21
+ } | {
22
+ ok: false;
23
+ reasons: string[];
24
+ };
25
+ export declare function hasBehaviorAnchor(values: readonly ResolvedApprovedRef[]): boolean;
26
+ export declare function reviewFixReason(claimKind: CodeReviewClaimKind, refs: readonly string[]): string;
@@ -0,0 +1,158 @@
1
+ // SuperSpec 代码审查 approved_refs:只做存在性解析,不做语义匹配。
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { parseTasksMd, parseTestContractEntries } from "./format.js";
5
+ export const CODE_REVIEW_CLAIM_KINDS = [
6
+ "missing_approved",
7
+ "breaks_existing",
8
+ "unjustified_addition",
9
+ ];
10
+ const TEST_ID_RE = /^TEST-[A-Za-z0-9_-]+$/;
11
+ const TEST_CONTRACT_REL = join(".superspec", "artifacts", "test-contract.md");
12
+ function escapeRegex(value) {
13
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
+ }
15
+ function isPathInside(root, target) {
16
+ const rel = relative(root, target);
17
+ return rel !== "" && !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
18
+ }
19
+ function headingExists(content, title) {
20
+ const heading = new RegExp(`^#{1,6}[\\t ]+${escapeRegex(title)}(?:[\\t ]+#+)?[\\t ]*$`, "m");
21
+ return heading.test(content);
22
+ }
23
+ function shortTestId(raw) {
24
+ const trimmed = raw.trim();
25
+ if (TEST_ID_RE.test(trimmed))
26
+ return trimmed;
27
+ const hash = trimmed.lastIndexOf("#");
28
+ if (hash >= 0) {
29
+ const id = trimmed.slice(hash + 1).trim();
30
+ if (TEST_ID_RE.test(id))
31
+ return id;
32
+ }
33
+ return null;
34
+ }
35
+ function readChangeFile(changeRoot, relPath) {
36
+ const target = resolve(changeRoot, relPath);
37
+ if (!isPathInside(resolve(changeRoot), target) && resolve(changeRoot) !== target)
38
+ return null;
39
+ try {
40
+ if (!statSync(target).isFile())
41
+ return null;
42
+ return readFileSync(target, "utf8");
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export function isCodeReviewClaimKind(value) {
49
+ return typeof value === "string" && CODE_REVIEW_CLAIM_KINDS.includes(value);
50
+ }
51
+ export function shortApprovedRef(raw) {
52
+ const testId = shortTestId(raw);
53
+ if (testId)
54
+ return testId;
55
+ const req = /#Requirement:\s*(.+)$/.exec(raw.trim());
56
+ if (req)
57
+ return `Requirement: ${req[1].trim()}`;
58
+ const hash = raw.lastIndexOf("#");
59
+ if (hash >= 0 && hash < raw.length - 1)
60
+ return raw.slice(hash + 1).trim();
61
+ return raw.trim();
62
+ }
63
+ export function resolveApprovedRef(changeRoot, raw) {
64
+ if (typeof raw !== "string" || raw.trim() === "") {
65
+ return { ok: false, reason: "approved_refs 条目必须是非空字符串" };
66
+ }
67
+ const ref = raw.trim();
68
+ if (/[\u0000-\u001f\u007f]/.test(ref)) {
69
+ return { ok: false, reason: `approved_refs 条目不能包含换行等控制字符:${JSON.stringify(ref)}` };
70
+ }
71
+ const testId = shortTestId(ref);
72
+ if (testId) {
73
+ if (ref.includes("#") && !ref.endsWith(`#${testId}`)) {
74
+ return { ok: false, reason: `TEST 引用只能是裸 TEST-ID 或以 #TEST-ID 结尾指向 test-contract.md:${ref}` };
75
+ }
76
+ if (ref.includes("#")) {
77
+ const path = ref.slice(0, ref.lastIndexOf("#")).replace(/\\/g, "/");
78
+ const allowed = path === TEST_CONTRACT_REL.replace(/\\/g, "/")
79
+ || path === "test-contract.md"
80
+ || path.endsWith("/test-contract.md");
81
+ if (!allowed)
82
+ return { ok: false, reason: `TEST 引用只能指向 test-contract.md:${ref}` };
83
+ }
84
+ const content = readChangeFile(changeRoot, TEST_CONTRACT_REL);
85
+ if (content == null)
86
+ return { ok: false, reason: `无法读取 ${TEST_CONTRACT_REL.replace(/\\/g, "/")},无法校验 TEST 引用:${ref}` };
87
+ const parsed = parseTestContractEntries(content);
88
+ if (!parsed.ok || !parsed.entries.some(entry => entry.test_id === testId)) {
89
+ return { ok: false, reason: `${TEST_CONTRACT_REL.replace(/\\/g, "/")} 中不存在 ${testId}` };
90
+ }
91
+ return { ok: true, value: { raw: ref, kind: "test", short: testId } };
92
+ }
93
+ const separator = ref.indexOf("#");
94
+ if (separator <= 0 || separator === ref.length - 1) {
95
+ return { ok: false, reason: `锚点格式无法解析,应为 文件#标题 或 TEST-ID:${ref}` };
96
+ }
97
+ const path = ref.slice(0, separator).replace(/\\/g, "/");
98
+ const anchor = ref.slice(separator + 1).trim();
99
+ const content = readChangeFile(changeRoot, path);
100
+ if (content == null)
101
+ return { ok: false, reason: `${path} 在当前 change 中不存在` };
102
+ if (path === "tasks.md") {
103
+ const tasks = parseTasksMd(content);
104
+ if (!tasks.some(task => task.taskId === anchor))
105
+ return { ok: false, reason: `tasks.md 中不存在任务 ${anchor}` };
106
+ return { ok: true, value: { raw: ref, kind: "task", short: anchor } };
107
+ }
108
+ if (path === "design.md") {
109
+ if (!headingExists(content, anchor))
110
+ return { ok: false, reason: `design.md 中不存在标题「${anchor}」` };
111
+ return { ok: true, value: { raw: ref, kind: "design", short: anchor } };
112
+ }
113
+ if (path === "proposal.md") {
114
+ if (!headingExists(content, anchor))
115
+ return { ok: false, reason: `proposal.md 中不存在标题「${anchor}」` };
116
+ return { ok: true, value: { raw: ref, kind: "proposal", short: anchor } };
117
+ }
118
+ if (/^specs\/[^/]+\/spec\.md$/.test(path)) {
119
+ const requirementTitle = anchor.startsWith("Requirement:")
120
+ ? anchor.slice("Requirement:".length).trim()
121
+ : "";
122
+ if (!requirementTitle)
123
+ return { ok: false, reason: `spec 锚点必须以 Requirement: 开头:${ref}` };
124
+ if (!headingExists(content, `Requirement: ${requirementTitle}`)) {
125
+ return { ok: false, reason: `${path} 中不存在 Requirement「${requirementTitle}」` };
126
+ }
127
+ return {
128
+ ok: true,
129
+ value: { raw: ref, kind: "requirement", short: `Requirement: ${requirementTitle}` },
130
+ };
131
+ }
132
+ return { ok: false, reason: `只支持 tasks.md、design.md、proposal.md、specs/*/spec.md 与 test-contract.md 的锚点:${ref}` };
133
+ }
134
+ export function resolveApprovedRefs(changeRoot, refs) {
135
+ if (!Array.isArray(refs) || refs.length === 0) {
136
+ return { ok: false, reasons: ["approved_refs 必须是非空字符串数组"] };
137
+ }
138
+ const values = [];
139
+ const reasons = [];
140
+ for (const raw of refs) {
141
+ const resolved = resolveApprovedRef(changeRoot, raw);
142
+ if (!resolved.ok)
143
+ reasons.push(resolved.reason);
144
+ else
145
+ values.push(resolved.value);
146
+ }
147
+ if (reasons.length > 0)
148
+ return { ok: false, reasons };
149
+ return { ok: true, values };
150
+ }
151
+ export function hasBehaviorAnchor(values) {
152
+ return values.some(value => value.kind === "test" || value.kind === "requirement");
153
+ }
154
+ export function reviewFixReason(claimKind, refs) {
155
+ const shorts = refs.map(shortApprovedRef).filter(Boolean);
156
+ const target = shorts.length > 0 ? shorts.join("、") : "已批准行为";
157
+ return `兑现 ${target}(${claimKind})`;
158
+ }
package/dist/cli.js CHANGED
@@ -13,7 +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
+ import { DEFAULT_WORKFLOW_HOSTS, parseWorkflowHostsFlag, WorkflowConfigError, workflowHostsDeclared, workflowHostsForProject, workflowRiskForProject, } from "./workflow_config.js";
17
17
  const PACKAGE_NAME = "@peterxiaoyang/superspec";
18
18
  const OPENSPEC_PACKAGE_NAME = "@fission-ai/openspec";
19
19
  const OPENSPEC_REQUIRED_VERSION = "1.4.1";
@@ -490,6 +490,55 @@ async function askToUpdateSelfIfNeeded(projectRoot, rerunArgs) {
490
490
  throw attachSelfUpdateLatest(err, "rerun", latest);
491
491
  }
492
492
  }
493
+ function parseSelectedHosts(raw) {
494
+ if (!raw)
495
+ return DEFAULT_WORKFLOW_HOSTS;
496
+ return parseWorkflowHostsFlag(raw);
497
+ }
498
+ function parseHostPromptAnswer(raw) {
499
+ const tokens = raw.toLowerCase().split(/[,\s]+/).filter(Boolean);
500
+ if (tokens.length === 0)
501
+ return DEFAULT_WORKFLOW_HOSTS;
502
+ const selected = [];
503
+ for (const token of tokens) {
504
+ if (token === "1" || token === "codex")
505
+ selected.push("codex");
506
+ else if (token === "2" || token === "omp")
507
+ selected.push("omp");
508
+ else if (token === "both" || token === "all")
509
+ selected.push("codex", "omp");
510
+ else
511
+ throw new WorkflowConfigError(`无法识别的宿主选项:${token}。可用 1/codex、2/omp,或 both`);
512
+ }
513
+ return parseWorkflowHostsFlag(selected.join(","));
514
+ }
515
+ async function resolveInstallHosts(projectRoot, opts, mode) {
516
+ if (opts.hosts)
517
+ return parseSelectedHosts(opts.hosts);
518
+ if (mode === "update" || workflowHostsDeclared(projectRoot))
519
+ return workflowHostsForProject(projectRoot);
520
+ const assumeTty = testEnv("SUPERSPEC_TEST_ASSUME_TTY") === "1";
521
+ if (!assumeTty && (!process.stdin.isTTY || !process.stdout.isTTY))
522
+ return DEFAULT_WORKFLOW_HOSTS;
523
+ const testAnswer = testEnv("SUPERSPEC_TEST_HOSTS_ANSWER");
524
+ if (testAnswer !== undefined)
525
+ return parseHostPromptAnswer(testAnswer);
526
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
527
+ try {
528
+ const answer = await rl.question("选择 SuperSpec 入口宿主:1) Codex 2) OMP。可多选,例如 1,2;直接回车默认 Codex。 ");
529
+ return parseHostPromptAnswer(answer);
530
+ }
531
+ finally {
532
+ rl.close();
533
+ }
534
+ }
535
+ function installOptionsFromCli(opts, hosts, allowLegacyState = false) {
536
+ return {
537
+ hosts,
538
+ allowLegacyState,
539
+ ...(opts["omp-home"] ? { ompHome: opts["omp-home"] } : {}),
540
+ };
541
+ }
493
542
  // ===== 初始化 transition init =====
494
543
  // ===== init/explore 现在在 transition.ts 中(走统一锁内路径)=====
495
544
  // ===== 主分发 =====
@@ -503,9 +552,9 @@ function topLevelHelp() {
503
552
  transition <子命令> --change <C> 状态流转(见下)
504
553
  record <子命令> --change <C> 登记证据(见下)
505
554
  jobs <子命令> --change <C> 工作项管理(见下)
506
- install 安装项目工作流入口
555
+ install [--hosts codex,omp] 安装项目工作流入口
507
556
  init --scope project install 的兼容别名
508
- update 升级 CLI 到 npm latest 并同步项目工作流模板
557
+ update [--hosts codex,omp] 升级 CLI 到 npm latest 并同步已选宿主入口
509
558
  version 版本号
510
559
 
511
560
  transition 子命令:
@@ -589,10 +638,14 @@ async function main(argv) {
589
638
  return 1;
590
639
  }
591
640
  try {
641
+ const hosts = await resolveInstallHosts(projectRoot, opts, "install");
592
642
  if (opts["skip-self-update"] !== "true") {
643
+ const hostArgs = ["--hosts", hosts.join(",")];
644
+ if (opts["omp-home"])
645
+ hostArgs.push("--omp-home", opts["omp-home"]);
593
646
  const rerunArgs = command === "init"
594
- ? ["init", "--scope", "project", "--skip-self-update"]
595
- : ["install", "--skip-self-update"];
647
+ ? ["init", "--scope", "project", "--skip-self-update", ...hostArgs]
648
+ : ["install", "--skip-self-update", ...hostArgs];
596
649
  const selfUpdate = await askToUpdateSelfIfNeeded(projectRoot, rerunArgs);
597
650
  if (selfUpdate.updated) {
598
651
  const rerun = updatedCliOutput(selfUpdate.output, selfUpdate.latest);
@@ -602,7 +655,7 @@ async function main(argv) {
602
655
  }
603
656
  const openspec = ensureOpenSpecCli();
604
657
  console.log(JSON.stringify({
605
- ...installProject(projectRoot),
658
+ ...installProject(projectRoot, installOptionsFromCli(opts, hosts)),
606
659
  openspec,
607
660
  }));
608
661
  return 0;
@@ -612,14 +665,20 @@ async function main(argv) {
612
665
  ? selfUpdateFailurePayload(err)
613
666
  : err instanceof OpenSpecDependencyError
614
667
  ? openSpecDependencyFailurePayload(err)
615
- : { ok: false, message: commandErrorMessage(err) }));
668
+ : err instanceof WorkflowConfigError
669
+ ? { ok: false, message: err.message }
670
+ : { ok: false, message: commandErrorMessage(err) }));
616
671
  return 1;
617
672
  }
618
673
  }
619
674
  if (command === "update") {
620
675
  try {
676
+ const hosts = await resolveInstallHosts(projectRoot, opts, "update");
621
677
  if (opts["skip-self-update"] !== "true") {
622
- const selfUpdate = updateSelfIfNeeded(projectRoot, ["update", "--skip-self-update"]);
678
+ const hostArgs = ["--hosts", hosts.join(",")];
679
+ if (opts["omp-home"])
680
+ hostArgs.push("--omp-home", opts["omp-home"]);
681
+ const selfUpdate = updateSelfIfNeeded(projectRoot, ["update", "--skip-self-update", ...hostArgs]);
623
682
  if (selfUpdate.updated) {
624
683
  const rerun = updatedCliOutput(selfUpdate.output, selfUpdate.latest);
625
684
  console.log(rerun.text);
@@ -627,7 +686,7 @@ async function main(argv) {
627
686
  }
628
687
  }
629
688
  const openspec = ensureOpenSpecCli();
630
- const result = installProject(projectRoot, { allowLegacyState: true });
689
+ const result = installProject(projectRoot, installOptionsFromCli(opts, hosts, true));
631
690
  console.log(JSON.stringify({
632
691
  ...result,
633
692
  openspec,
@@ -640,7 +699,9 @@ async function main(argv) {
640
699
  ? selfUpdateFailurePayload(err)
641
700
  : err instanceof OpenSpecDependencyError
642
701
  ? openSpecDependencyFailurePayload(err)
643
- : { ok: false, message: commandErrorMessage(err) }));
702
+ : err instanceof WorkflowConfigError
703
+ ? { ok: false, message: err.message }
704
+ : { ok: false, message: commandErrorMessage(err) }));
644
705
  return 1;
645
706
  }
646
707
  }
@@ -69,6 +69,7 @@ export declare function codeReviewPacketDigest(input: {
69
69
  packet_context?: JobPacketContext;
70
70
  previous_rejection?: ReviewPreviousRejection;
71
71
  }): string;
72
+ export declare function addedCodePathsForScope(projectRoot: string, scope: CodeReviewScope): string[];
72
73
  export declare function codeReviewPacketContext(changeRoot: string, projectRoot: string, scope: CodeReviewScope, events: Event[]): JobPacketContext;
73
74
  export declare function effectiveCoverageExemptionRefsFromEvents(events: Event[]): CoverageExemptionRef[];
74
75
  export declare function missingCoverageExemptionTestIds(changeRoot: string, events: Event[]): string[];
@@ -360,6 +360,41 @@ export function codeReviewJobStaleReason(projectRoot, job, currentPaths, events,
360
360
  export function codeReviewPacketDigest(input) {
361
361
  return sha256Text(JSON.stringify(input));
362
362
  }
363
+ export function addedCodePathsForScope(projectRoot, scope) {
364
+ if (!scope.scope_reliable)
365
+ return [];
366
+ const added = new Set();
367
+ let baseHead = scope.base_head;
368
+ if (!baseHead && scope.current_head) {
369
+ // 首轮 start-apply 前仓库还没有提交:以空树为基点,让 Apply 期间产生的首个提交也进入新增清单。
370
+ const emptyTree = gitLines(projectRoot, ["hash-object", "-t", "tree", "/dev/null"]);
371
+ if (emptyTree.ok)
372
+ baseHead = emptyTree.lines[0] ?? null;
373
+ }
374
+ if (baseHead && scope.current_head) {
375
+ const committed = gitLines(projectRoot, [
376
+ "diff", "--no-renames", "--diff-filter=A", "--name-only", `${baseHead}..${scope.current_head}`,
377
+ ]);
378
+ if (committed.ok) {
379
+ for (const path of committed.lines) {
380
+ if (isCodeLikePath(path))
381
+ added.add(path);
382
+ }
383
+ }
384
+ }
385
+ const dirty = dirtyCodeFiles(projectRoot);
386
+ if (dirty.ok) {
387
+ for (const file of dirty.files) {
388
+ if (file.status === "added" && isCodeLikePath(file.path))
389
+ added.add(file.path);
390
+ }
391
+ }
392
+ for (const path of scope.untracked_paths) {
393
+ if (isCodeLikePath(path))
394
+ added.add(path);
395
+ }
396
+ return [...added].sort();
397
+ }
363
398
  export function codeReviewPacketContext(changeRoot, projectRoot, scope, events) {
364
399
  const taskExecutionIndex = taskExecutionIndexFromEvents(projectRoot, events);
365
400
  // changed_paths 未知(快照缺失)或不完整(committed 段 diff 失败)的 task
@@ -379,6 +414,7 @@ export function codeReviewPacketContext(changeRoot, projectRoot, scope, events)
379
414
  task_execution_index: taskExecutionIndex,
380
415
  unattributed_paths: scope.review_paths.filter(path => !attributedPaths.has(path)).sort(),
381
416
  unknown_attribution_tasks: unknownAttributionTasks,
417
+ added_code_paths: addedCodePathsForScope(projectRoot, scope),
382
418
  };
383
419
  }
384
420
  export function effectiveCoverageExemptionRefsFromEvents(events) {
package/dist/install.d.ts CHANGED
@@ -1,14 +1,23 @@
1
+ import { type WorkflowHost } from "./workflow_config.ts";
1
2
  export declare const WORKFLOW_SKILLS: readonly ["superspec-explore", "superspec-propose", "superspec-apply", "superspec-review"];
2
3
  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
4
  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"];
5
+ export declare const WORKFLOW_MARKDOWN_AGENTS: readonly ["architect.md", "code-reviewer.md", "critic.md", "executor.md", "explore.md", "test-engineer.md", "test-runner.md", "verifier.md"];
6
+ export interface OmpInstallResult {
7
+ dest: string | null;
8
+ agents: string[];
9
+ skipped: string | null;
10
+ }
4
11
  export interface InstallResult {
5
12
  ok: boolean;
6
13
  message: string;
7
14
  installed: {
8
15
  engine_dir: string;
16
+ hosts: WorkflowHost[];
9
17
  skills: string[];
10
18
  prompts: string[];
11
19
  agents: string[];
20
+ omp: OmpInstallResult;
12
21
  config: string;
13
22
  workflow_config: string;
14
23
  agents_md: string;
@@ -18,5 +27,7 @@ export interface InstallResult {
18
27
  export interface InstallOptions {
19
28
  templateRoot?: string;
20
29
  allowLegacyState?: boolean;
30
+ hosts?: WorkflowHost[];
31
+ ompHome?: string;
21
32
  }
22
33
  export declare function installProject(projectRoot: string, options?: InstallOptions): InstallResult;
package/dist/install.js CHANGED
@@ -1,7 +1,8 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { dirname, join } from "node:path";
3
4
  import { SUPERSPEC_VERSION } from "./version.js";
4
- import { DEFAULT_WORKFLOW_RISK, WORKFLOW_CONFIG_PATH } from "./workflow_config.js";
5
+ import { DEFAULT_WORKFLOW_HOSTS, persistWorkflowHosts, workflowHostsDeclared, workflowHostsForProject, } from "./workflow_config.js";
5
6
  export const WORKFLOW_SKILLS = [
6
7
  "superspec-explore",
7
8
  "superspec-propose",
@@ -29,6 +30,16 @@ export const WORKFLOW_AGENTS = [
29
30
  "test-runner.toml",
30
31
  "verifier.toml",
31
32
  ];
33
+ export const WORKFLOW_MARKDOWN_AGENTS = [
34
+ "architect.md",
35
+ "code-reviewer.md",
36
+ "critic.md",
37
+ "executor.md",
38
+ "explore.md",
39
+ "test-engineer.md",
40
+ "test-runner.md",
41
+ "verifier.md",
42
+ ];
32
43
  function defaultTemplateRoot() {
33
44
  return join(import.meta.dirname, "..", "templates", "workflow");
34
45
  }
@@ -56,6 +67,11 @@ function assertWorkflowTemplates(templateRoot) {
56
67
  if (!existsSync(file))
57
68
  missing.push(file);
58
69
  }
70
+ for (const agent of WORKFLOW_MARKDOWN_AGENTS) {
71
+ const file = join(templateRoot, "agents-md", agent);
72
+ if (!existsSync(file))
73
+ missing.push(file);
74
+ }
59
75
  const agentsMdTemplate = join(templateRoot, "AGENTS.md");
60
76
  if (!existsSync(agentsMdTemplate))
61
77
  missing.push(agentsMdTemplate);
@@ -248,14 +264,44 @@ function ensureOpenSpecChineseContext(projectRoot) {
248
264
  writeFileSync(configPath, next);
249
265
  return OPENSPEC_CONFIG_PATH;
250
266
  }
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
- writeFileSync(configPath, JSON.stringify({ workflow: { mode: DEFAULT_WORKFLOW_RISK } }, null, 2) + "\n");
267
+ function copyOmpAgents(templateRoot, dest) {
268
+ const agentsDest = join(dest, "agents");
269
+ const installedAgents = [];
270
+ for (const agent of WORKFLOW_MARKDOWN_AGENTS) {
271
+ const src = join(templateRoot, "agents-md", agent);
272
+ writeBundledFile(src, join(agentsDest, agent));
273
+ installedAgents.push(agent);
257
274
  }
258
- return WORKFLOW_CONFIG_PATH;
275
+ return installedAgents;
276
+ }
277
+ function existingDirectory(path) {
278
+ try {
279
+ return statSync(path).isDirectory();
280
+ }
281
+ catch {
282
+ return false;
283
+ }
284
+ }
285
+ function resolveOmpAgentHome(explicitHome) {
286
+ if (explicitHome) {
287
+ return existingDirectory(explicitHome)
288
+ ? { dest: explicitHome, skipped: null }
289
+ : { dest: null, skipped: `OMP 用户目录不存在:${explicitHome}` };
290
+ }
291
+ const profile = (process.env.OMP_PROFILE ?? process.env.PI_PROFILE ?? "").trim();
292
+ const dest = profile && profile !== "default"
293
+ ? join(homedir(), ".omp", "profiles", profile, "agent")
294
+ : join(homedir(), ".omp", "agent");
295
+ if (!existingDirectory(dest)) {
296
+ return { dest: null, skipped: `OMP 用户目录不存在:${dest}。先启动一次 omp,或用 --omp-home 指定已有目录。` };
297
+ }
298
+ return { dest, skipped: null };
299
+ }
300
+ function installOmpAgents(templateRoot, ompHome) {
301
+ const resolved = resolveOmpAgentHome(ompHome);
302
+ if (!resolved.dest)
303
+ return { dest: null, agents: [], skipped: resolved.skipped };
304
+ return { dest: resolved.dest, agents: copyOmpAgents(templateRoot, resolved.dest), skipped: null };
259
305
  }
260
306
  const AGENTS_MD_PATH = "AGENTS.md";
261
307
  const SUPERSPEC_AGENTS_START = "<!-- SUPERSPEC:AGENTS:START -->";
@@ -299,22 +345,31 @@ export function installProject(projectRoot, options = {}) {
299
345
  const templateRoot = options.templateRoot ?? defaultTemplateRoot();
300
346
  assertWorkflowTemplates(templateRoot);
301
347
  const agentsMdTemplate = readAgentsMdTemplate(templateRoot);
348
+ const hosts = options.hosts
349
+ ?? (workflowHostsDeclared(projectRoot) ? workflowHostsForProject(projectRoot) : DEFAULT_WORKFLOW_HOSTS);
350
+ const wantsCodex = hosts.includes("codex");
351
+ const wantsOmp = hosts.includes("omp");
302
352
  const engineDir = join(projectRoot, ".superspec");
303
353
  mkdirSync(join(engineDir, "changes"), { recursive: true });
304
354
  const gitignorePath = join(engineDir, ".gitignore");
305
355
  if (!existsSync(gitignorePath))
306
356
  writeFileSync(gitignorePath, "changes/\n*.log\n*.tmp\n");
307
357
  migrateLegacyManagedHooks(projectRoot);
358
+ const omp = wantsOmp
359
+ ? installOmpAgents(templateRoot, options.ompHome)
360
+ : { dest: null, agents: [], skipped: null };
308
361
  return {
309
362
  ok: true,
310
363
  message: `SuperSpec ${SUPERSPEC_VERSION} 已安装`,
311
364
  installed: {
312
365
  engine_dir: ".superspec/",
366
+ hosts,
313
367
  skills: copySkills(templateRoot, projectRoot),
314
- prompts: copyPrompts(templateRoot, projectRoot),
315
- agents: copyAgents(templateRoot, projectRoot),
316
- config: ensureCodexConfig(projectRoot),
317
- workflow_config: ensureWorkflowConfig(projectRoot),
368
+ prompts: wantsCodex ? copyPrompts(templateRoot, projectRoot) : [],
369
+ agents: wantsCodex ? copyAgents(templateRoot, projectRoot) : [],
370
+ omp,
371
+ config: wantsCodex ? ensureCodexConfig(projectRoot) : "",
372
+ workflow_config: persistWorkflowHosts(projectRoot, hosts),
318
373
  agents_md: ensureAgentsMd(projectRoot, agentsMdTemplate),
319
374
  openspec_config: ensureOpenSpecChineseContext(projectRoot),
320
375
  },
package/dist/next.js CHANGED
@@ -115,14 +115,17 @@ function toNextOutput(change, plan) {
115
115
  resume: { argv: ["superspec", "transition", "next", "--change", change] },
116
116
  reason: plan.reason,
117
117
  };
118
- case "run_transition":
118
+ case "run_transition": {
119
+ const findingContext = plan.reopen?.reason === "review_fix" ? plan.reopen.findingContext : undefined;
119
120
  return {
120
121
  state: plan.state,
121
122
  path: "next_command",
122
123
  next_command: transitionCommand(change, plan.transition, formatTransitionArgs(plan)),
123
124
  reason: plan.reason,
124
125
  missing_inputs: [],
126
+ ...(findingContext ? { finding_context: findingContext } : {}),
125
127
  };
128
+ }
126
129
  case "done":
127
130
  return {
128
131
  state: plan.state,
@@ -1,7 +1,7 @@
1
1
  import type { ReviewGateRule } from "./review_job_gates.ts";
2
2
  import { exploreAnswerRegistrationPayload } from "./explore_round.ts";
3
3
  import { proposeAnswerRegistrationPayload } from "./propose_round.ts";
4
- import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, PlanningValidationProfile, WorkflowArtifactKind, State } from "./types.ts";
4
+ import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, PlanningValidationProfile, ReviewFindingContext, WorkflowArtifactKind, State } from "./types.ts";
5
5
  import type { Snapshot } from "./types.ts";
6
6
  import type { ReviewRisk } from "./review.ts";
7
7
  export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept";
@@ -29,6 +29,7 @@ export type ReopenNextStep = {
29
29
  jobId: string;
30
30
  findingId: string;
31
31
  reopenReason: string;
32
+ findingContext?: ReviewFindingContext;
32
33
  } | {
33
34
  to: "propose";
34
35
  reason: "review_finding";
@@ -12,6 +12,16 @@ import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, co
12
12
  import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
13
13
  import { taskEvidenceReadiness } from "./task_evidence.js";
14
14
  import { workflowRiskForProposeRound, workflowRiskForState } from "./workflow_config.js";
15
+ /** 从失败 finding 提取定位上下文:只回传 evidence(位置事实),不回传 description——那是审查建议叙事,不进执行上下文。 */
16
+ function reviewFindingContext(finding) {
17
+ const evidence = typeof finding?.evidence === "string" ? finding.evidence.trim() : "";
18
+ if (!evidence)
19
+ return undefined;
20
+ return {
21
+ evidence,
22
+ note: "非授权上下文:仅用于定位问题代码;实现范围仍以任务行锚定的已批准行为为准",
23
+ };
24
+ }
15
25
  function requiredJobs(state, jobs, reason) {
16
26
  return { kind: "required_jobs", state, jobs, reason };
17
27
  }
@@ -837,6 +847,7 @@ function planApplyDoneNext(context) {
837
847
  jobId: latest.job.job_id,
838
848
  findingId,
839
849
  reopenReason: `修复代码审查问题 ${findingId}`,
850
+ findingContext: reviewFindingContext(pendingFinding?.finding),
840
851
  },
841
852
  reason: `代码审查发现纯代码实现问题 ${findingId},回到实现阶段修复`,
842
853
  };
@@ -862,6 +873,7 @@ function planApplyDoneNext(context) {
862
873
  jobId: latest.job.job_id,
863
874
  findingId,
864
875
  reopenReason: `根据代码审查问题 ${findingId} 回到实现阶段修复`,
876
+ findingContext: reviewFindingContext(pendingFinding?.finding),
865
877
  },
866
878
  reason: `使用者已确认问题 ${findingId} 直接回到实现阶段修复`,
867
879
  };