@peterxiaoyang/superspec 0.1.10 → 0.1.12

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.
Files changed (45) hide show
  1. package/README.md +8 -10
  2. package/dist/src/apply_worker_chain.d.ts +6 -3
  3. package/dist/src/apply_worker_chain.js +125 -30
  4. package/dist/src/apply_worker_chain_lifecycle.d.ts +7 -5
  5. package/dist/src/apply_worker_chain_lifecycle.js +153 -22
  6. package/dist/src/cli_args.d.ts +1 -0
  7. package/dist/src/cli_args.js +33 -15
  8. package/dist/src/disclosure.js +2 -2
  9. package/dist/src/evidence.js +104 -8
  10. package/dist/src/gates.d.ts +1 -0
  11. package/dist/src/gates.js +160 -27
  12. package/dist/src/hooks/adapter.js +43 -7
  13. package/dist/src/hooks/guard_api.js +2 -2
  14. package/dist/src/hooks/health.js +0 -12
  15. package/dist/src/hooks/policy_event.js +11 -1
  16. package/dist/src/i18n.js +63 -3
  17. package/dist/src/init_cli.js +1 -1
  18. package/dist/src/install_engine.js +64 -0
  19. package/dist/src/openspec.d.ts +2 -0
  20. package/dist/src/openspec.js +31 -0
  21. package/dist/src/packet_render.d.ts +1 -0
  22. package/dist/src/packet_render.js +248 -31
  23. package/dist/src/packet_schema.d.ts +2 -0
  24. package/dist/src/tasks.d.ts +5 -0
  25. package/dist/src/tasks.js +62 -0
  26. package/dist/src/util.d.ts +4 -0
  27. package/dist/src/util.js +13 -0
  28. package/dist/superspec.js +4 -4
  29. package/package.json +3 -3
  30. package/templates/hooks/codex-hooks.json +0 -26
  31. package/templates/sidecar/discovery.md +9 -0
  32. package/templates/sidecar/test-contract.md +2 -1
  33. package/templates/workflow/prompts/code-reviewer.md +1 -1
  34. package/templates/workflow/prompts/executor.md +1 -1
  35. package/templates/workflow/prompts/test-engineer.md +1 -0
  36. package/templates/workflow/prompts/test-runner.md +3 -1
  37. package/templates/workflow/prompts/verifier.md +9 -9
  38. package/templates/workflow/skills/superspec-apply/SKILL.md +58 -40
  39. package/templates/workflow/skills/superspec-archive/SKILL.md +5 -13
  40. package/templates/workflow/skills/superspec-explore/SKILL.md +17 -18
  41. package/templates/workflow/skills/superspec-propose/SKILL.md +26 -22
  42. package/templates/workflow/skills/superspec-review/SKILL.md +17 -24
  43. package/dist/src/packet_measure.d.ts +0 -43
  44. package/dist/src/packet_measure.js +0 -382
  45. /package/bin/{superspec-guard.js → superspec-check.js} +0 -0
