@junghanacs/entwurf 0.20.1 → 0.22.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 (111) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +6 -3
  3. package/CHANGELOG.md +473 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +332 -60
  6. package/README.md +100 -22
  7. package/VERIFY.md +93 -7
  8. package/docs/acp-backend-rail.md +0 -1
  9. package/docs/external-mcp-host.md +64 -33
  10. package/docs/setup-clean-host.md +151 -17
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  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/entwurf-v2-surface.js +7 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  29. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  30. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  31. package/mcp/entwurf-bridge/src/index.ts +154 -116
  32. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  33. package/package.json +13 -4
  34. package/pi-extensions/entwurf-control.ts +71 -19
  35. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  36. package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
  37. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  38. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  39. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  41. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  42. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  43. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  44. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  46. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  47. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  48. package/pi-extensions/lib/mux-fresh-call.ts +233 -29
  49. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  50. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  51. package/pi-extensions/lib/native-push/register.ts +7 -9
  52. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  53. package/run.sh +342 -28
  54. package/scripts/check-agy-sender-identity.ts +1 -1
  55. package/scripts/check-codex-app-server-launch.ts +445 -0
  56. package/scripts/check-codex-birth-hook.ts +264 -0
  57. package/scripts/check-codex-bridge-identity.ts +179 -0
  58. package/scripts/check-codex-native-push.ts +386 -0
  59. package/scripts/check-codex-sender-identity.ts +495 -0
  60. package/scripts/check-compaction-send-guard.ts +130 -0
  61. package/scripts/check-copilot-receive-arm.ts +4 -1
  62. package/scripts/check-entwurf-fact-provider.ts +38 -0
  63. package/scripts/check-entwurf-peers-surface.ts +13 -1
  64. package/scripts/check-entwurf-self-address.ts +15 -16
  65. package/scripts/check-entwurf-v2-contract.ts +4 -3
  66. package/scripts/check-entwurf-v2-decider.ts +7 -5
  67. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  68. package/scripts/check-entwurf-v2-production.ts +245 -12
  69. package/scripts/check-entwurf-v2-runner.ts +1 -1
  70. package/scripts/check-entwurf-v2-send.ts +26 -7
  71. package/scripts/check-entwurf-v2-surface.ts +1 -1
  72. package/scripts/check-gate-qualification.ts +8 -3
  73. package/scripts/check-harness-admission-parity.ts +0 -1
  74. package/scripts/check-mux-launch-tmux.ts +345 -4
  75. package/scripts/check-native-push-adapter.ts +20 -16
  76. package/scripts/check-native-push-register.ts +5 -1
  77. package/scripts/check-release-gate-outcomes.ts +47 -1
  78. package/scripts/check-setup-qualification.sh +3 -1
  79. package/scripts/codex-app-server-launch.sh +275 -0
  80. package/scripts/codex-birth-doctor.sh +276 -0
  81. package/scripts/codex-birth-install.sh +414 -0
  82. package/scripts/codex-birth-uninstall.sh +170 -0
  83. package/scripts/codex-mcp-config.py +435 -0
  84. package/scripts/codex-socket-path.ts +33 -0
  85. package/scripts/codex-statusline-config.py +434 -0
  86. package/scripts/codex-terminal-title-config.py +500 -0
  87. package/scripts/codex_toml_io.py +653 -0
  88. package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
  89. package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
  90. package/scripts/lib/launch-receipt-windows.ts +46 -0
  91. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  92. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  93. package/scripts/mutants/codex-app-server-launch.json +157 -0
  94. package/scripts/mutants/codex-caller-seat.json +336 -0
  95. package/scripts/mutants/codex-native.json +838 -0
  96. package/scripts/mutants/compaction-send-guard.json +103 -0
  97. package/scripts/mutants/entwurf-peers.json +19 -0
  98. package/scripts/mutants/mux-fresh-call.json +93 -10
  99. package/scripts/mutants/omp-fresh.json +6 -4
  100. package/scripts/mutants/release-gate.json +13 -0
  101. package/scripts/mutants/v2-surface.json +75 -1
  102. package/scripts/raw-async-delivery/README.md +2 -1
  103. package/scripts/raw-codex-measure/README.md +114 -46
  104. package/scripts/smoke-agy-native-push-live.ts +3 -1
  105. package/scripts/smoke-codex-birth.sh +347 -0
  106. package/scripts/smoke-codex-config-state.sh +700 -0
  107. package/scripts/smoke-codex-fresh-live.ts +1426 -0
  108. package/scripts/smoke-codex-native-push-live.ts +75 -0
  109. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  110. package/scripts/smoke-setup-verdict.sh +125 -10
  111. package/scripts/tsconfig.json +1 -0
