@peterxiaoyang/superspec 0.1.11 → 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 (41) hide show
  1. package/README.md +4 -4
  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/guard_api.js +2 -2
  13. package/dist/src/hooks/policy_event.js +11 -1
  14. package/dist/src/i18n.js +63 -3
  15. package/dist/src/init_cli.js +1 -1
  16. package/dist/src/openspec.d.ts +2 -0
  17. package/dist/src/openspec.js +31 -0
  18. package/dist/src/packet_render.d.ts +1 -0
  19. package/dist/src/packet_render.js +248 -31
  20. package/dist/src/packet_schema.d.ts +2 -0
  21. package/dist/src/tasks.d.ts +5 -0
  22. package/dist/src/tasks.js +62 -0
  23. package/dist/src/util.d.ts +4 -0
  24. package/dist/src/util.js +13 -0
  25. package/dist/superspec.js +4 -4
  26. package/package.json +3 -3
  27. package/templates/sidecar/discovery.md +9 -0
  28. package/templates/sidecar/test-contract.md +2 -1
  29. package/templates/workflow/prompts/code-reviewer.md +1 -1
  30. package/templates/workflow/prompts/executor.md +1 -1
  31. package/templates/workflow/prompts/test-engineer.md +1 -0
  32. package/templates/workflow/prompts/test-runner.md +3 -1
  33. package/templates/workflow/prompts/verifier.md +9 -9
  34. package/templates/workflow/skills/superspec-apply/SKILL.md +58 -33
  35. package/templates/workflow/skills/superspec-archive/SKILL.md +5 -5
  36. package/templates/workflow/skills/superspec-explore/SKILL.md +18 -12
  37. package/templates/workflow/skills/superspec-propose/SKILL.md +26 -15
  38. package/templates/workflow/skills/superspec-review/SKILL.md +17 -17
  39. package/dist/src/packet_measure.d.ts +0 -43
  40. package/dist/src/packet_measure.js +0 -382
  41. /package/bin/{superspec-guard.js → superspec-check.js} +0 -0
@@ -48,6 +48,13 @@ function isPacketCommand(command) {
48
48
  || command === "apply-verify-packet"
49
49
  || command === "ledger-render";
50
50
  }
