@junghanacs/entwurf 0.18.1 → 0.19.0

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 (33) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +248 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +8 -6
  5. package/VERIFY.md +3 -3
  6. package/docs/external-mcp-host.md +2 -2
  7. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +18 -7
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +76 -9
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +54 -6
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resolve-tmux-session.js +114 -0
  11. package/mcp/entwurf-bridge/src/index.ts +22 -7
  12. package/package.json +1 -1
  13. package/pi-extensions/entwurf-control.ts +27 -3
  14. package/pi-extensions/lib/mux-fresh-call.ts +98 -11
  15. package/pi-extensions/lib/mux-placement.ts +65 -10
  16. package/pi-extensions/lib/resolve-tmux-session.ts +137 -0
  17. package/scripts/check-fresh-cut-gate.sh +54 -2
  18. package/scripts/check-gate-qualification.ts +5 -5
  19. package/scripts/check-mux-launch-tmux.ts +77 -2
  20. package/scripts/check-mux-launch.ts +17 -0
  21. package/scripts/check-mux-placement-tmux.ts +61 -1
  22. package/scripts/check-mux-placement.ts +33 -0
  23. package/scripts/check-release-gate-outcomes.ts +224 -9
  24. package/scripts/ci-qualify-decide.sh +159 -0
  25. package/scripts/fixtures/qualify-replay.json +149 -0
  26. package/scripts/mutants/fresh-cut.json +26 -0
  27. package/scripts/mutants/mux-boundary.json +24 -0
  28. package/scripts/mutants/mux-fresh-call.json +81 -0
  29. package/scripts/mutants/omp-birth.json +16 -3
  30. package/scripts/mutants/release-gate.json +45 -1
  31. package/scripts/omp-bridge-doctor.sh +11 -1
  32. package/scripts/smoke-mux-fresh-call-live.ts +175 -3
  33. package/scripts/smoke-omp-bridge-state.sh +5 -2
@@ -24,6 +24,9 @@
24
24
  * is still listed on an immediate re-read — which is why no post-launch presence check is
25
25
  * performed and why the precondition runs before the window exists
26
26
  * - a precondition refusal opens NO window at all
27
+ * - the #105 seat, through the real `freshCall`: an absent seat refuses and creates nothing,
28
+ * an existing one puts the window in THAT session with a receipt naming the resolved
29
+ * target, and the resulting handle closes through `closeWindow` from outside that session
27
30
  */
28
31
 
29
32
  import assert from "node:assert/strict";
@@ -97,7 +100,7 @@ function alive(pid: string): boolean {
97
100
  }
98
101
  }
99
102
 