@@ -5,8 +5,8 @@
5
5
  *
6
6
  * 1. ack success → outcome `sent`, release ×1, deadFallback NOT called.
7
7
  * 2. in-band reject (success:false) → outcome `rejected`, release ×1, NO fallback
8
- * (deadFallback + mailbox NOT called — the receiver was reached and refused); NO
9
- * rejectReason (an in-band refusal has no resolver taxonomy N3 boundary).
8
+ * (deadFallback + mailbox NOT called — the receiver was reached and refused); a
9
+ * supplied receiver error is carried verbatim as `rejectReason` (N3 boundary).
10
10
  * 3. dead → re-resolve(control-socket) → success → `fallback-sent`, release ×1,
11
11
  * deadFallback called EXACTLY once and UNDER the still-held lock (before release).
12
12
  * 4. dead → re-resolve reject → `rejected`, release ×1, and the resolver's reason is
@@ -193,16 +193,24 @@ async function main(): Promise<void> {
193
193
 
194
194
  // ── 2: in-band reject → rejected, release once, NO fallback ───────────────
195
195
  {
196
- const { result, trace } = await run({ firstSend: { result: { success: false, error: "refused" } } });
196
+ const { result, trace } = await run({ firstSend: { result: { success: false, error: "compacting" } } });
197
197
  ok("in-band reject → rejected", result.outcome === "rejected");
198
198
  ok("in-band reject → release ×1", trace.releases.length === 1);
199
- // N3 boundary: an in-band RPC refusal has NO resolver reason (only a dead-path
200
- // re-resolve reject carries one) the field stays undefined here.
201
- ok("in-band reject → no rejectReason (in-band has no resolver taxonomy)", result.rejectReason === undefined);
199
+ ok(
200
+ "[QK:V2SEND-INBAND-REJECT-REASON] in-band control reject carries its receiver error",
201
+ result.rejectReason === "compacting",
202
+ );
202
203
  ok(
203
204
  "in-band reject → no deadFallback, no mailbox",
204
205
  trace.deadFallbackCalls === 0 && trace.mailboxSends.length === 0,
205
206
  );
207
+ const unnamed = await run({ firstSend: { result: { success: false } } });
208
+ ok("in-band reject without an error does not invent rejectReason", unnamed.result.rejectReason === undefined);
209
+ const empty = await run({ firstSend: { result: { success: false, error: "" } } });
210
+ ok(
211
+ "in-band reject with an empty error does not expose an empty rejectReason",
212
+ empty.result.rejectReason === undefined,
213
+ );
206
214
  }
207
215
 
208
216
  // ── 3: dead → re-resolve(control) success → fallback-sent, fallback before release
@@ -223,6 +231,16 @@ async function main(): Promise<void> {
223
231
  "dead → deadFallback UNDER held lock (before release)",
224
232
  trace.order.indexOf("deadFallback") < trace.order.indexOf("releaseLock"),
225
233
  );
234
+
235
+ const refused = await run({
236
+ firstSend: { throwCode: "ECONNREFUSED" },
237
+ deadFallback: { kind: "execute", plan: RERESOLVED_CONTROL_PLAN },
238
+ fallbackSend: { result: { success: false, error: "retry-refused" } },
239
+ });
240
+ ok(
241
+ "dead → re-resolve(control) in-band reject carries the retry receiver error",
242
+ refused.result.outcome === "rejected" && refused.result.rejectReason === "retry-refused",
243
+ );
226
244
  }
227
245
 
228
246
  // ── 4: dead → re-resolve reject → rejected ────────────────────────────────
@@ -296,9 +314,10 @@ async function main(): Promise<void> {
296
314
  const refused = await run({
297
315
  firstSend: { throwCode: "ENOENT" },
298
316
  deadFallback: { kind: "execute", plan: MAILBOX_PLAN },
299
- fallbackSend: { result: { success: false } },
317
+ fallbackSend: { result: { success: false, error: "mailbox-refused" } },
300
318
  });
301
319
  ok("dead → mailbox enqueue success:false → rejected", refused.result.outcome === "rejected");
320
+ ok("dead → mailbox enqueue reject carries its receiver error", refused.result.rejectReason === "mailbox-refused");
302
321
  // No file was written, so there is nothing to name — never echo a dep's stray path.
303
322
  ok("dead → rejected enqueue carries NO messagePath", refused.result.messagePath === undefined);
304
323
  }
