@junghanacs/entwurf 0.17.2 → 0.18.1

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 (65) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +299 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +9 -5
  5. package/VERIFY.md +3 -3
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +46 -12
  8. package/docs/setup-clean-host.md +35 -3
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +6 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +38 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
  21. package/mcp/entwurf-bridge/src/index.ts +16 -8
  22. package/package.json +10 -9
  23. package/pi-extensions/entwurf-control.ts +12 -2
  24. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  25. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  26. package/pi-extensions/lib/acp/backend.ts +3 -3
  27. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  28. package/pi-extensions/lib/entwurf-deliverability.ts +104 -0
  29. package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
  30. package/pi-extensions/lib/entwurf-facts.ts +52 -3
  31. package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
  32. package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
  33. package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
  34. package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
  35. package/pi-extensions/lib/entwurf-v2-surface.ts +6 -0
  36. package/pi-extensions/lib/meta-session.ts +44 -0
  37. package/pi-extensions/meta-bridge-hook.ts +72 -3
  38. package/run.sh +146 -56
  39. package/scripts/check-acp-usage-accounting.ts +9 -9
  40. package/scripts/check-bridge-delivery.ts +42 -1
  41. package/scripts/check-entwurf-fact-provider.ts +22 -1
  42. package/scripts/check-entwurf-facts.ts +17 -2
  43. package/scripts/check-entwurf-peers-surface.ts +20 -2
  44. package/scripts/check-entwurf-self-address.ts +20 -2
  45. package/scripts/check-entwurf-v2-decider.ts +6 -3
  46. package/scripts/check-entwurf-v2-matrix.ts +10 -5
  47. package/scripts/check-entwurf-v2-production.ts +72 -1
  48. package/scripts/check-gate-qualification.ts +63 -12
  49. package/scripts/check-meta-hook-session-switch.ts +560 -0
  50. package/scripts/check-release-gate-outcomes.ts +160 -3
  51. package/scripts/meta-bridge-doctor.sh +29 -4
  52. package/scripts/mutants/bridge-command-boot.json +3 -3
  53. package/scripts/mutants/gate-qualification.json +28 -0
  54. package/scripts/mutants/meta-hook-session-switch.json +223 -0
  55. package/scripts/mutants/pack-install.json +14 -3
  56. package/scripts/mutants/release-gate.json +22 -0
  57. package/scripts/raw-acp-compaction-measure/README.md +77 -0
  58. package/scripts/raw-acp-compaction-measure/probe.ts +196 -0
  59. package/scripts/raw-claude-session-switch/README.md +170 -0
  60. package/scripts/smoke-acp-cortex-live.ts +22 -2
  61. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  62. package/scripts/smoke-acp-v2-send-live.ts +23 -2
  63. package/scripts/smoke-entwurf-chain-live.ts +85 -13
  64. package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
  65. package/scripts/smoke-mux-fresh-call-live.ts +13 -3
