@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
@@ -1,5 +1,6 @@
1
- import { isObject, reason, renderList, repr } from "./util.js";
2
- import { find_pass, live_pass } from "./evidence.js";
1
+ import { NO_TDD_REASONS, isObject, reason, renderList, repr } from "./util.js";
2
+ import { find_pass, live_pass, live_user_confirmations } from "./evidence.js";
3
+ import { effective_superseded_ids } from "./openspec.js";
3
4
  import { apply_worker_implementation_fingerprint, apply_worker_executor_input_ref_digest, compute_apply_worker_freshness, fingerprint_matches, pinned_artifact_ref_reasons as shared_pinned_artifact_ref_reasons, pre_edit_evidence_ref_reasons, read_pinned_artifact_json, worker_input_ref_digest, worker_test_run_reasons, } from "./apply_worker_chain.js";
4
5
  function pinned_artifact_ref_matches(changeRoot, refItem, role, taskId, chainId, kind = "worker_report") {
5
6
  return shared_pinned_artifact_ref_reasons(changeRoot, refItem, `${role}_ref`, { kind, role, taskId, chainId }).length === 0;
@@ -25,6 +26,23 @@ function chain_green_ids(ev) {
25
26
  : String(ev.green_test_run_evidence_ref ?? "");
26
27
  return greenId ? [greenId] : [];
27
28
  }
29
+ function chain_alternative_ids(ev) {
30
+ const refs = new Set();
31
+ if (Array.isArray(ev.alternative_verification_evidence_refs)) {
32
+ for (const item of ev.alternative_verification_evidence_refs) {
33
+ if (isObject(item) && typeof item.evidence_id === "string" && item.evidence_id)
34
+ refs.add(item.evidence_id);
35
+ else if (typeof item === "string" && item)
36
+ refs.add(item);
37
+ }
38
+ }
39
+ const single = ev.alternative_verification_evidence_ref;
40
+ if (isObject(single) && typeof single.evidence_id === "string" && single.evidence_id)
41
+ refs.add(single.evidence_id);
42
+ else if (typeof single === "string" && single)
43
+ refs.add(single);
44
+ return [...refs].sort();
45
+ }
28
46
  function active_pre_edit_input_refs(evidences, active) {
29
47
  const preIds = Array.isArray(active.pre_edit_evidence_refs) ? active.pre_edit_evidence_refs.map(String) : [];
30
48
  return preIds.map((evidenceId) => {
@@ -34,13 +52,42 @@ function active_pre_edit_input_refs(evidences, active) {
34
52
  : { kind: "test_run", evidence_id: evidenceId };
35
53
  });
36
54
  }
55
+ function active_chain_verifier_input_ref(active) {
56
+ const result = {
57
+ kind: "apply_worker_chain",
58
+ evidence_id: active.evidence_id,
59
+ task_id: active.task_id,
60
+ apply_worker_chain_id: active.apply_worker_chain_id,
61
+ pre_edit_proof_kind: active.pre_edit_proof_kind ?? "red_or_characterization",
62
+ pre_edit_evidence_refs: Array.isArray(active.pre_edit_evidence_refs) ? active.pre_edit_evidence_refs.map(String).filter(Boolean).sort() : [],
63
+ };
64
+ for (const field of ["apply_execution_surface", "tdd_required", "no_tdd_reason"]) {
65
+ if (active[field] !== undefined)
66
+ result[field] = active[field];
67
+ }
68
+ return result;
69
+ }
70
+ function active_no_tdd_declared_valid(active) {
71
+ const preIds = Array.isArray(active.pre_edit_evidence_refs) ? active.pre_edit_evidence_refs.map(String).filter(Boolean) : [];
72
+ return active.pre_edit_proof_kind === "no_tdd_declared"
73
+ && preIds.length === 0
74
+ && active.tdd_required === false
75
+ && (active.apply_execution_surface === "implementation" || active.apply_execution_surface === "runtime_config")
76
+ && NO_TDD_REASONS.has(String(active.no_tdd_reason ?? ""));
77
+ }
78
+ function alternative_evidence_input_ref(ev) {
79
+ return {
80
+ kind: ev.kind,
81
+ evidence_id: ev.evidence_id,
82
+ gate: ev.gate,
83
+ task_id: ev.task_id,
84
+ };
85
+ }
37
86
  function closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId) {
38
87
  const active = live_pass(evidences, { gate: "task_complete", kind: "apply_worker_chain", task_id: taskId })
39
88
  .find((item) => item.chain_state === "active" && item.apply_worker_chain_id === chainId);
40
89
  if (!active)
41
90
  return false;
42
- if (pre_edit_evidence_ref_reasons(evidences, active, taskId).length > 0)
43
- return false;
44
91
  if (!pinned_artifact_ref_matches(changeRoot, ev.executor_report_ref, "executor", taskId, chainId))
45
92
  return false;
46
93
  if (!pinned_artifact_ref_matches(changeRoot, ev.task_code_review_report_ref, "code-reviewer", taskId, chainId))
@@ -59,6 +106,47 @@ function closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, ta
59
106
  const verifierObserved = verifierReport?.observed_freshness_fingerprint ?? (isObject(ev.verifier_report_ref) ? ev.verifier_report_ref.observed_freshness_fingerprint : undefined);
60
107
  if (!fingerprint_matches(verifierObserved, ev.observed_freshness_fingerprint))
61
108
  return false;
109
+ const completionProofKind = String(ev.completion_proof_kind ?? "green_tests");
110
+ if (completionProofKind !== "green_tests" && completionProofKind !== "alternative_verification")
111
+ return false;
112
+ const preEditProofKind = String(active.pre_edit_proof_kind ?? "red_or_characterization");
113
+ if (completionProofKind === "green_tests" && preEditProofKind !== "red_or_characterization")
114
+ return false;
115
+ if (completionProofKind === "alternative_verification" && !active_no_tdd_declared_valid(active))
116
+ return false;
117
+ if (pre_edit_evidence_ref_reasons(changeRoot, evidences, active, taskId).length > 0)
118
+ return false;
119
+ if (completionProofKind === "alternative_verification") {
120
+ const alternativeIds = chain_alternative_ids(ev);
121
+ if (alternativeIds.length === 0)
122
+ return false;
123
+ const alternatives = alternativeIds.map((evidenceId) => live_pass(evidences, { gate: "task_complete", task_id: taskId })
124
+ .find((item) => (String(item.evidence_id ?? "") === evidenceId
125
+ && (item.kind === "alternative_verification" || item.kind === "manual_verification")))).filter((item) => isObject(item));
126
+ if (alternatives.length !== alternativeIds.length)
127
+ return false;
128
+ if (!report_input_refs_match(ev.verifier_report_ref, [
129
+ ev.executor_report_ref,
130
+ ev.task_code_review_report_ref,
131
+ ...alternatives.map(alternative_evidence_input_ref),
132
+ active_chain_verifier_input_ref(active),
133
+ ]))
134
+ return false;
135
+ const currentFreshness = compute_apply_worker_freshness(repoRoot, changeRoot, evidences, taskId, chainId, {
136
+ executor_report_ref: ev.executor_report_ref,
137
+ task_code_review_report_ref: ev.task_code_review_report_ref,
138
+ alternative_verification_evidence_refs: alternativeIds,
139
+ completion_proof_kind: "alternative_verification",
140
+ verifier_report_ref: ev.verifier_report_ref,
141
+ });
142
+ if (Array.isArray(currentFreshness.unexpected_guard_owned_dirty_paths) && currentFreshness.unexpected_guard_owned_dirty_paths.length > 0)
143
+ return false;
144
+ if (Array.isArray(currentFreshness.protected_dirty_paths) && currentFreshness.protected_dirty_paths.length > 0)
145
+ return false;
146
+ if (!fingerprint_matches(isObject(ev.task_code_review_report_ref) ? ev.task_code_review_report_ref.observed_implementation_fingerprint : undefined, currentFreshness.implementation_fingerprint))
147
+ return false;
148
+ return fingerprint_matches(currentFreshness, verifierObserved);
149
+ }
62
150
  const greenIds = chain_green_ids(ev);
63
151
  if (greenIds.length === 0)
64
152
  return false;
@@ -82,6 +170,7 @@ function closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, ta
82
170
  task_code_review_report_ref: ev.task_code_review_report_ref,
83
171
  green_test_run_evidence_ref: greenIds[0],
84
172
  green_test_run_evidence_refs: greenIds,
173
+ completion_proof_kind: "green_tests",
85
174
  verifier_report_ref: ev.verifier_report_ref,
86
175
  });
