@junghanacs/entwurf 0.21.0 → 0.23.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 (122) hide show
  1. package/AGENTS.md +3 -2
  2. package/BASELINE.md +3 -1
  3. package/CHANGELOG.md +658 -0
  4. package/DELIVERY.md +167 -29
  5. package/README.md +104 -14
  6. package/VERIFY.md +73 -11
  7. package/docs/external-mcp-host.md +16 -6
  8. package/docs/setup-clean-host.md +73 -22
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +54 -31
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +289 -57
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +171 -263
  26. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +65 -31
  29. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  30. package/package.json +4 -4
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  33. package/pi-extensions/lib/codex-declaration.js +612 -0
  34. package/pi-extensions/lib/codex-fresh-preflight.ts +325 -52
  35. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  36. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  37. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  38. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  39. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  40. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  41. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  42. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  43. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  44. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  45. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  46. package/pi-extensions/lib/herdr-placement.ts +276 -0
  47. package/pi-extensions/lib/mux-fresh-call.ts +257 -304
  48. package/run.sh +522 -10
  49. package/scripts/check-codex-app-server-launch.ts +445 -0
  50. package/scripts/check-codex-birth-hook.ts +187 -0
  51. package/scripts/check-entwurf-control-rpc.ts +138 -0
  52. package/scripts/check-entwurf-fact-provider.ts +4 -2
  53. package/scripts/check-entwurf-facts.ts +12 -6
  54. package/scripts/check-entwurf-peers-surface.ts +3 -1
  55. package/scripts/check-entwurf-self-address.ts +68 -0
  56. package/scripts/check-entwurf-v2-production.ts +42 -1
  57. package/scripts/check-entwurf-v2-send.ts +26 -7
  58. package/scripts/check-fresh-call-dispatch.ts +301 -0
  59. package/scripts/check-gate-qualification.ts +19 -5
  60. package/scripts/check-herdr-activation.ts +1293 -0
  61. package/scripts/check-herdr-fresh-call.ts +1250 -0
  62. package/scripts/check-herdr-placement.ts +383 -0
  63. package/scripts/check-herdr-plugin-build.ts +842 -0
  64. package/scripts/check-herdr-plugin-profile.ts +371 -0
  65. package/scripts/check-herdr-plugin.ts +523 -0
  66. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  67. package/scripts/check-herdr-sandbox.ts +456 -0
  68. package/scripts/check-herdr-supply.ts +184 -0
  69. package/scripts/check-mux-launch-tmux.ts +331 -35
  70. package/scripts/check-mux-launcher-fence.ts +138 -0
  71. package/scripts/check-peer-facts.ts +331 -0
  72. package/scripts/check-release-gate-outcomes.ts +12 -0
  73. package/scripts/check-typing-call-fence.ts +296 -0
  74. package/scripts/codex-app-server-launch.sh +275 -0
  75. package/scripts/codex-birth-doctor.sh +159 -16
  76. package/scripts/codex-birth-install.sh +178 -72
  77. package/scripts/codex-birth-uninstall.sh +162 -7
  78. package/scripts/codex-socket-path.ts +33 -0
  79. package/scripts/codex-terminal-title-config.py +500 -0
  80. package/scripts/codex_toml_io.py +121 -0
  81. package/scripts/fixtures/herdr-supply.json +29 -0
  82. package/scripts/herdr-activation.mjs +536 -0
  83. package/scripts/herdr-plugin-activate.mjs +270 -0
  84. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  85. package/scripts/herdr-runtime.mjs +1203 -0
  86. package/scripts/install-herdr-ci.sh +96 -0
  87. package/scripts/lib/codex-fresh-live-protocol.ts +13 -5
  88. package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
  89. package/scripts/lib/tree-digest.ts +97 -0
  90. package/scripts/meta-bridge-install.sh +19 -2
  91. package/scripts/meta-bridge-state.py +167 -11
  92. package/scripts/mutants/codex-app-server-launch.json +157 -0
  93. package/scripts/mutants/codex-caller-seat.json +334 -0
  94. package/scripts/mutants/codex-native.json +195 -29
  95. package/scripts/mutants/control-socket-disconnect.json +57 -0
  96. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  97. package/scripts/mutants/herdr-activation.json +305 -0
  98. package/scripts/mutants/herdr-fresh-call.json +448 -0
  99. package/scripts/mutants/herdr-placement.json +161 -0
  100. package/scripts/mutants/herdr-plugin-build.json +160 -0
  101. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  102. package/scripts/mutants/herdr-plugin.json +133 -0
  103. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  104. package/scripts/mutants/herdr-supply.json +133 -0
  105. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  106. package/scripts/mutants/mux-fresh-call.json +184 -25
  107. package/scripts/mutants/mux-launcher-fence.json +13 -0
  108. package/scripts/mutants/omp-fresh.json +4 -4
  109. package/scripts/mutants/peer-facts.json +98 -0
  110. package/scripts/mutants/self-address.json +33 -0
  111. package/scripts/mutants/typing-call-fence.json +17 -0
  112. package/scripts/mutants/v2-surface.json +22 -0
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-codex-birth.sh +281 -12
  117. package/scripts/smoke-codex-config-state.sh +192 -3
  118. package/scripts/smoke-codex-fresh-live.ts +277 -37
  119. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  120. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  121. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  122. package/scripts/smoke-setup-verdict.sh +13 -11