51
+ function isHookEventCommand(command) {
52
+ return command === "hook-check-write"
53
+ || command === "hook-check-command"
54
+ || command === "hook-record-test"
55
+ || command === "hook-record-subagent-start"
56
+ || command === "hook-record-subagent-stop";
57
+ }
51
58
  function requiredValueFlags(command) {
52
59
  const flags = ["--change"];
53
60
  if (command === "check-artifact")
@@ -67,11 +74,10 @@ function requiredValueFlags(command) {
67
74
  if (command === "apply-code-review-packet")
68
75
  flags.push("--task-id", "--executor-report-ref");
69
76
  if (command === "apply-verify-packet")
70
- flags.push("--task-id", "--executor-report-ref", "--task-code-review-report-ref", "--green-test-run-evidence-ref");
77
+ flags.push("--task-id", "--executor-report-ref", "--task-code-review-report-ref");
71
78
  if (command === "ledger-render")
72
79
  flags.push("--gate");
73
- if (command === "hook-check-write" || command === "hook-check-command" || command === "hook-record-test"
74
- || command === "hook-record-subagent-start" || command === "hook-record-subagent-stop")
80
+ if (isHookEventCommand(command))
75
81
  flags.push("--event-ref");
76
82
  if (command === "hook-session-begin")
77
83
  flags.push("--workflow", "--entrypoint-token");
@@ -97,7 +103,7 @@ function optionalValueFlags(command) {
97
103
  if (command === "apply-executor-packet")
98
104
  return ["--apply-worker-chain-ref"];
99
105
  if (command === "apply-verify-packet")
100
- return ["--red-test-run-evidence-ref", "--characterization-test-run-evidence-ref"];
106
+ return ["--green-test-run-evidence-ref", "--alternative-verification-evidence-ref", "--red-test-run-evidence-ref", "--characterization-test-run-evidence-ref"];
101
107
  if (command === "ledger-render")
102
108
  return ["--round"];
103
109
  if (command === "hook-session-end")
@@ -126,7 +132,7 @@ function requiresFormat(command) {
126
132
  || command === "apply-verify-packet";
127
133
  }
128
134
  function rootUsage() {
129
- return `usage: superspec_guard [-h]\n {${COMMAND_LIST}}\n ...\n`;
135
+ return `usage: superspec [-h]\n {${COMMAND_LIST}}\n ...\n`;
130
136
  }
131
137
  function rootHelp() {
132
138
  const commandLines = COMMANDS.map((command) => {
@@ -144,7 +150,7 @@ function commandUsage(command) {
144
150
  ...requiredBooleanFlags(command),
145
151
  ...optionalBooleanFlags(command),
146
152
  ];
147
- return `usage: superspec_guard ${command} ${usageFlags.join(" ")}\n`;
153
+ return `usage: superspec ${command} ${usageFlags.join(" ")}\n`;
148
154
  }
149
155
  function commandHelp(command) {
150
156
  const zh = command_zh(command);
@@ -179,7 +185,7 @@ function missingRequiredFlags(command, args) {
179
185
  }
180
186
  export function emitArgparsePreamble(argv) {
181
187
  if (argv.length === 0) {
182
- process.stderr.write(`${rootUsage()}superspec_guard:错误:缺少必填参数:command\n`);
188
+ process.stderr.write(`${rootUsage()}superspec:错误:缺少必填参数:command\n`);
183
189
  return 2;
184
190
  }
185
191
  const command = argv[0];
@@ -188,7 +194,7 @@ export function emitArgparsePreamble(argv) {
188
194
  return 0;
189
195
  }
190
196
  if (!COMMANDS.includes(command)) {
191
- process.stderr.write(`${rootUsage()}superspec_guard:错误:命令无效:'${command}';可选值:${COMMAND_CHOICES}\n`);
197
+ process.stderr.write(`${rootUsage()}superspec:错误:命令无效:'${command}';可选值:${COMMAND_CHOICES}\n`);
192
198
  return 2;
193
199
  }
194
200
  const args = argv.slice(1);
@@ -199,7 +205,11 @@ export function emitArgparsePreamble(argv) {
199
205
  if (!isPacketCommand(command)) {
200
206
  const missing = missingRequiredFlags(command, args);
201
207
  if (missing.length > 0) {
202
- process.stderr.write(`${commandUsage(command)}superspec_guard ${command}:错误:缺少必填参数:${missing.join(", ")}\n`);
208
+ if (isHookEventCommand(command) && missing.length === 1 && missing[0] === "--event-ref") {
209
+ process.stderr.write(`${commandUsage(command)}superspec ${command}:错误:--event-ref must point to a readable hook event JSON file\n`);
210
+ return 2;
211
+ }
212
+ process.stderr.write(`${commandUsage(command)}superspec ${command}:错误:缺少必填参数:${missing.join(", ")}\n`);
203
213
  return 2;
204
214
  }
205
215
  }
@@ -357,6 +367,7 @@ export function parse_argv(argv) {
357
367
  const executorRefs = getValues("--executor-report-ref");
358
368
  const codeReviewRefs = getValues("--task-code-review-report-ref");
359
369
  const greenRefs = getValues("--green-test-run-evidence-ref");
370
+ const alternativeRefs = getValues("--alternative-verification-evidence-ref");
360
371
  const redRefs = getValues("--red-test-run-evidence-ref");
361
372
  const characterizationRefs = getValues("--characterization-test-run-evidence-ref");
362
373
  if (!taskId)
@@ -365,11 +376,18 @@ export function parse_argv(argv) {
365
376
  throw new GuardError("apply-verify-packet 缺少必填参数 --executor-report-ref");
366
377
  if (codeReviewRefs.length === 0)
367
378
  throw new GuardError("apply-verify-packet 缺少必填参数 --task-code-review-report-ref");
368
- if (greenRefs.length === 0)
369
- throw new GuardError("apply-verify-packet 缺少必填参数 --green-test-run-evidence-ref");
370
- if (redRefs.length === 0 && characterizationRefs.length === 0) {
379
+ if (greenRefs.length === 0 && alternativeRefs.length === 0) {
380
+ throw new GuardError("apply-verify-packet requires --green-test-run-evidence-ref or --alternative-verification-evidence-ref");
381
+ }
382
+ if (greenRefs.length > 0 && alternativeRefs.length > 0) {
383
+ throw new GuardError("apply-verify-packet requires either GREEN refs or alternative verification refs, not both");
384
+ }
385
+ if (greenRefs.length > 0 && redRefs.length === 0 && characterizationRefs.length === 0) {
371
386
  throw new GuardError("apply-verify-packet requires --red-test-run-evidence-ref or --characterization-test-run-evidence-ref");
372
387
  }
388
+ if (alternativeRefs.length > 0 && (redRefs.length > 0 || characterizationRefs.length > 0)) {
389
+ throw new GuardError("apply-verify-packet alternative verification branch must not include RED or characterization refs");
390
+ }
373
391
  return {
374
392
  command,
375
393
  change,
@@ -377,6 +395,7 @@ export function parse_argv(argv) {
377
395
  executor_report_refs: executorRefs,
378
396
  task_code_review_report_refs: codeReviewRefs,
379
397
  green_test_run_evidence_refs: greenRefs,
398
+ alternative_verification_evidence_refs: alternativeRefs,
380
399
  red_test_run_evidence_refs: redRefs,
381
400
  characterization_test_run_evidence_refs: characterizationRefs,
382
401
  packet_format: parsePacketOutputFormat(selectedPacketFormat, { allowPrompt: true }),
@@ -394,11 +413,10 @@ export function parse_argv(argv) {
394
413
  throw new GuardError("--round 必须是大于等于 1 的整数");
395
414
  return { command, change, gate, round };
396
415
  }
397
- if (command === "hook-check-write" || command === "hook-check-command" || command === "hook-record-test"
398
- || command === "hook-record-subagent-start" || command === "hook-record-subagent-stop") {
416
+ if (isHookEventCommand(command)) {
399
417
  const eventRef = getValue("--event-ref");
400
418
  if (!eventRef)
401
- throw new Error("缺少必填参数 --event-ref");
419
+ throw new GuardError("--event-ref must point to a readable hook event JSON file");
402
420
  return { command, change, format, event_ref: eventRef };
403
421
  }
404
422
  if (command === "hook-session-begin") {
@@ -10,7 +10,7 @@
10
10
  import { existsSync, readFileSync, statSync } from "node:fs";
11
11
  import { join, relative } from "node:path";
12
12
  import { isObject, reason, renderList, repr, runtime, safe_within, toPosix, walkFiles } from "./util.js";
13
- import { normalize_gate } from "./openspec.js";
13
+ import { effective_superseded_ids, normalize_gate } from "./openspec.js";
14
14
  // Explicit target map (design §7); later phases extend this table gate by gate.
15
15
  // Entries containing "*" are globs enumerated at check time; set equality (P1-6) means a spec
16
16
  // file added after the digest makes the digest stale even though every pinned blob still matches.
@@ -462,7 +462,7 @@ export function review_disclosure_reasons(gate, changeRoot, evidences) {
462
462
  return [reason("missing_review_digest", `${gate}: the disclosure loop is mandatory on this gate; need round-tagged role review evidence (review_round_id ${gate}-r1, findings[]) plus a main_review_digest`)];
463
463
  }
464
464
  const out = [];
465
- const dead = new Set(valid.filter((ev) => ev.status === "superseded" && ev.supersedes).map((ev) => String(ev.supersedes)));
465
+ const dead = effective_superseded_ids(valid);
466
466
  const byId = new Map();
467
467
  for (const ev of valid) {
468
468
  if (typeof ev.evidence_id === "string" && ev.evidence_id)
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
- import { CLAIM_ADJUDICATION_DECISIONS, EVIDENCE_KINDS, EVIDENCE_STATUSES, FINAL_VERIFICATION_ROLES, HUMAN_CONFIRMATION_GATES, FINAL_TEST_REQUIRED_FIELDS, FINDING_ADJUDICATION_DECISIONS, FORBIDDEN_FIELDS, MAIN_ADJUDICATION_DECISIONS, MAIN_ADJUDICATION_REQUIRED_FIELDS, REQUEST_CHANGES_ROUTES, ROLE_EVIDENCE_FIELDS, REVIEW_GUIDANCE_ROLES, SELF_REVIEW_MARKERS, SOURCE_GUIDANCE_REQUIRED_FIELDS, TASK_REOPEN_INVALIDITY_CLASSES, TASK_REOPEN_REQUIRED_FIELDS, TASK_REOPEN_RESOLVED_REQUIRED_FIELDS, VERIFY_EVIDENCE_REQUIRED_FIELDS, fingerprint_obj, isObject, reason, renderList, repr, pinned_ref_key, runtime, safe_within, sha256_file, toPosix, walkFiles, } from "./util.js";
4
- import { normalize_gate } from "./openspec.js";
3
+ import { CLAIM_ADJUDICATION_DECISIONS, EVIDENCE_KINDS, EVIDENCE_STATUSES, FINAL_VERIFICATION_ROLES, HUMAN_CONFIRMATION_GATES, FINAL_TEST_REQUIRED_FIELDS, FINDING_ADJUDICATION_DECISIONS, FORBIDDEN_FIELDS, MAIN_ADJUDICATION_DECISIONS, MAIN_ADJUDICATION_REQUIRED_FIELDS, NO_TDD_REASONS, REQUEST_CHANGES_ROUTES, ROLE_EVIDENCE_FIELDS, REVIEW_GUIDANCE_ROLES, SELF_REVIEW_MARKERS, SOURCE_GUIDANCE_REQUIRED_FIELDS, TASK_REOPEN_INVALIDITY_CLASSES, TASK_REOPEN_REQUIRED_FIELDS, TASK_REOPEN_RESOLVED_REQUIRED_FIELDS, VERIFY_EVIDENCE_REQUIRED_FIELDS, confirmation_text_has_pending_wording, fingerprint_obj, isObject, reason, renderList, repr, pinned_ref_key, runtime, safe_within, sha256_file, toPosix, UNCHECKED_CHECKBOX_RE, walkFiles, } from "./util.js";
4
+ import { effective_superseded_ids, normalize_gate } from "./openspec.js";
5
5
  import { superspec_dir } from "./paths.js";
6
6
  import { findings_schema_reasons, review_digest_schema_reasons, standing_authorization_schema_reasons, user_decision_schema_reasons, } from "./disclosure.js";
7
7
  import { pinned_artifact_ref_reasons as shared_pinned_artifact_ref_reasons, worker_test_run_reasons, } from "./apply_worker_chain.js";
@@ -143,6 +143,24 @@ function string_list_field_reasons(ev, field, code, opts = {}) {
143
143
  }
144
144
  return [];
145
145
  }
146
+ const APPLY_WORKER_CHAIN_SURFACES = new Set(["implementation", "runtime_config"]);
147
+ function apply_worker_chain_ref_ids(ev, singleField, listField) {
148
+ const refs = new Set();
149
+ const single = ev[singleField];
150
+ if (isObject(single) && typeof single.evidence_id === "string" && single.evidence_id)
151
+ refs.add(single.evidence_id);
152
+ else if (typeof single === "string" && single)
153
+ refs.add(single);
154
+ if (Array.isArray(ev[listField])) {
155
+ for (const item of ev[listField]) {
156
+ if (isObject(item) && typeof item.evidence_id === "string" && item.evidence_id)
157
+ refs.add(item.evidence_id);
158
+ else if (typeof item === "string" && item)
159
+ refs.add(item);
160
+ }
161
+ }
162
+ return [...refs].sort();
163
+ }
146
164
  function fingerprint_field_reasons(ev, field, code) {
147
165
  const raw = ev[field];
148
166
  if (typeof raw === "string") {
@@ -161,7 +179,37 @@ function pinned_artifact_ref_item_reasons(refItem, field, changeRoot, expected =
161
179
  });
162
180
  }
163
181
  export function role_target_ref_reasons(ev, targetRoot) {
164
- return pinned_ref_reasons(ev, "target_refs", targetRoot, "stale_review");
182
+ const problems = [];
183
+ const raw = ev.target_refs;
184
+ if (!Array.isArray(raw)) {
185
+ problems.push(reason("target_ref_invalid", `${ev._path}: target_refs must be a list of {path, blob_sha}`));
186
+ return problems;
187
+ }
188
+ for (const refItem of raw) {
189
+ if (!isObject(refItem)) {
190
+ problems.push(reason("target_ref_invalid", `${ev._path}: target_refs item must be object`));
191
+ continue;
192
+ }
193
+ const rel = refItem.path;
194
+ const expected = refItem.blob_sha;
195
+ if (typeof rel !== "string" || !rel || typeof expected !== "string" || !expected) {
196
+ problems.push(reason("target_ref_invalid", `${ev._path}: target_refs requires path and blob_sha`));
197
+ continue;
198
+ }
199
+ const target = safe_within(targetRoot, rel);
200
+ if (target === null) {
201
+ problems.push(reason("evidence_unsafe_ref", `${ev._path}: target_refs escapes allowed root: ${rel}`));
202
+ continue;
203
+ }
204
+ if (!existsSync(target) || !statSync(target).isFile()) {
205
+ problems.push(reason("stale_review", `${ev._path}: target_refs is not readable: ${rel}`));
206
+ continue;
207
+ }
208
+ const actual = runtime.file_blob_sha(target);
209
+ if (actual !== expected)
210
+ problems.push(reason("stale_review", `${ev._path}: target_refs stale for ${rel}`));
211
+ }
212
+ return problems;
165
213
  }
166
214
  export function loaded_ref_reasons(ev, repoRoot) {
167
215
  return pinned_ref_reasons(ev, "loaded_refs", repoRoot, "stale_loaded_ref");
@@ -358,6 +406,16 @@ function human_confirmation_reasons(ev) {
358
406
  && (typeof ev.tasks_structure_hash !== "string" || !ev.tasks_structure_hash.trim())) {
359
407
  problems.push(reason("human_confirmation_invalid", `${ev._path}: ${gate} human_confirmation requires tasks_structure_hash pinning the approved tasks.md structure`));
360
408
  }
409
+ // B3: a pass confirmation must not carry unresolved signals. Strong signal: an unchecked checkbox
410
+ // "[ ]" pasted into confirmation_text (the agent copied an open question list into the confirmation).
411
+ // Weak signal: confirmation-pending wording shared with the explore gate (仍需确认 / 待确认 / ...),
412
+ // scoped to confirmation semantics so legitimate "细节仍需在 propose 细化" prose is not flagged.
413
+ // Real incident: secondment-hour-unit-support sealed status:"pass" while its confirmation_text said
414
+ // "还需要评估..." — the gate's presence-check then let propose through with 8 open questions.
415
+ const ctext = typeof ev.confirmation_text === "string" ? ev.confirmation_text : "";
416
+ if (UNCHECKED_CHECKBOX_RE.test(ctext) || confirmation_text_has_pending_wording(ctext)) {
417
+ problems.push(reason("human_confirmation_invalid", `${ev._path}: confirmation_text carries unresolved signals (unchecked checkbox or 「仍需确认/待确认」wording); do not record pass while open items remain`));
418
+ }
361
419
  return problems;
362
420
  }
363
421
  // FIX-10 (audit C-5/H-4): test_run evidence is archived per run, not as a bare self-report.
@@ -455,7 +513,28 @@ function apply_worker_chain_reasons(ev, changeRoot) {
455
513
  problems.push(...fingerprint_field_reasons(ev, "executor_packet_fingerprint", "apply_worker_chain_invalid"));
456
514
  problems.push(...fingerprint_field_reasons(ev, "source_implementation_fingerprint", "apply_worker_chain_invalid"));
457
515
  problems.push(...string_list_field_reasons(ev, "declared_task_write_scope", "apply_worker_chain_invalid"));
458
- problems.push(...string_list_field_reasons(ev, "pre_edit_evidence_refs", "apply_worker_chain_invalid"));
516
+ const proofKind = String(ev.pre_edit_proof_kind ?? "red_or_characterization");
517
+ if (proofKind !== "red_or_characterization" && proofKind !== "no_tdd_declared") {
518
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: active apply_worker_chain pre_edit_proof_kind must be red_or_characterization or no_tdd_declared`));
519
+ }
520
+ else if (proofKind === "no_tdd_declared") {
521
+ problems.push(...string_list_field_reasons(ev, "pre_edit_evidence_refs", "apply_worker_chain_invalid", { allowEmpty: true }));
522
+ if (Array.isArray(ev.pre_edit_evidence_refs) && ev.pre_edit_evidence_refs.length > 0) {
523
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: no_tdd_declared active apply_worker_chain requires empty pre_edit_evidence_refs`));
524
+ }
525
+ if (ev.tdd_required !== false) {
526
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: no_tdd_declared active apply_worker_chain requires tdd_required=false`));
527
+ }
528
+ if (!APPLY_WORKER_CHAIN_SURFACES.has(String(ev.apply_execution_surface ?? ""))) {
529
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: no_tdd_declared active apply_worker_chain requires apply_execution_surface implementation or runtime_config`));
530
+ }
531
+ if (!NO_TDD_REASONS.has(String(ev.no_tdd_reason ?? ""))) {
532
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: no_tdd_declared active apply_worker_chain requires valid no_tdd_reason`));
533
+ }
534
+ }
535
+ else {
536
+ problems.push(...string_list_field_reasons(ev, "pre_edit_evidence_refs", "apply_worker_chain_invalid"));
537
+ }
459
538
  }
460
539
  if (state === "closed") {
461
540
  const taskId = typeof ev.task_id === "string" ? ev.task_id : undefined;
@@ -468,8 +547,17 @@ function apply_worker_chain_reasons(ev, changeRoot) {
468
547
  problems.push(...refProblems);
469
548
  if (refProblems.length > 0)
470
549
  problems.push(reason("apply_worker_chain_invalid", `${ev._path}: closed apply_worker_chain report refs must be pinned same-chain worker reports`));
471
- if (!isObject(ev.green_test_run_evidence_ref) && typeof ev.green_test_run_evidence_ref !== "string") {
472
- problems.push(reason("apply_worker_chain_invalid", `${ev._path}: closed apply_worker_chain requires green_test_run_evidence_ref`));
550
+ const proofKind = String(ev.completion_proof_kind ?? "green_tests");
551
+ if (proofKind !== "green_tests" && proofKind !== "alternative_verification") {
552
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: closed apply_worker_chain completion_proof_kind must be green_tests or alternative_verification`));
553
+ }
554
+ else if (proofKind === "green_tests") {
555
+ if (apply_worker_chain_ref_ids(ev, "green_test_run_evidence_ref", "green_test_run_evidence_refs").length === 0) {
556
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: green_tests closed apply_worker_chain requires green_test_run_evidence_ref(s)`));
557
+ }
558
+ }
559
+ else if (apply_worker_chain_ref_ids(ev, "alternative_verification_evidence_ref", "alternative_verification_evidence_refs").length === 0) {
560
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: alternative_verification closed apply_worker_chain requires alternative_verification_evidence_ref(s)`));
473
561
  }
474
562
  problems.push(...fingerprint_field_reasons(ev, "observed_freshness_fingerprint", "apply_worker_chain_invalid"));
475
563
  }
@@ -479,6 +567,9 @@ function apply_worker_chain_reasons(ev, changeRoot) {
479
567
  if ("restored_implementation_fingerprint" in ev) {
480
568
  problems.push(...fingerprint_field_reasons(ev, "restored_implementation_fingerprint", "apply_worker_chain_invalid"));
481
569
  }
570
+ if ("restored_implementation_fingerprint" in ev && "serial_takeover_baseline_ref" in ev) {
571
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain restored_implementation_fingerprint and serial_takeover_baseline_ref are mutually exclusive`));
572
+ }
482
573
  if (!("restored_implementation_fingerprint" in ev) && !("serial_takeover_baseline_ref" in ev)) {
483
574
  problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain requires restored_implementation_fingerprint or serial_takeover_baseline_ref`));
484
575
  }
@@ -487,7 +578,12 @@ function apply_worker_chain_reasons(ev, changeRoot) {
487
578
  problems.push(...baselineProblems);
488
579
  if (baselineProblems.length > 0)
489
580
  problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain serial_takeover_baseline_ref must be a pinned same-chain status_report`));
490
- problems.push(...string_list_field_reasons(ev, "successor_green_evidence_refs", "apply_worker_chain_invalid"));
581
+ if (typeof ev.takeover_confirmation_evidence_id !== "string" || !ev.takeover_confirmation_evidence_id) {
582
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain serial takeover requires takeover_confirmation_evidence_id`));
583
+ }
584
+ if ("successor_green_evidence_refs" in ev) {
585
+ problems.push(...string_list_field_reasons(ev, "successor_green_evidence_refs", "apply_worker_chain_invalid"));
586
+ }
491
587
  }
492
588
  }
493
589
  return problems;
@@ -915,7 +1011,7 @@ export function find_pass(evidences, filters = {}) {
915
1011
  });
916
1012
  }
917
1013
  export function superseded_ids(evidences) {
918
- return new Set(evidences.filter((ev) => ev.status === "superseded" && ev.supersedes).map((ev) => ev.supersedes));
1014
+ return effective_superseded_ids(evidences);
919
1015
  }
920
1016
  // FIX-6 (audit C-2): supersede is a rollback mechanism, so it needs an authorization model.
921
1017
  // A supersede evidence must point at an existing evidence_id, and a cross-gate supersede
@@ -5,6 +5,7 @@ export declare function superspec_agent_reasons(repoRoot: string): Reason[];
5
5
  export declare function openspec_cli_capability_reasons(): Reason[];
6
6
  export declare function evidence_schema_guard(change: string, changeRoot: string, repoRoot: string, evidences: JsonMap[]): Reason[];
7
7
  export declare function check_init(change: string, status: JsonMap, repoRoot: string, changeRoot: string): Decision;
8
+ export declare function discovery_open_question_count(discoveryPath: string): number;
8
9
  export declare function check_superspec_gate(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], gateRaw: string): Decision;
9
10
  export declare function check_artifact(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], artifact: string): Decision;
10
11
  export declare function check_task_reopen(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
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 ?? "");