87
176
  if (Array.isArray(currentFreshness.unexpected_guard_owned_dirty_paths) && currentFreshness.unexpected_guard_owned_dirty_paths.length > 0)
@@ -97,9 +186,37 @@ function closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, ta
97
186
  function pinned_takeover_baseline_valid(changeRoot, refItem, taskId, chainId) {
98
187
  return shared_pinned_artifact_ref_reasons(changeRoot, refItem, "serial_takeover_baseline_ref", { kind: "status_report", role: "verifier", taskId, chainId }).length === 0;
99
188
  }
100
- function abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, active, taskId, chainId) {
189
+ function serial_takeover_baseline_ref_ids(ev) {
190
+ const refs = new Set();
191
+ if (isObject(ev.serial_takeover_baseline_ref)) {
192
+ for (const field of ["path", "ref_path", "evidence_id"]) {
193
+ const value = ev.serial_takeover_baseline_ref[field];
194
+ if (typeof value === "string" && value)
195
+ refs.add(value);
196
+ }
197
+ }
198
+ return [...refs].sort();
199
+ }
200
+ function serial_takeover_confirmation_valid(evidences, ev, active) {
201
+ const confirmationId = String(ev.takeover_confirmation_evidence_id ?? "");
202
+ const activeId = String(active.evidence_id ?? "");
203
+ const baselineIds = serial_takeover_baseline_ref_ids(ev);
204
+ if (!confirmationId || !activeId || baselineIds.length === 0)
205
+ return false;
206
+ const confirmation = live_user_confirmations(evidences, "serial_takeover")
207
+ .find((item) => String(item.evidence_id ?? "") === confirmationId);
208
+ if (!confirmation)
209
+ return false;
210
+ const confirmed = new Set(Array.isArray(confirmation.confirmed_refs) ? confirmation.confirmed_refs.map(String).filter(Boolean) : []);
211
+ return confirmed.has(activeId) && baselineIds.some((item) => confirmed.has(item));
212
+ }
213
+ function abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, active, evidences, taskId, chainId) {
101
214
  if (!active)
102
215
  return false;
216
+ const hasRestored = "restored_implementation_fingerprint" in ev;
217
+ const hasSerialTakeover = "serial_takeover_baseline_ref" in ev;
218
+ if (hasRestored && hasSerialTakeover)
219
+ return false;
103
220
  if ("restored_implementation_fingerprint" in ev) {
104
221
  const activeScope = Array.isArray(active.declared_task_write_scope) ? active.declared_task_write_scope.map(String).filter(Boolean) : [];
105
222
  const current = apply_worker_implementation_fingerprint(repoRoot, changeRoot, [], { declaredTaskWriteScope: activeScope });
@@ -107,21 +224,16 @@ function abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, active, ta
107
224
  && fingerprint_matches(current, active.source_implementation_fingerprint))
108
225
  return true;
109
226
  }
