@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.
@@ -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
+ }
@@ -26,3 +26,4 @@ export declare function live_pass(evidences: JsonMap[], filters?: {
26
26
  kind?: string | null;
27
27
  task_id?: string | null;
28
28
  }): JsonMap[];
29
+ export declare function live_user_confirmations(evidences: JsonMap[], gate: string): JsonMap[];
@@ -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
+ }
@@ -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, REQUIRED_OPENSPEC_CLI_SURFACES, REQUIRED_OPENSPEC_CODEX_SKILLS, REVIEW_GUIDANCE_ROLES, REVIEW_EVIDENCE_REQUIRED_FIELDS, TDD_MODES, VERIFY_EVIDENCE_REQUIRED_FIELDS, allow, block, commandExists, isObject, reason, renderList, repr, pinned_ref_key, safe_within, runCommand, sha256_text, runtime, toPosix, } from "./util.js";
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 and record native_subagent critic evidence"];
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
- if (!commandExists("openspec"))
405
- return [reason("openspec_cli_unavailable", "openspec CLI is not available in PATH")];
406
- const problems = [];
407
- for (const args of REQUIRED_OPENSPEC_CLI_SURFACES) {
408
- const proc = runCommand("openspec", [...args], { timeout: 15_000 });
409
- if (proc.error) {
410
- problems.push(reason("openspec_native_surface_missing", `\`openspec ${args.join(" ")}\` failed: ${proc.error.message}`));
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 (live_pass(evidences, { kind: "human_confirmation", gate: "design_complete" }).length === 0)
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 live_pass(evidences, { gate: "invariants_reviewed", kind: "human_confirmation" })) {
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
- return block(change, gate, reasons, { task_id: taskId, next_actions: [`keep ${taskId} checked, fix task_reopen evidence / supersedes, then rerun check-task-reopen`] });
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 = live_pass(evidences, { gate: "apply_isolation", kind: "human_confirmation" });
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, ...live_pass(evidences, { gate: "scope_expansion", kind: "human_confirmation" })];
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(live_pass(evidences, { gate: "verify_failure_handling", kind: "human_confirmation" })
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 (live_pass(evidences, { gate: "archive_ready", kind: "human_confirmation" }).length === 0) {
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 { live_pass } from "./evidence.js";
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 = live_pass(evidences, { gate: "branch_handling", kind: "human_confirmation" });
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)