@@ -729,7 +729,7 @@ async function main(): Promise<void> {
729
729
  v2Start !== -1 && after > v2Start,
730
730
  );
731
731
  const v2Block = src.slice(v2Start, after);
732
- ok("5: MCP — v2 handler builds buildSendSenderEnvelope()", /buildSendSenderEnvelope\(\)/.test(v2Block));
732
+ ok("5: MCP — v2 handler builds buildSendSenderEnvelope(context)", /buildSendSenderEnvelope\s*\(/.test(v2Block));
733
733
  ok("5: MCP — v2 handler passes senderProvider: () => sender", /senderProvider:\s*\(\)\s*=>\s*sender/.test(v2Block));
734
734
  ok(
735
735
  "5: MCP — v2 handler calls runAndRenderEntwurfV2FromSurface",
@@ -832,9 +832,14 @@ let manifestCount: number;
832
832
  "bridge-boot-resume": 3,
833
833
  "bridge-command-boot": 9,
834
834
  "capability-cache": 3,
835
+ "codex-app-server-launch": 9,
836
+ "codex-caller-seat": 28,
837
+ "codex-native": 68,
838
+ "compaction-send-guard": 7,
835
839
  "copilot-birth": 19,
836
840
  "copilot-launch": 14,
837
841
  "copilot-receive": 20,
842
+ "entwurf-peers": 1,
838
843
  "fresh-cut": 3,
839
844
  "gate-qualification": 2,
840
845
  "meta-facts": 4,
@@ -842,7 +847,7 @@ let manifestCount: number;
842
847
  "meta-identity": 4,
843
848
  "meta-retire": 3,
844
849
  "mux-boundary": 16,
845
- "mux-fresh-call": 44,
850
+ "mux-fresh-call": 51,
846
851
  "mux-launcher-fence": 7,
847
852
  "mux-parent-artifact": 3,
848
853
  "pack-install": 2,
@@ -852,13 +857,13 @@ let manifestCount: number;
852
857
  "omp-fresh": 24,
853
858
  "omp-receive": 11,
854
859
  "probe-ordering": 1,
855
- "release-gate": 18,
860
+ "release-gate": 19,
856
861
  "resume-args": 6,
857
862
  "resume-launch-identity": 6,
858
863
  "self-address": 5,
859
864
  "setup-verdict": 14,
860
865
  "source-install": 2,
861
- "v2-surface": 7,
866
+ "v2-surface": 13,
862
867
  "v2-visible-resume": 17,
863
868
  };
864
869
  const laneTally: Record<string, number> = {};
@@ -81,7 +81,6 @@ const PRE_82_LEGACY: Partial<Record<MetaCitizenBackend, [file: string, sentence:
81
81
  "DELIVERY.md",
82
82
  "Admitted before the #82 step 9 contract and not re-evaluated under it, so it is legacy citizen evidence, not a step-9 supported harness",
83
83
  ],
84
- codex: ["DELIVERY.md", "No owned native-citizen install/invocation lane."],
85
84
  };
86
85
 
87
86
  const fresh = new Set<string>(FRESH_CALL_BACKENDS);
@@ -8,7 +8,7 @@
8
8
  * and required to be byte-identical, and `TMUX`/`TMUX_PANE` INHERITED from a fixture pane's
9
9
  * own `/proc/<pid>/environ` rather than assembled.
10
10
  *
11
- * The runtime under test is a FIXTURE script, not the operator's real `pi`. T1-a's claim is
11
+ * The runtimes under test are FIXTURE scripts, not the operator's real `pi`/`codex`. T1-a's claim is
12
12
  * "the window opens in the right place and the named runtime is what starts in it" — a claim a
13
13
  * long-lived stand-in proves exactly, without an interactive agent, a model call, or a session
14
14
  * record. `resolvePiRuntime` (which finds the official `pi` on PATH) is proven by the
@@ -27,6 +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
+ * - 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
30
40
  */
31
41
 
32
42
  import assert from "node:assert/strict";
@@ -34,7 +44,7 @@ import { spawnSync } from "node:child_process";
34
44
  import fs from "node:fs";
35
45
  import path from "node:path";
36
46
  import { buildLaunchArgs, LaunchPreconditionError, launchPi } from "../pi-extensions/lib/mux-launch.ts";