package/dist/src/gates.js CHANGED
@@ -1,16 +1,16 @@
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, 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";
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, 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, PENDING_CONFIRMATION_RE, UNCHECKED_CHECKBOX_RE, CHECKED_CHECKBOX_RE, } from "./util.js";
4
+ import { all_done, artifact_status_map, effective_superseded_ids, 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 { hookInitReasons } from "./hooks/health.js";
7
7
  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";
8
- 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 { 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, task_apply_execution_surface, task_apply_execution_surface_reasons, 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";
9
9
  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";
10
10
  import { review_disclosure_reasons } from "./disclosure.js";
11
11
  import { archive_manifest_path } from "./archive.js";
12
- import { fingerprint_matches, } from "./apply_worker_chain.js";
13
- import { apply_worker_chain_lifecycle_reasons as shared_apply_worker_chain_lifecycle_reasons, apply_worker_chain_lifecycle_state, serial_completion_green, } from "./apply_worker_chain_lifecycle.js";
12
+ import { pre_edit_worker_test_run_reasons, } from "./apply_worker_chain.js";
13
+ import { apply_worker_chain_lifecycle_reasons as shared_apply_worker_chain_lifecycle_reasons, apply_worker_chain_lifecycle_state, } from "./apply_worker_chain_lifecycle.js";
14
14
  function action_list(...items) {
15
15
  return [...new Set(items.filter((item) => typeof item === "string" && item.length > 0))];
16
16
  }
@@ -398,8 +398,15 @@ export function openspec_cli_capability_reasons() {
398
398
  return [reason("openspec_native_surface_missing", probe.message)];
399
399
  }
400
400
  export function evidence_schema_guard(change, changeRoot, repoRoot, evidences) {
401
+ const effectiveDead = effective_superseded_ids(evidences);
401
402
  return [
402
- ...evidences.flatMap((ev) => validate_evidence_schema(ev, change, changeRoot, repoRoot)),
403
+ ...evidences.flatMap((ev) => {
404
+ const problems = validate_evidence_schema(ev, change, changeRoot, repoRoot);
405
+ const id = typeof ev.evidence_id === "string" ? ev.evidence_id : "";
406
+ if (!id || !effectiveDead.has(id) || !ev.agent_role)
407
+ return problems;
408
+ return problems.filter((item) => item.code !== "stale_review");
409
+ }),
403
410
  ...duplicate_evidence_id_reasons(evidences),
404
411
  ...dangling_evidence_ref_reasons(evidences),
405
412
  ...supersede_reasons(evidences),
@@ -450,6 +457,54 @@ function stale_artifact_review_reasons(reviews, changeRoot, artifactRel, code, g
450
457
  }
451
458
  return reasons;
452
459
  }
460
+ // B1: count unresolved open questions inside discovery.md's "待确认问题" section before explore_complete
461
+ // can pass. The section is located by a heading whose text carries confirmation semantics
462
+ // (待确认 / 需要用户确认 / open question / pending confirmation), tolerating inserted words such as
463
+ // 「用户」between 需要 and 确认 (the real secondment-hour-unit-support incident title was
464
+ // 「## 需要用户确认的问题」, which a naive contiguous 「需要确认」 substring would miss).
465
+ //
466
+ // Within a matched section, a list item ("- " / "* " / "+ " / "1. ") counts as open if it carries an
467
+ // unchecked checkbox ("[ ]", literal half-width space — full-width U+3000 is not treated as a box,
468
+ // matching GitHub task-list semantics) OR pending-confirmation wording (PENDING_CONFIRMATION_RE,
469
+ // shared with the B3 confirmation guard). An item is closed (not counted) if it begins with 「已确认」
470
+ // after its list marker or carries a checked box "[x]". Plain prose bullets with neither signal are
471
+ // ignored. When the section is absent the count is 0 (grandfathered: legacy changes with no such
472
+ // section are not retroactively blocked, matching this repo's treatment of legacy evidence).
473
+ const OPEN_QUESTION_SECTION_RE = /(待|需要|需)[^\n]{0,6}确认|待澄清|开放问题|open question|pending confirmation/iu;
474
+ const OPEN_QUESTION_LIST_ITEM_RE = /^\s*(?:[-*+]|\d+\.)\s+(.*)$/u;
475
+ function classify_open_question_line(line) {
476
+ const m = line.match(OPEN_QUESTION_LIST_ITEM_RE);
477
+ if (!m)
478
+ return "none";
479
+ const body = m[1].trimStart();
480
+ if (CHECKED_CHECKBOX_RE.test(line))
481
+ return "closed";
482
+ if (UNCHECKED_CHECKBOX_RE.test(line))
483
+ return "open";
484
+ if (/^已确认/u.test(body))
485
+ return "closed";
486
+ if (PENDING_CONFIRMATION_RE.test(line))
487
+ return "open";
488
+ return "none";
489
+ }
490
+ export function discovery_open_question_count(discoveryPath) {
491
+ if (!existsSync(discoveryPath) || !statSync(discoveryPath).isFile())
492
+ return 0;
493
+ const text = readFileSync(discoveryPath, "utf8");
494
+ if (!text.trim())
495
+ return 0;
496
+ let inSection = false;
497
+ let count = 0;
498
+ for (const line of text.split(/\r?\n/u)) {
499
+ if (/^#{1,6}\s/u.test(line)) {
500
+ inSection = OPEN_QUESTION_SECTION_RE.test(line);
501
+ continue;
502
+ }
503
+ if (inSection && classify_open_question_line(line) === "open")
504
+ count += 1;
505
+ }
506
+ return count;
507
+ }
453
508
  export function check_superspec_gate(change, status, changeRoot, evidences, gateRaw) {
454
509
  const gate = normalize_gate(gateRaw);
455
510
  const amap = artifact_status_map(status);
@@ -470,6 +525,13 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
470
525
  // DISC Phase 1: material findings raised by explore reviews must be disclosed to the user
471
526
  // (main_review_digest + user_review_decision) before the gate can pass.
472
527
  reasons.push(...review_disclosure_reasons("explore_complete", changeRoot, evidences));
528
+ // B1: discovery.md must not still carry unresolved open questions. Without this the gate is a
529
+ // presence-check on human_confirmation and will pass even when discovery lists 「仍需确认」items
530
+ // (real incident: secondment-hour-unit-support entered propose with 8 open questions).
531
+ const openQuestionCount = discovery_open_question_count(discovery);
532
+ if (openQuestionCount > 0) {
533
+ reasons.push(reason("explore_open_questions_unresolved", `${discovery}: discovery.md 待确认问题段仍有 ${openQuestionCount} 条未勾选/「仍需确认」项,逐条与用户确认并改为已确认后才能进入 propose`));
534
+ }
473
535
  }
474
536
  else if (gate === "proposal_reviewed") {
475
537
  // DISC Phase 2: proposal review is an internal gate, not an advisory note. It is born inside
@@ -626,6 +688,9 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
626
688
  if (Object.keys(tasks).length === 0)
627
689
  reasons.push(reason("invalid_task_graph", "tasks.md has no structured tasks"));
628
690
  reasons.push(...write_scope_conflict_reasons(tasks));
691
+ for (const [taskId, task] of Object.entries(tasks)) {
692
+ reasons.push(...task_apply_execution_surface_reasons(taskId, task));
693
+ }
629
694
  // DISC Phase 3: tasks_complete enters the disclosure loop only once round-tagged review
630
695
  // evidence appears (design: no mandatory role review on this gate until then).
631
696
  reasons.push(...review_disclosure_reasons("tasks_complete", changeRoot, evidences));
@@ -702,6 +767,19 @@ function task_reopen_history(evidences, taskId) {
702
767
  function task_reopen_match_key(ev) {
703
768
  return `${String(ev.evidence_id ?? "")}\u0000${String(ev.reopen_id ?? "")}\u0000${String(ev.task_id ?? "")}`;
704
769
  }
770
+ function worker_pre_edit_test_evidence(changeRoot, evidences, taskId, semanticStatus) {
771
+ const candidates = task_test_evidence(evidences, taskId, semanticStatus, "task_edit");
772
+ const valid = [];
773
+ const invalidReasons = [];
774
+ for (const ev of candidates) {
775
+ const problems = pre_edit_worker_test_run_reasons(changeRoot, ev, taskId, "pre_edit_test_run_invalid");
776
+ if (problems.length === 0)
777
+ valid.push(ev);
778
+ else
779
+ invalidReasons.push(...problems);
780
+ }
781
+ return { valid, invalid_reasons: invalidReasons, candidate_count: candidates.length };
782
+ }
705
783
  function matching_reopen_resolutions(evidences, reopen) {
706
784
  const key = task_reopen_match_key(reopen);
707
785
  return live_task_reopen_resolutions(evidences)
@@ -1118,18 +1196,24 @@ export function check_task_edit(change, status, changeRoot, evidences, taskId) {
1118
1196
  reasons.push(reason("invalid_no_tdd_reason", `task ${taskId}: no_tdd_reason=${repr(nr)}`));
1119
1197
  }
1120
1198
  else if (tddMode === "behavior-preserving-refactor") {
1121
- const characterization = task_test_evidence(evidences, taskId, "expected_success", "task_edit");
1122
- if (characterization.length === 0)
1123
- reasons.push(reason("missing_characterization", `task ${taskId} (behavior-preserving-refactor) needs GREEN characterization test first`));
1199
+ const characterizationProof = worker_pre_edit_test_evidence(changeRoot, evidences, taskId, "expected_success");
1200
+ const characterization = characterizationProof.valid;
1201
+ if (characterization.length === 0) {
1202
+ reasons.push(reason(characterizationProof.candidate_count === 0 ? "missing_characterization" : "missing_worker_characterization", `task ${taskId} (behavior-preserving-refactor) needs test-runner characterization evidence before edit`));
1203
+ reasons.push(...characterizationProof.invalid_reasons);
1204
+ }
1124
1205
  reasons.push(...evidence_test_id_reasons(characterization, taskId, declared, contractIds, "characterization"));
1125
1206
  reasons.push(...declared_test_evidence_reasons(characterization, taskId, declared, "characterization"));
1126
1207
  reasons.push(...evidence_invariant_ref_reasons(characterization, taskId, declaredInvariants, validInvariantIds, "characterization"));
1127
1208
  reasons.push(...evidence_test_contract_invariant_reasons(characterization, taskId, test_contract_invariant_refs_by_test(changeRoot), "characterization"));
1128
1209
  }
1129
1210
  else {
1130
- const red = task_test_evidence(evidences, taskId, "expected_failure", "task_edit");
1131
- if (red.length === 0)
1132
- reasons.push(reason("missing_red_evidence", `task ${taskId} requires RED evidence (expected_failure) before edit`));
1211
+ const redProof = worker_pre_edit_test_evidence(changeRoot, evidences, taskId, "expected_failure");
1212
+ const red = redProof.valid;
1213
+ if (red.length === 0) {
1214
+ reasons.push(reason(redProof.candidate_count === 0 ? "missing_red_evidence" : "missing_worker_red_evidence", `task ${taskId} requires test-runner RED evidence before edit`));
1215
+ reasons.push(...redProof.invalid_reasons);
1216
+ }
1133
1217
  reasons.push(...evidence_test_id_reasons(red, taskId, declared, contractIds, "RED"));
1134
1218
  reasons.push(...declared_test_evidence_reasons(red, taskId, declared, "RED"));
1135
1219
  reasons.push(...evidence_invariant_ref_reasons(red, taskId, declaredInvariants, validInvariantIds, "RED"));
@@ -1149,6 +1233,34 @@ export function check_task_edit(change, status, changeRoot, evidences, taskId) {
1149
1233
  export function apply_worker_chain_lifecycle_reasons(repoRoot, changeRoot, evidences, taskId) {
1150
1234
  return shared_apply_worker_chain_lifecycle_reasons(repoRoot, changeRoot, evidences, taskId);
1151
1235
  }
1236
+ function apply_worker_chain_ref_ids(ev, singular, plural) {
1237
+ const ids = new Set();
1238
+ if (Array.isArray(ev[plural])) {
1239
+ for (const item of ev[plural]) {
1240
+ if (isObject(item) && typeof item.evidence_id === "string" && item.evidence_id)
1241
+ ids.add(item.evidence_id);
1242
+ else if (typeof item === "string" && item)
1243
+ ids.add(item);
1244
+ }
1245
+ }
1246
+ const single = ev[singular];
1247
+ if (isObject(single) && typeof single.evidence_id === "string" && single.evidence_id)
1248
+ ids.add(single.evidence_id);
1249
+ else if (typeof single === "string" && single)
1250
+ ids.add(single);
1251
+ return [...ids].sort();
1252
+ }
1253
+ function active_no_tdd_metadata_matches(active, executionSurface, noTddReason) {
1254
+ const preIds = Array.isArray(active?.pre_edit_evidence_refs) ? active.pre_edit_evidence_refs.map(String).filter(Boolean) : [];
1255
+ return isObject(active)
1256
+ && active.pre_edit_proof_kind === "no_tdd_declared"
1257
+ && preIds.length === 0
1258
+ && active.tdd_required === false
1259
+ && active.apply_execution_surface === executionSurface
1260
+ && typeof noTddReason === "string"
1261
+ && active.no_tdd_reason === noTddReason
1262
+ && NO_TDD_REASONS.has(noTddReason);
1263
+ }
1152
1264
  export function check_task_complete(change, status, changeRoot, evidences, taskId) {
1153
1265
  const gate = "task_complete";
1154
1266
  const reasons = [];
@@ -1163,6 +1275,7 @@ export function check_task_complete(change, status, changeRoot, evidences, taskI
1163
1275
  const task = parse_tasks(changeRoot)[taskId];
1164
1276
  if (!task)
1165
1277
  return block(change, gate, [reason("unknown_task", `task ${taskId} not found`)], { task_id: taskId });
1278
+ reasons.push(...task_apply_execution_surface_reasons(taskId, task));
1166
1279
  const workerChainState = apply_worker_chain_lifecycle_state(get_repo_root(status), changeRoot, evidences, taskId);
1167
1280
  reasons.push(...workerChainState.completionReasons);
1168
1281
  const reopenHistory = task_reopen_history(evidences, taskId);
@@ -1185,21 +1298,11 @@ export function check_task_complete(change, status, changeRoot, evidences, taskI
1185
1298
  reasons.push(reason("invalid_tdd_mode", `task ${taskId}: tdd_mode=${repr(tddMode)}`));
1186
1299
  if (tddRequired) {
1187
1300
  const allGreen = task_test_evidence(evidences, taskId, "expected_success", "task_complete");
1188
- const green = workerChainState.validClosedGreenEvidenceIds.size > 0
1189
- ? allGreen.filter((ev) => (ev.apply_execution_chain === "executor_worker"
1190
- && workerChainState.validClosedChainIds.has(String(ev.apply_worker_chain_id ?? ""))
1191
- && workerChainState.validClosedGreenEvidenceIds.has(String(ev.evidence_id ?? ""))))
1192
- : workerChainState.takeoverBaselines.length > 0
1193
- ? allGreen.filter((ev) => serial_completion_green(ev) && workerChainState.takeoverBaselines.some((baseline) => (baseline.successorGreenRefs.includes(String(ev.evidence_id ?? ""))
1194
- && fingerprint_matches(ev.implementation_fingerprint, baseline.fingerprint))))
1195
- : allGreen.filter(serial_completion_green);
1301
+ const green = allGreen.filter((ev) => (ev.apply_execution_chain === "executor_worker"
1302
+ && workerChainState.validClosedChainIds.has(String(ev.apply_worker_chain_id ?? ""))
1303
+ && workerChainState.validClosedGreenEvidenceIds.has(String(ev.evidence_id ?? ""))));
1196
1304
  if (green.length === 0)
1197
1305
  reasons.push(reason("missing_green_evidence", `task ${taskId} requires GREEN evidence (expected_success) before completion`));
1198
- if (workerChainState.validClosedGreenEvidenceIds.size === 0 && workerChainState.takeoverBaselines.length > 0) {
1199
- if (green.length === 0) {
1200
- reasons.push(reason("apply_worker_chain_takeover_green_mismatch", `task ${taskId} requires declared successor serial GREEN evidence after takeover baseline with matching implementation fingerprint`));
1201
- }
1202
- }
1203
1306
  const declared = new Set(splitList(attrs.test_refs ?? ""));
1204
1307
  const declaredInvariants = new Set(splitList(attrs.invariant_refs ?? ""));
1205
1308
  if (declared.size === 0)
@@ -1235,9 +1338,39 @@ export function check_task_complete(change, status, changeRoot, evidences, taskI
1235
1338
  const nr = attrs.no_tdd_reason;
1236
1339
  if (!NO_TDD_REASONS.has(nr))
1237
1340
  reasons.push(reason("invalid_no_tdd_reason", `task ${taskId}: no_tdd_reason=${repr(nr)}`));
1238
- const verifications = task_alternative_verification(evidences, taskId);
1239
- if (verifications.length === 0)
1341
+ const executionSurface = task_apply_execution_surface(task);
1342
+ const directAlternativeAllowed = executionSurface === "docs_generated" || executionSurface === "no_code";
1343
+ const implementationAlternativeRequired = executionSurface === "implementation" || executionSurface === "runtime_config";
1344
+ const allVerifications = task_alternative_verification(evidences, taskId);
1345
+ let validAlternativeEvidenceIds = workerChainState.validClosedAlternativeEvidenceIds;
1346
+ if (implementationAlternativeRequired) {
1347
+ const activeByChain = new Map(live_pass(evidences, { gate: "task_complete", kind: "apply_worker_chain", task_id: taskId })
1348
+ .filter((ev) => ev.chain_state === "active" && typeof ev.apply_worker_chain_id === "string")
1349
+ .map((ev) => [String(ev.apply_worker_chain_id), ev]));
1350
+ const closedAlternativeChains = live_pass(evidences, { gate: "task_complete", kind: "apply_worker_chain", task_id: taskId })
1351
+ .filter((ev) => (ev.chain_state === "closed"
1352
+ && String(ev.completion_proof_kind ?? "green_tests") === "alternative_verification"
1353
+ && workerChainState.validClosedAlternativeChainIds.has(String(ev.apply_worker_chain_id ?? ""))));
1354
+ const matchingClosedAlternativeChains = closedAlternativeChains.filter((ev) => (active_no_tdd_metadata_matches(activeByChain.get(String(ev.apply_worker_chain_id ?? "")), executionSurface, nr)));
1355
+ const metadataMismatchChains = closedAlternativeChains
1356
+ .filter((ev) => !matchingClosedAlternativeChains.includes(ev))
1357
+ .map((ev) => String(ev.apply_worker_chain_id ?? ev.evidence_id ?? ""))
1358
+ .filter(Boolean)
1359
+ .sort();
1360
+ if (metadataMismatchChains.length > 0) {
1361
+ reasons.push(reason("apply_worker_chain_active_invalid", `task ${taskId} closed alternative apply_worker_chain active no-TDD metadata must match task metadata: ${renderList(metadataMismatchChains)}`, metadataMismatchChains));
1362
+ }
1363
+ validAlternativeEvidenceIds = new Set(matchingClosedAlternativeChains.flatMap((ev) => apply_worker_chain_ref_ids(ev, "alternative_verification_evidence_ref", "alternative_verification_evidence_refs")));
1364
+ }
1365
+ const verifications = directAlternativeAllowed
1366
+ ? allVerifications
1367
+ : allVerifications.filter((ev) => validAlternativeEvidenceIds.has(String(ev.evidence_id ?? "")));
1368
+ if (implementationAlternativeRequired && verifications.length === 0) {
1369
+ reasons.push(reason("missing_alternative_verification", `task ${taskId} apply_execution_surface=${executionSurface} requires closed apply_worker_chain alternative_verification proof before completion`));
1370
+ }
1371
+ else if (!implementationAlternativeRequired && verifications.length === 0) {
1240
1372
  reasons.push(reason("missing_alternative_verification", `task ${taskId} has tdd_required:false and needs alternative verification evidence`));
1373
+ }
1241
1374
  if (reopenMode.mode === "reopened") {
1242
1375
  const reopen = reopenMode.reopen;
1243
1376
  const reopenId = String(reopen.reopen_id ?? "");
@@ -224,11 +224,32 @@ function postToolUseOutput(decision) {
224
224
  },
225
225
  };
226
226
  }
227
+ function isSubagentHookEvent(eventName) {
228
+ return eventName === "SubagentStart" || eventName === "SubagentStop";
229
+ }
227
230
  function subagentOutput(decision, eventName) {
231
+ if (!decision.allowed) {
232
+ return {
233
+ systemMessage: `SuperSpec ${eventName} runlog skipped; ${decision.block_reasons.map((item) => item.message).join("; ") || "audit telemetry unavailable"}`,
234
+ hookSpecificOutput: {
235
+ hookEventName: eventName,
236
+ additionalContext: "SuperSpec subagent telemetry is best-effort and did not block the workflow.",
237
+ },
238
+ };
239
+ }
228
240
  return {
229
241
  systemMessage: `SuperSpec ${eventName} runlog recorded as ${decision.trust}; strict_profile=${decision.strict_profile}`,
230
242
  };
231
243
  }
244
+ function inertSubagentOutput(eventName, message) {
245
+ return {
246
+ systemMessage: message,
247
+ hookSpecificOutput: {
248
+ hookEventName: eventName,
249
+ additionalContext: "SuperSpec subagent telemetry is best-effort and did not block the workflow.",
250
+ },
251
+ };
252
+ }
232
253
  function writeTempEvent(event) {
233
254
  const path = join(tmpdir(), `superspec-hook-event-${randomUUID()}.json`);
234
255
  writeFileSync(path, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
@@ -241,8 +262,9 @@ function validateHookEvent(event) {
241
262
  if (!["PreToolUse", "PostToolUse", "SubagentStart", "SubagentStop"].includes(eventName)) {
242
263
  return `unsupported hook_event_name: ${eventName}`;
243
264
  }
244
- if (typeof event.cwd !== "string" || !event.cwd)
265
+ if ((eventName === "PreToolUse" || eventName === "PostToolUse") && (typeof event.cwd !== "string" || !event.cwd)) {
245
266
  return "hook event missing required cwd";
267
+ }
246
268
  if (eventName === "PreToolUse") {
247
269
  if (typeof event.tool_name !== "string" || !event.tool_name)
248
270
  return "PreToolUse hook event missing required tool_name";
@@ -254,6 +276,7 @@ function validateHookEvent(event) {
254
276
  export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSync(0, "utf8")) {
255
277
  const parsedArgs = parseArgs(argv);
256
278
  let event;
279
+ let eventNameForFailure = "";
257
280
  try {
258
281
  if (!stdin.trim())
259
282
  throw new Error("hook stdin is empty");
@@ -261,11 +284,16 @@ export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSyn
261
284
  if (!isObject(parsed))
262
285
  throw new Error("hook stdin must be a JSON object");
263
286
  event = parsed;
287
+ eventNameForFailure = typeof event.hook_event_name === "string" ? event.hook_event_name : "";
264
288
  const validationError = validateHookEvent(event);
265
289
  if (validationError)
266
290
  throw new Error(validationError);
267
291
  }
268
292
  catch (err) {
293
+ if (isSubagentHookEvent(eventNameForFailure)) {
294
+ const payload = inertSubagentOutput(eventNameForFailure, `SuperSpec ${eventNameForFailure} runlog skipped: ${err.message}`);
295
+ return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
296
+ }
269
297
  return { code: 2, stdout: "", stderr: `SuperSpec hook event parse failed: ${err.message}\n` };
270
298
  }
271
299
  const inference = parsedArgs.change ? { state: "unique", change: parsedArgs.change } : inferChangeFromActiveSessions(event);
@@ -274,8 +302,9 @@ export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSyn
274
302
  const payload = noChangeFallback(event, inference);
275
303
  return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
276
304
  }
277
- const eventRef = writeTempEvent(event);
305
+ let eventRef = null;
278
306
  try {
307
+ eventRef = writeTempEvent(event);
279
308
  const eventName = String(event.hook_event_name ?? "");
280
309
  let payload;
281
310
  if (eventName === "PreToolUse") {
@@ -298,14 +327,21 @@ export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSyn
298
327
  return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
299
328
  }
300
329
  catch (err) {
330
+ if (isSubagentHookEvent(String(event.hook_event_name ?? ""))) {
331
+ const eventName = String(event.hook_event_name ?? "");
332
+ const payload = inertSubagentOutput(eventName, `SuperSpec ${eventName} runlog skipped: ${err.message}`);
333
+ return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
334
+ }
301
335
  return { code: 2, stdout: "", stderr: `SuperSpec hook failed closed: ${err.message}\n` };
302
336
  }
303
337
  finally {
304
- try {
305
- unlinkSync(resolve(eventRef));
306
- }
307
- catch {
308
- // best effort temp cleanup
338
+ if (eventRef) {
339
+ try {
340
+ unlinkSync(resolve(eventRef));
341
+ }
342
+ catch {
343
+ // best effort temp cleanup
344
+ }
309
345
  }
310
346
  }
311
347
  }
@@ -457,7 +457,7 @@ export function hookCheckWrite(change, eventRef) {
457
457
  block_reasons: blockReasons,
458
458
  audit_only_reasons: auditOnlyReasons,
459
459
  target_paths: intent.target_paths,
460
- next_allowed_actions: ["rerun the relevant superspec guard check and use the expected workflow command surface"],
460
+ next_allowed_actions: ["rerun the relevant superspec check and use the expected workflow command surface"],
461
461
  });
462
462
  }
463
463
  return hookDecision(ctx, "hook_check_write", true, {
@@ -510,7 +510,7 @@ export function hookCheckWrite(change, eventRef) {
510
510
  block_reasons: blockReasons,
511
511
  audit_only_reasons: auditOnlyReasons,
512
512
  target_paths: intent.target_paths,
513
- next_allowed_actions: ["rerun the relevant superspec guard check and use the expected workflow command surface"],
513
+ next_allowed_actions: ["rerun the relevant superspec check and use the expected workflow command surface"],
514
514
  });
515
515
  }
516
516
  return hookDecision(ctx, "hook_check_write", true, {
@@ -4,18 +4,6 @@ import { reason, sha256_file } from "../util.js";
4
4
  import { HOOK_ADAPTER_VERSION } from "./types.js";
5
5
  const HOOK_COMMAND = 'superspec-hook --change "$SUPERSPEC_CHANGE"';
6
6
  const EXPECTED_HOOK_MATRIX = [
7
- {
8
- eventName: "PreToolUse",
9
- matcher: "Bash|apply_patch|Edit|Write|mcp__.*",
10
- timeout: 120,
11
- statusMessage: "SuperSpec 写入策略检查",
12
- },
13
- {
14
- eventName: "PostToolUse",
15
- matcher: "Bash",
16
- timeout: 120,
17
- statusMessage: "SuperSpec 运行证据记录",
18
- },
19
7
  {
20
8
  eventName: "SubagentStart",
21
9
  matcher: ".*",
@@ -2340,7 +2340,9 @@ function tokenAtSameCommand(tokens, idx) {
2340
2340
  return tokens[idx];
2341
2341
  }
2342
2342
  function isHookEntrypointName(name) {
2343
- return name === "superspec-guard"
2343
+ return name === "superspec-check"
2344
+ || name === "superspec-check.js"
2345
+ || name === "superspec-guard"
2344
2346
  || name === "superspec-guard.js"
2345
2347
  || name === "superspec-hook"
2346
2348
  || name === "superspec-hook.js";
@@ -2392,9 +2394,13 @@ function commandLooksLikeInternalHookWriter(command, depth = 0) {
2392
2394
  return true;
2393
2395
  if (next === "guard" && afterNext?.startsWith("hook-record-"))
2394
2396
  return true;
2397
+ if (next === "check" && afterNext?.startsWith("hook-record-"))
2398
+ return true;
2395
2399
  }
2396
2400
  if (name === "superspec-guard" && tokenAtSameCommand(tokens, commandIdx + 1)?.startsWith("hook-record-"))
2397
2401
  return true;
2402
+ if (name === "superspec-check" && tokenAtSameCommand(tokens, commandIdx + 1)?.startsWith("hook-record-"))
2403
+ return true;
2398
2404
  if (isHookAdapterEntrypointName(name))
2399
2405
  return true;
2400
2406
  if (isHookEntrypointName(name) && tokenAtSameCommand(tokens, commandIdx + 1)?.startsWith("hook-record-"))
@@ -2442,6 +2448,10 @@ function commandLooksLikeUnsafeLifecycleTermination(command, depth = 0) {
2442
2448
  if (segmentHasDangerousReason(commandIdx + 3, segmentEnd))
2443
2449
  return true;
2444
2450
  }
2451
+ if (token === "superspec" && tokens[commandIdx + 1] === "check" && tokens[commandIdx + 2] === "hook-session-end") {
2452
+ if (segmentHasDangerousReason(commandIdx + 3, segmentEnd))
2453
+ return true;
2454
+ }
2445
2455
  if (token === "superspec" && tokens[commandIdx + 1] === "hook-session-end") {
2446
2456
  if (segmentHasDangerousReason(commandIdx + 2, segmentEnd))
2447
2457
  return true;
package/dist/src/i18n.js CHANGED
@@ -73,6 +73,7 @@ const REASON_ZH = {
73
73
  invalid_tdd_mode: { label_zh: "TDD 模式无效", hint_zh: "task 上声明的 tdd_mode 不在允许枚举里。" },
74
74
  invalid_no_tdd_reason: { label_zh: "免 TDD 理由无效", hint_zh: "task 上声明的 no_tdd_reason 不在允许枚举里。" },
75
75
  missing_discovery: { label_zh: "缺少探索记录", hint_zh: "探索阶段缺少 discovery.md,或者该文件内容为空。" },
76
+ explore_open_questions_unresolved: { label_zh: "探索待确认问题未清空", hint_zh: "discovery.md 的待确认问题还有未勾选或「仍需确认」的项,需要逐条与用户确认并改成已确认后才能进入 propose。" },
76
77
  missing_proposal: { label_zh: "缺少提案工件", hint_zh: "提案工件还没有完成。" },
77
78
  missing_proposal_review: { label_zh: "缺少提案审查", hint_zh: "提案审查阶段还没有收集到严格审查证据。" },
78
79
  explore_complete_failed: { label_zh: "探索阶段未通过", hint_zh: "后续阶段依赖的探索完成检查还没有通过。" },
@@ -95,6 +96,65 @@ const REASON_ZH = {
95
96
  user_uninstall_failed: { label_zh: "用户级卸载失败", hint_zh: "用户级 SuperSpec 卸载失败。" },
96
97
  guard_error: { label_zh: "检查执行失败", hint_zh: "guard 命令执行过程中发生了可报告错误。" },
97
98
  guard_internal_error: { label_zh: "检查器内部错误", hint_zh: "guard 内部抛出了异常,需要查看错误信息排查。" },
99
+ "apply_worker_chain_active": { label_zh: "活跃工作链存在", hint_zh: "当前任务已有活跃的 apply worker chain。" },
100
+ "apply_worker_chain_active_conflict": { label_zh: "活跃工作链冲突", hint_zh: "存在多条活跃 apply worker chain,需要先收敛到一条。" },
101
+ "apply_worker_chain_missing_active": { label_zh: "缺少活跃工作链", hint_zh: "当前任务没有活跃的 apply worker chain,需要先启动。" },
102
+ "apply_worker_chain_terminal_conflict": { label_zh: "工作链终态冲突", hint_zh: "存在多条终态 apply worker chain,需要先收敛。" },
103
+ "archive_not_found": { label_zh: "归档未找到", hint_zh: "没有找到已归档的 change,确认归档是否完成。" },
104
+ "bash_write_target_unknown": { label_zh: "Bash 写入目标未知", hint_zh: "Bash 命令的写入目标无法识别,需要明确目标路径。" },
105
+ "curl_config_write_target_unknown": { label_zh: "curl 配置写入目标未知", hint_zh: "curl 配置类命令的写入目标无法识别。" },
106
+ "curl_write_target_unknown": { label_zh: "curl 写入目标未知", hint_zh: "curl 命令的写入目标无法识别。" },
107
+ "custom_superspec_schema_present": { label_zh: "自定义 schema 存在", hint_zh: "检测到自定义 OpenSpec schema,SuperSpec 只支持默认 spec-driven。" },
108
+ "dangling_evidence_ref": { label_zh: "证据引用悬空", hint_zh: "evidence 引用的 evidence_id 不存在,检查引用或恢复被删证据。" },
109
+ "dirty_worktree_unattributed": { label_zh: "脏工作区未归属", hint_zh: "工作区有改动但无法归因到当前 change,需要先确认归属。" },
110
+ "duplicate_test_command": { label_zh: "测试命令重复", hint_zh: "检测到重复的测试命令声明。" },
111
+ "evidence_bad_status": { label_zh: "证据状态非法", hint_zh: "evidence 的 status 字段不在合法枚举内。" },
112
+ "evidence_forbidden_field": { label_zh: "证据含禁止字段", hint_zh: "evidence 包含当前 kind 不允许的字段。" },
113
+ "evidence_missing_field": { label_zh: "证据缺字段", hint_zh: "evidence 缺少必需字段。" },
114
+ "evidence_unknown_kind": { label_zh: "证据 kind 未知", hint_zh: "evidence 的 kind 不在白名单内。" },
115
+ "evidence_unparsable": { label_zh: "证据不可解析", hint_zh: "evidence 文件不是合法 JSON。" },
116
+ "evidence_unsafe_ref": { label_zh: "证据引用不安全", hint_zh: "evidence 的引用路径逃逸了允许范围。" },
117
+ "forbidden_alias_path": { label_zh: "禁用别名路径", hint_zh: "使用了被禁用的配置或状态文件别名。" },
118
+ "hook_internal_writer_invocation": { label_zh: "内部写入命令被调用", hint_zh: "模型控制的工具调用不得触发 SuperSpec hook 写入命令。" },
119
+ "hook_manifest_missing_or_unmanaged": { label_zh: "hook manifest 缺失或不受管", hint_zh: ".codex/hooks.json 缺失或不是 SuperSpec 受管版本。" },
120
+ "hook_manifest_unmanaged": { label_zh: "hook manifest 不受管", hint_zh: ".codex/hooks.json 不是 SuperSpec 受管版本。" },
121
+ "hook_session_audit_only": { label_zh: "hook 会话仅审计", hint_zh: "当前 hook 会话是 audit-only,不能作为可信强制来源。" },
122
+ "hook_session_corrupt": { label_zh: "hook 会话损坏", hint_zh: "活跃 hook 会话记录损坏,需要重建。" },
123
+ "hook_session_expired": { label_zh: "hook 会话过期", hint_zh: "活跃 hook 会话已过期。" },
124
+ "hook_session_termination_untrusted": { label_zh: "hook 会话终止不可信", hint_zh: "会话终止来源不可信,不能关闭 lease。" },
125
+ "invariants_not_reviewed": { label_zh: "业务约束未审查", hint_zh: "business-invariants 尚未通过审查。" },
126
+ "mixed_request_changes_route": { label_zh: "混合返工路由", hint_zh: "request_changes 同时包含不一致的 route。" },
127
+ "openspec_canonical_direct_edit": { label_zh: "直接编辑 OpenSpec 正本", hint_zh: "不允许绕过 openspec instructions 直接编辑正本 artifact。" },
128
+ "openspec_status_incompatible": { label_zh: "OpenSpec 状态不兼容", hint_zh: "openspec status 输出与预期结构不一致。" },
129
+ "openspec_tasks_direct_edit": { label_zh: "直接编辑 tasks", hint_zh: "不允许绕过流程直接编辑 tasks.md。" },
130
+ "post_implementation_invariant_backfill": { label_zh: "实现后补不变量", hint_zh: "不允许在实现后才补业务不变量。" },
131
+ "protected_path_dirty": { label_zh: "受保护路径被改动", hint_zh: "受保护路径处于脏状态。" },
132
+ "protected_trust_root_link_source": { label_zh: "信任根符号链接", hint_zh: "信任根路径通过符号链接指向外部来源。" },
133
+ "protected_trust_root_write": { label_zh: "写入信任根", hint_zh: "不允许写入 SuperSpec 信任根路径。" },
134
+ "ref_empty": { label_zh: "引用为空", hint_zh: "引用字段为空字符串。" },
135
+ "ref_not_readable": { label_zh: "引用不可读", hint_zh: "引用指向的文件不可读。" },
136
+ "ref_path_unsafe": { label_zh: "引用路径不安全", hint_zh: "引用路径逃逸了允许的根。" },
137
+ "reopen_lifecycle_exhausted": { label_zh: "重开次数耗尽", hint_zh: "task reopen 的生命周期次数已用尽。" },
138
+ "required_claim_unadjudicated": { label_zh: "必需 claim 未裁决", hint_zh: "存在尚未裁决的必需 claim。" },
139
+ "review_diff_not_covered": { label_zh: "review 未覆盖 diff", hint_zh: "review 未覆盖当前 diff。" },
140
+ "runtime_evidence_missing_command_fingerprint": { label_zh: "运行时证据缺命令指纹", hint_zh: "runtime evidence 缺少 command fingerprint。" },
141
+ "runtime_evidence_missing_exit_code": { label_zh: "运行时证据缺退出码", hint_zh: "runtime evidence 缺少 exit code。" },
142
+ "runtime_evidence_missing_hook_event": { label_zh: "运行时证据缺 hook 事件", hint_zh: "runtime evidence 缺少可信 hook 事件。" },
143
+ "runtime_evidence_missing_raw_log_pin": { label_zh: "运行时证据缺日志锚定", hint_zh: "runtime evidence 缺少 raw log pinned refs。" },
144
+ "runtime_evidence_missing_token": { label_zh: "运行时证据缺 token", hint_zh: "runtime evidence 缺少绑定 token。" },
145
+ "runtime_evidence_untrusted_provenance": { label_zh: "运行时证据来源不可信", hint_zh: "runtime evidence 缺少可信 provenance。" },
146
+ "scope_expansion_requires_propose": { label_zh: "范围扩大需回 propose", hint_zh: "任务范围扩大需要回 propose 重新确认。" },
147
+ "self_review_not_allowed": { label_zh: "不允许自审", hint_zh: "主线程不能充当审查角色。" },
148
+ "shell_path_may_touch_trust_root": { label_zh: "shell 路径可能触及信任根", hint_zh: "shell 展开的路径可能触及 SuperSpec 信任根。" },
149
+ "superspec_not_preserved": { label_zh: "SuperSpec 记录未保全", hint_zh: "归档后 .superspec 记录未能从 preservation manifest 追溯。" },
150
+ "target_path_outside_repo": { label_zh: "写入目标在仓库外", hint_zh: "写入目标路径逃逸了仓库根。" },
151
+ "task_already_done": { label_zh: "任务已完成", hint_zh: "目标 task 已经勾选完成。" },
152
+ "test_id_not_in_log": { label_zh: "测试 id 不在日志中", hint_zh: "声称的 test_id 未出现在引用日志里。" },
153
+ "test_not_declared_for_task": { label_zh: "测试未为任务声明", hint_zh: "测试未在任务的 test_refs 中声明。" },
154
+ "untrusted_test_command_ref": { label_zh: "测试命令引用不可信", hint_zh: "测试命令引用来源不可信。" },
155
+ "unsafe_write_scope": { label_zh: "写范围不安全", hint_zh: "任务的 write_scope 包含不安全路径。" },
156
+ "unsupported_write_surface": { label_zh: "不支持的写入面", hint_zh: "当前面不支持该写入操作。" },
157
+ "write_scope_conflict": { label_zh: "写范围冲突", hint_zh: "多个任务的 write_scope 存在冲突。" },
98
158
  };
99
159
  const WORKFLOW_TERMS_ZH = {
100
160
  "check-enter": { term: "check-enter", label_zh: "进入阶段前检查", hint_zh: "进入某个流程阶段前先运行的检查命令。" },
@@ -110,9 +170,9 @@ const TRUST_WARNING_ZH = {
110
170
  "project init installs project surfaces only; change sidecars are created lazily by later superspec phases": "project init 只安装项目级内容;change sidecar 会在后续 superspec 阶段按需创建。",
111
171
  };
112
172
  const ACTION_ZH_EXACT = {
113
- "continue with superspec-explore/propose/apply guard checks": "继续执行探索、提案和实现阶段的前置检查。",
114
- "create/select a change during superspec-explore, then run change-scoped guard checks": "先在探索阶段创建或选择一个变更,然后运行该变更范围内的前置检查。",
115
- "review *.new files for user-modified surfaces, then rerun superspec guard check-init": "先检查因用户改动而生成的 *.new 文件,然后重新运行初始化前置检查;Windows PowerShell 中使用 superspec.cmd guard check-init。",
173
+ "continue with superspec-explore/propose/apply check checks": "继续执行探索、提案和实现阶段的前置检查。",
174
+ "create/select a change during superspec-explore, then run change-scoped check checks": "先在探索阶段创建或选择一个变更,然后运行该变更范围内的前置检查。",
175
+ "review *.new files for user-modified surfaces, then rerun superspec check check-init": "先检查因用户改动而生成的 *.new 文件,然后重新运行初始化前置检查;Windows PowerShell 中使用 superspec.cmd check check-init。",
116
176
  "review *.new files for user-modified user-level surfaces": "先检查用户级内容对应的 *.new 文件。",
117
177
  "user-level SuperSpec Codex surfaces installed; use superspec init --scope project inside a repo when project-local surfaces are needed": "用户级 SuperSpec 配套内容已安装;如果仓库里还需要项目级内容,请在仓库内运行 superspec init --scope project;Windows PowerShell 中使用 superspec.cmd init --scope project。",
118
178
  "finish remaining unchecked tasks and mark them complete only after check-task-complete passes": "先完成剩余未勾选任务,并且仅在任务完成检查通过后再标记完成。",
@@ -318,7 +318,7 @@ function run_init(args, scope) {
318
318
  }
319
319
  else if (args.mode === "update") {
320
320
  summary = engineDecision(`${gatePrefix}_update`, targetRoot, update_workflow(targetRoot, { scope }), [
321
- scope === "project" ? "review *.new files for user-modified surfaces, then rerun superspec guard check-init" : "review *.new files for user-modified user-level surfaces",
321
+ scope === "project" ? "review *.new files for user-modified surfaces, then rerun superspec check check-init" : "review *.new files for user-modified user-level surfaces",
322
322
  ]);
323
323
  }
324
324
  else if (scope === "user") {