@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
|
@@ -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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
-
import { ARTIFACT_ENTER_GATE, MAIN_ADJUDICATION_DECISIONS, REQUEST_CHANGES_ROUTES, NO_TDD_REASONS, OPENSPEC_ARTIFACTS, FINAL_VERIFICATION_ROLES, REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_SUPERSPEC_WORKFLOW_SKILLS,
|
|
4
|
-
import { all_done, artifact_status_map, get_repo_root, is_done, normalize_gate } from "./openspec.js";
|
|
3
|
+
import { ARTIFACT_ENTER_GATE, MAIN_ADJUDICATION_DECISIONS, REQUEST_CHANGES_ROUTES, NO_TDD_REASONS, OPENSPEC_ARTIFACTS, FINAL_VERIFICATION_ROLES, REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_SUPERSPEC_WORKFLOW_SKILLS, REQUIRED_OPENSPEC_CODEX_SKILLS, REVIEW_GUIDANCE_ROLES, REVIEW_EVIDENCE_REQUIRED_FIELDS, TDD_MODES, VERIFY_EVIDENCE_REQUIRED_FIELDS, allow, block, isObject, reason, renderList, repr, pinned_ref_key, safe_within, sha256_text, runtime, toPosix, } from "./util.js";
|
|
4
|
+
import { all_done, artifact_status_map, get_repo_root, is_done, normalize_gate, openspec_cli_probe } from "./openspec.js";
|
|
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":
|
|
@@ -356,7 +356,7 @@ export function superspec_workflow_skill_reasons(repoRoot) {
|
|
|
356
356
|
const skillsRoot = join(repoRoot, ".codex", "skills");
|
|
357
357
|
const missing = REQUIRED_SUPERSPEC_WORKFLOW_SKILLS.filter((name) => !existsSync(join(skillsRoot, name, "SKILL.md")));
|
|
358
358
|
if (missing.length > 0) {
|
|
359
|
-
reasons.push(reason("superspec_init_missing", "SuperSpec workflow skills are missing; run superspec init --scope project to (re)install them", missing));
|
|
359
|
+
reasons.push(reason("superspec_init_missing", "SuperSpec workflow skills are missing; run `superspec init --scope project` to (re)install them", missing));
|
|
360
360
|
}
|
|
361
361
|
for (const name of REQUIRED_SUPERSPEC_WORKFLOW_SKILLS) {
|
|
362
362
|
const skillPath = join(skillsRoot, name, "SKILL.md");
|
|
@@ -401,19 +401,14 @@ export function superspec_agent_reasons(repoRoot) {
|
|
|
401
401
|
return reasons;
|
|
402
402
|
}
|
|
403
403
|
export function openspec_cli_capability_reasons() {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
else if (proc.status !== 0) {
|
|
413
|
-
problems.push(reason("openspec_native_surface_missing", `\`openspec ${args.join(" ")}\` failed: ${(proc.stderr || proc.stdout).trim()}`));
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return problems;
|
|
404
|
+
const probe = openspec_cli_probe();
|
|
405
|
+
if (probe.ok)
|
|
406
|
+
return [];
|
|
407
|
+
if (probe.state === "missing")
|
|
408
|
+
return [reason("openspec_cli_unavailable", probe.message)];
|
|
409
|
+
if (probe.state === "too_old")
|
|
410
|
+
return [reason("openspec_cli_too_old", probe.message)];
|
|
411
|
+
return [reason("openspec_native_surface_missing", probe.message)];
|
|
417
412
|
}
|
|
418
413
|
export function evidence_schema_guard(change, changeRoot, repoRoot, evidences) {
|
|
419
414
|
return [
|
|
@@ -483,6 +478,9 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
483
478
|
reasons.push(reason("missing_native_subagent_evidence", `explore_complete requires native_subagent ${role} report`));
|
|
484
479
|
}
|
|
485
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
|
+
}
|
|
486
484
|
// DISC Phase 1: material findings raised by explore reviews must be disclosed to the user
|
|
487
485
|
// (main_review_digest + user_review_decision) before the gate can pass.
|
|
488
486
|
reasons.push(...review_disclosure_reasons("explore_complete", changeRoot, evidences));
|
|
@@ -515,7 +513,7 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
515
513
|
if (!designReviews.some((ev) => ev.agent_role === role))
|
|
516
514
|
reasons.push(reason(`missing_${role}_review`, `design_complete requires native_subagent ${role} report`));
|
|
517
515
|
}
|
|
518
|
-
if (
|
|
516
|
+
if (live_user_confirmations(evidences, "design_complete").length === 0)
|
|
519
517
|
reasons.push(reason("missing_human_confirmation", "design_complete requires human confirmation"));
|
|
520
518
|
reasons.push(...stale_artifact_review_reasons(designReviews, changeRoot, "design.md", "stale_design_review", "design_complete"));
|
|
521
519
|
// DISC Phase 2: design reviews carrying round-tagged findings enter the disclosure loop
|
|
@@ -541,7 +539,7 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
541
539
|
const humanRequired = human_confirmation_business_invariant_ids(changeRoot);
|
|
542
540
|
if (humanRequired.size > 0) {
|
|
543
541
|
const confirmed = new Set();
|
|
544
|
-
for (const ev of
|
|
542
|
+
for (const ev of live_user_confirmations(evidences, "invariants_reviewed")) {
|
|
545
543
|
for (const id of evidence_invariant_refs(ev))
|
|
546
544
|
confirmed.add(id);
|
|
547
545
|
}
|
|
@@ -659,6 +657,9 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
659
657
|
}
|
|
660
658
|
}
|
|
661
659
|
}
|
|
660
|
+
else if (gate === "apply_ready") {
|
|
661
|
+
return check_apply_ready(change, status, changeRoot, evidences);
|
|
662
|
+
}
|
|
662
663
|
else {
|
|
663
664
|
return block(change, gate, [reason("unknown_gate", `unknown superspec gate: ${gate}`)]);
|
|
664
665
|
}
|
|
@@ -1034,6 +1035,7 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1034
1035
|
reasons.push(reason("propose_not_complete", "task_reopen requires propose_complete"));
|
|
1035
1036
|
reasons.push(...propose.block_reasons);
|
|
1036
1037
|
}
|
|
1038
|
+
reasons.push(...apply_scope_confirmation_reasons(changeRoot, evidences));
|
|
1037
1039
|
const tasks = parse_tasks(changeRoot);
|
|
1038
1040
|
const task = tasks[taskId];
|
|
1039
1041
|
if (!task)
|
|
@@ -1043,8 +1045,13 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1043
1045
|
reasons.push(...request_changes_round_reasons(evidences));
|
|
1044
1046
|
const reopenCheck = active_task_reopen_reasons(changeRoot, evidences, task, taskId, "pre_revert");
|
|
1045
1047
|
reasons.push(...reopenCheck.reasons);
|
|
1046
|
-
if (reasons.length > 0)
|
|
1047
|
-
|
|
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
|
+
}
|
|
1048
1055
|
return allow(change, gate, { task_id: taskId });
|
|
1049
1056
|
}
|
|
1050
1057
|
// FIX-8 (audit A-5): apply work requires the user's explicit isolation/execution-mode choice,
|
|
@@ -1053,7 +1060,7 @@ export function check_task_reopen(change, status, changeRoot, evidences, taskId)
|
|
|
1053
1060
|
// apply-phase scope expansion (SPEC §14.7) and demands explicit user re-approval — redesign,
|
|
1054
1061
|
// split into a new change, or record a scope_expansion confirmation re-pinning the structure.
|
|
1055
1062
|
function apply_scope_confirmation_reasons(changeRoot, evidences) {
|
|
1056
|
-
const isolation =
|
|
1063
|
+
const isolation = live_user_confirmations(evidences, "apply_isolation");
|
|
1057
1064
|
const currentHash = tasks_structure_hash(changeRoot);
|
|
1058
1065
|
if (isolation.length === 0) {
|
|
1059
1066
|
const hashHint = currentHash ? ` with tasks_structure_hash=${currentHash}` : "";
|
|
@@ -1061,11 +1068,28 @@ function apply_scope_confirmation_reasons(changeRoot, evidences) {
|
|
|
1061
1068
|
}
|
|
1062
1069
|
if (currentHash === null)
|
|
1063
1070
|
return [];
|
|
1064
|
-
const approvals = [...isolation, ...
|
|
1071
|
+
const approvals = [...isolation, ...live_user_confirmations(evidences, "scope_expansion")];
|
|
1065
1072
|
if (approvals.some((ev) => String(ev.tasks_structure_hash ?? "") === currentHash))
|
|
1066
1073
|
return [];
|
|
1067
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}`)];
|
|
1068
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
|
+
}
|
|
1069
1093
|
export function check_task_edit(change, status, changeRoot, evidences, taskId) {
|
|
1070
1094
|
const gate = "task_edit";
|
|
1071
1095
|
const reasons = [];
|
|
@@ -1383,7 +1407,7 @@ export function check_review_complete(change, status, changeRoot, evidences) {
|
|
|
1383
1407
|
&& (ev.kind === "verification_review" || ev.kind === "final_test")
|
|
1384
1408
|
&& ev.status === "fail");
|
|
1385
1409
|
if (failedVerifications.length > 0) {
|
|
1386
|
-
const dispositions = new Set(
|
|
1410
|
+
const dispositions = new Set(live_user_confirmations(evidences, "verify_failure_handling")
|
|
1387
1411
|
.flatMap((ev) => (Array.isArray(ev.confirmed_refs) ? ev.confirmed_refs.map((item) => String(item)) : [])));
|
|
1388
1412
|
const unhandled = failedVerifications
|
|
1389
1413
|
.map((ev) => String(ev.evidence_id ?? ev._path ?? "unknown"))
|
|
@@ -1461,7 +1485,7 @@ export function check_archive_ready(change, status, changeRoot, evidences) {
|
|
|
1461
1485
|
if (!ok && !reviewCodes.has("validate_failed")) {
|
|
1462
1486
|
reasons.push(reason("validate_failed", "openspec validate did not pass"));
|
|
1463
1487
|
}
|
|
1464
|
-
if (
|
|
1488
|
+
if (live_user_confirmations(evidences, "archive_ready").length === 0) {
|
|
1465
1489
|
reasons.push(reason("missing_final_confirmation", "archive_ready requires human confirmation evidence"));
|
|
1466
1490
|
}
|
|
1467
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)
|