37
- import { inspectPlacement } from "../pi-extensions/lib/mux-placement.ts";
47
+ import { inspectPlacement, runTmux } from "../pi-extensions/lib/mux-placement.ts";
38
48
  import { skipLive } from "./lib/live-skip.ts";
39
49
 
40
50
  const LABEL = "check-mux-launch-tmux";
@@ -118,8 +128,14 @@ async function main(): Promise<void> {
118
128
  const runtimeDir = fs.mkdtempSync(path.join(process.env.XDG_RUNTIME_DIR ?? "/tmp", "entwurf-mux-rt-"));
119
129
  // A long-lived stand-in for the official runtime. `exec` so no shell survives: the pane's
120
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).
121
135
  const runtime = path.join(runtimeDir, "pi");
122
- fs.writeFileSync(runtime, "#!/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 });
123
139
 
124
140
  try {
125
141
  assert.equal(fx("-f", "/dev/null", "new-session", "-d", "-s", SESSION).status, 0, "fixture new-session");
@@ -353,7 +369,7 @@ async function main(): Promise<void> {
353
369
  fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").join(" ") === callerWindowsBefore,
354
370
  );
355
371
  ok(
356
- "seat: an omitted seat still lands in the caller's own session and the receipt names none",
372
+ "seat: an omitted Pi seat still lands in the caller's own session and the receipt names none",
357
373
  (() => {
358
374
  const own = call();
359
375
  if (!own.ok) return false;
@@ -370,8 +386,333 @@ async function main(): Promise<void> {
370
386
  closeWindow(receipt, inherited) === "closed" &&
371
387
  !fxLines("list-windows", "-a", "-F", "#{window_id}").includes(receipt.windowId),
372
388
  );
389
+
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(
400
+ {
401
+ backend: "codex",
402
+ model: "fixture-model",
403
+ task: "fixture task",
404
+ callerGardenId: "20260101T000000-fixture",
405
+ },
406
+ inherited,
407
+ );
408
+ assert.ok(
409
+ omittedCodex.ok,
410
+ `the omitted-placement Codex call must succeed: ${omittedCodex.ok ? "" : omittedCodex.reason}`,
411
+ );
412
+ ok(
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,
419
+ );
420
+ process.kill(Number(omittedCodex.receipt.panePid), "SIGKILL");
373
421
  fx("kill-session", "-t", seatId);
422
+ fx("kill-session", "-t", decoyHomeId);
374
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
+ }
375
716
  }
376
717
  } finally {
377
718
  fx("kill-server");
@@ -29,6 +29,7 @@ import {
29
29
  AGY_SEND_TIMEOUT_MS,
30
30
  antigravityAdapter,
31
31
  createAntigravityAdapter,
32
+ type NativePushRoute,
32
33
  type NativePushRunner,
33
34
  resolveNativePushAdapter,
34
35
  } from "../pi-extensions/lib/native-push/adapter.ts";
@@ -45,6 +46,11 @@ function eq(label: string, actual: unknown, expected: unknown): void {
45
46
  passed++;
46
47
  }
47
48
 
49
+ function agyAddress(route: NativePushRoute): string {
50
+ assert.equal(route.backend, "antigravity", "Antigravity adapter returned its own route kind");
51
+ return route.backend === "antigravity" ? route.lsAddress : "(wrong backend)";
52
+ }
53
+
48
54
  const FAKE_BINARY = "/fake/bin/agy";
49
55
  const CONV = "conv-abc-123";
50
56
 
@@ -129,7 +135,7 @@ function makeFakeRunner(config: FakeConfig): { runner: NativePushRunner; calls:
129
135
  ok("full-scan: probe finds the route on the SECOND pid (not head -1)", result.status === "alive");
130
136
  eq(
131
137
  "full-scan: route is pid 456's port (5002)",
132
- result.status === "alive" ? result.route.lsAddress : "(dead)",
138
+ result.status === "alive" ? agyAddress(result.route) : "(dead)",
133
139
  "127.0.0.1:5002",
134
140
  );
135
141
  // proof it did NOT stop at pid 123: it probed BOTH 5001 (miss) and 5002 (hit).
@@ -219,7 +225,11 @@ for (const [code, what] of [
219
225
  const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
220
226
 
221
227
  const r1 = await adapter.probe(CONV);
222
- eq("volatile-route: probe #1 route = 5002", r1.status === "alive" ? r1.route.lsAddress : "(dead)", "127.0.0.1:5002");
228
+ eq(
229
+ "volatile-route: probe #1 route = 5002",
230
+ r1.status === "alive" ? agyAddress(r1.route) : "(dead)",
231
+ "127.0.0.1:5002",
232
+ );
223
233
  const pgrepAfter1 = calls.filter((c) => c.argv[0] === "pgrep").length;
224
234
 
225
235
  // the LS port shifts (per-process, volatile) — a fresh probe must RE-DISCOVER it.
@@ -227,7 +237,7 @@ for (const [code, what] of [
227
237
  const r2 = await adapter.probe(CONV);
228
238
  eq(
229
239
  "volatile-route: probe #2 RE-DISCOVERS the changed route = 5003 (no cache)",
230
- r2.status === "alive" ? r2.route.lsAddress : "(dead)",
240
+ r2.status === "alive" ? agyAddress(r2.route) : "(dead)",
231
241
  "127.0.0.1:5003",
232
242
  );
233
243
  ok(
@@ -241,7 +251,7 @@ for (const [code, what] of [
241
251
  {
242
252
  const { runner, calls } = makeFakeRunner({ pids: [123], ss: ssLine(5002, 123), serving: () => true });
243
253
  const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
244
- await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hello world");
254
+ await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hello world");
245
255
  const sendCall = calls.find((c) => c.argv.includes("send-message"));
246
256
  eq("send: argv === [binary, agentapi, send-message, conv, content]", sendCall?.argv, [
247
257
  FAKE_BINARY,
@@ -262,7 +272,7 @@ for (const [code, what] of [
262
272
  const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
263
273
  let threw = false;
264
274
  try {
265
- await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
275
+ await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
266
276
  } catch (err) {
267
277
  threw = true;
268
278
  ok("send-fail: error names the non-zero exit", /native-push send failed/.test((err as Error).message));
@@ -286,7 +296,7 @@ for (const [code, what] of [
286
296
  const { runner, calls } = makeFakeRunner({ pids: [123], ss: ssLine(5002, 123), serving: () => true });
287
297
  const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
288
298
  await adapter.probe(CONV);
289
- await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
299
+ await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
290
300
  const metaCall = calls.find((c) => c.argv.includes("get-conversation-metadata"));
291
301
  const sendCall = calls.find((c) => c.argv.includes("send-message"));
292
302
  eq("timeout: get-conversation-metadata carries the metadata timeout", metaCall?.timeoutMs, AGY_METADATA_TIMEOUT_MS);
@@ -317,7 +327,7 @@ for (const [code, what] of [
317
327
  const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
318
328
  let threw = false;
319
329
  try {
320
- await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
330
+ await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
321
331
  } catch {
322
332
  threw = true;
323
333
  }
@@ -327,15 +337,9 @@ for (const [code, what] of [
327
337
  // ── resolver fail-fast (mirror resolveAcpBackendAdapter) ─────────────────────
328
338
  eq("resolve: antigravity → the antigravity adapter", resolveNativePushAdapter("antigravity").id, "antigravity");
329
339
  eq("adapter id: antigravityAdapter.id === antigravity", antigravityAdapter.id, "antigravity");
330
- {
331
- let threw = false;
332
- try {
333
- resolveNativePushAdapter("codex");
334
- } catch {
335
- threw = true;
336
- }
337
- ok("resolve: unknown backend (codex) → throw (no silent default)", threw);
338
- }
340
+ eq("resolve: codex → the Codex adapter", resolveNativePushAdapter("codex").id, "codex");
341
+ eq("retry policy: Antigravity may retry", resolveNativePushAdapter("antigravity").retriable, true);
342
+ eq("retry policy: Codex never retries", resolveNativePushAdapter("codex").retriable, false);
339
343
  {
340
344
  let threw = false;
341
345
  try {
@@ -39,6 +39,7 @@ const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
39
39
  function fakeAdapter(probe: NativePushProbeResult): NativePushAdapter {
40
40
  return {
41
41
  id: "antigravity",
42
+ retriable: true,
42
43
  async probe() {
43
44
  return probe;
44
45
  },
@@ -48,7 +49,10 @@ function fakeAdapter(probe: NativePushProbeResult): NativePushAdapter {
48
49
  };
49
50
  }
50
51
 
51
- const ALIVE: NativePushProbeResult = { status: "alive", route: { lsAddress: "127.0.0.1:5599" } };
52
+ const ALIVE: NativePushProbeResult = {
53
+ status: "alive",
54
+ route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" },
55
+ };
52
56
 
53
57
  async function main(): Promise<void> {
54
58
  const dir = mkdtempSync(path.join(os.tmpdir(), "entwurf-register-"));