@peterxiaoyang/superspec 0.1.1 → 0.1.3
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 +182 -115
- package/dist/src/core.js +5 -5
- package/dist/src/doctor.d.ts +44 -0
- package/dist/src/doctor.js +230 -0
- package/dist/src/evidence.d.ts +1 -0
- package/dist/src/evidence.js +7 -0
- package/dist/src/gates.d.ts +1 -0
- package/dist/src/gates.js +50 -26
- package/dist/src/git.js +2 -2
- 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/self_update.d.ts +14 -0
- package/dist/src/self_update.js +56 -0
- package/dist/src/util.d.ts +1 -0
- package/dist/src/util.js +27 -4
- package/dist/superspec.d.ts +2 -0
- package/dist/superspec.js +42 -3
- package/package.json +2 -2
- 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 +27 -9
- package/templates/workflow/skills/superspec-archive/SKILL.md +18 -2
- package/templates/workflow/skills/superspec-explore/SKILL.md +40 -21
- package/templates/workflow/skills/superspec-propose/SKILL.md +46 -28
- package/templates/workflow/skills/superspec-review/SKILL.md +60 -42
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" });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runCommand } from "./util.ts";
|
|
2
|
+
type RunFn = typeof runCommand;
|
|
3
|
+
type CommandExistsFn = (cmd: string, meta?: {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
}) => boolean;
|
|
6
|
+
export declare function update_self_then_rerun(opts: {
|
|
7
|
+
args: string[];
|
|
8
|
+
cwd?: string;
|
|
9
|
+
run?: RunFn;
|
|
10
|
+
commandExistsFn?: CommandExistsFn;
|
|
11
|
+
writeStdout?: (text: string) => void;
|
|
12
|
+
writeStderr?: (text: string) => void;
|
|
13
|
+
}): number;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { commandExists, runCommand } from "./util.js";
|
|
2
|
+
const SUPERSPEC_NPM_PACKAGE = "@peterxiaoyang/superspec";
|
|
3
|
+
function renderCommand(cmd, args) {
|
|
4
|
+
return [cmd, ...args].join(" ");
|
|
5
|
+
}
|
|
6
|
+
function commandFailure(proc) {
|
|
7
|
+
const output = (proc.error?.message ?? (proc.stderr || proc.stdout)).trim();
|
|
8
|
+
if (output)
|
|
9
|
+
return output;
|
|
10
|
+
return proc.status === null ? "command failed" : `command exited with status ${proc.status}`;
|
|
11
|
+
}
|
|
12
|
+
function shouldRetrySuperSpecInstallWithForce(proc) {
|
|
13
|
+
const output = `${proc.error?.message ?? ""}\n${proc.stderr}\n${proc.stdout}`;
|
|
14
|
+
const binConflict = /\bEEXIST\b|already exists|file exists|Refusing to delete|will not overwrite|would overwrite/iu.test(output);
|
|
15
|
+
const superspecBin = /\bsuperspec(?:\.(?:cmd|ps1))?\b/iu.test(output);
|
|
16
|
+
return binConflict && superspecBin;
|
|
17
|
+
}
|
|
18
|
+
export function update_self_then_rerun(opts) {
|
|
19
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
20
|
+
const run = opts.run ?? runCommand;
|
|
21
|
+
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
|
|
22
|
+
const writeStdout = opts.writeStdout ?? ((text) => process.stdout.write(text));
|
|
23
|
+
const writeStderr = opts.writeStderr ?? ((text) => process.stderr.write(text));
|
|
24
|
+
if (!commandExistsFn("npm", { cwd })) {
|
|
25
|
+
writeStderr("SuperSpec update requires npm on PATH to install @peterxiaoyang/superspec@latest. Use `superspec update --local-only` to update surfaces from the currently installed package.\n");
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const installArgs = ["install", "-g", `${SUPERSPEC_NPM_PACKAGE}@latest`];
|
|
29
|
+
writeStderr(`Updating SuperSpec CLI: ${renderCommand("npm", installArgs)}\n`);
|
|
30
|
+
let install = run("npm", installArgs, { cwd, timeout: 300_000 });
|
|
31
|
+
if ((install.error || install.status !== 0) && shouldRetrySuperSpecInstallWithForce(install)) {
|
|
32
|
+
const forcedArgs = ["install", "-g", "--force", `${SUPERSPEC_NPM_PACKAGE}@latest`];
|
|
33
|
+
writeStderr(`SuperSpec CLI install hit a global bin conflict; retrying with: ${renderCommand("npm", forcedArgs)}\n`);
|
|
34
|
+
install = run("npm", forcedArgs, { cwd, timeout: 300_000 });
|
|
35
|
+
}
|
|
36
|
+
if (install.error || install.status !== 0) {
|
|
37
|
+
writeStderr(`SuperSpec CLI update failed: ${commandFailure(install)}\n`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
if (!commandExistsFn("superspec", { cwd })) {
|
|
41
|
+
writeStderr("SuperSpec CLI update completed, but `superspec` is not resolvable from PATH. Reopen the shell or check npm global bin configuration.\n");
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
const rerunArgs = ["update", ...opts.args, "--skip-self-update"];
|
|
45
|
+
const rerun = run("superspec", rerunArgs, { cwd, timeout: 300_000 });
|
|
46
|
+
if (rerun.stdout)
|
|
47
|
+
writeStdout(rerun.stdout);
|
|
48
|
+
if (rerun.stderr)
|
|
49
|
+
writeStderr(rerun.stderr);
|
|
50
|
+
if (rerun.error || rerun.status !== 0) {
|
|
51
|
+
if (rerun.error && !rerun.stderr)
|
|
52
|
+
writeStderr(`SuperSpec update rerun failed: ${rerun.error.message}\n`);
|
|
53
|
+
return rerun.status ?? 1;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
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
|
@@ -78,6 +78,7 @@ export const EVIDENCE_KINDS = new Set([
|
|
|
78
78
|
// FIX-8 (audit A-5) adds the previously anchor-less human pause points:
|
|
79
79
|
// apply isolation choice, apply-phase scope expansion, and verify-failure disposition.
|
|
80
80
|
export const HUMAN_CONFIRMATION_GATES = new Set([
|
|
81
|
+
"explore_complete",
|
|
81
82
|
"design_complete",
|
|
82
83
|
"invariants_reviewed",
|
|
83
84
|
"archive_ready",
|
|
@@ -215,8 +216,8 @@ export const GATE_ALIASES = {
|
|
|
215
216
|
"propose.invariants_reviewed": "invariants_reviewed",
|
|
216
217
|
"propose.test_plan_drafted": "test_contract_drafted",
|
|
217
218
|
"propose.tasks_mapped": "tasks_complete",
|
|
218
|
-
"propose.apply_ready": "
|
|
219
|
-
apply_ready: "
|
|
219
|
+
"propose.apply_ready": "apply_ready",
|
|
220
|
+
apply_ready: "apply_ready",
|
|
220
221
|
};
|
|
221
222
|
export const GATE_ROUTE = {
|
|
222
223
|
explore_complete: "explore",
|
|
@@ -227,6 +228,7 @@ export const GATE_ROUTE = {
|
|
|
227
228
|
test_contract_honored: "propose",
|
|
228
229
|
tasks_complete: "propose",
|
|
229
230
|
propose_complete: "propose",
|
|
231
|
+
apply_ready: "propose",
|
|
230
232
|
review_complete: "review",
|
|
231
233
|
verify_complete: "review",
|
|
232
234
|
archive_ready: "archive",
|
|
@@ -287,6 +289,10 @@ export function decorateDecision(decision, opts = {}) {
|
|
|
287
289
|
})
|
|
288
290
|
: [];
|
|
289
291
|
const nextActions = Array.isArray(decision.next_allowed_actions) ? decision.next_allowed_actions.map((item) => String(item)) : [];
|
|
292
|
+
const windowsPowerShellHints = windowsPowerShellCommandHints([
|
|
293
|
+
...nextActions,
|
|
294
|
+
...reasons.map((item) => String(item.message ?? "")),
|
|
295
|
+
]);
|
|
290
296
|
return {
|
|
291
297
|
...decision,
|
|
292
298
|
command: command || decision.command,
|
|
@@ -297,10 +303,23 @@ export function decorateDecision(decision, opts = {}) {
|
|
|
297
303
|
command_hint_zh: commandInfo?.hint_zh,
|
|
298
304
|
block_reasons: reasons,
|
|
299
305
|
next_allowed_actions_zh: nextActions.map((item) => translate_action_zh(item)),
|
|
306
|
+
windows_powershell_command_hints: windowsPowerShellHints.length > 0 ? windowsPowerShellHints : undefined,
|
|
300
307
|
trust_warnings_zh: Array.isArray(decision.trust_warnings) ? decision.trust_warnings.map((item) => trust_warning_zh(String(item))) : [],
|
|
301
308
|
workflow_terms_zh: workflow_terms_zh_for(command || undefined, String(decision.gate ?? ""), reasons.map((item) => String(item.code ?? ""))),
|
|
302
309
|
};
|
|
303
310
|
}
|
|
311
|
+
function windowsPowerShellCommandHints(texts) {
|
|
312
|
+
const hints = new Set();
|
|
313
|
+
for (const text of texts) {
|
|
314
|
+
for (const match of text.matchAll(/`((?:superspec|openspec)\s+[^`]+)`/giu)) {
|
|
315
|
+
const command = match[1]?.trim();
|
|
316
|
+
if (!command)
|
|
317
|
+
continue;
|
|
318
|
+
hints.add(`Windows PowerShell: use \`${command.replace(/^(superspec|openspec)\b/iu, "$1.cmd")}\``);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return [...hints];
|
|
322
|
+
}
|
|
304
323
|
function sanitizeReasonForOutput(item) {
|
|
305
324
|
const refs = Array.isArray(item.refs) ? item.refs.map((ref) => String(ref)) : [];
|
|
306
325
|
return {
|
|
@@ -475,6 +494,10 @@ export function commandLookupInvocation(cmd, platform = process.platform) {
|
|
|
475
494
|
return { cmd: "where.exe", args: [cmd], shell: false };
|
|
476
495
|
return { cmd: "sh", args: ["-c", `command -v ${cmd}`], shell: false };
|
|
477
496
|
}
|
|
497
|
+
export function selectWindowsCommandCandidate(cmd, whereStdout) {
|
|
498
|
+
const candidates = whereStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
499
|
+
return candidates.find((candidate) => /\.(?:cmd|bat)$/i.test(candidate)) ?? candidates[0] ?? cmd;
|
|
500
|
+
}
|
|
478
501
|
function resolveWindowsCommand(cmd, cwd) {
|
|
479
502
|
if (cmd.includes("\\") || cmd.includes("/") || extname(cmd))
|
|
480
503
|
return cmd;
|
|
@@ -485,7 +508,7 @@ function resolveWindowsCommand(cmd, cwd) {
|
|
|
485
508
|
});
|
|
486
509
|
if (lookup.status !== 0 || typeof lookup.stdout !== "string")
|
|
487
510
|
return cmd;
|
|
488
|
-
return lookup.stdout
|
|
511
|
+
return selectWindowsCommandCandidate(cmd, lookup.stdout);
|
|
489
512
|
}
|
|
490
513
|
export function windowsShellEscapeArg(arg) {
|
|
491
514
|
let escaped = String(arg);
|
|
@@ -497,7 +520,7 @@ export function windowsShellEscapeArg(arg) {
|
|
|
497
520
|
export function windowsCmdShimInvocation(cmdPath, args, comspec = "cmd.exe") {
|
|
498
521
|
return {
|
|
499
522
|
cmd: comspec,
|
|
500
|
-
args: ["/d", "/
|
|
523
|
+
args: ["/d", "/c", cmdPath, ...args],
|
|
501
524
|
};
|
|
502
525
|
}
|
|
503
526
|
function windowsCommandInvocation(cmd, args, cwd) {
|
package/dist/superspec.d.ts
CHANGED
package/dist/superspec.js
CHANGED
|
@@ -3,8 +3,12 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
export * from "./src/init_cli.js";
|
|
5
5
|
export * from "./src/cli.js";
|
|
6
|
+
export * from "./src/doctor.js";
|
|
7
|
+
export * from "./src/self_update.js";
|
|
6
8
|
import { main } from "./src/cli.js";
|
|
9
|
+
import { main_doctor, superspec_package_version } from "./src/doctor.js";
|
|
7
10
|
import { main_init_async } from "./src/init_cli.js";
|
|
11
|
+
import { update_self_then_rerun } from "./src/self_update.js";
|
|
8
12
|
function realpathMaybe(filePath) {
|
|
9
13
|
try {
|
|
10
14
|
return realpathSync(filePath);
|
|
@@ -19,14 +23,34 @@ function help() {
|
|
|
19
23
|
"",
|
|
20
24
|
"commands:",
|
|
21
25
|
" init install SuperSpec Codex surfaces (asks project/user; default project)",
|
|
22
|
-
" update update manifest-managed
|
|
26
|
+
" update update SuperSpec CLI, then update manifest-managed surfaces",
|
|
23
27
|
" uninstall remove manifest-managed SuperSpec surfaces",
|
|
24
28
|
" guard run the SuperSpec guard command surface",
|
|
29
|
+
" doctor diagnose SuperSpec/OpenSpec/npm/PATH wiring",
|
|
30
|
+
" version print SuperSpec CLI version",
|
|
25
31
|
"",
|
|
26
32
|
"examples:",
|
|
27
33
|
" superspec init --scope project",
|
|
28
34
|
" superspec init --scope user",
|
|
29
35
|
" superspec guard check-init --change <change>",
|
|
36
|
+
" superspec doctor",
|
|
37
|
+
"",
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
function updateHelp() {
|
|
41
|
+
return [
|
|
42
|
+
"usage: superspec update [--scope {project,user}] [--path PATH] [--codex-home PATH] [--local-only]",
|
|
43
|
+
"",
|
|
44
|
+
"updates the global SuperSpec CLI from npm, then updates manifest-managed SuperSpec surfaces.",
|
|
45
|
+
"",
|
|
46
|
+
"options:",
|
|
47
|
+
" --scope {project,user} update project .codex surfaces or user Codex home surfaces (default: project)",
|
|
48
|
+
" --project equivalent to --scope project",
|
|
49
|
+
" --user, --global equivalent to --scope user",
|
|
50
|
+
" --path PATH project root for project scope (default: current directory)",
|
|
51
|
+
" --codex-home PATH Codex user home for user scope (default: $CODEX_HOME or ~/.codex)",
|
|
52
|
+
" --local-only skip npm self-update and use the currently installed package",
|
|
53
|
+
" -h, --help show this help",
|
|
30
54
|
"",
|
|
31
55
|
].join("\n");
|
|
32
56
|
}
|
|
@@ -36,14 +60,29 @@ export async function main_superspec(argv = process.argv.slice(2)) {
|
|
|
36
60
|
process.stdout.write(help());
|
|
37
61
|
return 0;
|
|
38
62
|
}
|
|
63
|
+
if (command === "-v" || command === "--version" || command === "version") {
|
|
64
|
+
process.stdout.write(`${superspec_package_version()}\n`);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
39
67
|
if (command === "init")
|
|
40
68
|
return main_init_async(rest);
|
|
41
|
-
if (command === "update")
|
|
42
|
-
|
|
69
|
+
if (command === "update") {
|
|
70
|
+
if (rest.includes("-h") || rest.includes("--help")) {
|
|
71
|
+
process.stdout.write(updateHelp());
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const localOnly = rest.includes("--local-only") || rest.includes("--skip-self-update");
|
|
75
|
+
const updateArgs = rest.filter((arg) => arg !== "--local-only" && arg !== "--skip-self-update");
|
|
76
|
+
if (!localOnly)
|
|
77
|
+
return update_self_then_rerun({ args: updateArgs });
|
|
78
|
+
return main_init_async([...updateArgs, "--update"]);
|
|
79
|
+
}
|
|
43
80
|
if (command === "uninstall")
|
|
44
81
|
return main_init_async([...rest, "--uninstall"]);
|
|
45
82
|
if (command === "guard")
|
|
46
83
|
return main(rest);
|
|
84
|
+
if (command === "doctor")
|
|
85
|
+
return main_doctor(rest);
|
|
47
86
|
// Convenience fallback: `superspec check-init ...` behaves like `superspec guard check-init ...`.
|
|
48
87
|
if (command.startsWith("check-") || command === "status" || command === "recompute" || command === "init") {
|
|
49
88
|
return main(argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peterxiaoyang/superspec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "SuperSpec workflow package: guard runtime, generic workflow templates, and Codex adapter payload.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "node build.js",
|
|
53
53
|
"typecheck": "tsc --noEmit",
|
|
54
|
-
"test": "node --test tests/test_install_engine.test.ts tests/test_real_openspec_smoke.test.ts tests/test_superspec_guard.test.ts tests/test_superspec_skills.test.ts",
|
|
54
|
+
"test": "node --test tests/test_install_engine.test.ts tests/test_real_openspec_smoke.test.ts tests/test_superspec_cli.test.ts tests/test_superspec_guard.test.ts tests/test_superspec_skills.test.ts",
|
|
55
55
|
"prepack": "npm run build",
|
|
56
56
|
"prepublishOnly": "npm run build",
|
|
57
57
|
"pack:dry-run": "npm pack --dry-run"
|
|
@@ -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>
|