@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.
- package/README.md +7 -6
- package/dist/approved_ref.d.ts +26 -0
- package/dist/approved_ref.js +158 -0
- package/dist/cli.js +71 -10
- package/dist/code_review.d.ts +1 -0
- package/dist/code_review.js +36 -0
- package/dist/install.d.ts +11 -0
- package/dist/install.js +68 -13
- package/dist/next.js +4 -1
- package/dist/phase_plan.d.ts +2 -1
- package/dist/phase_plan.js +12 -0
- package/dist/record.js +25 -8
- package/dist/transition.js +19 -4
- package/dist/types.d.ts +11 -0
- package/dist/workflow_config.d.ts +10 -0
- package/dist/workflow_config.js +78 -20
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +3 -1
- package/templates/workflow/agents-md/architect.md +13 -0
- package/templates/workflow/agents-md/code-reviewer.md +13 -0
- package/templates/workflow/agents-md/critic.md +13 -0
- package/templates/workflow/agents-md/executor.md +13 -0
- package/templates/workflow/agents-md/explore.md +13 -0
- package/templates/workflow/agents-md/test-engineer.md +13 -0
- package/templates/workflow/agents-md/test-runner.md +13 -0
- package/templates/workflow/agents-md/verifier.md +13 -0
- package/templates/workflow/prompts/code-reviewer.md +3 -4
- package/templates/workflow/prompts/critic.md +1 -0
- package/templates/workflow/prompts/executor.md +1 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +1 -1
- package/templates/workflow/skills/superspec-explore/SKILL.md +1 -1
- package/templates/workflow/skills/superspec-propose/SKILL.md +1 -1
- 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。新需求通常从
|
|
36
|
+
在 Codex 中显式调用对应 Skill。新需求通常从 `superspec-explore` 开始;已有 change 则从当前阶段继续。
|
|
37
37
|
|
|
38
38
|
| Skill | 作用 |
|
|
39
39
|
| --- | --- |
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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,
|
|
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
|
-
:
|
|
702
|
+
: err instanceof WorkflowConfigError
|
|
703
|
+
? { ok: false, message: err.message }
|
|
704
|
+
: { ok: false, message: commandErrorMessage(err) }));
|
|
644
705
|
return 1;
|
|
645
706
|
}
|
|
646
707
|
}
|
package/dist/code_review.d.ts
CHANGED
|
@@ -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[];
|
package/dist/code_review.js
CHANGED
|
@@ -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 {
|
|
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
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|
-
|
|
317
|
-
|
|
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,
|
package/dist/phase_plan.d.ts
CHANGED
|
@@ -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";
|
package/dist/phase_plan.js
CHANGED
|
@@ -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
|
};
|