@@ -469,8 +469,26 @@ const metaBody = functionBody("buildTrustedMetaSenderEnvelope");
469
469
  ok("buildTrustedMetaSenderEnvelope calls computeSelfAddressability", /computeSelfAddressability\s*\(/.test(metaBody));
470
470
  ok("buildTrustedMetaSenderEnvelope no longer hardcodes `replyable: true`", !/replyable:\s*true/.test(metaBody));
471
471
  ok(
472
- "buildTrustedMetaSenderEnvelope derives active-receiver from the receiver marker (identity-matched)",
473
- /readMetaReceiverMarker\s*\(/.test(metaBody) && /receiverMarkerMatchesIdentity\s*\(/.test(metaBody),
472
+ // #101: the identity match alone is no longer the whole answer. `entwurf_self` composes
473
+ // the SAME `resolveMailboxReceiverFacts` the v2 dispatch seam uses — reading the receiver
474
+ // marker AND, where the watch owner is the sender-marker process, the join that says that
475
+ // owner is still serving this garden. A citizen's self-reported replyability and what
476
+ // dispatch decides about it come from one measurement, so they cannot disagree.
477
+ "buildTrustedMetaSenderEnvelope derives active-receiver from the SHARED receiver composition (both markers)",
478
+ /resolveMailboxReceiverFacts\s*\(/.test(metaBody) &&
479
+ /readMetaReceiverMarker\s*\(/.test(metaBody) &&
480
+ /readMetaSenderMarker\s*\(/.test(metaBody),
481
+ );
482
+ ok(
483
+ "buildTrustedMetaSenderEnvelope no longer copies one match into both receiver facts",
484
+ !/ownerAlive:\s*active/.test(metaBody) && !/watchArmed:\s*active/.test(metaBody),
485
+ );
486
+ ok(
487
+ // #101 (cross-review): both markers are read with their liveness guards ON. A reader that
488
+ // opted out would accept a dead session's leftover file as "which garden this pid serves
489
+ // now", and every fixture pid in a gate is live, so nothing dynamic here could tell.
490
+ "buildTrustedMetaSenderEnvelope reads neither marker with the owner guard disabled",
491
+ !/verifyOwner:\s*false/.test(metaBody),
474
492
  );
475
493
  ok(
476
494
  "buildTrustedMetaSenderEnvelope keeps meta identity + derived replyable (inactive → not null)",
@@ -207,11 +207,13 @@ async function main(): Promise<void> {
207
207
  ok("target-locked: diagnostic carried", d.kind === "reject" && d.diagnostic?.kind === "target-locked");
208
208
  ok(
209
209
  "target-locked: diagnostic holder pid preserved",
210
- d.kind === "reject" && d.diagnostic?.conflict.holder?.pid === 4242,
210
+ d.kind === "reject" && d.diagnostic?.kind === "target-locked" && d.diagnostic.conflict.holder?.pid === 4242,
211
211
  );
212
212
  ok(
213
213
  "target-locked: diagnostic lockPath preserved",
214
- d.kind === "reject" && d.diagnostic?.conflict.lockPath === `/fake/locks/${GID}.lock`,
214
+ d.kind === "reject" &&
215
+ d.diagnostic?.kind === "target-locked" &&
216
+ d.diagnostic.conflict.lockPath === `/fake/locks/${GID}.lock`,
215
217
  );
216
218
  }
217
219
 
@@ -253,7 +255,8 @@ async function main(): Promise<void> {
253
255
  ok(
254
256
  "target-locked(corrupt): diagnostic detail preserved",
255
257
  d.kind === "reject" &&
256
- d.diagnostic?.conflict.detail === "lockfile is empty, corrupt, or holds a different garden id",
258
+ d.diagnostic?.kind === "target-locked" &&
259
+ d.diagnostic.conflict.detail === "lockfile is empty, corrupt, or holds a different garden id",
257
260
  );
258
261
  }
259
262
 
@@ -42,6 +42,7 @@ import {
42
42
  type DispatchDecision,
43
43
  decideDispatch,
44
44
  type ExecutionPlan,
45
+ type RejectDiagnostic,
45
46
  type TargetResolution,
46
47
  } from "../pi-extensions/lib/entwurf-v2-decider.ts";
47
48
  import type { AcquireLockResult, LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
@@ -156,7 +157,10 @@ type LockClass = "none" | "held" | "mailbox-null" | "released" | "acquire-fail";
156
157
 
157
158
  type Expect =
158
159
  | { decision: "execute"; transport: EntwurfV2Transport; lock: LockClass }
159
- | { decision: "reject"; reason: string; lock: LockClass; diagnostic?: boolean };
160
+ // `diagnostic` names the KIND a reject must carry, not a boolean: two reject cells now
161
+ // carry machine-readable evidence, and the axis has to say which — a reject that carried
162
+ // the wrong diagnostic would pass a yes/no cell (#101 갭 C).
163
+ | { decision: "reject"; reason: string; lock: LockClass; diagnostic?: RejectDiagnostic["kind"] };
160
164
 
161
165
  /**
162
166
  * Which IO seams the decider is ALLOWED to touch — the "어느 축을 만지면 안 되는지"
@@ -211,7 +215,7 @@ const ROWS: Row[] = [
211
215
  targetKind: "pi, lock held by another",
212
216
  intent: "fire-and-forget",
213
217
  scenario: { resolution: { identity: identity("pi"), preProbeAddressConflict: false }, lock: "conflict" },
214
- expect: { decision: "reject", reason: "target-locked", lock: "acquire-fail", diagnostic: true },
218
+ expect: { decision: "reject", reason: "target-locked", lock: "acquire-fail", diagnostic: "target-locked" },
215
219
  },
216
220
  {
217
221
  name: "unsupported self-fetch active → meta-mailbox",
@@ -231,7 +235,8 @@ const ROWS: Row[] = [
231
235
  resolution: { identity: identity("claude-code"), preProbeAddressConflict: false },
232
236
  mailboxDeliverable: false,
233
237
  },
234
- expect: { decision: "reject", reason: "mailbox-undeliverable", lock: "none" },
238
+ // #101 갭 C: an undeliverable mailbox reject carries the receiver axis that failed.
239
+ expect: { decision: "reject", reason: "mailbox-undeliverable", lock: "none", diagnostic: "mailbox-undeliverable" },
235
240
  },
236
241
  {
237
242
  name: "in-domain live ff → control-socket",
@@ -338,8 +343,8 @@ async function runRow(row: Row): Promise<void> {
338
343
  }
339
344
  if (row.expect.diagnostic) {
340
345
  ok(
341
- `${row.name}: carries target-locked diagnostic`,
342
- d.kind === "reject" && d.diagnostic?.kind === "target-locked",
346
+ `${row.name}: carries the ${row.expect.diagnostic} diagnostic`,
347
+ d.kind === "reject" && d.diagnostic?.kind === row.expect.diagnostic,
343
348
  );
344
349
  } else {
345
350
  ok(`${row.name}: no diagnostic`, d.kind === "reject" && d.diagnostic === undefined);
@@ -13,6 +13,9 @@
13
13
  * E. Q3 + Q5 — a dead control send re-resolves (claude-code citizen) to the mailbox and
14
14
  * enqueues through the SAME sendViaMailbox instance (same enqueue spy) on the SAME dirs
15
15
  * the direct hand used — direct send and fallback send never drift.
16
+ * E4. #101 — the deliverability seam reads BOTH markers: a live receiver marker whose
17
+ * owner's sender marker names ANOTHER garden is undeliverable. The SAME closure serves
18
+ * the direct send and the dead-control fallback, so neither can be fooled alone.
16
19
  *
17
20
  * No real IO — every seam is a spy; the factory's COMPOSITION is what is under test.
18
21
  */
@@ -26,7 +29,7 @@ import {
26
29
  } from "../pi-extensions/lib/entwurf-v2-production.ts";
27
30
  import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
28
31
  import type { ControlSocketPlan, MetaMailboxPlan } from "../pi-extensions/lib/entwurf-v2-send.ts";
29
- import type { MetaIdentity, MetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
32
+ import type { MetaIdentity, MetaReceiverMarker, MetaSenderMarker } from "../pi-extensions/lib/meta-session.ts";
30
33
  import type { NativePushAdapter, NativePushProbeResult } from "../pi-extensions/lib/native-push/adapter.ts";
31
34
  import type { TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
32
35
 
@@ -38,6 +41,8 @@ function ok(label: string, cond: boolean): void {
38
41
  }
39
42
 
40
43
  const GID = "20260613T100000-aaaaaa";
44
+ /** The garden a switched-away owner serves instead (#101). */
45
+ const OTHER_GID = "20260613T100000-bbbbbb";
41
46
  const LOCK_DIR = "/fake/locks";
42
47
  const SESSIONS_DIR = "/fake/sessions";
43
48
  const MAILBOX_DIR = "/fake/mailbox";
@@ -74,6 +79,19 @@ function receiverMarker(gid: string, backend: string, nativeSessionId = "n"): Me
74
79
  };
75
80
  }
76
81
 
82
+ /** The receiver owner's sender marker — "which garden does this pid serve NOW?" (#101). */
83
+ function senderMarker(gardenId: string, backend: string, ownerPid: number): MetaSenderMarker {
84
+ return {
85
+ gardenId,
86
+ backend: backend as MetaSenderMarker["backend"],
87
+ nativeSessionId: "n",
88
+ cwd: "/cwd",
89
+ ownerPid,
90
+ ownerStartKey: "x",
91
+ updatedAt: "t",
92
+ };
93
+ }
94
+
77
95
  function lockClaim(gardenId = GID): LockClaim {
78
96
  return {
79
97
  gardenId,
@@ -133,6 +151,11 @@ function makeSpiedFactory(over: {
133
151
  /** SE-2 2d-3 — the target's receiver presence marker: "active" (matches identity,
134
152
  * default), "absent" (terminated/never-armed), or "mismatch" (drifted native id). */
135
153
  receiverMarker?: "active" | "absent" | "mismatch";
154
+ /** #101 — the receiver owner's SENDER marker, i.e. which garden that pid serves NOW:
155
+ * "same" (still this one, default), "other-garden" (the owner switched sessions in
156
+ * place — a live marker whose watch is retired), or "absent" (no sender marker at all).
157
+ * Injected rather than defaulted so this gate never stats the operator's real roots. */
158
+ senderMarker?: "same" | "other-garden" | "absent";
136
159
  }) {
137
160
  const spies: Spies = {
138
161
  acquire: [],
@@ -161,6 +184,10 @@ function makeSpiedFactory(over: {
161
184
  const nsid = over.receiverMarker === "mismatch" ? "DRIFT" : "n";
162
185
  return receiverMarker(gid, over.backend ?? "pi", nsid);
163
186
  },
187
+ readSenderMarker: (backend, ownerPid) => {
188
+ if (over.senderMarker === "absent") return null;
189
+ return senderMarker(over.senderMarker === "other-garden" ? OTHER_GID : GID, backend, ownerPid);
190
+ },
164
191
  inspectPath: async (socketPath) => {
165
192
  spies.inspectPath.push({ socketPath });
166
193
  if (over.inspectKind === "indeterminate") {
@@ -388,6 +415,50 @@ async function main(): Promise<void> {
388
415
  ok("E3: drifted marker → enqueue NEVER called (presence ≠ identity match)", spies.enqueue.length === 0);
389
416
  }
390
417
 
418
+ // ── E4: #101 — a LIVE receiver marker whose owner switched gardens. Every axis the
419
+ // pre-#101 seam looked at says "active": the marker exists, its owner is live, and it
420
+ // matches this identity exactly. Only the owner's sender marker — which garden that pid
421
+ // serves NOW — says the watch is retired. This is the cell where a real message was
422
+ // enqueued into a mailbox nobody was draining (oracle, 2026-09-04). ─────────────────
423
+ {
424
+ const { deps, spies } = makeSpiedFactory({
425
+ backend: "claude-code",
426
+ rpc: "dead-throw",
427
+ classifyDead: true,
428
+ receiverMarker: "active",
429
+ senderMarker: "other-garden",
430
+ });
431
+ const res = await deps.executor.sendControl(CONTROL_PLAN, lockClaim());
432
+ ok(
433
+ "E4: owner switched gardens → rejected (mailbox-undeliverable), never fallback-sent",
434
+ res.outcome === "rejected" && res.rejectReason === "mailbox-undeliverable",
435
+ );
436
+ ok("E4: nothing enqueued into the retired garden's mailbox", spies.enqueue.length === 0);
437
+ // …and the same wiring still DELIVERS when that owner is serving this garden, so the
438
+ // join is a measurement and not a blanket refusal.
439
+ const served = makeSpiedFactory({
440
+ backend: "claude-code",
441
+ rpc: "dead-throw",
442
+ classifyDead: true,
443
+ receiverMarker: "active",
444
+ senderMarker: "same",
445
+ });
446
+ const ok2 = await served.deps.executor.sendControl(CONTROL_PLAN, lockClaim());
447
+ ok("E4: the served garden still falls back to a real mailbox enqueue", ok2.outcome === "fallback-sent");
448
+ ok(
449
+ "E4: a MISSING sender marker is fail-closed, not optimistic",
450
+ (
451
+ await makeSpiedFactory({
452
+ backend: "claude-code",
453
+ rpc: "dead-throw",
454
+ classifyDead: true,
455
+ receiverMarker: "active",
456
+ senderMarker: "absent",
457
+ }).deps.executor.sendControl(CONTROL_PLAN, lockClaim())
458
+ ).outcome === "rejected",
459
+ );
460
+ }
461
+
391
462
  // ── F: #50 C4 — record-LESS control socket → pre-probe record-less-socket reject ──
392
463
  // resolveTarget finds no meta-record, does ONE record-side lstat (inspectPath), sees a
393
464
  // non-symlink socket → recordLessSocket. EVERY intent then rejects pre-probe as
@@ -1,6 +1,18 @@
1
1
  /**
2
2
  * check-gate-qualification — kill-proof qualification of the shipped gates.
3
3
  *
4
+ * TWO ENTRYPOINTS, ONE FILE. `--manifests-only` (shipped as `run.sh
5
+ * check-gate-manifests`) stops after the HEAD — the runner self-test plus the real
6
+ * manifests' validation and lane inventory — having executed ZERO mutants from
7
+ * `scripts/mutants/` and having made NO snapshot of this repo. The default
8
+ * entrypoint is unchanged: it runs the same head first and then the body. The head
9
+ * is separable because of what it has actually caught: across 549 CI runs the
10
+ * qualification step went red five times, and three of those died in the head in
11
+ * 4-5 seconds, before a single mutant gate was invoked (#99 B-3). The head costs
12
+ * ~8s, so it belongs in the deterministic floor every push already pays for; the
13
+ * body's contract — which mutants run, how they are classified, what it prints —
14
+ * is untouched by the split.
15
+ *
4
16
  * Two phases, in a fixed order:
5
17
  *
6
18
  * 1. RUNNER SELF-TEST. The runner is itself a SUT: a qualification harness that
@@ -58,6 +70,10 @@ import {
58
70
  const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
59
71
  const MUTANTS_DIR = path.join(REPO_DIR, "scripts", "mutants");
60
72
 
73
+ /** Head-only mode: validate, count, and stop — no mutant is executed, no repo snapshot is made. */
74
+ const MANIFESTS_ONLY = process.argv.includes("--manifests-only");
75
+ const SURFACE = MANIFESTS_ONLY ? "check-gate-manifests" : "gate-qualification";
76
+
61
77
  let passed = 0;
62
78
  function ok(label: string, cond: boolean): void {
63
79
  assert.ok(cond, label);
@@ -775,10 +791,16 @@ const quiet = (): void => {};
775
791
  }
776
792
  }
777
793
 
778
- console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
794
+ console.log(`\n[${SURFACE}] self-test: ${passed} checks passed`);
779
795
 
780
- // ═══ Phase 2 — the real manifests against a snapshot of THIS repo ═══════════
796
+ // ═══ Phase 1d — the real manifests, VALIDATED but not yet run ═══════════════
797
+ //
798
+ // The HEAD ends here. Everything above and in this block is pure reading and
799
+ // checking: no mutant from `scripts/mutants/` is executed and no snapshot of THIS
800
+ // repo exists yet. `--manifests-only` returns from exactly this point.
781
801
 
802
+ let selected: MutantSpec[];
803
+ let manifestCount: number;
782
804
  {
783
805
  const manifests: MutantManifest[] = fs
784
806
  .readdirSync(MUTANTS_DIR)
@@ -813,21 +835,23 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
813
835
  "copilot-launch": 14,
814
836
  "copilot-receive": 18,
815
837
  "fresh-cut": 1,
838
+ "gate-qualification": 2,
816
839
  "meta-facts": 4,
840
+ "meta-hook-session-switch": 17,
817
841
  "meta-identity": 4,
818
842
  "meta-retire": 3,
819
843
  "mux-boundary": 14,
820
844
  "mux-fresh-call": 37,
821
845
  "mux-launcher-fence": 7,
822
846
  "mux-parent-artifact": 3,
823
- "pack-install": 1,
847
+ "pack-install": 2,
824
848
  "pi-package-ownership": 6,
825
849
  "mux-resume-call": 12,
826
850
  "omp-birth": 11,
827
851
  "omp-fresh": 24,
828
852
  "omp-receive": 11,
829
853
  "probe-ordering": 1,
830
- "release-gate": 12,
854
+ "release-gate": 14,
831
855
  "resume-args": 6,
832
856
  "resume-launch-identity": 6,
833
857
  "self-address": 5,
@@ -838,17 +862,44 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
838
862
  };
839
863
  const laneTally: Record<string, number> = {};
840
864
  for (const man of manifests) laneTally[man.lane] = (laneTally[man.lane] ?? 0) + man.mutants.length;
841
- assert.deepEqual(
842
- laneTally,
843
- EXPECTED_LANE_MUTANTS,
844
- "mutant lane inventory drifted from the declared contract — extend EXPECTED_LANE_MUTANTS and the manifests together, never silently",
845
- );
846
- const allMutants = validateManifestSet(manifests, makeOriginChecks(REPO_DIR));
847
- const selected = allMutants;
865
+ try {
866
+ assert.deepEqual(laneTally, EXPECTED_LANE_MUTANTS);
867
+ } catch {
868
+ assert.fail(
869
+ "[QK:LANE-INVENTORY-DECLARED] the mutant lane inventory drifted from the declared contract — extend " +
870
+ "EXPECTED_LANE_MUTANTS and the manifests together, never silently. A lane that lost its manifest in a " +
871
+ "merge, or gained mutants nobody declared, would otherwise pass as `n/n killed` over whatever set " +
872
+ `happened to be on disk. declared=${JSON.stringify(EXPECTED_LANE_MUTANTS)} onDisk=${JSON.stringify(laneTally)}`,
873
+ );
874
+ }
875
+ try {
876
+ selected = validateManifestSet(manifests, makeOriginChecks(REPO_DIR));
877
+ } catch (err) {
878
+ assert.fail(
879
+ "[QK:MANIFEST-SET-INTEGRITY-REFUSED] the committed manifest set broke its cross-manifest contract (global " +
880
+ "claim uniqueness, subject tracked + lstat-regular in the ORIGIN index, signatureSource on the origin work " +
881
+ "surface, claim token present exactly once in its gate source). Every one of those is what makes a KILLED " +
882
+ "verdict mean something, so the set is refused rather than run. Refusal: " +
883
+ (err instanceof Error ? err.message : String(err)),
884
+ );
885
+ }
886
+ manifestCount = manifests.length;
887
+ console.log(`[${SURFACE}] ${selected.length} mutants across ${manifestCount} lanes (no tiers — full set every run)`);
888
+ }
889
+
890
+ if (MANIFESTS_ONLY) {
848
891
  console.log(
849
- `[gate-qualification] ${selected.length} mutants across ${manifests.length} lanes (no tiers full set every run)`,
892
+ `[check-gate-manifests] ok runner self-test green, ${selected.length} committed mutants across ` +
893
+ `${manifestCount} lanes validated against the origin index, and the lane inventory matches its declared ` +
894
+ "contract. ZERO mutants were executed and this repo was never snapshotted: the body " +
895
+ "(check-gate-qualification) owns that, unchanged.",
850
896
  );
897
+ process.exit(0);
898
+ }
899
+
900
+ // ═══ Phase 2 — the real manifests against a snapshot of THIS repo ═══════════
851
901
 
902
+ {
852
903
  const headBefore = originHead(REPO_DIR);
853
904
  const workSurfaceBefore = originWorkSurfaceSha(REPO_DIR);
854
905