110
- if ("serial_takeover_baseline_ref" in ev) {
227
+ if (hasSerialTakeover) {
111
228
  return pinned_takeover_baseline_valid(changeRoot, ev.serial_takeover_baseline_ref, taskId, chainId)
112
- && Array.isArray(ev.successor_green_evidence_refs)
113
- && ev.successor_green_evidence_refs.length > 0
114
- && ev.successor_green_evidence_refs.every((item) => typeof item === "string" && item.length > 0);
229
+ && serial_takeover_confirmation_valid(evidences, ev, active);
115
230
  }
116
231
  return false;
117
232
  }
118
- export function serial_completion_green(ev) {
119
- return (ev.runner_origin === undefined || ev.runner_origin === "main-thread")
120
- && ev.apply_execution_chain !== "executor_worker"
121
- && typeof ev.apply_worker_chain_id !== "string";
122
- }
123
233
  export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidences, taskId) {
124
234
  const chains = find_pass(evidences, { gate: "task_complete", kind: "apply_worker_chain", task_id: taskId });
235
+ const supersededIds = effective_superseded_ids(evidences);
236
+ const liveChainMarker = (ev) => !supersededIds.has(String(ev.evidence_id ?? ""));
125
237
  const activeByChain = new Map();
126
238
  const activeCounts = new Map();
127
239
  for (const ev of chains) {
@@ -147,9 +259,9 @@ export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidenc
147
259
  if (String(ev.chain_state ?? "") === "closed")
148
260
  valid = closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId);
149
261
  if (String(ev.chain_state ?? "") === "abandoned")
150
- valid = abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, activeByChain.get(chainId), taskId, chainId);
262
+ valid = abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, activeByChain.get(chainId), evidences, taskId, chainId);
151
263
  terminalValidity.set(ev, valid);
152
- return valid;
264
+ return valid && liveChainMarker(ev);
153
265
  })
154
266
  .map((ev) => String(ev.apply_worker_chain_id ?? ""))
155
267
  .filter(Boolean));
@@ -158,7 +270,7 @@ export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidenc
158
270
  .filter((ev) => {
159
271
  const chainId = String(ev.apply_worker_chain_id ?? "");
160
272
  const valid = String(ev.chain_state ?? "") === "closed" && (terminalValidity.get(ev) ?? closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId));
161
- return chainId && valid;
273
+ return chainId && valid && liveChainMarker(ev);
162
274
  })
163
275
  .map((ev) => String(ev.apply_worker_chain_id ?? ""))
164
276
  .filter(Boolean));
@@ -166,10 +278,24 @@ export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidenc
166
278
  .filter((ev) => {
167
279
  const chainId = String(ev.apply_worker_chain_id ?? "");
168
280
  const valid = String(ev.chain_state ?? "") === "closed" && (terminalValidity.get(ev) ?? closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId));
169
- return chainId && valid;
281
+ return chainId && valid && liveChainMarker(ev);
170
282
  })
