@peterxiaoyang/superspec 0.1.2 → 0.1.4
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 +181 -116
- package/dist/src/cli.js +2 -2
- package/dist/src/cli_args.d.ts +2 -0
- package/dist/src/cli_args.js +28 -10
- 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 +39 -10
- package/dist/src/git.js +2 -2
- package/dist/src/init_cli.d.ts +2 -1
- package/dist/src/init_cli.js +31 -17
- package/dist/src/self_update.d.ts +14 -0
- package/dist/src/self_update.js +56 -0
- package/dist/src/util.d.ts +11 -0
- package/dist/src/util.js +218 -2
- package/dist/superspec.d.ts +2 -0
- package/dist/superspec.js +45 -5
- package/package.json +2 -2
- package/templates/workflow/skills/superspec-apply/SKILL.md +16 -6
- package/templates/workflow/skills/superspec-archive/SKILL.md +10 -2
- package/templates/workflow/skills/superspec-explore/SKILL.md +15 -4
- package/templates/workflow/skills/superspec-propose/SKILL.md +19 -9
- package/templates/workflow/skills/superspec-review/SKILL.md +15 -5
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { PACKAGE_ROOT } from "./install_engine.js";
|
|
4
|
+
import { openspec_cli_probe } from "./openspec.js";
|
|
5
|
+
import { commandExists, runCommand } from "./util.js";
|
|
6
|
+
function readPackageJson(packageRoot) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
9
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function superspec_package_version(packageRoot = PACKAGE_ROOT) {
|
|
16
|
+
const version = readPackageJson(packageRoot).version;
|
|
17
|
+
return typeof version === "string" && version ? version : "0.0.0";
|
|
18
|
+
}
|
|
19
|
+
function realpathMaybe(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
return realpathSync(filePath);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return resolve(filePath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function pathInfo(filePath) {
|
|
28
|
+
if (!filePath)
|
|
29
|
+
return null;
|
|
30
|
+
const info = { path: filePath, exists: existsSync(filePath) };
|
|
31
|
+
if (!info.exists)
|
|
32
|
+
return info;
|
|
33
|
+
try {
|
|
34
|
+
const stat = lstatSync(filePath);
|
|
35
|
+
info.kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : stat.isFile() ? "file" : "other";
|
|
36
|
+
if (stat.isSymbolicLink())
|
|
37
|
+
info.link_target = readlinkSync(filePath);
|
|
38
|
+
info.realpath = realpathMaybe(filePath);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
info.error = err.message;
|
|
42
|
+
}
|
|
43
|
+
return info;
|
|
44
|
+
}
|
|
45
|
+
function firstNonEmptyLine(text) {
|
|
46
|
+
return text.split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0) ?? null;
|
|
47
|
+
}
|
|
48
|
+
function commandPath(cmd, opts) {
|
|
49
|
+
if (!opts.commandExistsFn(cmd, { cwd: opts.cwd }))
|
|
50
|
+
return null;
|
|
51
|
+
const proc = opts.platform === "win32"
|
|
52
|
+
? opts.run("where.exe", [cmd], { cwd: opts.cwd, timeout: 15_000, platform: opts.platform })
|
|
53
|
+
: opts.run("sh", ["-c", `command -v ${cmd}`], { cwd: opts.cwd, timeout: 15_000, platform: opts.platform });
|
|
54
|
+
if (proc.error || proc.status !== 0)
|
|
55
|
+
return null;
|
|
56
|
+
return firstNonEmptyLine(proc.stdout);
|
|
57
|
+
}
|
|
58
|
+
function npmValue(args, opts) {
|
|
59
|
+
if (!opts.commandExistsFn("npm", { cwd: opts.cwd }))
|
|
60
|
+
return { available: false, value: null, error: "npm not found on PATH" };
|
|
61
|
+
const proc = opts.run("npm", args, { cwd: opts.cwd, timeout: 15_000, platform: opts.platform });
|
|
62
|
+
if (proc.error || proc.status !== 0) {
|
|
63
|
+
return {
|
|
64
|
+
available: true,
|
|
65
|
+
value: null,
|
|
66
|
+
error: (proc.error?.message ?? (proc.stderr || proc.stdout)).trim(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { available: true, value: firstNonEmptyLine(proc.stdout), error: null };
|
|
70
|
+
}
|
|
71
|
+
function containsUnscopedSuperspecNodeModule(value) {
|
|
72
|
+
if (!value)
|
|
73
|
+
return false;
|
|
74
|
+
return /[/\\]node_modules[/\\]superspec[/\\]/u.test(value);
|
|
75
|
+
}
|
|
76
|
+
function add(checks, status, name, detail, refs) {
|
|
77
|
+
checks.push({ status, name, detail, refs });
|
|
78
|
+
}
|
|
79
|
+
export function build_doctor_report(opts = {}) {
|
|
80
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
81
|
+
const packageRoot = opts.packageRoot ?? PACKAGE_ROOT;
|
|
82
|
+
const platform = opts.platform ?? process.platform;
|
|
83
|
+
const run = opts.run ?? runCommand;
|
|
84
|
+
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd, platform }));
|
|
85
|
+
const packageJson = readPackageJson(packageRoot);
|
|
86
|
+
const version = superspec_package_version(packageRoot);
|
|
87
|
+
const packageName = typeof packageJson.name === "string" ? packageJson.name : "unknown";
|
|
88
|
+
const expectedBin = join(packageRoot, "bin", "superspec.js");
|
|
89
|
+
const resolvedCommand = commandPath("superspec", { cwd, platform, commandExistsFn, run });
|
|
90
|
+
const command = pathInfo(resolvedCommand);
|
|
91
|
+
const prefix = npmValue(["prefix", "-g"], { cwd, platform, commandExistsFn, run });
|
|
92
|
+
const root = npmValue(["root", "-g"], { cwd, platform, commandExistsFn, run });
|
|
93
|
+
const npmRoot = typeof root.value === "string" ? root.value : null;
|
|
94
|
+
const scopedPackage = npmRoot ? pathInfo(join(npmRoot, "@peterxiaoyang", "superspec")) : null;
|
|
95
|
+
const unscopedPackage = npmRoot ? pathInfo(join(npmRoot, "superspec")) : null;
|
|
96
|
+
const openspecProbe = openspec_cli_probe({ cwd, commandExistsFn, run });
|
|
97
|
+
const checks = [];
|
|
98
|
+
const nextActions = [];
|
|
99
|
+
if (packageName === "@peterxiaoyang/superspec" && version !== "0.0.0") {
|
|
100
|
+
add(checks, "ok", "package metadata", `${packageName}@${version}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
add(checks, "fail", "package metadata", `could not identify @peterxiaoyang/superspec package metadata under ${packageRoot}`);
|
|
104
|
+
}
|
|
105
|
+
if (openspecProbe.ok) {
|
|
106
|
+
add(checks, "ok", "OpenSpec CLI", openspecProbe.message);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
add(checks, "fail", "OpenSpec CLI", openspecProbe.message);
|
|
110
|
+
nextActions.push("npm install -g @fission-ai/openspec@latest");
|
|
111
|
+
}
|
|
112
|
+
if (!command) {
|
|
113
|
+
add(checks, "warn", "superspec command", "`superspec` is not resolvable from PATH in this shell");
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const commandRealpath = typeof command.realpath === "string" ? command.realpath : undefined;
|
|
117
|
+
const commandTarget = typeof command.link_target === "string" ? command.link_target : undefined;
|
|
118
|
+
if (containsUnscopedSuperspecNodeModule(commandRealpath) || containsUnscopedSuperspecNodeModule(commandTarget)) {
|
|
119
|
+
add(checks, "fail", "superspec command owner", "`superspec` points at the unscoped `superspec` package, which conflicts with @peterxiaoyang/superspec", [String(command.path)]);
|
|
120
|
+
nextActions.push("npm uninstall -g superspec");
|
|
121
|
+
nextActions.push("npm install -g @peterxiaoyang/superspec@latest");
|
|
122
|
+
}
|
|
123
|
+
else if (existsSync(expectedBin) && commandRealpath && realpathMaybe(expectedBin) !== commandRealpath) {
|
|
124
|
+
add(checks, "warn", "superspec command owner", "`superspec` on PATH does not point at this package root", [String(command.path), expectedBin]);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
add(checks, "ok", "superspec command owner", "`superspec` resolves to this package");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (root.error) {
|
|
131
|
+
add(checks, "warn", "npm global root", String(root.error));
|
|
132
|
+
}
|
|
133
|
+
else if (npmRoot) {
|
|
134
|
+
add(checks, "ok", "npm global root", npmRoot);
|
|
135
|
+
}
|
|
136
|
+
if (unscopedPackage?.exists) {
|
|
137
|
+
add(checks, "warn", "unscoped superspec package", "global package `superspec` is installed and can claim the same `superspec` binary", [String(unscopedPackage.path)]);
|
|
138
|
+
nextActions.push("npm uninstall -g superspec");
|
|
139
|
+
}
|
|
140
|
+
if (scopedPackage && !scopedPackage.exists) {
|
|
141
|
+
add(checks, "warn", "scoped superspec package", "@peterxiaoyang/superspec is not present under npm global root", [String(scopedPackage.path)]);
|
|
142
|
+
}
|
|
143
|
+
const dedupedNextActions = [...new Set(nextActions)];
|
|
144
|
+
return {
|
|
145
|
+
ok: !checks.some((check) => check.status === "fail"),
|
|
146
|
+
cwd,
|
|
147
|
+
superspec: {
|
|
148
|
+
name: packageName,
|
|
149
|
+
version,
|
|
150
|
+
package_root: packageRoot,
|
|
151
|
+
expected_bin: expectedBin,
|
|
152
|
+
entry: opts.argv0 ?? process.argv[1] ?? null,
|
|
153
|
+
command,
|
|
154
|
+
scoped_global_package: scopedPackage,
|
|
155
|
+
unscoped_global_package: unscopedPackage,
|
|
156
|
+
},
|
|
157
|
+
node: {
|
|
158
|
+
version: process.versions.node,
|
|
159
|
+
exec_path: process.execPath,
|
|
160
|
+
platform,
|
|
161
|
+
},
|
|
162
|
+
npm: {
|
|
163
|
+
prefix,
|
|
164
|
+
root,
|
|
165
|
+
},
|
|
166
|
+
openspec: {
|
|
167
|
+
ok: openspecProbe.ok,
|
|
168
|
+
state: openspecProbe.state,
|
|
169
|
+
version: openspecProbe.version,
|
|
170
|
+
message: openspecProbe.message,
|
|
171
|
+
},
|
|
172
|
+
checks,
|
|
173
|
+
next_actions: dedupedNextActions,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function render_doctor_report(report) {
|
|
177
|
+
const lines = [
|
|
178
|
+
"SuperSpec doctor",
|
|
179
|
+
"",
|
|
180
|
+
`SuperSpec: ${report.superspec.name}@${report.superspec.version}`,
|
|
181
|
+
`Package root: ${report.superspec.package_root}`,
|
|
182
|
+
`Command path: ${report.superspec.command?.path ?? "not found"}`,
|
|
183
|
+
`OpenSpec: ${report.openspec.message}`,
|
|
184
|
+
`Node: ${report.node.version} (${report.node.platform})`,
|
|
185
|
+
`npm prefix: ${report.npm.prefix.value ?? "unknown"}`,
|
|
186
|
+
`npm root: ${report.npm.root.value ?? "unknown"}`,
|
|
187
|
+
"",
|
|
188
|
+
"Checks:",
|
|
189
|
+
...report.checks.map((check) => {
|
|
190
|
+
const refs = check.refs && check.refs.length > 0 ? ` (${check.refs.join(", ")})` : "";
|
|
191
|
+
return ` [${check.status}] ${check.name}: ${check.detail}${refs}`;
|
|
192
|
+
}),
|
|
193
|
+
];
|
|
194
|
+
if (report.next_actions.length > 0) {
|
|
195
|
+
lines.push("", "Next actions:", ...report.next_actions.map((action) => ` ${action}`));
|
|
196
|
+
}
|
|
197
|
+
lines.push("");
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
function doctorHelp() {
|
|
201
|
+
return [
|
|
202
|
+
"usage: superspec doctor [--json]",
|
|
203
|
+
"",
|
|
204
|
+
"diagnoses SuperSpec, OpenSpec, npm global package, and PATH wiring.",
|
|
205
|
+
"",
|
|
206
|
+
"options:",
|
|
207
|
+
" --json print machine-readable JSON",
|
|
208
|
+
" -h, --help show this help",
|
|
209
|
+
"",
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
212
|
+
export function main_doctor(argv = process.argv.slice(2)) {
|
|
213
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
214
|
+
process.stdout.write(doctorHelp());
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
const unknown = argv.filter((arg) => arg !== "--json");
|
|
218
|
+
if (unknown.length > 0) {
|
|
219
|
+
process.stderr.write(`superspec doctor: unknown option ${JSON.stringify(unknown[0])}\n\n${doctorHelp()}`);
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
const report = build_doctor_report();
|
|
223
|
+
if (argv.includes("--json")) {
|
|
224
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
process.stdout.write(render_doctor_report(report));
|
|
228
|
+
}
|
|
229
|
+
return report.ok ? 0 : 1;
|
|
230
|
+
}
|
package/dist/src/evidence.d.ts
CHANGED
package/dist/src/evidence.js
CHANGED
|
@@ -272,6 +272,9 @@ function finding_string_list_reasons(ev, finding, field, opts = {}) {
|
|
|
272
272
|
function human_confirmation_reasons(ev) {
|
|
273
273
|
const problems = [];
|
|
274
274
|
const gate = normalize_gate(String(ev.gate ?? ""));
|
|
275
|
+
if (String(ev.created_by ?? "") !== "user") {
|
|
276
|
+
problems.push(reason("human_confirmation_invalid", `${ev._path}: human_confirmation must be created_by user`));
|
|
277
|
+
}
|
|
275
278
|
if (!HUMAN_CONFIRMATION_GATES.has(gate)) {
|
|
276
279
|
problems.push(reason("human_confirmation_invalid", `${ev._path}: human_confirmation gate=${repr(ev.gate)} is not consumed by any guard gate; expected one of ${renderList([...HUMAN_CONFIRMATION_GATES].sort())}`));
|
|
277
280
|
}
|
|
@@ -847,3 +850,7 @@ export function live_pass(evidences, filters = {}) {
|
|
|
847
850
|
const dead = superseded_ids(evidences);
|
|
848
851
|
return find_pass(evidences, filters).filter((ev) => !dead.has(ev.evidence_id));
|
|
849
852
|
}
|
|
853
|
+
export function live_user_confirmations(evidences, gate) {
|
|
854
|
+
return live_pass(evidences, { gate, kind: "human_confirmation" })
|
|
855
|
+
.filter((ev) => String(ev.created_by ?? "") === "user");
|
|
856
|
+
}
|
package/dist/src/gates.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare function check_init(change: string, status: JsonMap, repoRoot: st
|
|
|
8
8
|
export declare function check_superspec_gate(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], gateRaw: string): Decision;
|
|
9
9
|
export declare function check_artifact(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], artifact: string): Decision;
|
|
10
10
|
export declare function check_task_reopen(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
|
|
11
|
+
export declare function check_apply_ready(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[]): Decision;
|
|
11
12
|
export declare function check_task_edit(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
|
|
12
13
|
export declare function check_task_complete(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
|
|
13
14
|
export declare function check_review_ready(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[]): Decision;
|
package/dist/src/gates.js
CHANGED
|
@@ -5,7 +5,7 @@ import { all_done, artifact_status_map, get_repo_root, is_done, normalize_gate,
|
|
|
5
5
|
import { read_agent_toml_name, read_skill_frontmatter_name, sidecar_business_invariants_path, sidecar_discovery_path, sidecar_test_contract_path } from "./paths.js";
|
|
6
6
|
import { business_invariant_ids, business_invariant_validation_reasons, automated_hard_business_invariant_ids, evidence_invariant_refs, evidence_invariant_ref_reasons, evidence_test_contract_invariant_reasons, human_confirmation_business_invariant_ids, invariant_matrix_coverage_reasons, post_implementation_business_invariant_ids, red_green_invariant_ids, test_contract_invariant_ids, } from "./invariants.js";
|
|
7
7
|
import { evidence_test_id_reasons, declared_test_evidence_reasons, parse_spec_scenarios, parse_tasks, parse_test_contract_ids, parse_test_contract_records, red_green_test_ids, splitList, tasks_structure_hash, task_alternative_verification, task_test_evidence, task_test_refs, test_contract_covers_scenario, test_contract_invariant_refs_by_test, write_scope_conflict_reasons, } from "./tasks.js";
|
|
8
|
-
import { duplicate_evidence_id_reasons, dangling_evidence_ref_reasons, final_verification_evidences, live_task_reopens, live_task_reopen_resolutions, live_pass, pass_task_reopens, supersede_reasons, unresolved_live_task_reopens, validate_evidence_schema, verify_reference_reasons, } from "./evidence.js";
|
|
8
|
+
import { duplicate_evidence_id_reasons, dangling_evidence_ref_reasons, final_verification_evidences, live_task_reopens, live_task_reopen_resolutions, live_pass, live_user_confirmations, pass_task_reopens, supersede_reasons, unresolved_live_task_reopens, validate_evidence_schema, verify_reference_reasons, } from "./evidence.js";
|
|
9
9
|
import { review_disclosure_reasons } from "./disclosure.js";
|
|
10
10
|
import { archive_manifest_path } from "./archive.js";
|
|
11
11
|
function action_list(...items) {
|
|
@@ -311,7 +311,7 @@ function archive_ready_actions(change, reasons, review) {
|
|
|
311
311
|
function default_gate_next_actions(gate) {
|
|
312
312
|
switch (gate) {
|
|
313
313
|
case "explore_complete":
|
|
314
|
-
return ["write .superspec/artifacts/discovery.md
|
|
314
|
+
return ["write .superspec/artifacts/discovery.md, record native_subagent critic evidence, and record explore_complete human confirmation"];
|
|
315
315
|
case "proposal_reviewed":
|
|
316
316
|
return ["run the proposal critic review (round-tagged, findings[]) and record a main_review_digest disclosing every finding"];
|
|
317
317
|
case "design_complete":
|
|
@@ -478,6 +478,9 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
478
478
|
reasons.push(reason("missing_native_subagent_evidence", `explore_complete requires native_subagent ${role} report`));
|
|
479
479
|
}
|
|
480
480
|
reasons.push(...stale_artifact_review_reasons(exploreReviews, changeRoot, ".superspec/artifacts/discovery.md", "stale_explore_review", "explore_complete"));
|
|
481
|
+
if (live_user_confirmations(evidences, "explore_complete").length === 0) {
|
|
482
|
+
reasons.push(reason("missing_human_confirmation", "explore_complete requires human confirmation before entering propose"));
|
|
483
|
+
}
|
|
481
484
|
// DISC Phase 1: material findings raised by explore reviews must be disclosed to the user
|
|
482
485
|
// (main_review_digest + user_review_decision) before the gate can pass.
|
|
483
486
|
reasons.push(...review_disclosure_reasons("explore_complete", changeRoot, evidences));
|
|
@@ -510,7 +513,7 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
510
513
|
if (!designReviews.some((ev) => ev.agent_role === role))
|
|
511
514
|
reasons.push(reason(`missing_${role}_review`, `design_complete requires native_subagent ${role} report`));
|
|
512
515
|
}
|
|
513
|
-
if (
|
|
516
|
+
if (live_user_confirmations(evidences, "design_complete").length === 0)
|
|
514
517
|
reasons.push(reason("missing_human_confirmation", "design_complete requires human confirmation"));
|
|
515
518
|
reasons.push(...stale_artifact_review_reasons(designReviews, changeRoot, "design.md", "stale_design_review", "design_complete"));
|
|
516
519
|
// DISC Phase 2: design reviews carrying round-tagged findings enter the disclosure loop
|
|
@@ -536,7 +539,7 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
536
539
|
const humanRequired = human_confirmation_business_invariant_ids(changeRoot);
|
|
537
540
|
if (humanRequired.size > 0) {
|
|
538
541
|
const confirmed = new Set();
|
|
539
|
-
for (const ev of
|
|
542
|
+
for (const ev of live_user_confirmations(evidences, "invariants_reviewed")) {
|
|
540
543
|
for (const id of evidence_invariant_refs(ev))
|
|
541
544
|
confirmed.add(id);
|
|
542
545
|
}
|
|
@@ -654,6 +657,9 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
654
657
|
}
|
|
655
658
|
}
|
|
656
659
|
}
|
|
660
|
+
else if (gate === "apply_ready") {
|
|
661
|
+
return check_apply_ready(change, status, changeRoot, evidences);
|
|
662
|
+
}
|
|
657
663
|
else {
|
|
658
664
|
return block(change, gate, [reason("unknown_gate", `unknown superspec gate: ${gate}`)]);
|
|
659
665
|
}
|
|
@@ -1029,6 +1035,7 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1029
1035
|
reasons.push(reason("propose_not_complete", "task_reopen requires propose_complete"));
|
|
1030
1036
|
reasons.push(...propose.block_reasons);
|
|
1031
1037
|
}
|
|
1038
|
+
reasons.push(...apply_scope_confirmation_reasons(changeRoot, evidences));
|
|
1032
1039
|
const tasks = parse_tasks(changeRoot);
|
|
1033
1040
|
const task = tasks[taskId];
|
|
1034
1041
|
if (!task)
|
|
@@ -1038,8 +1045,13 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1038
1045
|
reasons.push(...request_changes_round_reasons(evidences));
|
|
1039
1046
|
const reopenCheck = active_task_reopen_reasons(changeRoot, evidences, task, taskId, "pre_revert");
|
|
1040
1047
|
reasons.push(...reopenCheck.reasons);
|
|
1041
|
-
if (reasons.length > 0)
|
|
1042
|
-
|
|
1048
|
+
if (reasons.length > 0) {
|
|
1049
|
+
const reasonSet = reason_codes(reasons);
|
|
1050
|
+
return block(change, gate, reasons, {
|
|
1051
|
+
task_id: taskId,
|
|
1052
|
+
next_actions: action_list(reasonSet.has("apply_isolation_unconfirmed") ? "AskUserQuestion for apply isolation/execution mode and record gate=\"apply_isolation\" human_confirmation" : null, reasonSet.has("scope_expansion_unconfirmed") ? "stop: redesign/split the change or record gate=\"scope_expansion\" human_confirmation re-approving tasks.md structure" : null, `keep ${taskId} checked, fix task_reopen evidence / supersedes, then rerun check-task-reopen`),
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1043
1055
|
return allow(change, gate, { task_id: taskId });
|
|
1044
1056
|
}
|
|
1045
1057
|
// FIX-8 (audit A-5): apply work requires the user's explicit isolation/execution-mode choice,
|
|
@@ -1048,7 +1060,7 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1048
1060
|
// apply-phase scope expansion (SPEC §14.7) and demands explicit user re-approval — redesign,
|
|
1049
1061
|
// split into a new change, or record a scope_expansion confirmation re-pinning the structure.
|
|
1050
1062
|
function apply_scope_confirmation_reasons(changeRoot, evidences) {
|
|
1051
|
-
const isolation =
|
|
1063
|
+
const isolation = live_user_confirmations(evidences, "apply_isolation");
|
|
1052
1064
|
const currentHash = tasks_structure_hash(changeRoot);
|
|
1053
1065
|
if (isolation.length === 0) {
|
|
1054
1066
|
const hashHint = currentHash ? ` with tasks_structure_hash=${currentHash}` : "";
|
|
@@ -1056,11 +1068,28 @@ function apply_scope_confirmation_reasons(changeRoot, evidences) {
|
|
|
1056
1068
|
}
|
|
1057
1069
|
if (currentHash === null)
|
|
1058
1070
|
return [];
|
|
1059
|
-
const approvals = [...isolation, ...
|
|
1071
|
+
const approvals = [...isolation, ...live_user_confirmations(evidences, "scope_expansion")];
|
|
1060
1072
|
if (approvals.some((ev) => String(ev.tasks_structure_hash ?? "") === currentHash))
|
|
1061
1073
|
return [];
|
|
1062
1074
|
return [reason("scope_expansion_unconfirmed", `tasks.md structure changed after the last user-approved apply scope; ask the user to redesign/split the change or re-approve by recording gate="scope_expansion" human_confirmation with tasks_structure_hash=${currentHash}`)];
|
|
1063
1075
|
}
|
|
1076
|
+
export function check_apply_ready(change, status, changeRoot, evidences) {
|
|
1077
|
+
const gate = "apply_ready";
|
|
1078
|
+
const reasons = [];
|
|
1079
|
+
const propose = check_superspec_gate(change, status, changeRoot, evidences, "propose_complete");
|
|
1080
|
+
if (!propose.allowed) {
|
|
1081
|
+
reasons.push(reason("propose_not_complete", "apply_ready requires propose_complete"));
|
|
1082
|
+
reasons.push(...propose.block_reasons);
|
|
1083
|
+
}
|
|
1084
|
+
reasons.push(...apply_scope_confirmation_reasons(changeRoot, evidences));
|
|
1085
|
+
if (reasons.length > 0) {
|
|
1086
|
+
const reasonSet = reason_codes(reasons);
|
|
1087
|
+
return block(change, gate, reasons, {
|
|
1088
|
+
next_actions: action_list(!propose.allowed ? "pass propose_complete before apply" : null, reasonSet.has("apply_isolation_unconfirmed") ? "AskUserQuestion for apply isolation/execution mode and record gate=\"apply_isolation\" human_confirmation" : null, reasonSet.has("scope_expansion_unconfirmed") ? "stop: redesign/split the change or record gate=\"scope_expansion\" human_confirmation re-approving tasks.md structure" : null),
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
return allow(change, gate, { openspec_summary: artifact_status_map(status) });
|
|
1092
|
+
}
|
|
1064
1093
|
export function check_task_edit(change, status, changeRoot, evidences, taskId) {
|
|
1065
1094
|
const gate = "task_edit";
|
|
1066
1095
|
const reasons = [];
|
|
@@ -1378,7 +1407,7 @@ export function check_review_complete(change, status, changeRoot, evidences) {
|
|
|
1378
1407
|
&& (ev.kind === "verification_review" || ev.kind === "final_test")
|
|
1379
1408
|
&& ev.status === "fail");
|
|
1380
1409
|
if (failedVerifications.length > 0) {
|
|
1381
|
-
const dispositions = new Set(
|
|
1410
|
+
const dispositions = new Set(live_user_confirmations(evidences, "verify_failure_handling")
|
|
1382
1411
|
.flatMap((ev) => (Array.isArray(ev.confirmed_refs) ? ev.confirmed_refs.map((item) => String(item)) : [])));
|
|
1383
1412
|
const unhandled = failedVerifications
|
|
1384
1413
|
.map((ev) => String(ev.evidence_id ?? ev._path ?? "unknown"))
|
|
@@ -1456,7 +1485,7 @@ export function check_archive_ready(change, status, changeRoot, evidences) {
|
|
|
1456
1485
|
if (!ok && !reviewCodes.has("validate_failed")) {
|
|
1457
1486
|
reasons.push(reason("validate_failed", "openspec validate did not pass"));
|
|
1458
1487
|
}
|
|
1459
|
-
if (
|
|
1488
|
+
if (live_user_confirmations(evidences, "archive_ready").length === 0) {
|
|
1460
1489
|
reasons.push(reason("missing_final_confirmation", "archive_ready requires human confirmation evidence"));
|
|
1461
1490
|
}
|
|
1462
1491
|
if (reasons.length > 0)
|
package/dist/src/git.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { dirname, isAbsolute, relative } from "node:path";
|
|
3
3
|
import { GuardError, reason, renderList, runCommand, runtime } from "./util.js";
|
|
4
4
|
import { splitList, task_test_evidence } from "./tasks.js";
|
|
5
|
-
import {
|
|
5
|
+
import { live_user_confirmations } from "./evidence.js";
|
|
6
6
|
export function file_blob_sha(filePath) {
|
|
7
7
|
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
8
8
|
throw new GuardError(`git_blob_inspection_failure: reviewed target missing: ${filePath}`);
|
|
@@ -50,7 +50,7 @@ export function dirty_worktree_reasons(repoRoot, changeRoot, evidences) {
|
|
|
50
50
|
const unknown = dirty.filter((item) => !(changeRel && item.startsWith(`${changeRel}/`)));
|
|
51
51
|
if (unknown.length === 0)
|
|
52
52
|
return [];
|
|
53
|
-
const confirmations =
|
|
53
|
+
const confirmations = live_user_confirmations(evidences, "branch_handling");
|
|
54
54
|
const confirmedScopes = confirmations.flatMap((ev) => (Array.isArray(ev.confirmed_paths) ? ev.confirmed_paths.map((item) => String(item)).filter(Boolean) : []));
|
|
55
55
|
const unconfirmed = unknown.filter((item) => !confirmedScopes.some((scope) => pathInScope(item, scope)));
|
|
56
56
|
if (unconfirmed.length === 0)
|
package/dist/src/init_cli.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { runCommand, type OpenspecCliProbe } from "./core.ts";
|
|
1
|
+
import { runCommand, type DecisionOutputFormat, type OpenspecCliProbe } from "./core.ts";
|
|
2
2
|
import { type InstallScope } from "./install_engine.ts";
|
|
3
3
|
type InitArgs = {
|
|
4
4
|
path: string;
|
|
5
5
|
codexHome: string;
|
|
6
6
|
mode: "install" | "update" | "uninstall";
|
|
7
7
|
scope: InstallScope | null;
|
|
8
|
+
format: DecisionOutputFormat;
|
|
8
9
|
dryRun: boolean;
|
|
9
10
|
force: boolean;
|
|
10
11
|
};
|
package/dist/src/init_cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { block, commandExists, GuardError, printDecision, reason, runCommand, openspec_cli_probe, REQUIRED_OPENSPEC_MIN_VERSION, } from "./core.js";
|
|
1
|
+
import { block, commandExists, GuardError, printDecision, reason, runCommand, openspec_cli_probe, parseDecisionOutputFormat, REQUIRED_OPENSPEC_MIN_VERSION, } from "./core.js";
|
|
2
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";
|
|
@@ -6,21 +6,25 @@ import { resolve } from "node:path";
|
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { system_failure_zh } from "./i18n.js";
|
|
8
8
|
function usage() {
|
|
9
|
-
return "usage: superspec init [-h] [--scope {project,user}] [--path PATH] [--codex-home PATH] [--create] [--update] [--uninstall] [--dry-run] [--force]\n";
|
|
9
|
+
return "usage: superspec init [-h] [--scope {project,user}] [--path PATH] [--codex-home PATH] [--format {json,agent,user}] [--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 更新 SuperSpec 管理的文件;用户改动文件保留,新的版本写入 *.new\n --uninstall 按 manifest 卸载 SuperSpec 管理的文件;.superspec 数据与既有/用户改动文件会保留\n --dry-run 配合 --uninstall 时只预览将删除的文件,不实际修改\n --force 安装时覆盖已有且内容不同的文件,并保留 *.bak 备份\n`;
|
|
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 --format {json,agent,user} 输出格式;json 用于诊断,agent/user 用于安全展示\n --user-facing 等价于 --format user\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
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
const getValues = (flag) => {
|
|
16
|
+
const values = [];
|
|
17
|
+
for (let idx = 0; idx < argv.length; idx += 1) {
|
|
18
|
+
if (argv[idx] !== flag)
|
|
19
|
+
continue;
|
|
20
|
+
const value = argv[idx + 1];
|
|
21
|
+
if (value === undefined || value.startsWith("--"))
|
|
22
|
+
throw new GuardError(`${flag} 缺少取值`);
|
|
23
|
+
values.push(value);
|
|
24
|
+
}
|
|
25
|
+
return values;
|
|
23
26
|
};
|
|
27
|
+
const getValue = (flag) => getValues(flag)[0];
|
|
24
28
|
const update = argv.includes("--update");
|
|
25
29
|
const uninstall = argv.includes("--uninstall");
|
|
26
30
|
if (update && uninstall)
|
|
@@ -36,11 +40,19 @@ function parse_init_argv(argv) {
|
|
|
36
40
|
scope = "user";
|
|
37
41
|
if (argv.includes("--project"))
|
|
38
42
|
scope = "project";
|
|
43
|
+
const formatValues = getValues("--format");
|
|
44
|
+
for (const value of formatValues)
|
|
45
|
+
parseDecisionOutputFormat(value);
|
|
46
|
+
const selectedFormat = argv.includes("--user-facing")
|
|
47
|
+
? "user"
|
|
48
|
+
: (formatValues.length > 0 ? formatValues[formatValues.length - 1] : "json");
|
|
49
|
+
const format = parseDecisionOutputFormat(selectedFormat);
|
|
39
50
|
return {
|
|
40
51
|
path: resolve(getValue("--path") ?? process.cwd()),
|
|
41
52
|
codexHome: resolve(getValue("--codex-home") ?? process.env.CODEX_HOME ?? joinHomeCodex()),
|
|
42
53
|
mode: uninstall ? "uninstall" : update ? "update" : "install",
|
|
43
54
|
scope,
|
|
55
|
+
format,
|
|
44
56
|
dryRun: argv.includes("--dry-run"),
|
|
45
57
|
force: argv.includes("--force"),
|
|
46
58
|
};
|
|
@@ -149,7 +161,7 @@ function openspecPreflightBlocked(args, scope, installResult) {
|
|
|
149
161
|
project_root: args.path,
|
|
150
162
|
install_scope: scope,
|
|
151
163
|
install_root: targetRoot,
|
|
152
|
-
}, { command: "init" });
|
|
164
|
+
}, { command: "init", format: args.format });
|
|
153
165
|
return 1;
|
|
154
166
|
}
|
|
155
167
|
function run_init(args, scope) {
|
|
@@ -176,16 +188,17 @@ function run_init(args, scope) {
|
|
|
176
188
|
}
|
|
177
189
|
summary.install_scope = scope;
|
|
178
190
|
summary.install_root = targetRoot;
|
|
179
|
-
printDecision(summary, { command: "init" });
|
|
191
|
+
printDecision(summary, { command: "init", format: args.format });
|
|
180
192
|
return summary.allowed ? 0 : 1;
|
|
181
193
|
}
|
|
182
194
|
export function main_init(argv = process.argv.slice(2)) {
|
|
195
|
+
let args = null;
|
|
183
196
|
try {
|
|
184
197
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
185
198
|
process.stdout.write(help());
|
|
186
199
|
return 0;
|
|
187
200
|
}
|
|
188
|
-
|
|
201
|
+
args = parse_init_argv(argv);
|
|
189
202
|
const scope = args.scope ?? "project";
|
|
190
203
|
const openspecInstall = maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
|
|
191
204
|
if (openspecInstall === "failed" || openspecInstall === "skipped")
|
|
@@ -195,17 +208,18 @@ export function main_init(argv = process.argv.slice(2)) {
|
|
|
195
208
|
catch (err) {
|
|
196
209
|
const change = "project";
|
|
197
210
|
const errReason = err instanceof GuardError ? reason("guard_error", err.message) : reason("guard_internal_error", `${err.name}: ${err.message}`);
|
|
198
|
-
printDecision(block(change, "guard_error", [errReason]), { command: "init" });
|
|
211
|
+
printDecision(block(change, "guard_error", [errReason]), { command: "init", format: args?.format });
|
|
199
212
|
return 2;
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
215
|
export async function main_init_async(argv = process.argv.slice(2)) {
|
|
216
|
+
let args = null;
|
|
203
217
|
try {
|
|
204
218
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
205
219
|
process.stdout.write(help());
|
|
206
220
|
return 0;
|
|
207
221
|
}
|
|
208
|
-
|
|
222
|
+
args = parse_init_argv(argv);
|
|
209
223
|
const scope = args.scope ?? (canPrompt() ? await promptInstallScope() : "project");
|
|
210
224
|
const openspecInstall = maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
|
|
211
225
|
if (openspecInstall === "failed" || openspecInstall === "skipped")
|
|
@@ -215,7 +229,7 @@ export async function main_init_async(argv = process.argv.slice(2)) {
|
|
|
215
229
|
catch (err) {
|
|
216
230
|
const change = "project";
|
|
217
231
|
const errReason = err instanceof GuardError ? reason("guard_error", err.message) : reason("guard_internal_error", `${err.name}: ${err.message}`);
|
|
218
|
-
printDecision(block(change, "guard_error", [errReason]), { command: "init" });
|
|
232
|
+
printDecision(block(change, "guard_error", [errReason]), { command: "init", format: args?.format });
|
|
219
233
|
return 2;
|
|
220
234
|
}
|
|
221
235
|
}
|
|
@@ -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
|
+
}
|