100
- function main(): void {
103
+ async function main(): Promise<void> {
101
104
  if (spawnSync("tmux", ["-V"], { encoding: "utf8" }).status !== 0) {
102
105
  skipLive(LABEL, "tmux is not installed — install tmux to run the launch acceptance");
103
106
  }
@@ -298,6 +301,78 @@ function main(): void {
298
301
  originalPanes.every((p) => panes().includes(p)),
299
302
  );
300
303
  ok("restored: focus never moved", windows().find((w) => w.endsWith("|1")) === activeBefore);
304
+
305
+ // ── the fresh-call composition, against a real server (#105) ──────────────────
306
+ // The seat's deterministic gate can prove the argv, the leaf and the refusals, but the
307
+ // two things that only exist once tmux has answered — WHICH session the window landed
308
+ // in, and what the receipt says about it — had no oracle independent of the production
309
+ // source. This cell is that oracle: the same hermetic runtime above, a SECOND session
310
+ // on this same private server, and the real `freshCall`, read through its return value
311
+ // and the server's own inventory.
312
+ {
313
+ const { freshCall } = await import("../pi-extensions/lib/mux-fresh-call.ts");
314
+ const { closeWindow } = await import("../pi-extensions/lib/mux-placement.ts");
315
+ const call = (placementInput?: { tmuxSession: string }) =>
316
+ freshCall(
317
+ {
318
+ backend: "pi",
319
+ model: "fixture/model",
320
+ task: "fixture task",
321
+ placement: placementInput,
322
+ callerGardenId: "20260101T000000-fixture",
323
+ },
324
+ inherited,
325
+ );
326
+ const inventory = (): string => fxLines("list-windows", "-a", "-F", "#{session_id}|#{window_id}").join(" ");
327
+
328
+ // (i) an ABSENT seat: named refusal, and the server is byte-identical afterwards.
329
+ const beforeAbsent = inventory();
330
+ const absent = call({ tmuxSession: "nosuchseat" });
331
+ ok(
332
+ "seat: an absent seat refuses as tmux-session-missing and creates NOTHING — no window, no session",
333
+ !absent.ok && absent.reason === "tmux-session-missing" && inventory() === beforeAbsent,
334
+ );
335
+
336
+ // (ii) an EXISTING seat on the same server: the window lands THERE, the caller's own
337
+ // session is untouched, and the receipt names the resolved target rather than the caller.
338
+ assert.equal(fx("new-session", "-d", "-s", `${SESSION}-seat`).status, 0, "fixture seat session");
339
+ const seatId = fxLines("list-windows", "-t", `=${SESSION}-seat`, "-F", "#{session_id}")[0];
340
+ const callerWindowsBefore = fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").join(" ");
341
+ const seated = call({ tmuxSession: `${SESSION}-seat` });
342
+ assert.ok(seated.ok, `the seated fresh call must succeed: ${seated.ok ? "" : seated.reason}`);
343
+ const receipt = seated.receipt;
344
+ ok(
345
+ "seat: the receipt reports the RESOLVED target session and echoes the REQUESTED name — not the caller's session",
346
+ receipt.sessionId === seatId &&
347
+ receipt.sessionId !== placement.sessionId &&
348
+ receipt.tmuxSession === `${SESSION}-seat`,
349
+ );
350
+ ok(
351
+ "seat: tmux agrees — the window is in the seat, and the caller's own session is byte-identical",
352
+ fxLines("list-windows", "-t", seatId, "-F", "#{window_id}").includes(receipt.windowId) &&
353
+ fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").join(" ") === callerWindowsBefore,
354
+ );
355
+ ok(
356
+ "seat: an omitted seat still lands in the caller's own session and the receipt names none",
357
+ (() => {
358
+ const own = call();
359
+ if (!own.ok) return false;
360
+ const here = own.receipt.sessionId === placement.sessionId && own.receipt.tmuxSession === undefined;
361
+ process.kill(Number(own.receipt.panePid), "SIGKILL");
362
+ return here;
363
+ })(),
364
+ );
365
+
366
+ // (iii) the close side: that handle closes through the production verb, in a session
367
+ // that is NOT the caller's — the reason close binds to the server half.
368
+ ok(
369
+ "seat: the placed window closes through its own handle and tmux stops listing it",
370
+ closeWindow(receipt, inherited) === "closed" &&
371
+ !fxLines("list-windows", "-a", "-F", "#{window_id}").includes(receipt.windowId),
372
+ );
373
+ fx("kill-session", "-t", seatId);
374
+ ok("seat: the fixture is back to one session", sessionCount() === 1);
375
+ }
301
376
  } finally {
302
377
  fx("kill-server");
303
378
  if (fs.existsSync(SOCKET)) fs.rmSync(SOCKET, { force: true });
@@ -313,4 +388,4 @@ function main(): void {
313
388
  console.log(`\n${LABEL}: ${passed} checks passed`);
314
389
  }
315
390
 
316
- main();
391
+ await main();
@@ -19,6 +19,7 @@
19
19
  * MUX-LAUNCH-NO-SHELL-ARGV the fixed runtime is passed after `--`, never as a string
20
20
  * MUX-LAUNCH-NO-CARRIER argv is the append shape plus the runtime, nothing else
21
21
  * MUX-LAUNCH-CORE-IMPORT-FREE the launch module and entwurf delivery never import each other
22
+ * RESOLVE-TMUX-SESSION-IMPORT-FREE the session-lookup leaf imports nothing at all
22
23
  */
23
24
 
24
25
  import assert from "node:assert/strict";
@@ -270,6 +271,22 @@ function main(): void {
270
271
  "boundary: the placement leaf does not import the launch module — the leaf stays deletable on its own",
271
272
  !importsLaunch("pi-extensions/lib/mux-placement.ts"),
272
273
  );
274
+ // ── the session-lookup leaf imports NOTHING (docs §11, #105) ─────────────────
275
+ // It is the one leaf whose entire safety argument is that it cannot acquire an
276
+ // opinion: no tmux of its own (the runner is injected), no mux module, no entwurf
277
+ // core, and not even a node builtin. §11 states that; nothing held it, and the first
278
+ // `import { runTmux } from "./mux-placement.ts"` would quietly turn a decision leaf
279
+ // into a second place that can run tmux.
280
+ ok(
281
+ "[QK:RESOLVE-TMUX-SESSION-IMPORT-FREE] the session-lookup leaf imports nothing at all — not mux, not entwurf core, not even a node builtin — so the only power it has is the runner its caller injects",
282
+ importsOf(fs.readFileSync("pi-extensions/lib/resolve-tmux-session.ts", "utf8")).length === 0,
283
+ );
284
+ ok(
285
+ "boundary: the fresh-call composition is the ONLY shipped source that imports that leaf",
286
+ PRODUCTION_SOURCES.filter((m) =>
287
+ importsOf(fs.readFileSync(m, "utf8")).some((spec) => spec.includes("resolve-tmux-session")),
288
+ ).join(",") === FRESH_CALL_MODULE,
289
+ );
273
290
  // Prose is not behaviour: the module header names identity vocabulary precisely to say
274
291
  // it owns none of it, so this assertion reads CODE with the comments stripped. A check
275
292
  // that failed on its own documentation would push the boundary out of the docs.
@@ -27,6 +27,8 @@
27
27
  * - handles are stable @window/%pane
28
28
  * - the rc=0 trap is real, and inspectPlacement refuses it instead of guessing
29
29
  * - close reports `closed` for a live window and `already-gone` after a natural exit
30
+ * - a window placed in ANOTHER session of the same server closes through its own handle,
31
+ * while a handle from another SERVER is still refused (#105 close-side binding)
30
32
  */
31
33
 
32
34
  import assert from "node:assert/strict";
@@ -259,9 +261,10 @@ function main(): void {
259
261
  /context changed/,
260
262
  "appendWindow must refuse a placement from another session",
261
263
  );
264
+ // Close binds to the SERVER half (#105), so its refusal names that half by its own word.
262
265
  assert.throws(
263
266
  () => closeWindow({ ...w4, serverPid: "999999" }, inherited),
264
- /context changed/,
267
+ /server changed/,
265
268
  "closeWindow must refuse a handle from another server",
266
269
  );
267
270
  ok("binding: append/close refuse a foreign server or session before mutating", windows().length === 4);
@@ -305,6 +308,63 @@ function main(): void {
305
308
  originalPanes.every((p) => panes().includes(p)),
306
309
  );
307
310
  ok("restored: focus never moved", windows().find((w) => w.endsWith("|1")) === activeBefore);
311
+
312
+ // ── close binds to the SERVER, not the session (#105) ─────────────────────────
313
+ // Since #105 a launched window may live in a session that is not the caller's, so the
314
+ // close side had to stop requiring the session half. This is that decision judged
315
+ // against a real server: a SECOND session, a window opened into it from the caller's
316
+ // pane the way the fresh-call composition does, and a close through the same handle.
317
+ // Both foreign-server refusal and the two close outcomes above stay exactly as they
318
+ // were — the session half is what changed, and only for close.
319
+ assert.equal(fx("new-session", "-d", "-s", `${SESSION}-b`).status, 0, "fixture second session");
320
+ ok("cross-session: the fixture now holds two sessions", sessionCount() === 2);
321
+ const otherSessionId = fxLines("list-windows", "-t", `=${SESSION}-b`, "-F", "#{session_id}")[0];
322
+ ok("cross-session: the second session resolves to a native id by exact name", /^\$[0-9]+$/.test(otherSessionId));
323
+ const placedRow = fxLines(
324
+ "new-window",
325
+ "-d",
326
+ "-a",
327
+ "-t",
328
+ `${otherSessionId}:{end}`,
329
+ "-P",
330
+ "-F",
331
+ "#{window_id}|#{window_index}|#{pane_id}|#{pane_pid}",
332
+ )[0].split("|");
333
+ const placed = {
334
+ serverPid: placement.serverPid,
335
+ sessionId: otherSessionId,
336
+ windowId: placedRow[0],
337
+ windowIndex: placedRow[1],
338
+ paneId: placedRow[2],
339
+ panePid: placedRow[3],
340
+ };
341
+ ok(
342
+ "cross-session: the window really landed in the OTHER session, and the caller's session is untouched",
343
+ fxLines("list-windows", "-t", otherSessionId, "-F", "#{window_id}").includes(placed.windowId) &&
344
+ !fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").includes(placed.windowId),
345
+ );
346
+ ok(
347
+ "cross-session: the caller's own session still shows windows 1,2 and its focus is unmoved",
348
+ fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").length === 2 &&
349
+ windows().find((w) => w.endsWith("|1")) === activeBefore,
350
+ );
351
+ // The predicate change, stated as the two facts that had to move together.
352
+ assert.throws(
353
+ () => closeWindow({ ...placed, serverPid: "999999" }, inherited),
354
+ /server changed/,
355
+ "closeWindow must still refuse a handle from another server",
356
+ );
357
+ ok(
358
+ "cross-session: a window in another session of the SAME server closes through its own handle — the release lifecycle smoke's close path reaches a placed window",
359
+ closeWindow(placed, inherited) === "closed",
360
+ );
361
+ ok(
362
+ "cross-session: it is gone from the other session, and nothing else moved",
363
+ !fxLines("list-windows", "-a", "-F", "#{window_id}").includes(placed.windowId) &&
364
+ fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").length === 2,
365
+ );
366
+ fx("kill-session", "-t", otherSessionId);
367
+ ok("cross-session: the fixture is back to one session", sessionCount() === 1);
308
368
  } finally {
309
369
  fx("kill-server");
310
370
  if (fs.existsSync(SOCKET)) fs.rmSync(SOCKET, { force: true });
@@ -22,6 +22,7 @@
22
22
  * MUX-TMUX-FAILURE-LOUD a nonzero/signalled tmux run is raised, never read as a fact
23
23
  * MUX-APPEND-END-DETACHED append is `-d -a -t <session_id>:{end}`, no carrier
24
24
  * MUX-CONTEXT-BOUND-MUTATION a mutation matches the server pid AND the session id
25
+ * MUX-CLOSE-SERVER-BOUND a close binds to the server half only — same server, any session
25
26
  * MUX-CLOSE-BY-WINDOW-ID close targets `@window`, and absence needs positive proof
26
27
  */
27
28
 
@@ -38,6 +39,7 @@ import {
38
39
  isDecimal,
39
40
  isPaneId,
40
41
  isSameContext,
42
+ isSameServer,
41
43
  isSessionId,
42
44
  isWindowId,
43
45
  parsePlacement,
@@ -254,6 +256,37 @@ function main(): void {
254
256
  );
255
257
  })(),
256
258
  );
259
+ // ── a close binds to the server, and the session half is covered by id uniqueness ────
260
+ // `appendWindow` and `closeWindow` are answering different questions, so they bind to
261
+ // different halves of the same fact. Append asks "is this target still the caller's own
262
+ // seat?" and needs both. Close asks "is this handle still the window it was born as?" —
263
+ // and since #105 a launched window may legitimately live in a session that is not the
264
+ // caller's, so requiring the session half would refuse a legitimate close. `[측정
265
+ // 2026-09-07]` window ids are handed out monotonically and never recycled within one
266
+ // server's life (after `@2` was killed the next was `@3`; after a whole session holding
267
+ // `@4`/`@5` was killed the next was `@6`), so on a matching server the `@id` alone
268
+ // identifies the window. What must still be refused is a handle from a DIFFERENT or
269
+ // restarted server, where the same id names something else.
270
+ ok(
271
+ "[QK:MUX-CLOSE-SERVER-BOUND] a close-side context matches on the server pid ALONE — another session on the same server is accepted, and every foreign server is refused however familiar its session id looks",
272
+ (() => {
273
+ const origin = { serverPid: "8150", sessionId: "$11" };
274
+ return (
275
+ isSameServer(origin, { serverPid: "8150", sessionId: "$11" }) &&
276
+ isSameServer(origin, { serverPid: "8150", sessionId: "$12" }) &&
277
+ !isSameServer(origin, { serverPid: "9999", sessionId: "$11" }) &&
278
+ !isSameServer(origin, { serverPid: "9999", sessionId: "$12" })
279
+ );
280
+ })(),
281
+ );
282
+ ok(
283
+ "binding: the two predicates are different contracts — append's refuses a foreign session that close's accepts, so neither may be expressed in terms of the other",
284
+ (() => {
285
+ const origin = { serverPid: "8150", sessionId: "$11" };
286
+ const otherSession = { serverPid: "8150", sessionId: "$12" };
287
+ return !isSameContext(origin, otherSession) && isSameServer(origin, otherSession);
288
+ })(),
289
+ );
257
290
  ok(
258
291
  "binding: a window handle carries the context it was born in",
259
292
  (() => {
@@ -44,7 +44,8 @@
44
44
 
45
45
  import { strict as assert } from "node:assert";
46
46
  import { execFileSync } from "node:child_process";
47
- import { globSync, readFileSync } from "node:fs";
47
+ import { copyFileSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
48
+ import { tmpdir } from "node:os";
48
49
  import { basename, join } from "node:path";
49
50
  import { fileURLToPath } from "node:url";
50
51
  import { LIVE_SKIP_EXIT, LIVE_SKIP_MARKER } from "./lib/live-skip.ts";
@@ -337,17 +338,20 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
337
338
  // check-gate-qualification left the default check chains (operator
338
339
  // inner-loop cost, 2026-08 subtraction). That move is a gate/release
339
340
  // contract: the step must stay REACHABLE on the axes that now own it — the
340
- // CI check job on every branch push and release_gate as its own MUST step — and
341
+ // CI check job (on the pushes #103's filter still sends it) and release_gate
342
+ // as its own MUST step — and
341
343
  // must not silently return to the default chain. Without this cell,
342
344
  // deleting the release_gate qualification block or the CI line leaves every
343
345
  // focused gate green while a cut quietly loses its discriminating-power
344
346
  // step.
345
347
  //
346
- // TWO claims, because #70 added a second independent contract here. 8a is
347
- // REACHABILITY (absent from the default chain; present exactly once in CI and
348
- // exactly once as a wired release_gate MUST step; named in VERIFY). 8b is what
349
- // the CI step qualifies (the FULL floor, before the qualification run). Each
350
- // carries its own replant one mutant must never stand in for both.
348
+ // THREE claims, because each is an independent contract. 8a is REACHABILITY
349
+ // (absent from the default chain; present exactly once in CI and exactly once
350
+ // as a wired release_gate MUST step; named in VERIFY). 8b is what the CI step
351
+ // qualifies (the FULL floor, before the qualification run), added by #70. 8c is
352
+ // what the RELEASE path ACCEPTS as evidence for one SHA that the body step
353
+ // actually ran there (#103). Each carries its own replant — one mutant must
354
+ // never stand in for another.
351
355
  // ===========================================================================
352
356
  {
353
357
  // The default chain is tiered (#70): `check` (core) and `check:full` compose the
@@ -382,7 +386,11 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
382
386
  !qualGateBody.includes('results+=("FAIL check-gate-qualification")')
383
387
  )
384
388
  holes.push("the release_gate qualification step does not wire PASS/FAIL into the MUST counters");
385
- if (!verifyDoc.includes("in the CI `check` job on every branch push, and as a release-gate MUST step"))
389
+ if (
390
+ !verifyDoc.includes(
391
+ "in the CI `check` job on a branch push that touched the qualification surface, and as a release-gate MUST step",
392
+ )
393
+ )
386
394
  holes.push("VERIFY.md no longer names the owners of the moved qualification step");
387
395
  assert.ok(
388
396
  holes.length === 0,
@@ -412,6 +420,210 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
412
420
  "downgrade/omission axis; ordering is directly asserted by this same oracle. " +
413
421
  `Broken: check:full at index ${ciFloorAt}, qualification at index ${ciQualAt}.`,
414
422
  );
423
+
424
+ // 8c. The RELEASE oracle requires that body to have actually run at the release
425
+ // SHA -- its own contract, not a branch of 8a. 8a is REACHABILITY (the step
426
+ // exists in the CI job and in release_gate); this is about what the release
427
+ // path ACCEPTS as evidence for one SHA. `verify-exact-ci.sh` read only three
428
+ // job names, and a job conclusion says nothing about whether the step inside
429
+ // it ran: the moment ci.yml can skip that step (#103 piece 2), a green oracle
430
+ // would certify a SHA whose qualification body never executed. That is the
431
+ // "green with no evidence" class this repo fails closed on, which is why the
432
+ // oracle gets the fourth axis BEFORE the skip exists.
433
+ //
434
+ // This is a TEXT oracle, the same grade as check-install-surface S7g. A
435
+ // behavioural oracle would need either a second file (S7a/S7g bind the
436
+ // release surface to one script read from the index) or an injectable
437
+ // RUN_JSON seam -- and that seam would be a way to launder release evidence
438
+ // past gh. Both cost more than the axis is worth here.
439
+ const ciOracle = readFileSync(join(REPO_DIR, ".claude/skills/entwurf-release/scripts/verify-exact-ci.sh"), "utf8");
440
+ const axis: string[] = [];
441
+ if (!ciOracle.includes('QUAL_JOB = "check"') || !ciOracle.includes('"Run ./run.sh check-gate-qualification"'))
442
+ axis.push("the oracle does not name the check job's check-gate-qualification step");
443
+ if (!ciOracle.includes('qual != "success"'))
444
+ axis.push("the oracle does not guard that step's conclusion against 'success'");
445
+ if (!ciOracle.includes('qual == "skipped"'))
446
+ axis.push("the oracle does not classify a SKIPPED body as a failure of its own");
447
+ assert.ok(
448
+ axis.length === 0,
449
+ "[QK:RELEASE-SHA-QUALIFIED-IN-CI] the exact-SHA CI oracle must require the qualification BODY to have run " +
450
+ "at the release SHA -- the `check` job's `Run ./run.sh check-gate-qualification` step concluding 'success', " +
451
+ "with skipped named as its own failure. Three green job names do not prove the step inside one of them " +
452
+ `executed. Broken: ${axis.join("; ")}`,
453
+ );
454
+ }
455
+
456
+ // ===========================================================================
457
+ // 8d. The qualification filter COVERS every mutant subject (#103 piece 2)
458
+ //
459
+ // The body no longer runs on every branch push; a decision script reads the
460
+ // push range and answers. Its whole safety argument is that the path set is
461
+ // DERIVED from the committed manifests rather than copied into a list, so a
462
+ // new mutant subject cannot land outside the filter and quietly stop being
463
+ // re-proven in CI. This asserts that property behaviourally: every subject
464
+ // and signatureSource in scripts/mutants/*.json, fed to the script as a
465
+ // one-file change, must decide `run_body=true`. The oracle is the manifests
466
+ // themselves, read here independently of the script.
467
+ // ===========================================================================
468
+ {
469
+ const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
470
+ const paths = new Set<string>();
471
+ for (const file of globSync("scripts/mutants/*.json", { cwd: REPO_DIR })) {
472
+ const manifest = JSON.parse(readFileSync(join(REPO_DIR, file), "utf8")) as {
473
+ mutants?: { subject?: string; signatureSource?: string }[];
474
+ };
475
+ for (const mutant of manifest.mutants ?? []) {
476
+ if (mutant.subject) paths.add(mutant.subject);
477
+ if (mutant.signatureSource) paths.add(mutant.signatureSource);
478
+ }
479
+ }
480
+ assert.ok(paths.size > 50, `read only ${paths.size} mutant paths from the manifests`);
481
+ const uncovered = [...paths].filter((path) => {
482
+ const out = execFileSync("bash", [decider, "--files-from", "-"], {
483
+ cwd: REPO_DIR,
484
+ encoding: "utf8",
485
+ input: `${path}\n`,
486
+ stdio: ["pipe", "pipe", "ignore"],
487
+ });
488
+ return out.trim() !== "run_body=true";
489
+ });
490
+ assert.ok(
491
+ uncovered.length === 0,
492
+ "[QK:QUALIFY-FILTER-COVERS-SUBJECTS] the CI qualification filter must run the body for a change to ANY " +
493
+ "committed mutant subject or signature source — it derives that set from scripts/mutants/*.json for " +
494
+ "exactly this reason, so a path it cannot see is a claim that silently stops being re-proven in CI. " +
495
+ `Uncovered (${uncovered.length} of ${paths.size}): ${uncovered.slice(0, 8).join(", ")}`,
496
+ );
497
+ }
498
+
499
+ // ===========================================================================
500
+ // 8e. The filter still runs the body for every RED the body has ever produced
501
+ //
502
+ // Five reds in 549 runs (#99 stage-2). The first reading of them used the
503
+ // tip COMMIT and concluded the filter would have missed four; replaying the
504
+ // real two-dot push range — what GitHub actually compares — showed all five
505
+ // hit. That reversal is the whole evidentiary basis for this filter.
506
+ //
507
+ // It is asserted from RECORDED file lists, not from live history, and that
508
+ // is a constraint rather than a convenience: check-gate-qualification runs
509
+ // every gate inside a snapshot with its own fresh git baseline, where these
510
+ // 2026-07/08 commits do not exist. A cell that read history there would be
511
+ // CONTROL-RED for the whole lane — measured, not predicted (this cell did
512
+ // exactly that on 2026-09-06 and cost the release-gate lane its 17 kills).
513
+ // The fixture carries what history said, measured once, in a repo where the
514
+ // objects are present.
515
+ //
516
+ // It has its own replant, and the reason is the SHAPE of the input: 8d feeds
517
+ // ONE path per call, this feeds a whole push at once. A matcher that only
518
+ // looked at the first entry would leave 8d green — every single-file call is
519
+ // its own first entry — while every one of these five pushes opens with a
520
+ // non-surface path (DELIVERY.md, AGENTS.md, NEXT.md, AGENTS.md,
521
+ // demo/demo-baseline.sh). Invisible to 8d, fatal here.
522
+ //
523
+ // The two-dot READING itself is proven separately, below.
524
+ // ===========================================================================
525
+ {
526
+ const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
527
+ const fixture = JSON.parse(readFileSync(join(REPO_DIR, "scripts/fixtures/qualify-replay.json"), "utf8")) as {
528
+ runs: { runId: string; before: string; head: string; files: string[]; tipOnlyFiles: string[] }[];
529
+ };
530
+ assert.equal(fixture.runs.length, 5, "the replay fixture holds all five historical qualification reds");
531
+ const wrong: string[] = [];
532
+ for (const run of fixture.runs) {
533
+ assert.ok(run.files.length > 0, `run ${run.runId} has no recorded push range`);
534
+ const out = execFileSync("bash", [decider, "--files-from", "-"], {
535
+ cwd: REPO_DIR,
536
+ encoding: "utf8",
537
+ input: `${run.files.join("\n")}\n`,
538
+ stdio: ["pipe", "pipe", "ignore"],
539
+ }).trim();
540
+ if (out !== "run_body=true") wrong.push(`run ${run.runId} (${run.files.length} files) decided ${out}`);
541
+ }
542
+ assert.ok(
543
+ wrong.length === 0,
544
+ "[QK:QUALIFY-FILTER-REPLAYS-PAST-CATCHES] every qualification RED in this repo's CI history must still " +
545
+ "run the body under the filter, over the two-dot push range GitHub compares — the measurement that " +
546
+ "overturned the tip-commit reading and justified filtering at all. This reads a whole push at once, " +
547
+ "which 8d cannot: each of these five opens with a non-surface path, so a matcher that stopped after " +
548
+ "the first entry would pass every single-file check 8d makes and fail here. " +
549
+ `Broken: ${wrong.join("; ")}`,
550
+ );
551
+ }
552
+
553
+ // ===========================================================================
554
+ // 8f. The filter reads the two-dot PUSH RANGE, not the tip commit
555
+ //
556
+ // The fixture above proves the matcher's verdict on recorded file lists; it
557
+ // cannot prove which git range produced them, because it calls no git. This
558
+ // does, hermetically: a throwaway repo shaped like the real defect — a push
559
+ // of two commits whose TIP is docs-only while the commit under it touched a
560
+ // mutant subject. The two-dot range sees the code; the tip alone does not.
561
+ // That is the exact misreading #99 corrected, and it is what four of the
562
+ // five recorded pushes look like when read the wrong way.
563
+ //
564
+ // Hermetic on purpose: no repo history, so it runs identically inside the
565
+ // qualification snapshot.
566
+ // ===========================================================================
567
+ {
568
+ const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
569
+ const tmp = mkdtempSync(join(tmpdir(), "entwurf-qualify-range-"));
570
+ // core.hooksPath is set globally on this operator's machine; a fixture repo
571
+ // must not run their hooks.
572
+ const git = (...args: string[]) =>
573
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.email=g@e", "-c", "user.name=g", ...args], {
574
+ cwd: tmp,
575
+ stdio: "ignore",
576
+ });
577
+ try {
578
+ git("init", "-q", "-b", "main");
579
+ // The decider reads the repo it SITS IN, so the fixture gets a real copy
580
+ // of it and its own one-entry manifest: no seam, no env override, and the
581
+ // manifest-reading path is exercised too.
582
+ mkdirSync(join(tmp, "scripts/mutants"), { recursive: true });
583
+ mkdirSync(join(tmp, "pi-extensions/lib"), { recursive: true });
584
+ copyFileSync(decider, join(tmp, "scripts/ci-qualify-decide.sh"));
585
+ writeFileSync(
586
+ join(tmp, "scripts/mutants/fixture.json"),
587
+ `${JSON.stringify(
588
+ {
589
+ schemaVersion: 1,
590
+ lane: "fixture",
591
+ mutants: [{ claim: "FIXTURE", subject: "pi-extensions/lib/fixture-subject.ts" }],
592
+ },
593
+ null,
594
+ "\t",
595
+ )}\n`,
596
+ );
597
+ writeFileSync(join(tmp, "seed.txt"), "seed\n");
598
+ git("add", "-A");
599
+ git("commit", "-qm", "seed");
600
+ const base = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
601
+ writeFileSync(join(tmp, "pi-extensions/lib/fixture-subject.ts"), "// a mutant subject\n");
602
+ git("add", "-A");
603
+ git("commit", "-qm", "code commit (mid-push)");
604
+ writeFileSync(join(tmp, "README.md"), "docs only\n");
605
+ git("add", "-A");
606
+ git("commit", "-qm", "docs commit (tip)");
607
+ const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
608
+ const decide = (from: string) =>
609
+ execFileSync("bash", [join(tmp, "scripts/ci-qualify-decide.sh"), from, head], {
610
+ cwd: tmp,
611
+ encoding: "utf8",
612
+ env: { ...process.env, CI_EVENT_NAME: "push", CI_FORCED: "false" },
613
+ stdio: ["ignore", "pipe", "ignore"],
614
+ }).trim();
615
+ const overRange = decide(base);
616
+ const overTip = decide(`${head}~1`);
617
+ assert.ok(
618
+ overRange === "run_body=true" && overTip === "run_body=false",
619
+ "[QK:QUALIFY-FILTER-READS-PUSH-RANGE] the filter must diff the whole two-dot push range, not the tip " +
620
+ "commit: a push whose tip is docs-only can still carry a mutant subject underneath it, which is how " +
621
+ "four of the five historical reds look like docs pushes when read tip-first. " +
622
+ `Broken: range=${overRange}, tip-only=${overTip} (tip-only must be false, or this fixture proves nothing).`,
623
+ );
624
+ } finally {
625
+ rmSync(tmp, { recursive: true, force: true });
626
+ }
415
627
  }
416
628
 
417
629
  // ===========================================================================
@@ -655,7 +867,10 @@ console.log(
655
867
  "a run.sh wrapper declining its own prerequisite (including the measured LIVE=1 no-cortex-connection cell); and every " +
656
868
  "LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry; and the moved " +
657
869
  "check-gate-qualification stays reachable on its owners (absent from the default chain, exactly once in CI, " +
658
- "exactly once as a release-gate MUST step) and the CI step qualifies the FULL floor, which runs before it; and " +
870
+ "exactly once as a release-gate MUST step) and the CI step qualifies the FULL floor, which runs before it, " +
871
+ "while the exact-SHA release oracle requires that BODY step to have concluded success at the release SHA, " +
872
+ "and the CI filter that decides when that body runs covers every mutant subject and still runs it for all " +
873
+ "five historical reds; and " +
659
874
  "every gate a committed mutant names is itself inside check:full or states its exclusion in prose an operator " +
660
875
  "reads; and the CI push trigger is filtered to branch refs, so a release tag creates no duplicate run; and " +
661
876
  "the operator's CONFIGURED bridge invocation is booted exactly once through run_step, before the ACP LIVE tier",