171
283
  .flatMap(chain_green_ids));
172
- const takeoverBaselines = [];
284
+ const validClosedAlternativeChainIds = new Set(chains
285
+ .filter((ev) => {
286
+ const chainId = String(ev.apply_worker_chain_id ?? "");
287
+ const valid = String(ev.chain_state ?? "") === "closed" && String(ev.completion_proof_kind ?? "green_tests") === "alternative_verification" && (terminalValidity.get(ev) ?? closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId));
288
+ return chainId && valid && liveChainMarker(ev);
289
+ })
290
+ .map((ev) => String(ev.apply_worker_chain_id ?? ""))
291
+ .filter(Boolean));
292
+ const validClosedAlternativeEvidenceIds = new Set(chains
293
+ .filter((ev) => {
294
+ const chainId = String(ev.apply_worker_chain_id ?? "");
295
+ const valid = String(ev.chain_state ?? "") === "closed" && String(ev.completion_proof_kind ?? "green_tests") === "alternative_verification" && (terminalValidity.get(ev) ?? closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId));
296
+ return chainId && valid && liveChainMarker(ev);
297
+ })
298
+ .flatMap(chain_alternative_ids));
173
299
  const completionReasons = [];
174
300
  const duplicateActives = [...activeCounts.entries()]
175
301
  .filter(([chainId]) => openActiveChainIds.has(chainId))
@@ -206,6 +332,7 @@ export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidenc
206
332
  if (duplicateTerminals.length > 0) {
207
333
  completionReasons.push(reason("apply_worker_chain_terminal_conflict", `task ${taskId} has duplicate apply worker chain terminal markers: ${renderList(duplicateTerminals)}`, duplicateTerminals));
208
334
  }
335
+ const takeoverbaselines = [];
209
336
  const invalidTerminals = chains
210
337
  .filter((ev) => String(ev.chain_state ?? "") === "closed" || String(ev.chain_state ?? "") === "abandoned")
211
338
  .filter((ev) => {
@@ -214,11 +341,11 @@ export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidenc
214
341
  return true;
215
342
  if (String(ev.chain_state ?? "") === "closed")
216
343
  return !(terminalValidity.get(ev) ?? closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId));
217
- const valid = terminalValidity.get(ev) ?? abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, activeByChain.get(chainId), taskId, chainId);
344
+ const valid = terminalValidity.get(ev) ?? abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, activeByChain.get(chainId), evidences, taskId, chainId);
218
345
  if (valid && isObject(ev.serial_takeover_baseline_ref)) {
219
346
  const baseline = read_pinned_artifact_json(changeRoot, ev.serial_takeover_baseline_ref);
220
347
  if (baseline?.implementation_fingerprint !== undefined) {
221
- takeoverBaselines.push({
348
+ takeoverbaselines.push({
222
349
  fingerprint: baseline.implementation_fingerprint,
223
350
  successorGreenRefs: Array.isArray(ev.successor_green_evidence_refs) ? ev.successor_green_evidence_refs.map(String).filter(Boolean) : [],
224
351
  });
@@ -250,6 +377,8 @@ export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidenc
250
377
  if (duplicateTerminals.length > 0 || activeConflicts.length > 0) {
251
378
  validClosedChainIds.clear();
252
379
  validClosedGreenEvidenceIds.clear();
380
+ validClosedAlternativeChainIds.clear();
381
+ validClosedAlternativeEvidenceIds.clear();
253
382
  }
254
383
  const activeBlockingReasons = completionReasons.filter((item) => (item.code === "apply_worker_chain_active_conflict"
255
384
  || item.code === "apply_worker_chain_terminal_conflict"
@@ -270,7 +399,9 @@ export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidenc
270
399
  validTerminalChainIds: chainIdsWithTerminal,
271
400
  validClosedChainIds,
272
401
  validClosedGreenEvidenceIds,
273
- takeoverBaselines,
402
+ validClosedAlternativeEvidenceIds,
403
+ validClosedAlternativeChainIds,
404
+ takeoverbaselines,
274
405
  };
275
406
  }
276
407
  export function apply_worker_chain_lifecycle_reasons(repoRoot, changeRoot, evidences, taskId) {
@@ -21,6 +21,7 @@ export type ParsedArgs = {
21
21
  task_code_review_report_refs?: string[];
22
22
  apply_worker_chain_refs?: string[];
23
23
  green_test_run_evidence_refs?: string[];
24
+ alternative_verification_evidence_refs?: string[];
24
25
  red_test_run_evidence_refs?: string[];
25
26
  characterization_test_run_evidence_refs?: string[];
26
27
  create?: boolean;
@@ -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;