@@ -27,7 +27,16 @@
27
27
  * - the #105 seat, through the real `freshCall`: an absent seat refuses and creates nothing,
28
28
  * an existing one puts the window in THAT session with a receipt naming the resolved
29
29
  * target, and the resulting handle closes through `closeWindow` from outside that session
30
- * - #95's omitted Codex seat resolves exact existing `codex`, while omitted Pi stays caller-local
30
+ * - an omitted seat stays caller-local for EVERY backend (#95 D1 retired the Codex home)
31
+ * - #95 lane B's caller-seat leaf against REAL pane titles: 0 / 1 / 2 matching panes, and the
32
+ * activity-adjacent title a ` | `-segment rule would miss
33
+ * - #95 lane B's COMPOSITION: a Codex caller's omitted-placement fresh call lands in the
34
+ * session its own titled pane is in, an explicit seat still overrides it, and an
35
+ * unresolvable anchor refuses with the server byte-identical
36
+ * - #95 lane C: WHERE a sibling starts — a caller's record directory places the pane, an
37
+ * explicit request still wins, an omitted one inherits THIS process's directory rather
38
+ * than the target session's `session_path`, and codex carries that same directory to its
39
+ * thread as `-C`, read off the runtime's own recorded argv
31
40
  */
32
41
 
33
42
  import assert from "node:assert/strict";
@@ -35,7 +44,7 @@ import { spawnSync } from "node:child_process";
35
44
  import fs from "node:fs";
36
45
  import path from "node:path";
37
46
  import { buildLaunchArgs, LaunchPreconditionError, launchPi } from "../pi-extensions/lib/mux-launch.ts";
38
- import { inspectPlacement } from "../pi-extensions/lib/mux-placement.ts";
47
+ import { inspectPlacement, runTmux } from "../pi-extensions/lib/mux-placement.ts";
39
48
  import { skipLive } from "./lib/live-skip.ts";
40
49
 
41
50
  const LABEL = "check-mux-launch-tmux";
@@ -119,9 +128,14 @@ async function main(): Promise<void> {
119
128
  const runtimeDir = fs.mkdtempSync(path.join(process.env.XDG_RUNTIME_DIR ?? "/tmp", "entwurf-mux-rt-"));
120
129
  // A long-lived stand-in for the official runtime. `exec` so no shell survives: the pane's
121
130
  // process must be the runtime itself for the pane_pid claim to mean anything.
131
+ // Each stand-in also DUMPS its own argv beside itself before it sleeps. That file is the
132
+ // only oracle in this repo for "what actually reached the runtime", independent of the
133
+ // builder that composed it — #95 lane C needs it because codex's thread directory rides a
134
+ // VENDOR flag whose absence is silent (the thread simply opens in the app-server's repo).
122
135
  const runtime = path.join(runtimeDir, "pi");
123
- fs.writeFileSync(runtime, "#!/bin/sh\nexec sleep 900\n", { mode: 0o755 });
124
- fs.writeFileSync(path.join(runtimeDir, "codex"), "#!/bin/sh\nexec sleep 900\n", { mode: 0o755 });
136
+ const DUMP_ARGV = '#!/bin/sh\nprintf \'%s\\n\' "$@" > "$0.argv"\nexec sleep 900\n';
137
+ fs.writeFileSync(runtime, DUMP_ARGV, { mode: 0o755 });
138
+ fs.writeFileSync(path.join(runtimeDir, "codex"), DUMP_ARGV, { mode: 0o755 });
125
139
 
126
140
  try {
127
141
  assert.equal(fx("-f", "/dev/null", "new-session", "-d", "-s", SESSION).status, 0, "fixture new-session");
@@ -373,11 +387,16 @@ async function main(): Promise<void> {
373
387
  !fxLines("list-windows", "-a", "-F", "#{window_id}").includes(receipt.windowId),
374
388
  );
375
389
 
376
- // (iv) Codex's omitted seat is a product default, not fixture prose. Missing home
377
- // refuses before mutation; then a private session named exactly `codex` makes the real
378
- // composition resolve that name while the Pi omitted-seat control stays caller-local.
379
- const beforeMissingHome = inventory();
380
- const missingHome = freshCall(
390
+ // (iv) #95 D1 (GLG, 2026-09-16): an omitted seat is caller-local for EVERY backend,
391
+ // Codex included. Until then an omitted-placement Codex selected a fixed existing
392
+ // session named `codex` a room the operator had to keep because nothing could find
393
+ // the pane a Codex caller was sitting in. The caller-pane anchor below IS that
394
+ // mapping, so this cell now asserts the ABSENCE of the old default: with a session
395
+ // literally named `codex` present on this server, an omitted-placement Codex call
396
+ // must still land in the caller's own session and name no seat at all.
397
+ assert.equal(fx("new-session", "-d", "-s", "codex").status, 0, "fixture session named codex");
398
+ const decoyHomeId = fxLines("list-windows", "-t", "=codex", "-F", "#{session_id}")[0];
399
+ const omittedCodex = freshCall(
381
400
  {
382
401
  backend: "codex",
383
402
  model: "fixture-model",
@@ -386,37 +405,314 @@ async function main(): Promise<void> {
386
405
  },
387
406
  inherited,
388
407
  );
389
- ok(
390
- "codex home: omitted placement refuses a missing exact home without mutation",
391
- !missingHome.ok && missingHome.reason === "tmux-session-missing" && inventory() === beforeMissingHome,
392
- );
393
- assert.equal(fx("new-session", "-d", "-s", "codex").status, 0, "fixture Codex home session");
394
- const codexHomeId = fxLines("list-windows", "-t", "=codex", "-F", "#{session_id}")[0];
395
- const home = freshCall(
396
- {
397
- backend: "codex",
398
- model: "fixture-model",
399
- task: "fixture task",
400
- callerGardenId: "20260101T000000-fixture",
401
- },
402
- inherited,
403
- );
404
- assert.ok(home.ok, `the omitted-placement Codex call must succeed: ${home.ok ? "" : home.reason}`);
405
- ok(
406
- "codex home: omitted placement lands in exact existing `codex`, not the caller session",
407
- home.receipt.sessionId === codexHomeId &&
408
- home.receipt.sessionId !== placement.sessionId &&
409
- home.receipt.tmuxSession === "codex" &&
410
- home.receipt.tmuxSessionSource === "codex-home",
408
+ assert.ok(
409
+ omittedCodex.ok,
410
+ `the omitted-placement Codex call must succeed: ${omittedCodex.ok ? "" : omittedCodex.reason}`,
411
411
  );
412
412
  ok(
413
- "codex home: the home receipt closes by stable handle",
414
- closeWindow(home.receipt, inherited) === "closed" &&
415
- !fxLines("list-windows", "-a", "-F", "#{window_id}").includes(home.receipt.windowId),
413
+ "no fixed home: an omitted Codex seat is caller-local and names NO seat, even with a session called `codex` right there",
414
+ omittedCodex.receipt.sessionId === placement.sessionId &&
415
+ omittedCodex.receipt.sessionId !== decoyHomeId &&
416
+ omittedCodex.receipt.tmuxSession === undefined &&
417
+ omittedCodex.receipt.tmuxSessionSource === undefined &&
418
+ fxLines("list-windows", "-t", decoyHomeId, "-F", "#{window_id}").length === 1,
416
419
  );
420
+ process.kill(Number(omittedCodex.receipt.panePid), "SIGKILL");
417
421
  fx("kill-session", "-t", seatId);
418
- fx("kill-session", "-t", codexHomeId);
422
+ fx("kill-session", "-t", decoyHomeId);
419
423
  ok("seat: the fixture is back to one session", sessionCount() === 1);
424
+
425
+ // (v) #95 lane B: the caller-seat leaf, against REAL pane titles on this server.
426
+ // The deterministic gate can pin the anchor and the counting, but "what tmux
427
+ // actually reports as `#{pane_title}`, and which `$session` that pane is in" had
428
+ // no oracle independent of the leaf's own parse. This cell is that oracle. The
429
+ // titles are set with `select-pane -T` rather than by a Codex process: the claim
430
+ // under test is the LOOKUP, and a real TUI would add a model turn, a record and a
431
+ // vendor version to a gate that must stay hermetic.
432
+ {
433
+ const { resolveCodexCallerSeat } = await import("../pi-extensions/lib/codex-caller-seat.ts");
434
+ const thread = "01a0a7f9-ed9c-7aa2-a4dd-b1a39c0d4e11";
435
+ const anchor = `${thread.slice(0, 29)}...`;
436
+ const seatRun = (args: string[]) => runTmux(args, inherited);
437
+ const titleOf = (pane: string): string =>
438
+ fxLines("list-panes", "-a", "-F", "#{pane_id}|#{pane_title}").find((row) => row.startsWith(`${pane}|`)) ?? "";
439
+
440
+ // 0 matches: every pane on this server carries the default title, which is the
441
+ // same shape a server with `allow-set-title off` reports for a live Codex.
442
+ ok(
443
+ "caller seat: no pane naming the thread refuses as unresolved — no fallback session",
444
+ (() => {
445
+ const r = resolveCodexCallerSeat(thread, seatRun);
446
+ return !r.ok && r.reason === "codex-caller-seat-unresolved";
447
+ })(),
448
+ );
449
+
450
+ // 1 match, in a session that is NOT the caller's: the leaf must report THAT
451
+ // session, which is the whole point of the anchor.
452
+ assert.equal(fx("new-session", "-d", "-s", `${SESSION}-tui`).status, 0, "fixture TUI session");
453
+ const tuiSessionId = fxLines("list-windows", "-t", `=${SESSION}-tui`, "-F", "#{session_id}")[0];
454
+ const tuiPane = fxLines("list-panes", "-t", `=${SESSION}-tui`, "-F", "#{pane_id}")[0];
455
+ assert.equal(fx("select-pane", "-t", tuiPane, "-T", `entwurf | ${anchor}`).status, 0, "fixture TUI title");
456
+ ok(
457
+ "caller seat: tmux really reports the OSC-shaped title we are matching against",
458
+ titleOf(tuiPane) === `${tuiPane}|entwurf | ${anchor}`,
459
+ );
460
+ ok(
461
+ "caller seat: the one matching pane resolves to ITS session, not the caller's",
462
+ (() => {
463
+ const r = resolveCodexCallerSeat(thread, seatRun);
464
+ return (
465
+ r.ok &&
466
+ r.seat.paneId === tuiPane &&
467
+ r.seat.sessionId === tuiSessionId &&
468
+ r.seat.sessionId !== placement.sessionId &&
469
+ r.seat.source === "codex-title-anchor"
470
+ );
471
+ })(),
472
+ );
473
+
474
+ // The ACTIVITY-adjacent shape. `title_setup.rs:183-193` joins the activity item
475
+ // to its neighbour with a plain space, so an operator list ending in `activity`
476
+ // renders the thread id INSIDE one ` | ` segment. A segment-equality rule reads
477
+ // this pane as no match at all; the token rule resolves it.
478
+ assert.equal(fx("select-pane", "-t", tuiPane, "-T", `entwurf | Working ${anchor}`).status, 0, "activity title");
479
+ ok(
480
+ "caller seat: an activity-adjacent title still resolves — the id is a TOKEN, not a whole segment",
481
+ (() => {
482
+ const r = resolveCodexCallerSeat(thread, seatRun);
483
+ return r.ok && r.seat.paneId === tuiPane && r.seat.sessionId === tuiSessionId;
484
+ })(),
485
+ );
486
+
487
+ // 2 matches: a stale duplicate in another session. Picking either would seat a
488
+ // sibling by guess, so nothing is chosen.
489
+ assert.equal(fx("split-window", "-d", "-t", tuiPane).status, 0, "fixture duplicate pane");
490
+ const duplicate = fxLines("list-panes", "-t", `=${SESSION}-tui`, "-F", "#{pane_id}").find(
491
+ (id) => id !== tuiPane,
492
+ );
493
+ assert.ok(duplicate, "the fixture duplicate pane must exist");
494
+ assert.equal(fx("select-pane", "-t", duplicate, "-T", `tmp | ${anchor}`).status, 0, "duplicate title");
495
+ ok(
496
+ "caller seat: two panes naming one thread refuse as ambiguous — never the first match",
497
+ (() => {
498
+ const r = resolveCodexCallerSeat(thread, seatRun);
499
+ return !r.ok && r.reason === "codex-caller-seat-ambiguous";
500
+ })(),
501
+ );
502
+
503
+ // ── the COMPOSITION, through the real `freshCall` (#95 lane B C3) ─────────
504
+ // The leaf's decision is proven above; what has no oracle outside production is
505
+ // whether the omitted-placement path for a CODEX CALLER actually reaches that
506
+ // decision and turns it into the `-t` target. These cells run the real
507
+ // composition with the same hermetic runtime the rest of this gate uses, and
508
+ // read the answer from the server's own inventory rather than the receipt alone.
509
+ // Back to EXACTLY one matching pane: the duplicate above must stop naming the
510
+ // thread, or the composition below would (correctly) refuse as ambiguous.
511
+ assert.equal(fx("select-pane", "-t", duplicate, "-T", "plain-shell").status, 0, "clear duplicate title");
512
+ assert.equal(fx("select-pane", "-t", tuiPane, "-T", `entwurf | ${anchor}`).status, 0, "restore TUI title");
513
+ const anchoredCall = (over?: { tmuxSession: string }) =>
514
+ freshCall(
515
+ {
516
+ backend: "pi",
517
+ model: "fixture/model",
518
+ task: "fixture task",
519
+ placement: over,
520
+ callerGardenId: "20260101T000000-fixture",
521
+ callerNativeSessionId: thread,
522
+ },
523
+ inherited,
524
+ );
525
+
526
+ const anchored = anchoredCall();
527
+ assert.ok(anchored.ok, `the anchored fresh call must succeed: ${anchored.ok ? "" : anchored.reason}`);
528
+ ok(
529
+ "caller seat composition: the window lands in the session the CALLER's pane is in, not the caller session",
530
+ anchored.receipt.sessionId === tuiSessionId &&
531
+ anchored.receipt.sessionId !== placement.sessionId &&
532
+ fxLines("list-windows", "-t", tuiSessionId, "-F", "#{window_id}").includes(anchored.receipt.windowId),
533
+ );
534
+ ok(
535
+ "caller seat composition: the receipt names the SOURCE and no session name — the pane was observed, not requested",
536
+ anchored.receipt.tmuxSessionSource === "codex-title-anchor" && anchored.receipt.tmuxSession === undefined,
537
+ );
538
+ ok(
539
+ "caller seat composition: that window closes by its own handle",
540
+ closeWindow(anchored.receipt, inherited) === "closed" &&
541
+ !fxLines("list-windows", "-a", "-F", "#{window_id}").includes(anchored.receipt.windowId),
542
+ );
543
+
544
+ // Rule 1 over rule 2, against a real server: an explicit seat is never rewritten
545
+ // by the anchor, even when the anchor would have resolved.
546
+ assert.equal(fx("new-session", "-d", "-s", `${SESSION}-over`).status, 0, "fixture override session");
547
+ const overrideId = fxLines("list-windows", "-t", `=${SESSION}-over`, "-F", "#{session_id}")[0];
548
+ const overridden = anchoredCall({ tmuxSession: `${SESSION}-over` });
549
+ assert.ok(overridden.ok, `the overridden fresh call must succeed: ${overridden.ok ? "" : overridden.reason}`);
550
+ ok(
551
+ "caller seat composition: an explicit seat still wins over a resolvable anchor",
552
+ overridden.receipt.sessionId === overrideId &&
553
+ overridden.receipt.sessionId !== tuiSessionId &&
554
+ overridden.receipt.tmuxSession === `${SESSION}-over` &&
555
+ overridden.receipt.tmuxSessionSource === "requested",
556
+ );
557
+ closeWindow(overridden.receipt, inherited);
558
+ fx("kill-session", "-t", overrideId);
559
+
560
+ // An unresolvable anchor is a refusal with NOTHING created anywhere — the D2
561
+ // contract, read from the server rather than from the return value alone.
562
+ const beforeUnresolved = inventory();
563
+ const unresolved = freshCall(
564
+ {
565
+ backend: "pi",
566
+ model: "fixture/model",
567
+ task: "fixture task",
568
+ callerGardenId: "20260101T000000-fixture",
569
+ callerNativeSessionId: "01a0ffff-ffff-7fff-bfff-ffffffffffff",
570
+ },
571
+ inherited,
572
+ );
573
+ ok(
574
+ "caller seat composition: an unresolvable anchor refuses and creates NOTHING — no window, no session, no fallback",
575
+ !unresolved.ok && unresolved.reason === "codex-caller-seat-unresolved" && inventory() === beforeUnresolved,
576
+ );
577
+
578
+ fx("kill-session", "-t", tuiSessionId);
579
+ ok("caller seat: the fixture is back to one session", sessionCount() === 1);
580
+ }
581
+
582
+ // (vi) #95 lane C: WHERE the sibling starts, read off the pane and off the runtime's
583
+ // own argv. The deterministic gate can pin which value the composition selects; what
584
+ // only tmux and the runtime can answer is whether that value actually placed the
585
+ // pane, and whether the vendor token carrying it survived to the process. A codex
586
+ // thread's directory is the sharper half: with no `-C` the TUI attaches to the
587
+ // operator's app-server and the THREAD opens in the app-server's repo while the pane
588
+ // sits somewhere else entirely — a divergence no receipt in this repo would show.
589
+ {
590
+ const callerRepo = path.join(runtimeDir, "caller-repo");
591
+ const requestedRepo = path.join(runtimeDir, "requested-repo");
592
+ const sessionRepo = path.join(runtimeDir, "session-repo");
593
+ for (const dir of [callerRepo, requestedRepo, sessionRepo]) fs.mkdirSync(dir);
594
+ const paneCwd = (paneId: string): string =>
595
+ fxLines("display-message", "-p", "-t", paneId, "#{pane_current_path}")[0];
596
+ const argvAt = (backend: "pi" | "codex"): string => `${path.join(runtimeDir, backend)}.argv`;
597
+ /** `[측정]` a pane's `#{pane_current_path}` is read from the child's `/proc` at
598
+ * QUERY time, and tmux chdirs in that child AFTER forking it — so a read taken
599
+ * straight after the launch receipt can still answer the SERVER's directory and
600
+ * make a correct `-c` look ignored. The runtime's own argv dump is the settle
601
+ * signal: once it exists the fixture is running and has been chdir'ed. Bounded,
602
+ * never unbounded — a runtime that never starts is a failed assertion, not a hang.
603
+ * Each launch clears the previous file first, so a stale dump can never be read
604
+ * as this launch's. */
605
+ const clearArgv = (backend: "pi" | "codex"): void => fs.rmSync(argvAt(backend), { force: true });
606
+ const awaitRuntime = (backend: "pi" | "codex"): void => {
607
+ const end = Date.now() + 5000;
608
+ while (!fs.existsSync(argvAt(backend)) && Date.now() < end) spawnSync("sleep", ["0.05"]);
609
+ assert.ok(fs.existsSync(argvAt(backend)), `the ${backend} fixture started and recorded its argv`);
610
+ };
611
+ const runtimeArgv = (backend: "pi" | "codex"): string[] => {
612
+ awaitRuntime(backend);
613
+ const argv = fs.readFileSync(argvAt(backend), "utf8").split("\n").slice(0, -1);
614
+ clearArgv(backend);
615
+ return argv;
616
+ };
617
+ const freshWith = (over: { backend?: "pi" | "codex"; cwd?: string; callerCwd?: string }) =>
618
+ freshCall(
619
+ {
620
+ backend: over.backend ?? "pi",
621
+ model: "fixture/model",
622
+ task: "fixture task",
623
+ cwd: over.cwd,
624
+ callerCwd: over.callerCwd,
625
+ callerGardenId: "20260101T000000-fixture",
626
+ },
627
+ inherited,
628
+ );
629
+
630
+ clearArgv("pi");
631
+ const fromRecord = freshWith({ callerCwd: callerRepo });
632
+ assert.ok(fromRecord.ok, `the caller-cwd fresh call must succeed: ${fromRecord.ok ? "" : fromRecord.reason}`);
633
+ awaitRuntime("pi");
634
+ ok(
635
+ "caller cwd: a caller that HAS a record directory opens the pane THERE, not in this process's directory",
636
+ paneCwd(fromRecord.receipt.paneId) === callerRepo &&
637
+ callerRepo !== process.cwd() &&
638
+ fromRecord.receipt.cwd === callerRepo &&
639
+ fromRecord.receipt.cwdSource === "codex-caller-record",
640
+ );
641
+ ok(
642
+ "caller cwd: the runtime argv carries no tmux carrier — the directory reached the PANE through `-c`",
643
+ !runtimeArgv("pi").includes("-c"),
644
+ );
645
+ closeWindow(fromRecord.receipt, inherited);
646
+
647
+ clearArgv("pi");
648
+ const requested = freshWith({ cwd: requestedRepo, callerCwd: callerRepo });
649
+ assert.ok(requested.ok, `the requested-cwd fresh call must succeed: ${requested.ok ? "" : requested.reason}`);
650
+ awaitRuntime("pi");
651
+ ok(
652
+ "caller cwd: an explicit request still wins over the caller's record directory, and the receipt says which rule chose it",
653
+ paneCwd(requested.receipt.paneId) === requestedRepo &&
654
+ requested.receipt.cwd === requestedRepo &&
655
+ requested.receipt.cwdSource === "requested",
656
+ );
657
+ closeWindow(requested.receipt, inherited);
658
+ runtimeArgv("pi");
659
+
660
+ // The inherited case, and the measurement `-C`'s default rests on: with no `-c`
661
+ // the pane takes the directory of the process that ran `new-window` — NOT the
662
+ // target session's `session_path`, which is why a seat in another directory is
663
+ // what makes this readable at all.
664
+ assert.equal(
665
+ fx("new-session", "-d", "-s", `${SESSION}-cwd`, "-c", sessionRepo).status,
666
+ 0,
667
+ "fixture session rooted elsewhere",
668
+ );
669
+ const seatCwdId = fxLines("list-windows", "-t", `=${SESSION}-cwd`, "-F", "#{session_id}")[0];
670
+ clearArgv("pi");
671
+ const inherit = freshCall(
672
+ {
673
+ backend: "pi",
674
+ model: "fixture/model",
675
+ task: "fixture task",
676
+ placement: { tmuxSession: `${SESSION}-cwd` },
677
+ callerGardenId: "20260101T000000-fixture",
678
+ },
679
+ inherited,
680
+ );
681
+ assert.ok(inherit.ok, `the inherited-cwd fresh call must succeed: ${inherit.ok ? "" : inherit.reason}`);
682
+ awaitRuntime("pi");
683
+ ok(
684
+ "caller cwd: with NO directory named, the pane inherits this process's — not the target session's session_path — and the receipt invents nothing",
685
+ paneCwd(inherit.receipt.paneId) === process.cwd() &&
686
+ process.cwd() !== sessionRepo &&
687
+ inherit.receipt.sessionId === seatCwdId &&
688
+ inherit.receipt.cwd === undefined &&
689
+ inherit.receipt.cwdSource === undefined,
690
+ );
691
+ closeWindow(inherit.receipt, inherited);
692
+ runtimeArgv("pi");
693
+ fx("kill-session", "-t", seatCwdId);
694
+
695
+ // The codex half: the SAME value the pane got, carried to the vendor as `-C`,
696
+ // read off the process's own argv rather than off the builder that wrote it.
697
+ clearArgv("codex");
698
+ const codexThread = freshWith({ backend: "codex", callerCwd: callerRepo });
699
+ assert.ok(codexThread.ok, `the codex fresh call must succeed: ${codexThread.ok ? "" : codexThread.reason}`);
700
+ const codexArgv = runtimeArgv("codex");
701
+ const dashC = codexArgv.indexOf("-C");
702
+ ok(
703
+ "codex thread cwd: the vendor argv carries `-C <dir>` with the SAME directory the pane landed in — one value, two carriers",
704
+ dashC >= 0 &&
705
+ codexArgv[dashC + 1] === callerRepo &&
706
+ codexArgv.filter((a) => a === "-C").length === 1 &&
707
+ paneCwd(codexThread.receipt.paneId) === callerRepo,
708
+ );
709
+ ok(
710
+ "codex thread cwd: the flag rides the SAME argv as the remote attachment — an attached thread with no override takes the app-server's directory",
711
+ codexArgv.includes("--remote") && dashC > codexArgv.indexOf("--remote"),
712
+ );
713
+ closeWindow(codexThread.receipt, inherited);
714
+ ok("caller cwd: the fixture is back to one session", sessionCount() === 1);
715
+ }
420
716
  }
421
717
  } finally {
422
718
  fx("kill-server");
@@ -26,6 +26,20 @@
26
26
  * LAUNCHFENCE-LIFECYCLE-CELL-BRANCH lifecycle keeps its two truthful cell branches: ACP pi
27
27
  * retains the four MEASURED canonical REAL_XDG_* roots,
28
28
  * the direct Claude cell gets exact-parity restore
29
+ * LAUNCHFENCE-EXPOSED-SMOKE-WIRED the population, not a list of names: EVERY LIVE smoke
30
+ * that hands a child a fixture XDG_DATA_HOME either
31
+ * consumes this fence or carries a stated exemption that
32
+ * is itself true of its source
33
+ *
34
+ * WHY THE LAST CLAIM EXISTS `[측정 oracle 2026-09-18]`. The two WIRED claims above name two files,
35
+ * and a rail born after them walked straight through the gap: `smoke-herdr-fresh-call-live` fenced
36
+ * XDG while keeping the operator's real HOME, and its Claude children reinstalled themselves into
37
+ * the fixture and repointed `~/.local/bin/claude` at `<fixture>/claude/versions/2.1.267`. Seven
38
+ * preserved fixture roots each held that install; the operator's launcher pointed into the newest
39
+ * of them until it was relinked by hand. The vendor's own two halves are why — the version store
40
+ * follows `XDG_DATA_HOME` and the launcher follows `HOME` (2.1.267: `EZe = join(Wge(), "claude",
41
+ * "versions")` against `TN = join(home, ".local", "bin")`) — so a fixture data root beside a real
42
+ * HOME is the incident's precondition, and THAT is what this claim enumerates.
29
43
  */
30
44
 
31
45
  import assert from "node:assert/strict";
@@ -248,6 +262,130 @@ function main(): void {
248
262
  "[QK:LAUNCHFENCE-WIRED-FRESH-CALL] wiring: smoke-mux-fresh-call-live consumes the SAME shared fence rather than a private copy — one protection, two smokes, per the issue's shared-repair requirement",
249
263
  wired(read("scripts/smoke-mux-fresh-call-live.ts")),
250
264
  );
265
+ // ── the population: who ELSE hands a child a fixture data root ───────────
266
+ {
267
+ // A smoke that assigns its own fixture XDG_DATA_HOME beside the operator's real HOME is
268
+ // standing in the incident's precondition. Three things are read STRUCTURALLY rather than
269
+ // by name `[sol 재검 2026-09-18]`, because the first version of this claim asked only
270
+ // whether four strings appeared anywhere in the file — which a comment, or dead code,
271
+ // satisfies as well as a wired smoke:
272
+ //
273
+ // 1. the preflight is BOUND (`const x = snapshotClaudeLauncher(`) and comes BEFORE the
274
+ // smoke's first child. That boundary is the whole point `[sol 재검 2026-09-18]`: an
275
+ // ordering of snapshot < verify < cleanup alone is satisfied by moving the entire
276
+ // block AFTER the children, which pins the damage as the baseline and makes the
277
+ // oracle green on a launcher that has already moved. Each smoke's first child is
278
+ // named here by its own marker, and a smoke in the population with no marker is
279
+ // RED rather than waved through — a new rail must say where its children begin.
280
+ // 2. the integrity oracle and the cleanup verdict both run on THAT binding, in that
281
+ // order — one snapshot, not three unrelated calls;
282
+ // 3. a smoke that REMOVES its fixture does both before the removal. The herdr smoke
283
+ // preserves its fixture as evidence and therefore has no removal to precede; that
284
+ // is a real difference between the two shapes, so the rule is written as a
285
+ // condition on removal rather than as a `finally` that only one of them has.
286
+ //
287
+ // An exemption is read the same way: not "the file contains HOME somewhere", but "the
288
+ // env object that assigns the fixture XDG_DATA_HOME also relocates HOME", which is the
289
+ // relation that actually keeps the vendor's store and its launcher in one tree.
290
+ const dataRootAssignment = (file: string): RegExp => (file.endsWith(".sh") ? /XDG_DATA_HOME=/ : /XDG_DATA_HOME:/);
291
+ /** The object literal / env block that carries the fixture data root, not the whole file. */
292
+ const envBlockAround = (src: string, file: string): string => {
293
+ const at = src.search(dataRootAssignment(file));
294
+ if (at < 0) return "";
295
+ const from = src.lastIndexOf("{", at);
296
+ if (from < 0) return src.slice(Math.max(0, at - 800), at + 800);
297
+ let depth = 0;
298
+ for (let i = from; i < src.length; i += 1) {
299
+ if (src[i] === "{") depth += 1;
300
+ else if (src[i] === "}") {
301
+ depth -= 1;
302
+ if (depth === 0) return src.slice(from, i + 1);
303
+ }
304
+ }
305
+ return src.slice(from);
306
+ };
307
+ const EXEMPT: Record<string, { reason: string; holds: (src: string, file: string) => boolean }> = {
308
+ "smoke-herdr-plugin-build-live.ts": {
309
+ reason: "it relocates HOME into the same sandbox, so store and launcher stay in one tree",
310
+ holds: (src, file) => /(^|\n)\s*HOME[,:]/.test(envBlockAround(src, file)),
311
+ },
312
+ };
313
+ /** Where each smoke's FIRST Claude-capable child begins. Named per smoke, because the
314
+ * three rails start children in three different ways and a generic "spawn" would match
315
+ * the setup probes (`tmux -V`, `command -v claude`) that run long before any child. */
316
+ const FIRST_CHILD: Record<string, RegExp> = {
317
+ "smoke-herdr-fresh-call-live.ts": /herdr\(bin, env, \[\s*"agent",\s*"start"/,
318
+ "smoke-mux-fresh-call-live.ts": /\n\t*const \w+ = freshCall\(/,
319
+ "smoke-mux-lifecycle-live.ts": /\bbridge\.call\("entwurf_fresh_call"/,
320
+ };
321
+ const consumesFence = (src: string, file: string): boolean => {
322
+ if (!src.includes('from "./lib/claude-launcher-fence.ts"')) return false;
323
+ const bound = /const (\w+) = snapshotClaudeLauncher\(/.exec(src);
324
+ if (bound === null) return false;
325
+ const marker = FIRST_CHILD[file];
326
+ if (marker === undefined) return false;
327
+ const firstChildAt = src.search(marker);
328
+ if (firstChildAt < 0) return false;
329
+ const snapshot = bound[1];
330
+ const preflightAt = src.indexOf(bound[0]);
331
+ const verifyAt = src.indexOf(`verifyClaudeLauncher(${snapshot})`);
332
+ const cleanupAt = src.indexOf(`assessLauncherCleanup(${snapshot})`);
333
+ const removalAt = src.indexOf("rmSync(root");
334
+ if (preflightAt < 0 || preflightAt > firstChildAt) return false;
335
+ if (verifyAt < firstChildAt || cleanupAt < verifyAt) return false;
336
+ return removalAt < 0 || (verifyAt < removalAt && cleanupAt < removalAt);
337
+ };
338
+ const exposed = fs
339
+ .readdirSync(path.join(ROOT, "scripts"))
340
+ .filter((f) => f.startsWith("smoke-") && (f.endsWith("-live.ts") || f.endsWith("-live.sh")))
341
+ .filter((f) => dataRootAssignment(f).test(read(path.join("scripts", f))));
342
+ const unguarded = exposed.filter((f) => {
343
+ const src = read(path.join("scripts", f));
344
+ if (consumesFence(src, f)) return false;
345
+ const exemption = EXEMPT[f];
346
+ return !(exemption && exemption.holds(src, f));
347
+ });
348
+ ok(
349
+ `population: ${exposed.length} LIVE smokes assign a fixture data root (measured across .ts AND .sh, not listed), and the three real-HOME ones are the fence's constituency`,
350
+ exposed.length >= 4 &&
351
+ ["smoke-herdr-fresh-call-live.ts", "smoke-mux-fresh-call-live.ts", "smoke-mux-lifecycle-live.ts"].every((f) =>
352
+ exposed.includes(f),
353
+ ),
354
+ );
355
+ ok(
356
+ `[QK:LAUNCHFENCE-EXPOSED-SMOKE-WIRED] every LIVE smoke that hands a child a fixture XDG_DATA_HOME beside the operator's real HOME consumes this fence AS A LIFECYCLE — one bound preflight BEFORE its first child, then its integrity oracle and cleanup verdict on that same snapshot after the children, and all of it before any fixture removal — or carries an exemption proved against the very env block that assigns the data root; unguarded: ${unguarded.join(", ") || "none"}`,
357
+ unguarded.length === 0,
358
+ );
359
+ ok(
360
+ "the exemption discriminates rather than excuses: the exempt smoke relocates HOME in the SAME env block that fences its data root, and moving that line out of the block would put it back in the constituency",
361
+ Object.entries(EXEMPT).every(([f, e]) => e.holds(read(path.join("scripts", f)), f)),
362
+ );
363
+ {
364
+ const IMPORT =
365
+ 'import { assessLauncherCleanup, snapshotClaudeLauncher, verifyClaudeLauncher } from "./lib/claude-launcher-fence.ts";';
366
+ const CHILD = '\tconst launch = await bridge.call("entwurf_fresh_call", {});';
367
+ const PIN = "\tconst snap = snapshotClaudeLauncher({ env: process.env, fixtureRoot: root });";
368
+ const ORACLE = "\tverifyClaudeLauncher(snap);\n\tassessLauncherCleanup(snap);";
369
+ const file = "smoke-mux-lifecycle-live.ts";
370
+ ok(
371
+ "the lifecycle read is not satisfied by the STRINGS alone — a source carrying all four names with no ordering is refused",
372
+ !consumesFence(
373
+ [IMPORT, "// snapshotClaudeLauncher( verifyClaudeLauncher( assessLauncherCleanup("].join("\n"),
374
+ file,
375
+ ),
376
+ );
377
+ ok(
378
+ "snapshot < verify < cleanup in the RIGHT order is still refused when the whole block sits after the first child — that shape pins the damage as the baseline, which is the exact way an intact-looking oracle would report a launcher that had already moved",
379
+ !consumesFence([IMPORT, CHILD, PIN, ORACLE].join("\n"), file) &&
380
+ consumesFence([IMPORT, PIN, CHILD, ORACLE].join("\n"), file),
381
+ );
382
+ ok(
383
+ "a smoke in the population whose first child this gate cannot locate is refused rather than waved through — a new rail must say where its children begin",
384
+ !consumesFence([IMPORT, PIN, CHILD, ORACLE].join("\n"), "smoke-brand-new-live.ts"),
385
+ );
386
+ }
387
+ }
388
+
251
389
  ok(
252
390
  "[QK:LAUNCHFENCE-LIFECYCLE-CELL-BRANCH] wiring: lifecycle's real-HOME cell env keeps its two truthful branches — the ACP-backed pi cell retains all four MEASURED canonical REAL_XDG_* assignments, and the direct Claude Code cell (the else branch) gets exact-parity restoreOriginalXdg instead",
253
391
  /if \(backend === "pi"\) \{\s*serverEnv\.XDG_CONFIG_HOME = REAL_XDG_CONFIG_HOME;\s*serverEnv\.XDG_DATA_HOME = REAL_XDG_DATA_HOME;\s*serverEnv\.XDG_STATE_HOME = REAL_XDG_STATE_HOME;\s*serverEnv\.XDG_CACHE_HOME = REAL_XDG_CACHE_HOME;\s*\} else \{\s*restoreOriginalXdg\(serverEnv, ORIGINAL_XDG\);\s*\}/.test(