@peterxiaoyang/superspec 0.1.1 → 0.1.2
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 +4 -2
- package/dist/src/gates.js +11 -16
- package/dist/src/i18n.js +113 -93
- package/dist/src/init_cli.d.ts +5 -3
- package/dist/src/init_cli.js +56 -40
- package/dist/src/openspec.d.ts +17 -0
- package/dist/src/openspec.js +89 -3
- package/dist/src/project_init.d.ts +8 -1
- package/dist/src/project_init.js +28 -9
- package/dist/src/util.d.ts +1 -0
- package/dist/src/util.js +23 -2
- package/package.json +1 -1
- package/templates/workflow/prompts/architect.md +2 -2
- package/templates/workflow/prompts/code-reviewer.md +8 -8
- package/templates/workflow/prompts/critic.md +2 -2
- package/templates/workflow/prompts/verifier.md +1 -1
- package/templates/workflow/skills/superspec-apply/SKILL.md +18 -8
- package/templates/workflow/skills/superspec-archive/SKILL.md +12 -2
- package/templates/workflow/skills/superspec-explore/SKILL.md +31 -21
- package/templates/workflow/skills/superspec-propose/SKILL.md +38 -28
- package/templates/workflow/skills/superspec-review/SKILL.md +50 -40
package/dist/src/init_cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { block, commandExists, GuardError, printDecision, reason, runCommand } from "./core.js";
|
|
2
|
-
import { project_init, recommended_openspec_install_plan } from "./project_init.js";
|
|
1
|
+
import { block, commandExists, GuardError, printDecision, reason, runCommand, openspec_cli_probe, REQUIRED_OPENSPEC_MIN_VERSION, } from "./core.js";
|
|
2
|
+
import { forced_openspec_install_plan, project_init, recommended_openspec_install_plan } from "./project_init.js";
|
|
3
3
|
import { install_workflow, uninstall_workflow, update_workflow } from "./install_engine.js";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { resolve } from "node:path";
|
|
@@ -9,7 +9,7 @@ function usage() {
|
|
|
9
9
|
return "usage: superspec init [-h] [--scope {project,user}] [--path PATH] [--codex-home PATH] [--create] [--update] [--uninstall] [--dry-run] [--force]\n";
|
|
10
10
|
}
|
|
11
11
|
function help() {
|
|
12
|
-
return `${usage()}\n可选参数:\n -h, --help 显示帮助并退出\n --scope {project,user} 安装到当前项目的 .codex 目录,或安装到用户级 Codex 目录(默认:project)\n --project 等价于 --scope project\n --user 等价于 --scope user\n --global 兼容别名,等价于 --user\n --path PATH --scope project 时使用的项目根目录(默认:当前目录)\n --codex-home PATH --scope user 时使用的 Codex 用户目录(默认:$CODEX_HOME 或 ~/.codex)\n --create 兼容参数;init 默认就会创建缺失内容\n --update 按 manifest
|
|
12
|
+
return `${usage()}\n可选参数:\n -h, --help 显示帮助并退出\n --scope {project,user} 安装到当前项目的 .codex 目录,或安装到用户级 Codex 目录(默认:project)\n --project 等价于 --scope project\n --user 等价于 --scope user\n --global 兼容别名,等价于 --user\n --path PATH --scope project 时使用的项目根目录(默认:当前目录)\n --codex-home PATH --scope user 时使用的 Codex 用户目录(默认:$CODEX_HOME 或 ~/.codex)\n --create 兼容参数;init 默认就会创建缺失内容\n --update 按 manifest 更新 SuperSpec 管理的文件;用户改动文件保留,新的版本写入 *.new\n --uninstall 按 manifest 卸载 SuperSpec 管理的文件;.superspec 数据与既有/用户改动文件会保留\n --dry-run 配合 --uninstall 时只预览将删除的文件,不实际修改\n --force 安装时覆盖已有且内容不同的文件,并保留 *.bak 备份\n`;
|
|
13
13
|
}
|
|
14
14
|
function parse_init_argv(argv) {
|
|
15
15
|
const getValue = (flag) => {
|
|
@@ -56,6 +56,12 @@ function commandFailure(proc) {
|
|
|
56
56
|
return `安装命令执行失败(退出状态码 ${proc.status})。`;
|
|
57
57
|
return "安装命令执行失败,请查看终端日志后重试。";
|
|
58
58
|
}
|
|
59
|
+
function shouldRetryOpenSpecInstallWithForce(proc) {
|
|
60
|
+
const output = `${proc.error?.message ?? ""}\n${proc.stderr}\n${proc.stdout}`;
|
|
61
|
+
const binConflict = /\bEEXIST\b|already exists|file exists|Refusing to delete|will not overwrite|would overwrite/iu.test(output);
|
|
62
|
+
const openspecBin = /\bopenspec(?:\.(?:cmd|ps1))?\b/iu.test(output);
|
|
63
|
+
return binConflict && openspecBin;
|
|
64
|
+
}
|
|
59
65
|
function engineDecision(gate, projectRoot, result, nextActions) {
|
|
60
66
|
if (result.problems.length > 0) {
|
|
61
67
|
const decision = block("project", gate, result.problems.map((item) => reason(`${gate}_failed`, item)));
|
|
@@ -92,56 +98,60 @@ async function promptInstallScope() {
|
|
|
92
98
|
function canPrompt() {
|
|
93
99
|
return Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
94
100
|
}
|
|
95
|
-
|
|
96
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
97
|
-
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
98
|
-
try {
|
|
99
|
-
for (;;) {
|
|
100
|
-
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
101
|
-
if (answer === "")
|
|
102
|
-
return defaultYes;
|
|
103
|
-
if (["y", "yes", "是", "好", "确认"].includes(answer))
|
|
104
|
-
return true;
|
|
105
|
-
if (["n", "no", "否", "不", "取消"].includes(answer))
|
|
106
|
-
return false;
|
|
107
|
-
process.stderr.write("请输入 y / n,或输入 是 / 否。\n");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
rl.close();
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
export async function maybe_install_missing_openspec(opts) {
|
|
101
|
+
export function maybe_install_missing_openspec(opts) {
|
|
115
102
|
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
|
|
116
|
-
if (opts.
|
|
103
|
+
if (opts.mode !== "install")
|
|
117
104
|
return "not-needed";
|
|
118
|
-
|
|
105
|
+
const run = opts.run ?? runCommand;
|
|
106
|
+
const probeOpenspecFn = opts.probeOpenspecFn ?? ((meta) => openspec_cli_probe({ cwd: meta?.cwd, commandExistsFn, run }));
|
|
107
|
+
const before = probeOpenspecFn({ cwd: opts.cwd });
|
|
108
|
+
if (before.ok)
|
|
119
109
|
return "not-needed";
|
|
120
|
-
if (!(opts.interactive ?? canPrompt()))
|
|
121
|
-
return "skipped";
|
|
122
|
-
const plan = recommended_openspec_install_plan({ cwd: opts.cwd, commandExistsFn });
|
|
123
|
-
if (plan === null)
|
|
124
|
-
return "skipped";
|
|
125
|
-
const confirm = opts.confirm ?? ((question) => promptYesNo(question));
|
|
126
|
-
const accepted = await confirm(`未检测到 openspec CLI。是否现在尝试自动安装?\n将执行:${plan.rendered}`);
|
|
127
|
-
if (!accepted)
|
|
128
|
-
return "skipped";
|
|
129
110
|
const writeStderr = opts.writeStderr ?? ((text) => {
|
|
130
111
|
process.stderr.write(text);
|
|
131
112
|
});
|
|
113
|
+
const plan = recommended_openspec_install_plan({ cwd: opts.cwd, commandExistsFn });
|
|
114
|
+
if (plan === null) {
|
|
115
|
+
writeStderr(`${before.message}。SuperSpec 需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION},但未找到 npm、pnpm、yarn 或 bun,无法自动安装。\n`);
|
|
116
|
+
return "skipped";
|
|
117
|
+
}
|
|
118
|
+
writeStderr(`${before.message}。SuperSpec 需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION},将自动安装或升级。\n`);
|
|
132
119
|
writeStderr(`正在安装 OpenSpec CLI:${plan.rendered}\n`);
|
|
133
|
-
|
|
120
|
+
let proc = run(plan.cmd, plan.args, { cwd: opts.cwd, timeout: 300_000 });
|
|
121
|
+
if ((proc.error || proc.status !== 0) && shouldRetryOpenSpecInstallWithForce(proc)) {
|
|
122
|
+
const forcedPlan = forced_openspec_install_plan(plan);
|
|
123
|
+
writeStderr(`OpenSpec CLI 安装遇到全局 bin 冲突,正在覆盖重试:${forcedPlan.rendered}\n`);
|
|
124
|
+
proc = run(forcedPlan.cmd, forcedPlan.args, { cwd: opts.cwd, timeout: 300_000 });
|
|
125
|
+
}
|
|
134
126
|
if (proc.error || proc.status !== 0) {
|
|
135
127
|
writeStderr(`自动安装 OpenSpec CLI 失败:${commandFailure(proc)}\n`);
|
|
136
128
|
return "failed";
|
|
137
129
|
}
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
const after = probeOpenspecFn({ cwd: opts.cwd });
|
|
131
|
+
if (!after.ok) {
|
|
132
|
+
writeStderr(`安装命令已完成,但当前 PATH 里的 openspec 仍不可用:${after.message}。请重新打开终端或确认全局 bin 已在 PATH 中,然后重新运行 superspec init;Windows PowerShell 请运行 superspec.cmd init。\n`);
|
|
140
133
|
return "failed";
|
|
141
134
|
}
|
|
142
|
-
writeStderr("OpenSpec CLI
|
|
135
|
+
writeStderr("OpenSpec CLI 安装或升级完成,继续执行 superspec init。\n");
|
|
143
136
|
return "installed";
|
|
144
137
|
}
|
|
138
|
+
function openspecPreflightBlocked(args, scope, installResult) {
|
|
139
|
+
const targetRoot = scope === "user" ? args.codexHome : args.path;
|
|
140
|
+
const code = installResult === "skipped" ? "openspec_auto_install_unavailable" : "openspec_auto_install_failed";
|
|
141
|
+
const message = installResult === "skipped"
|
|
142
|
+
? `OpenSpec CLI 不满足 SuperSpec 要求,且未找到可用包管理器自动安装 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}。`
|
|
143
|
+
: `OpenSpec CLI 自动安装或升级 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION} 未完成。`;
|
|
144
|
+
const decision = block("project", "openspec_preflight", [reason(code, message)], {
|
|
145
|
+
next_actions: [`install or upgrade @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}, then rerun \`superspec init --scope ${scope}\``],
|
|
146
|
+
});
|
|
147
|
+
printDecision({
|
|
148
|
+
...decision,
|
|
149
|
+
project_root: args.path,
|
|
150
|
+
install_scope: scope,
|
|
151
|
+
install_root: targetRoot,
|
|
152
|
+
}, { command: "init" });
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
145
155
|
function run_init(args, scope) {
|
|
146
156
|
const targetRoot = scope === "user" ? args.codexHome : args.path;
|
|
147
157
|
const gatePrefix = scope === "user" ? "user" : "project";
|
|
@@ -176,7 +186,11 @@ export function main_init(argv = process.argv.slice(2)) {
|
|
|
176
186
|
return 0;
|
|
177
187
|
}
|
|
178
188
|
const args = parse_init_argv(argv);
|
|
179
|
-
|
|
189
|
+
const scope = args.scope ?? "project";
|
|
190
|
+
const openspecInstall = maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
|
|
191
|
+
if (openspecInstall === "failed" || openspecInstall === "skipped")
|
|
192
|
+
return openspecPreflightBlocked(args, scope, openspecInstall);
|
|
193
|
+
return run_init(args, scope);
|
|
180
194
|
}
|
|
181
195
|
catch (err) {
|
|
182
196
|
const change = "project";
|
|
@@ -193,7 +207,9 @@ export async function main_init_async(argv = process.argv.slice(2)) {
|
|
|
193
207
|
}
|
|
194
208
|
const args = parse_init_argv(argv);
|
|
195
209
|
const scope = args.scope ?? (canPrompt() ? await promptInstallScope() : "project");
|
|
196
|
-
|
|
210
|
+
const openspecInstall = maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
|
|
211
|
+
if (openspecInstall === "failed" || openspecInstall === "skipped")
|
|
212
|
+
return openspecPreflightBlocked(args, scope, openspecInstall);
|
|
197
213
|
return run_init(args, scope);
|
|
198
214
|
}
|
|
199
215
|
catch (err) {
|
package/dist/src/openspec.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import type { JsonMap, Reason } from "./util.ts";
|
|
2
|
+
import { runCommand } from "./util.ts";
|
|
3
|
+
export declare const REQUIRED_OPENSPEC_MIN_VERSION = "1.4.1";
|
|
4
|
+
export type OpenspecCliProbe = {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
state: "ok" | "missing" | "invalid" | "too_old";
|
|
7
|
+
version: string | null;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function parse_openspec_version(raw: string): string | null;
|
|
11
|
+
export declare function compare_versions(actual: string, required: string): number;
|
|
12
|
+
export declare function openspec_cli_probe(opts?: {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
commandExistsFn?: (cmd: string, meta?: {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
}) => boolean;
|
|
17
|
+
run?: typeof runCommand;
|
|
18
|
+
}): OpenspecCliProbe;
|
|
2
19
|
export declare function openspec_status(change: string): JsonMap;
|
|
3
20
|
export declare function openspec_validate(change: string): [boolean, string];
|
|
4
21
|
export declare function openspec_version(): string;
|
package/dist/src/openspec.js
CHANGED
|
@@ -1,6 +1,93 @@
|
|
|
1
1
|
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { GATE_ALIASES, GATE_ROUTE, GuardError, ROUTE_ALIASES, ROUTE_ORDER, fingerprint_obj, isObject, reason, repr, runCommand, } from "./util.js";
|
|
3
|
+
import { REQUIRED_OPENSPEC_CLI_SURFACES, GATE_ALIASES, GATE_ROUTE, GuardError, ROUTE_ALIASES, ROUTE_ORDER, commandExists, fingerprint_obj, isObject, reason, repr, runCommand, } from "./util.js";
|
|
4
|
+
export const REQUIRED_OPENSPEC_MIN_VERSION = "1.4.1";
|
|
5
|
+
export function parse_openspec_version(raw) {
|
|
6
|
+
const match = raw.match(/\b(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?\b/);
|
|
7
|
+
return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
|
|
8
|
+
}
|
|
9
|
+
export function compare_versions(actual, required) {
|
|
10
|
+
const actualParts = actual.split(".").map((part) => Number.parseInt(part, 10));
|
|
11
|
+
const requiredParts = required.split(".").map((part) => Number.parseInt(part, 10));
|
|
12
|
+
for (let index = 0; index < Math.max(actualParts.length, requiredParts.length); index += 1) {
|
|
13
|
+
const actualPart = actualParts[index] ?? 0;
|
|
14
|
+
const requiredPart = requiredParts[index] ?? 0;
|
|
15
|
+
if (!Number.isFinite(actualPart))
|
|
16
|
+
return -1;
|
|
17
|
+
if (!Number.isFinite(requiredPart))
|
|
18
|
+
return 1;
|
|
19
|
+
if (actualPart > requiredPart)
|
|
20
|
+
return 1;
|
|
21
|
+
if (actualPart < requiredPart)
|
|
22
|
+
return -1;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
export function openspec_cli_probe(opts = {}) {
|
|
27
|
+
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
|
|
28
|
+
const run = opts.run ?? runCommand;
|
|
29
|
+
if (!commandExistsFn("openspec", { cwd: opts.cwd })) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
state: "missing",
|
|
33
|
+
version: null,
|
|
34
|
+
message: "PATH 中缺少 OpenSpec CLI(openspec)",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const versionProc = run("openspec", ["--version"], { cwd: opts.cwd, timeout: 15_000 });
|
|
38
|
+
if (versionProc.error || versionProc.status !== 0) {
|
|
39
|
+
const output = (versionProc.error?.message ?? (versionProc.stderr || versionProc.stdout)).trim();
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
state: "invalid",
|
|
43
|
+
version: null,
|
|
44
|
+
message: `\`openspec --version\` 执行失败:${output}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const rawVersion = `${versionProc.stdout}${versionProc.stderr}`.trim();
|
|
48
|
+
if (/openspec[-_\s]*chinese/iu.test(rawVersion)) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
state: "invalid",
|
|
52
|
+
version: null,
|
|
53
|
+
message: "PATH 上的 openspec 来自 openspec-chinese,不是受支持的 @fission-ai/openspec CLI",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const version = parse_openspec_version(rawVersion);
|
|
57
|
+
if (version === null) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
state: "invalid",
|
|
61
|
+
version: null,
|
|
62
|
+
message: "PATH 上的 OpenSpec CLI(openspec)无法报告语义版本,可能不是受支持的 @fission-ai/openspec",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (compare_versions(version, REQUIRED_OPENSPEC_MIN_VERSION) < 0) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
state: "too_old",
|
|
69
|
+
version,
|
|
70
|
+
message: `openspec ${version} 低于 SuperSpec 要求的最低版本 ${REQUIRED_OPENSPEC_MIN_VERSION}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
for (const args of REQUIRED_OPENSPEC_CLI_SURFACES) {
|
|
74
|
+
const proc = run("openspec", [...args], { cwd: opts.cwd, timeout: 15_000 });
|
|
75
|
+
if (proc.error || proc.status !== 0) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
state: "invalid",
|
|
79
|
+
version,
|
|
80
|
+
message: `openspec ${version} 缺少必需的原生能力:\`openspec ${args.join(" ")}\` 执行失败`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
state: "ok",
|
|
87
|
+
version,
|
|
88
|
+
message: `openspec ${version} satisfies SuperSpec requirements`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
4
91
|
export function openspec_status(change) {
|
|
5
92
|
const proc = runCommand("openspec", ["status", "--change", change, "--json"], { timeout: 30_000 });
|
|
6
93
|
if (proc.error) {
|
|
@@ -28,8 +115,7 @@ export function openspec_version() {
|
|
|
28
115
|
if (proc.error || proc.status !== 0)
|
|
29
116
|
return "unknown";
|
|
30
117
|
const raw = (proc.stdout || proc.stderr).trim();
|
|
31
|
-
|
|
32
|
-
return match ? match[0] : raw || "unknown";
|
|
118
|
+
return (parse_openspec_version(raw) ?? raw) || "unknown";
|
|
33
119
|
}
|
|
34
120
|
export function openspec_status_shape_reasons(status) {
|
|
35
121
|
const problems = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type JsonMap } from "./core.ts";
|
|
1
|
+
import { type JsonMap, type OpenspecCliProbe } from "./core.ts";
|
|
2
2
|
export declare const OPENSPEC_NPM_PACKAGE = "@fission-ai/openspec";
|
|
3
3
|
export declare const OPENSPEC_INSTALL_DOC_URL = "https://github.com/Fission-AI/OpenSpec#readme";
|
|
4
4
|
export type OpenspecInstallPlan = {
|
|
@@ -13,12 +13,19 @@ export declare function recommended_openspec_install_plan(opts?: {
|
|
|
13
13
|
cwd?: string;
|
|
14
14
|
}) => boolean;
|
|
15
15
|
}): OpenspecInstallPlan | null;
|
|
16
|
+
export declare function forced_openspec_install_plan(plan: OpenspecInstallPlan): OpenspecInstallPlan;
|
|
16
17
|
export declare function missing_openspec_cli_message(opts?: {
|
|
17
18
|
cwd?: string;
|
|
18
19
|
commandExistsFn?: (cmd: string, meta?: {
|
|
19
20
|
cwd?: string;
|
|
20
21
|
}) => boolean;
|
|
21
22
|
}): string;
|
|
23
|
+
export declare function openspec_cli_requirement_message(probe: OpenspecCliProbe, opts?: {
|
|
24
|
+
cwd?: string;
|
|
25
|
+
commandExistsFn?: (cmd: string, meta?: {
|
|
26
|
+
cwd?: string;
|
|
27
|
+
}) => boolean;
|
|
28
|
+
}): string;
|
|
22
29
|
export declare function project_init(repoRootRaw?: string, opts?: {
|
|
23
30
|
force?: boolean;
|
|
24
31
|
}): JsonMap;
|
package/dist/src/project_init.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_OPENSPEC_CODEX_SKILLS, block, reason, read_agent_toml_name, read_skill_frontmatter_name,
|
|
3
|
+
import { REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_OPENSPEC_CODEX_SKILLS, REQUIRED_OPENSPEC_MIN_VERSION, block, commandExists, openspec_cli_probe, reason, read_agent_toml_name, read_skill_frontmatter_name, runCommand, } from "./core.js";
|
|
4
4
|
import { install_workflow } from "./install_engine.js";
|
|
5
5
|
import { system_failure_zh } from "./i18n.js";
|
|
6
6
|
export const OPENSPEC_NPM_PACKAGE = "@fission-ai/openspec";
|
|
7
7
|
export const OPENSPEC_INSTALL_DOC_URL = "https://github.com/Fission-AI/OpenSpec#readme";
|
|
8
8
|
const ROLE_DESCRIPTIONS = {
|
|
9
9
|
architect: "系统设计、边界、接口与长期取舍",
|
|
10
|
-
critic: "
|
|
10
|
+
critic: "严格审查计划、证据、假设与范围漂移",
|
|
11
11
|
"test-engineer": "测试策略、覆盖率与 RED/GREEN 证据审查",
|
|
12
12
|
"code-reviewer": "代码 / 规格 / 安全审查",
|
|
13
13
|
verifier: "最终完成证据与验证审查",
|
|
@@ -34,12 +34,30 @@ export function recommended_openspec_install_plan(opts = {}) {
|
|
|
34
34
|
}
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
|
+
export function forced_openspec_install_plan(plan) {
|
|
38
|
+
const versionedPackage = `${OPENSPEC_NPM_PACKAGE}@latest`;
|
|
39
|
+
const forced = {
|
|
40
|
+
npm: ["install", "-g", "--force", versionedPackage],
|
|
41
|
+
pnpm: ["add", "-g", "--force", versionedPackage],
|
|
42
|
+
yarn: ["global", "add", versionedPackage, "--force"],
|
|
43
|
+
bun: ["add", "-g", "--force", versionedPackage],
|
|
44
|
+
};
|
|
45
|
+
return { ...plan, args: forced[plan.manager], rendered: renderCommand(plan.cmd, forced[plan.manager]) };
|
|
46
|
+
}
|
|
37
47
|
export function missing_openspec_cli_message(opts = {}) {
|
|
48
|
+
return openspec_cli_requirement_message({
|
|
49
|
+
ok: false,
|
|
50
|
+
state: "missing",
|
|
51
|
+
version: null,
|
|
52
|
+
message: "PATH 中缺少 OpenSpec CLI(openspec)",
|
|
53
|
+
}, opts);
|
|
54
|
+
}
|
|
55
|
+
export function openspec_cli_requirement_message(probe, opts = {}) {
|
|
38
56
|
const plan = recommended_openspec_install_plan(opts);
|
|
39
57
|
if (plan) {
|
|
40
|
-
return
|
|
58
|
+
return `${probe.message}。需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}。可先运行 \`${plan.rendered}\` 安装或升级,然后重新运行 \`superspec init --scope project\`。`;
|
|
41
59
|
}
|
|
42
|
-
return
|
|
60
|
+
return `${probe.message}。请先安装或升级 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}(${OPENSPEC_INSTALL_DOC_URL}),然后重新运行 \`superspec init --scope project\`。`;
|
|
43
61
|
}
|
|
44
62
|
function openspecSkillProblems(repoRoot) {
|
|
45
63
|
const skillsRoot = join(repoRoot, ".codex", "skills");
|
|
@@ -64,8 +82,8 @@ function writeSuperSpecAgent(repoRoot, name) {
|
|
|
64
82
|
'model_reasoning_effort = "high"',
|
|
65
83
|
'developer_instructions = """',
|
|
66
84
|
`你是仓库本地的 superspec ${name} native subagent。`,
|
|
67
|
-
"遵循分配给你的 superspec gate
|
|
68
|
-
"
|
|
85
|
+
"遵循分配给你的 superspec gate 证据任务,引用具体文件,并把未通过的问题上报主流程。",
|
|
86
|
+
"不要用主流程自审替代必须的角色证据。",
|
|
69
87
|
'"""',
|
|
70
88
|
"",
|
|
71
89
|
].join("\n"), "utf8");
|
|
@@ -82,14 +100,15 @@ function writeSuperSpecPrompt(repoRoot, name) {
|
|
|
82
100
|
"",
|
|
83
101
|
`你是仓库本地的 superspec ${name} 角色。`,
|
|
84
102
|
"",
|
|
85
|
-
"请基于具体文件证据审查提供的 superspec gate 上下文,输出简洁的通过 /
|
|
103
|
+
"请基于具体文件证据审查提供的 superspec gate 上下文,输出简洁的通过 / 未通过报告,引用 source anchors 与 target refs,并且不要用主流程自审替代必须的 native-subagent 证据。",
|
|
86
104
|
"",
|
|
87
105
|
].join("\n"), "utf8");
|
|
88
106
|
return filePath;
|
|
89
107
|
}
|
|
90
108
|
function ensureOpenSpecCodex(repoRoot, actions) {
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
const probe = openspec_cli_probe({ cwd: repoRoot });
|
|
110
|
+
if (!probe.ok)
|
|
111
|
+
return [openspec_cli_requirement_message(probe, { cwd: repoRoot })];
|
|
93
112
|
let problems = openspecSkillProblems(repoRoot);
|
|
94
113
|
if (problems.length === 0) {
|
|
95
114
|
actions.push({ action: "openspec_codex_skills", status: "ok" });
|
package/dist/src/util.d.ts
CHANGED
|
@@ -127,6 +127,7 @@ export declare function commandLookupInvocation(cmd: string, platform?: NodeJS.P
|
|
|
127
127
|
args: string[];
|
|
128
128
|
shell: boolean;
|
|
129
129
|
};
|
|
130
|
+
export declare function selectWindowsCommandCandidate(cmd: string, whereStdout: string): string;
|
|
130
131
|
export declare function windowsShellEscapeArg(arg: string): string;
|
|
131
132
|
export declare function windowsCmdShimInvocation(cmdPath: string, args: string[], comspec?: string): {
|
|
132
133
|
cmd: string;
|
package/dist/src/util.js
CHANGED
|
@@ -287,6 +287,10 @@ export function decorateDecision(decision, opts = {}) {
|
|
|
287
287
|
})
|
|
288
288
|
: [];
|
|
289
289
|
const nextActions = Array.isArray(decision.next_allowed_actions) ? decision.next_allowed_actions.map((item) => String(item)) : [];
|
|
290
|
+
const windowsPowerShellHints = windowsPowerShellCommandHints([
|
|
291
|
+
...nextActions,
|
|
292
|
+
...reasons.map((item) => String(item.message ?? "")),
|
|
293
|
+
]);
|
|
290
294
|
return {
|
|
291
295
|
...decision,
|
|
292
296
|
command: command || decision.command,
|
|
@@ -297,10 +301,23 @@ export function decorateDecision(decision, opts = {}) {
|
|
|
297
301
|
command_hint_zh: commandInfo?.hint_zh,
|
|
298
302
|
block_reasons: reasons,
|
|
299
303
|
next_allowed_actions_zh: nextActions.map((item) => translate_action_zh(item)),
|
|
304
|
+
windows_powershell_command_hints: windowsPowerShellHints.length > 0 ? windowsPowerShellHints : undefined,
|
|
300
305
|
trust_warnings_zh: Array.isArray(decision.trust_warnings) ? decision.trust_warnings.map((item) => trust_warning_zh(String(item))) : [],
|
|
301
306
|
workflow_terms_zh: workflow_terms_zh_for(command || undefined, String(decision.gate ?? ""), reasons.map((item) => String(item.code ?? ""))),
|
|
302
307
|
};
|
|
303
308
|
}
|
|
309
|
+
function windowsPowerShellCommandHints(texts) {
|
|
310
|
+
const hints = new Set();
|
|
311
|
+
for (const text of texts) {
|
|
312
|
+
for (const match of text.matchAll(/`((?:superspec|openspec)\s+[^`]+)`/giu)) {
|
|
313
|
+
const command = match[1]?.trim();
|
|
314
|
+
if (!command)
|
|
315
|
+
continue;
|
|
316
|
+
hints.add(`Windows PowerShell: use \`${command.replace(/^(superspec|openspec)\b/iu, "$1.cmd")}\``);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return [...hints];
|
|
320
|
+
}
|
|
304
321
|
function sanitizeReasonForOutput(item) {
|
|
305
322
|
const refs = Array.isArray(item.refs) ? item.refs.map((ref) => String(ref)) : [];
|
|
306
323
|
return {
|
|
@@ -475,6 +492,10 @@ export function commandLookupInvocation(cmd, platform = process.platform) {
|
|
|
475
492
|
return { cmd: "where.exe", args: [cmd], shell: false };
|
|
476
493
|
return { cmd: "sh", args: ["-c", `command -v ${cmd}`], shell: false };
|
|
477
494
|
}
|
|
495
|
+
export function selectWindowsCommandCandidate(cmd, whereStdout) {
|
|
496
|
+
const candidates = whereStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
497
|
+
return candidates.find((candidate) => /\.(?:cmd|bat)$/i.test(candidate)) ?? candidates[0] ?? cmd;
|
|
498
|
+
}
|
|
478
499
|
function resolveWindowsCommand(cmd, cwd) {
|
|
479
500
|
if (cmd.includes("\\") || cmd.includes("/") || extname(cmd))
|
|
480
501
|
return cmd;
|
|
@@ -485,7 +506,7 @@ function resolveWindowsCommand(cmd, cwd) {
|
|
|
485
506
|
});
|
|
486
507
|
if (lookup.status !== 0 || typeof lookup.stdout !== "string")
|
|
487
508
|
return cmd;
|
|
488
|
-
return lookup.stdout
|
|
509
|
+
return selectWindowsCommandCandidate(cmd, lookup.stdout);
|
|
489
510
|
}
|
|
490
511
|
export function windowsShellEscapeArg(arg) {
|
|
491
512
|
let escaped = String(arg);
|
|
@@ -497,7 +518,7 @@ export function windowsShellEscapeArg(arg) {
|
|
|
497
518
|
export function windowsCmdShimInvocation(cmdPath, args, comspec = "cmd.exe") {
|
|
498
519
|
return {
|
|
499
520
|
cmd: comspec,
|
|
500
|
-
args: ["/d", "/
|
|
521
|
+
args: ["/d", "/c", cmdPath, ...args],
|
|
501
522
|
};
|
|
502
523
|
}
|
|
503
524
|
function windowsCommandInvocation(cmd, args, cwd) {
|
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@ argument-hint: "任务说明"
|
|
|
40
40
|
- 建议必须具体且可执行。
|
|
41
41
|
- 必须说明取舍。
|
|
42
42
|
- 在 ralplan 共识审查中,要包含反论、张力和综合方案。
|
|
43
|
-
- 在 `superspec-review` 中,要输出基于来源证据的架构 guidance
|
|
43
|
+
- 在 `superspec-review` 中,要输出基于来源证据的架构 guidance 和升级点;最终判断由主流程完成,不由本角色直接下判。
|
|
44
44
|
</success_criteria>
|
|
45
45
|
|
|
46
46
|
<verification_loop>
|
|
@@ -78,7 +78,7 @@ argument-hint: "任务说明"
|
|
|
78
78
|
1. [最高优先级] - [工作量] - [影响]
|
|
79
79
|
2. [下一优先级] - [工作量] - [影响]
|
|
80
80
|
|
|
81
|
-
##
|
|
81
|
+
## 主流程判断建议
|
|
82
82
|
- 关键架构判断
|
|
83
83
|
- 建议直接加载的 source refs
|
|
84
84
|
- 建议升级或后续动作
|
|
@@ -6,7 +6,7 @@ argument-hint: "任务说明"
|
|
|
6
6
|
你是 Code Reviewer。你的任务是通过系统化、带严重级别的审查来保障代码质量与安全性。
|
|
7
7
|
你负责规格符合性验证、安全检查、代码质量评估、性能审视和最佳实践约束。
|
|
8
8
|
你不负责直接实现修复(executor)、架构设计(architect)或编写测试(test-engineer)。
|
|
9
|
-
当你在 `superspec-review` 中与 `architect` / `critic` 配合时,你负责代码 / 规格 / 安全这一条审查线,需要产出带证据的 guidance
|
|
9
|
+
当你在 `superspec-review` 中与 `architect` / `critic` 配合时,你负责代码 / 规格 / 安全这一条审查线,需要产出带证据的 guidance,供主流程做最终判断,而不是自己充当最终判官。
|
|
10
10
|
|
|
11
11
|
代码审查是缺陷和漏洞进入生产前的最后一道防线。之所以强调这些规则,是因为漏掉安全问题会造成真实损害,而只盯格式细枝末节会浪费所有人的时间。
|
|
12
12
|
</identity>
|
|
@@ -39,8 +39,8 @@ argument-hint: "任务说明"
|
|
|
39
39
|
<explore>
|
|
40
40
|
1) 先跑 `git diff` 看最近改动,重点关注被修改的文件。
|
|
41
41
|
2) 阶段 1:规格符合性(必须先通过)。检查实现是否覆盖全部要求,是否解决了正确的问题,是否有缺漏或多做,需求提出者会不会认得这是他要的东西。
|
|
42
|
-
3)
|
|
43
|
-
4) 阶段 2:代码质量(只有阶段 1
|
|
42
|
+
3) 根因检查(在正常质量放行前必须通过):如果新引入的 fallback / workaround 会掩盖故障、压掉证据、增加宽泛绕路,或回避修主合同,就直接驳回。要求作者回到根因修复:保留失败证据、收紧主合同、删除掩盖分支,并补上真正故障的回归覆盖。
|
|
43
|
+
4) 阶段 2:代码质量(只有阶段 1 和根因检查都通过后才做)。对每个修改文件运行 `lsp_diagnostics`。使用 `ast_grep_search` 检查高风险模式,例如 `console.log`、空 `catch`、硬编码密钥、宽泛 `try/catch` fallback、静默默认值、尽力而为式绕路。然后按安全、质量、性能、最佳实践清单审查。
|
|
44
44
|
5) 给每个问题评严重级别,并给出修复建议。
|
|
45
45
|
6) 根据最高严重级别得出总体结论。
|
|
46
46
|
</explore>
|
|
@@ -53,7 +53,7 @@ argument-hint: "任务说明"
|
|
|
53
53
|
- 每个问题都包含明确修复建议。
|
|
54
54
|
- 所有修改文件都已运行 `lsp_diagnostics`,不能在有类型错误时放行。
|
|
55
55
|
- guidance 包必须清晰:包括 findings、source refs、required claim ids 和建议的下一步。
|
|
56
|
-
- 在 superspec review 中,架构问题要向 `architect`
|
|
56
|
+
- 在 superspec review 中,架构问题要向 `architect` 上抛,最终判断留给主流程。
|
|
57
57
|
</success_criteria>
|
|
58
58
|
|
|
59
59
|
<verification_loop>
|
|
@@ -71,7 +71,7 @@ argument-hint: "任务说明"
|
|
|
71
71
|
|
|
72
72
|
<root_cause_fallback_policy>
|
|
73
73
|
- 当 fallback / workaround 会掩盖真实缺陷时,要把它当成审查阻塞项:比如吞错、降级诊断、静默默认值、宽泛兼容垫片、重复的备用执行路径、绕开损坏主路径的功能开关,或没有证明主合同被修好却让故障“消失”的尽力分支。
|
|
74
|
-
-
|
|
74
|
+
- 对这类掩盖式问题,即使测试通过也要给出 REQUEST CHANGES。要明确说明:只要问题压掉证据或绕开失败合同,单纯“能跑通”就不够;要求最小化的根因修复、明确的失败行为,以及没有真实修复就会失败的回归测试。
|
|
75
75
|
- 不要无差别否定所有 fallback。若 fallback 明确说明为不可避免、被限制在已知外部/版本边界内、主路径与 fallback 路径都经过测试、失败证据仍然可见,并且没有替代可控主合同的修复,那么窄范围兼容 fallback 可以接受。
|
|
76
76
|
- 需要细腻判断时,要把条件写清楚:例如“只有当这个 fallback 始终限制在 [boundary]、保持 [evidence/error] 可见,并且同时覆盖 [primary] 与 [compatibility] 行为测试时,才可以接受。”否则就建议删除 fallback / workaround,回到根因修复。
|
|
77
77
|
</root_cause_fallback_policy>
|
|
@@ -114,7 +114,7 @@ argument-hint: "任务说明"
|
|
|
114
114
|
|
|
115
115
|
### 主线程建议
|
|
116
116
|
- 推荐下一步
|
|
117
|
-
-
|
|
117
|
+
- 需要主流程判断的 claims
|
|
118
118
|
- 建议主线程直接加载的 source refs
|
|
119
119
|
</output_contract>
|
|
120
120
|
|
|
@@ -124,7 +124,7 @@ argument-hint: "任务说明"
|
|
|
124
124
|
- 没有证据:没跑 `lsp_diagnostics` 就说 “looks good”。必须对修改文件跑诊断。
|
|
125
125
|
- 问题描述含糊:比如只说“这里可以更好”。应改成类似:`[MEDIUM] utils.ts:42 - 函数超过 50 行,建议把 42-65 行的校验逻辑提取到 validateInput()。`
|
|
126
126
|
- 严重级别膨胀:把缺失 JSDoc 评成 CRITICAL。CRITICAL 只留给安全漏洞和数据损坏风险。
|
|
127
|
-
-
|
|
127
|
+
- 纵容掩盖式问题:看到用 fallback、静默默认值、宽泛绕路去掩盖主路径故障却仍然放行。应要求回到根因修复,并补回归证据。
|
|
128
128
|
</anti_patterns>
|
|
129
129
|
|
|
130
130
|
<scenario_handling>
|
|
@@ -142,7 +142,7 @@ argument-hint: "任务说明"
|
|
|
142
142
|
- 我是否拦下了会掩盖故障或绕开根因修复的 fallback / workaround?
|
|
143
143
|
- 我是否对所有修改文件都运行了 lsp_diagnostics?
|
|
144
144
|
- 每个问题是否都有 file:line、严重级别和修复建议?
|
|
145
|
-
-
|
|
145
|
+
- 我是否给主流程留下了足够证据,使其无需盲信我也能判断?
|
|
146
146
|
- 我是否检查了安全问题(硬编码密钥、注入、XSS)?
|
|
147
147
|
</final_checklist>
|
|
148
148
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "
|
|
2
|
+
description: "计划与方案的严格审查角色(深度)"
|
|
3
3
|
argument-hint: "任务说明"
|
|
4
4
|
---
|
|
5
5
|
<identity>
|
|
@@ -7,7 +7,7 @@ argument-hint: "任务说明"
|
|
|
7
7
|
</identity>
|
|
8
8
|
|
|
9
9
|
<goal>
|
|
10
|
-
针对计划,要审查清晰度、完整性、验证方式、整体适配性、引用文件以及代表性实现路径。在 `superspec-review` 中,你输出带证据的 guidance、required claims 与 required loads
|
|
10
|
+
针对计划,要审查清晰度、完整性、验证方式、整体适配性、引用文件以及代表性实现路径。在 `superspec-review` 中,你输出带证据的 guidance、required claims 与 required loads,交给主流程做最终判断,而不是自己做最终判定。
|
|
11
11
|
</goal>
|
|
12
12
|
|
|
13
13
|
<language>
|