@junghanacs/entwurf 0.13.0 → 0.14.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 (193) hide show
  1. package/AGENTS.md +52 -17
  2. package/BASELINE.md +60 -206
  3. package/CHANGELOG.md +64 -0
  4. package/CONTRIBUTING.md +14 -10
  5. package/DELIVERY.md +118 -261
  6. package/README.md +91 -431
  7. package/VERIFY.md +74 -95
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +236 -0
  12. package/docs/external-mcp-host.md +131 -0
  13. package/docs/fresh-cut-policy.md +99 -0
  14. package/docs/setup-clean-host.md +124 -328
  15. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  37. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  38. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  39. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  40. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  41. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  42. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  43. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  44. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  45. package/mcp/entwurf-bridge/src/index.ts +176 -67
  46. package/mcp/entwurf-bridge/start.sh +2 -2
  47. package/mcp/entwurf-bridge/test.sh +23 -9
  48. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  49. package/package.json +25 -11
  50. package/pi-extensions/entwurf-control.ts +220 -71
  51. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  52. package/pi-extensions/lib/acp/augment.ts +42 -3
  53. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  54. package/pi-extensions/lib/acp/backend.ts +366 -38
  55. package/pi-extensions/lib/acp/config.ts +1 -1
  56. package/pi-extensions/lib/acp/engraving.ts +43 -1
  57. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  58. package/pi-extensions/lib/acp/models.ts +4 -4
  59. package/pi-extensions/lib/acp/overlay.ts +11 -9
  60. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  61. package/pi-extensions/lib/entwurf-core.ts +15 -15
  62. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  63. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  64. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  65. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  66. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  67. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  68. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  69. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  70. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  71. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  72. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  73. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  74. package/pi-extensions/lib/meta-session.ts +93 -5
  75. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  76. package/pi-extensions/lib/mux-launch.ts +267 -0
  77. package/pi-extensions/lib/mux-placement.ts +387 -0
  78. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  79. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  80. package/pi-extensions/lib/session-id.js +8 -5
  81. package/pi-extensions/lib/socket-discovery.ts +3 -3
  82. package/prompts/engraving.md +13 -5
  83. package/run.sh +688 -282
  84. package/scripts/agy-bridge-config.py +5 -1
  85. package/scripts/check-acp-backend-preflight.ts +1 -1
  86. package/scripts/check-acp-carrier-augment.ts +246 -10
  87. package/scripts/check-acp-cortex.ts +5 -5
  88. package/scripts/check-acp-overlay.ts +13 -3
  89. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  90. package/scripts/check-acp-stop-reason.ts +342 -0
  91. package/scripts/check-acp-stream-hooks.ts +504 -0
  92. package/scripts/check-elapsed.sh +25 -0
  93. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  94. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  95. package/scripts/check-entwurf-control-rpc.ts +4 -3
  96. package/scripts/check-entwurf-resume-args.ts +72 -70
  97. package/scripts/check-entwurf-session-identity.ts +14 -10
  98. package/scripts/check-entwurf-v2-contract.ts +34 -59
  99. package/scripts/check-entwurf-v2-decider.ts +17 -177
  100. package/scripts/check-entwurf-v2-lock.ts +5 -2
  101. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  102. package/scripts/check-entwurf-v2-production.ts +2 -91
  103. package/scripts/check-entwurf-v2-release.ts +10 -105
  104. package/scripts/check-entwurf-v2-runner.ts +4 -85
  105. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  106. package/scripts/check-entwurf-v2-send.ts +0 -28
  107. package/scripts/check-entwurf-v2-surface.ts +198 -128
  108. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  109. package/scripts/check-fresh-cut-gate.sh +1 -1
  110. package/scripts/check-gate-qualification.ts +101 -7
  111. package/scripts/check-install-container.sh +10 -2
  112. package/scripts/check-install-surface.ts +1 -1
  113. package/scripts/check-keyset-overlap.py +1 -1
  114. package/scripts/check-meta-facts.ts +249 -0
  115. package/scripts/check-meta-identity-consumers.ts +1 -1
  116. package/scripts/check-meta-session.ts +169 -0
  117. package/scripts/check-mux-launch-tmux.ts +316 -0
  118. package/scripts/check-mux-launch.ts +288 -0
  119. package/scripts/check-mux-launcher-fence.ts +264 -0
  120. package/scripts/check-mux-parent-artifact.ts +195 -0
  121. package/scripts/check-mux-placement-tmux.ts +322 -0
  122. package/scripts/check-mux-placement.ts +323 -0
  123. package/scripts/check-mux-resume-call.ts +282 -0
  124. package/scripts/check-probe-cli-shim.ts +25 -22
  125. package/scripts/check-probe-ordering.ts +121 -81
  126. package/scripts/check-release-gate-outcomes.ts +452 -0
  127. package/scripts/check-resume-launch-identity.ts +244 -0
  128. package/scripts/check-socket-discovery.ts +1 -1
  129. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  130. package/scripts/inventory-verification-surface.ts +349 -0
  131. package/scripts/lib/claude-launcher-fence.ts +322 -0
  132. package/scripts/lib/live-skip.ts +33 -0
  133. package/scripts/lib/mutation-qualify.ts +109 -3
  134. package/scripts/lib/probe-acp-turn.ts +33 -12
  135. package/scripts/lib/step-outcome.sh +88 -0
  136. package/scripts/meta-bridge-doctor.sh +6 -8
  137. package/scripts/meta-facts.ts +60 -0
  138. package/scripts/mutants/acp-augment.json +106 -0
  139. package/scripts/mutants/acp-cortex.json +2 -2
  140. package/scripts/mutants/acp-overlay.json +17 -0
  141. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  142. package/scripts/mutants/acp-stop-reason.json +80 -0
  143. package/scripts/mutants/acp-stream-hooks.json +158 -0
  144. package/scripts/mutants/bridge-boot-resume.json +45 -0
  145. package/scripts/mutants/meta-facts.json +50 -0
  146. package/scripts/mutants/meta-identity.json +36 -0
  147. package/scripts/mutants/mux-boundary.json +196 -0
  148. package/scripts/mutants/mux-fresh-call.json +185 -0
  149. package/scripts/mutants/mux-launcher-fence.json +123 -0
  150. package/scripts/mutants/mux-parent-artifact.json +39 -0
  151. package/scripts/mutants/mux-resume-call.json +148 -0
  152. package/scripts/mutants/probe-ordering.json +6 -1021
  153. package/scripts/mutants/release-gate.json +140 -0
  154. package/scripts/mutants/resume-args.json +76 -0
  155. package/scripts/mutants/resume-launch-identity.json +96 -0
  156. package/scripts/mutants/v2-surface.json +74 -23
  157. package/scripts/mutants/v2-visible-resume.json +215 -0
  158. package/scripts/new-session-id.ts +9 -4
  159. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  160. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  161. package/scripts/smoke-acp-cortex-live.ts +8 -14
  162. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  163. package/scripts/smoke-acp-mcp-live.ts +2 -2
  164. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  165. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  166. package/scripts/smoke-acp-overlay-live.ts +2 -2
  167. package/scripts/smoke-acp-provider-live.ts +2 -2
  168. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  169. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  170. package/scripts/smoke-acp-skill-live.ts +2 -2
  171. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  172. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  173. package/scripts/smoke-agy-native-push-live.ts +10 -20
  174. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  175. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  176. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
  177. package/scripts/smoke-meta-honesty.sh +1 -1
  178. package/scripts/smoke-meta-install-state.sh +4 -0
  179. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  180. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  181. package/scripts/smoke-pi-attach.ts +1 -1
  182. package/scripts/smoke-user-scope-citizen.sh +1 -1
  183. package/scripts/tsconfig.json +1 -0
  184. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  185. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  186. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  187. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  188. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  189. package/scripts/check-acp-sdk-surface.ts +0 -236
  190. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  191. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  192. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  193. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
@@ -0,0 +1,316 @@
1
+ /**
2
+ * check-mux-launch-tmux — REAL tmux acceptance for the T1-a visible launch.
3
+ *
4
+ * `check-mux-launch` pins preconditions, argv and the import boundary; here the production
5
+ * `launchPi` runs against an actual tmux server and the assertions read the server's own
6
+ * `list-windows`/`list-panes`. Same isolation contract as the T0-b acceptance: a private `-S`
7
+ * socket under $XDG_RUNTIME_DIR, the operator's server snapshotted read-only before and after
8
+ * and required to be byte-identical, and `TMUX`/`TMUX_PANE` INHERITED from a fixture pane's
9
+ * own `/proc/<pid>/environ` rather than assembled.
10
+ *
11
+ * The runtime under test is a FIXTURE script, not the operator's real `pi`. T1-a's claim is
12
+ * "the window opens in the right place and the named runtime is what starts in it" — a claim a
13
+ * long-lived stand-in proves exactly, without an interactive agent, a model call, or a session
14
+ * record. `resolvePiRuntime` (which finds the official `pi` on PATH) is proven by the
15
+ * deterministic gate; here PATH is pointed at the fixture so this gate judges topology.
16
+ *
17
+ * Proven here:
18
+ * - the window lands at {end} of the caller's OWN session; one session throughout
19
+ * - the launched process is the runtime itself — no shell wrapper survives in the pane
20
+ * - pane_pid is that process, and it is alive
21
+ * - focus never moves, and the original windows and their pane pids survive
22
+ * - the handle is bound to the context it was born in
23
+ * - THE TRAP: a failed exec still exits 0, still prints a well-formed handle, and its window
24
+ * is still listed on an immediate re-read — which is why no post-launch presence check is
25
+ * performed and why the precondition runs before the window exists
26
+ * - a precondition refusal opens NO window at all
27
+ */
28
+
29
+ import assert from "node:assert/strict";
30
+ import { spawnSync } from "node:child_process";
31
+ import fs from "node:fs";
32
+ import path from "node:path";
33
+ import { buildLaunchArgs, LaunchPreconditionError, launchPi } from "../pi-extensions/lib/mux-launch.ts";
34
+ import { inspectPlacement } from "../pi-extensions/lib/mux-placement.ts";
35
+ import { skipLive } from "./lib/live-skip.ts";
36
+
37
+ const LABEL = "check-mux-launch-tmux";
38
+ let passed = 0;
39
+ function ok(label: string, cond: boolean): void {
40
+ assert.ok(cond, label);
41
+ console.log(` ok ${label}`);
42
+ passed++;
43
+ }
44
+
45
+ const SOCKET = path.join(process.env.XDG_RUNTIME_DIR ?? "/run/user/1000", `entwurf-mux-t1a-${process.pid}.sock`);
46
+ const SESSION = `muxt1a${process.pid}`;
47
+
48
+ function fx(...args: string[]): { status: number | null; stdout: string; stderr: string } {
49
+ const env = { ...process.env };
50
+ delete env.TMUX;
51
+ delete env.TMUX_PANE;
52
+ const r = spawnSync("tmux", ["-S", SOCKET, ...args], { env, encoding: "utf8" });
53
+ if (r.error) throw r.error;
54
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
55
+ }
56
+
57
+ function fxLines(...args: string[]): string[] {
58
+ const r = fx(...args);
59
+ assert.equal(r.status, 0, `fixture tmux ${args.join(" ")} failed: ${r.stderr}`);
60
+ return r.stdout
61
+ .split("\n")
62
+ .map((l) => l.trim())
63
+ .filter((l) => l.length > 0);
64
+ }
65
+
66
+ function windows(): string[] {
67
+ return fxLines("list-windows", "-a", "-F", "#{window_id}|#{window_index}|#{window_active}");
68
+ }
69
+
70
+ function panes(): string[] {
71
+ return fxLines("list-panes", "-a", "-F", "#{window_id}|#{pane_id}|#{pane_pid}");
72
+ }
73
+
74
+ function sessionCount(): number {
75
+ return fxLines("list-sessions", "-F", "#{session_id}").length;
76
+ }
77
+
78
+ function windowPresent(windowId: string): boolean {
79
+ return fxLines("list-windows", "-a", "-F", "#{window_id}").includes(windowId);
80
+ }
81
+
82
+ /** Read-only snapshot of the CALLER'S OWN server — the ambient TMUX is kept deliberately. */
83
+ function operatorSnapshot(): string | null {
84
+ const r = spawnSync("tmux", ["list-panes", "-a", "-F", "#{session_id}|#{window_id}|#{pane_id}|#{pane_pid}"], {
85
+ env: process.env,
86
+ encoding: "utf8",
87
+ });
88
+ return r.status === 0 ? (r.stdout ?? "") : null;
89
+ }
90
+
91
+ function alive(pid: string): boolean {
92
+ try {
93
+ process.kill(Number(pid), 0);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ function main(): void {
101
+ if (spawnSync("tmux", ["-V"], { encoding: "utf8" }).status !== 0) {
102
+ skipLive(LABEL, "tmux is not installed — install tmux to run the launch acceptance");
103
+ }
104
+ if (!fs.existsSync("/proc")) {
105
+ skipLive(LABEL, "/proc is unavailable — the inherited-anchor read is Linux-only");
106
+ }
107
+ if (!process.env.TMUX || !process.env.TMUX_PANE) {
108
+ skipLive(LABEL, "run this acceptance from INSIDE tmux — the isolation proof snapshots the caller's own server");
109
+ }
110
+ if (fs.existsSync(SOCKET)) {
111
+ throw new Error(`${LABEL}: fixture socket ${SOCKET} already exists — refusing to reuse a foreign server`);
112
+ }
113
+
114
+ const before = operatorSnapshot();
115
+ const runtimeDir = fs.mkdtempSync(path.join(process.env.XDG_RUNTIME_DIR ?? "/tmp", "entwurf-mux-rt-"));
116
+ // A long-lived stand-in for the official runtime. `exec` so no shell survives: the pane's
117
+ // process must be the runtime itself for the pane_pid claim to mean anything.
118
+ const runtime = path.join(runtimeDir, "pi");
119
+ fs.writeFileSync(runtime, "#!/bin/sh\nexec sleep 900\n", { mode: 0o755 });
120
+
121
+ try {
122
+ assert.equal(fx("-f", "/dev/null", "new-session", "-d", "-s", SESSION).status, 0, "fixture new-session");
123
+ fx("set-option", "-g", "base-index", "1");
124
+ fx("set-option", "-gw", "pane-base-index", "1");
125
+ fx("set-option", "-g", "renumber-windows", "on");
126
+ assert.equal(fx("move-window", "-s", `${SESSION}:0`, "-t", `${SESSION}:1`).status, 0, "fixture move-window");
127
+ assert.equal(fx("new-window", "-d", "-a", "-t", `${SESSION}:{end}`).status, 0, "fixture second window");
128
+
129
+ const w0 = windows();
130
+ ok("fixture: exactly one session", sessionCount() === 1);
131
+ ok(`fixture: windows are 1,2 (${w0.join(" ")})`, w0.length === 2);
132
+
133
+ // ── the anchor is INHERITED, not assembled ─────────────────────────────────────
134
+ const callerPanePid = fxLines("display-message", "-p", "-t", `${SESSION}:1`, "#{pane_pid}")[0];
135
+ const environ = fs.readFileSync(`/proc/${callerPanePid}/environ`, "utf8").split("\0");
136
+ const inherited: NodeJS.ProcessEnv = { ...process.env };
137
+ delete inherited.TMUX;
138
+ delete inherited.TMUX_PANE;
139
+ for (const entry of environ) {
140
+ const eq = entry.indexOf("=");
141
+ if (eq < 0) continue;
142
+ const key = entry.slice(0, eq);
143
+ if (key === "TMUX" || key === "TMUX_PANE") inherited[key] = entry.slice(eq + 1);
144
+ }
145
+ // The fixture runtime dir is PREPENDED, never substituted: the same env spawns tmux
146
+ // itself, so replacing PATH outright would take tmux away from the production code and
147
+ // this gate would be measuring a broken harness instead of a launch. Prepending also
148
+ // makes the stand-in win over any real `pi` further down the operator's PATH.
149
+ inherited.PATH = `${runtimeDir}${path.delimiter}${process.env.PATH ?? ""}`;
150
+ ok("anchor: the fixture pane really inherited TMUX/TMUX_PANE", Boolean(inherited.TMUX && inherited.TMUX_PANE));
151
+ ok("anchor: TMUX names the fixture socket, not the operator's", String(inherited.TMUX).startsWith(SOCKET));
152
+
153
+ const got = inspectPlacement(inherited);
154
+ assert.ok(got.ok, "inspectPlacement must resolve inside the fixture");
155
+ const placement = got.placement;
156
+ const fixtureServerPid = fxLines("display-message", "-p", "#{pid}")[0];
157
+ const originalPanes = panes();
158
+ const activeBefore = windows().find((w) => w.endsWith("|1"));
159
+
160
+ // ── a precondition refusal opens NO window ─────────────────────────────────────
161
+ // The whole reason preconditions run first: a caller who cannot start the runtime must
162
+ // not be left with a window to clean up.
163
+ {
164
+ // A PATH that still finds tmux but has no `pi` anywhere: drop every entry holding an
165
+ // executable `pi` rather than emptying PATH, so the refusal under test is the
166
+ // runtime's absence and not a broken harness.
167
+ const noPiEntries = (process.env.PATH ?? "")
168
+ .split(path.delimiter)
169
+ .filter((d) => d.length > 0 && path.isAbsolute(d))
170
+ .filter((d) => {
171
+ try {
172
+ fs.accessSync(path.join(d, "pi"), fs.constants.X_OK);
173
+ return false;
174
+ } catch {
175
+ return true;
176
+ }
177
+ });
178
+ const noRuntime = { ...inherited, PATH: noPiEntries.join(path.delimiter) };
179
+ assert.equal(
180
+ spawnSync("tmux", ["-V"], { env: noRuntime, encoding: "utf8" }).status,
181
+ 0,
182
+ "the no-pi PATH must still find tmux, or this would test the harness",
183
+ );
184
+ let reason: string | null = null;
185
+ try {
186
+ launchPi(placement, noRuntime);
187
+ } catch (err) {
188
+ reason = err instanceof LaunchPreconditionError ? err.reason : null;
189
+ }
190
+ ok("refusal: an unresolvable runtime refuses as runtime-unresolved", reason === "runtime-unresolved");
191
+ ok("refusal: and NO window was opened by the refused launch", windows().length === 2);
192
+ }
193
+
194
+ // ── the launch ────────────────────────────────────────────────────────────────
195
+ const launch = launchPi(placement, inherited);
196
+ ok(`launch: 1,2,3 (${windows().join(" ")})`, windows().length === 3);
197
+ ok("launch: landed at the END (index 3)", launch.windowIndex === "3");
198
+ ok("launch: still exactly one session — a window, never a new session", sessionCount() === 1);
199
+ ok("launch: stable handles", launch.windowId.startsWith("@") && launch.paneId.startsWith("%"));
200
+ ok(
201
+ "launch: focus unchanged — visible means a real window, not stolen focus",
202
+ windows().find((w) => w.endsWith("|1")) === activeBefore,
203
+ );
204
+ ok(
205
+ "launch: original windows and their pane pids untouched",
206
+ originalPanes.every((p) => panes().includes(p)),
207
+ );
208
+ ok("launch: the receipt names the runtime it handed tmux", launch.runtimePath === runtime);
209
+ ok(
210
+ "launch: the handle carries the context it was born in",
211
+ launch.serverPid === fixtureServerPid && launch.sessionId === placement.sessionId,
212
+ );
213
+
214
+ // ── the launched process IS the runtime — no shell wrapper ─────────────────────
215
+ // Measured with `--`: tmux execs the target directly. If a shell survived in the pane,
216
+ // pane_pid would be the wrapper and every later fact about "the runtime" would be about
217
+ // the wrong process.
218
+ const deadline = Date.now() + 5000;
219
+ let cmdline = "";
220
+ while (Date.now() < deadline) {
221
+ try {
222
+ cmdline = fs.readFileSync(`/proc/${launch.panePid}/cmdline`, "utf8").split("\0").filter(Boolean).join(" ");
223
+ } catch {
224
+ cmdline = "";
225
+ }
226
+ if (cmdline.includes("sleep")) break;
227
+ spawnSync("sleep", ["0.05"]);
228
+ }
229
+ ok(`launch: pane_pid is the runtime's own process, no shell wrapper (${cmdline})`, cmdline.includes("sleep 900"));
230
+ ok("launch: that process is alive", alive(launch.panePid));
231
+ ok(
232
+ "launch: tmux agrees the pane belongs to the launched window",
233
+ panes().some((p) => p.startsWith(`${launch.windowId}|${launch.paneId}|${launch.panePid}`)),
234
+ );
235
+
236
+ // ── THE TRAP: a failed exec is indistinguishable at launch time ────────────────
237
+ // This is the measured reason `launchPi` performs no post-launch presence check. The
238
+ // gate asserts the trap directly so the honesty of the receipt is a pinned fact rather
239
+ // than a claim in a comment.
240
+ {
241
+ const bogus = path.join(runtimeDir, "nope-not-here");
242
+ const args = [
243
+ "new-window",
244
+ "-d",
245
+ "-a",
246
+ "-t",
247
+ `${placement.sessionId}:{end}`,
248
+ "-P",
249
+ "-F",
250
+ "#{window_id}",
251
+ "--",
252
+ bogus,
253
+ ];
254
+ const r = spawnSync("tmux", args, { env: inherited, encoding: "utf8" });
255
+ const printed = (r.stdout ?? "").trim();
256
+ ok(
257
+ "trap: a failed exec still exits 0 and still prints a well-formed handle",
258
+ r.status === 0 && /^@[0-9]+$/.test(printed),
259
+ );
260
+ ok(
261
+ "trap: and that window is STILL LISTED on an immediate re-read — no synchronous check can tell it from a good launch",
262
+ windowPresent(printed),
263
+ );
264
+ const gone = Date.now() + 5000;
265
+ while (windowPresent(printed) && Date.now() < gone) spawnSync("sleep", ["0.05"]);
266
+ ok("trap: it disappears milliseconds later, through the same remain-on-exit path", !windowPresent(printed));
267
+ }
268
+
269
+ // ── binding: a foreign context is refused before mutating ─────────────────────
270
+ assert.throws(
271
+ () => launchPi({ ...placement, serverPid: "999999" }, inherited),
272
+ /context changed/,
273
+ "launchPi must refuse a placement from another server",
274
+ );
275
+ assert.throws(
276
+ () => launchPi({ ...placement, sessionId: "$999" }, inherited),
277
+ /context changed/,
278
+ "launchPi must refuse a placement from another session",
279
+ );
280
+ ok("binding: launch refuses a foreign server or session before opening anything", windows().length === 3);
281
+
282
+ // ── the argv the production builder emits is what actually ran ────────────────
283
+ ok(
284
+ "argv: the builder's shape is the one this acceptance exercised",
285
+ buildLaunchArgs(placement.sessionId, runtime).slice(-2).join(" ") === `-- ${runtime}`,
286
+ );
287
+
288
+ // ── cleanup by the operator's own hand ────────────────────────────────────────
289
+ process.kill(Number(launch.panePid), "SIGKILL");
290
+ const end = Date.now() + 5000;
291
+ while (windowPresent(launch.windowId) && Date.now() < end) spawnSync("sleep", ["0.05"]);
292
+ ok("teardown: killing the runtime removes its window (remain-on-exit off)", !windowPresent(launch.windowId));
293
+ const w5 = windows();
294
+ ok(`restored: windows are 1,2 (${w5.join(" ")})`, w5.length === 2);
295
+ ok("restored: exactly one session", sessionCount() === 1);
296
+ ok(
297
+ "restored: the original pane pids are the SAME processes",
298
+ originalPanes.every((p) => panes().includes(p)),
299
+ );
300
+ ok("restored: focus never moved", windows().find((w) => w.endsWith("|1")) === activeBefore);
301
+ } finally {
302
+ fx("kill-server");
303
+ if (fs.existsSync(SOCKET)) fs.rmSync(SOCKET, { force: true });
304
+ fs.rmSync(runtimeDir, { recursive: true, force: true });
305
+ }
306
+
307
+ ok("cleanup: the fixture socket is gone", !fs.existsSync(SOCKET));
308
+ ok("cleanup: the fixture server is unreachable", fx("list-sessions").status !== 0);
309
+
310
+ const after = operatorSnapshot();
311
+ ok("isolation: the operator's server is byte-identical before and after", before === after);
312
+
313
+ console.log(`\n${LABEL}: ${passed} checks passed`);
314
+ }
315
+
316
+ main();
@@ -0,0 +1,288 @@
1
+ /**
2
+ * check-mux-launch — deterministic gate for the T1-a visible launch
3
+ * (`pi-extensions/lib/mux-launch.ts`).
4
+ *
5
+ * Same scope discipline as `check-mux-placement`: this gate pins only what is decidable
6
+ * without tmux — the preconditions, the resolution rule, the argv shape, and the import
7
+ * boundary. The topology and the "a failed exec still prints a handle" trap are judged
8
+ * against a real private tmux server by `./run.sh check-mux-launch-tmux`, which drives THIS
9
+ * production code. There is still no fake tmux in this repo.
10
+ *
11
+ * The precondition assertions use a real temporary directory rather than a stubbed `fs`: the
12
+ * claim is about the filesystem answers (missing / directory / non-executable), and a stub
13
+ * would be asserting against a second implementation of `statSync` instead of the one the
14
+ * production path calls.
15
+ *
16
+ * MUX-LAUNCH-PRECONDITION-FIRST the runtime is proven launchable BEFORE any window opens
17
+ * MUX-LAUNCH-PATH-NO-SPLIT a whitespace-bearing runtime path is refused (tmux re-splits it)
18
+ * MUX-LAUNCH-PATH-HIT-NOT-SKIPPED that refusal is NOT a PATH skip — a later entry never takes over
19
+ * MUX-LAUNCH-NO-SHELL-ARGV the fixed runtime is passed after `--`, never as a string
20
+ * MUX-LAUNCH-NO-CARRIER argv is the append shape plus the runtime, nothing else
21
+ * MUX-LAUNCH-CORE-IMPORT-FREE the launch module and entwurf delivery never import each other
22
+ */
23
+
24
+ import assert from "node:assert/strict";
25
+ import fs from "node:fs";
26
+ import os from "node:os";
27
+ import path from "node:path";
28
+ import {
29
+ assertLaunchTarget,
30
+ buildLaunchArgs,
31
+ LaunchPreconditionError,
32
+ type LaunchRejectReason,
33
+ PI_RUNTIME_COMMAND,
34
+ resolvePiRuntime,
35
+ } from "../pi-extensions/lib/mux-launch.ts";
36
+ import { APPEND_FORMAT } from "../pi-extensions/lib/mux-placement.ts";
37
+
38
+ let passed = 0;
39
+ function ok(label: string, cond: boolean): void {
40
+ assert.ok(cond, label);
41
+ console.log(` ok ${label}`);
42
+ passed++;
43
+ }
44
+
45
+ /** Run `f` and report the LaunchRejectReason it refused with, or null if it did not refuse. */
46
+ function refusalOf(f: () => unknown): LaunchRejectReason | null {
47
+ try {
48
+ f();
49
+ return null;
50
+ } catch (err) {
51
+ return err instanceof LaunchPreconditionError ? err.reason : null;
52
+ }
53
+ }
54
+
55
+ const SESSION = "$11";
56
+
57
+ function main(): void {
58
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-mux-launch-"));
59
+ try {
60
+ const good = path.join(tmp, "pi");
61
+ fs.writeFileSync(good, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
62
+ const notExec = path.join(tmp, "pi-noexec");
63
+ fs.writeFileSync(notExec, "#!/bin/sh\nexit 0\n", { mode: 0o644 });
64
+ const dir = path.join(tmp, "pi-dir");
65
+ fs.mkdirSync(dir);
66
+ const spaced = path.join(tmp, "a b");
67
+ fs.mkdirSync(spaced);
68
+ const spacedPi = path.join(spaced, "pi");
69
+ fs.writeFileSync(spacedPi, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
70
+
71
+ // ── the runtime is proven BEFORE a window can exist ──────────────────────────────
72
+ // Measured on tmux 3.6a: `new-window -- /nonexistent/bin/nope` exits 0, prints a
73
+ // well-formed handle, and the window is still listed on an immediate re-read (10/10).
74
+ // So there is no post-launch check that can tell a failed exec from a good one — the
75
+ // only honest place to catch it is before the window exists.
76
+ ok(
77
+ "[QK:MUX-LAUNCH-PRECONDITION-FIRST] every unlaunchable runtime is refused with a NAMED reason before tmux is invoked — relative, missing, directory, and non-executable each refuse as themselves",
78
+ refusalOf(() => assertLaunchTarget("pi")) === "runtime-not-absolute" &&
79
+ refusalOf(() => assertLaunchTarget("./bin/pi")) === "runtime-not-absolute" &&
80
+ refusalOf(() => assertLaunchTarget(path.join(tmp, "nope"))) === "runtime-missing" &&
81
+ refusalOf(() => assertLaunchTarget(dir)) === "runtime-not-regular-file" &&
82
+ refusalOf(() => assertLaunchTarget(notExec)) === "runtime-not-executable",
83
+ );
84
+ ok("precondition: a real absolute executable passes", refusalOf(() => assertLaunchTarget(good)) === null);
85
+
86
+ // tmux re-splits a single argv element on whitespace (measured, 3.6a): a runtime at
87
+ // `/tmp/a b/pi` is launched as `/tmp/a` with `b/pi` as an argument. `--` prevents the
88
+ // shell, not the re-split, so this is its own invariant and not a tidier spelling of
89
+ // the checks above.
90
+ ok(
91
+ "[QK:MUX-LAUNCH-PATH-NO-SPLIT] a runtime path containing ANY whitespace is refused — tmux would re-split it into a truncated binary plus an argument — and the refusal precedes the filesystem questions, whose answers would be about the wrong file",
92
+ refusalOf(() => assertLaunchTarget(spacedPi)) === "runtime-path-whitespace" &&
93
+ refusalOf(() => assertLaunchTarget(path.join(tmp, "no such dir", "pi"))) === "runtime-path-whitespace" &&
94
+ refusalOf(() => assertLaunchTarget(`${tmp}/p\ti`)) === "runtime-path-whitespace" &&
95
+ refusalOf(() => assertLaunchTarget(`${tmp}/p\ni`)) === "runtime-path-whitespace",
96
+ );
97
+ ok(
98
+ "precondition: the refusal is a typed LaunchPreconditionError carrying the reason, not a bare Error",
99
+ (() => {
100
+ try {
101
+ assertLaunchTarget("pi");
102
+ return false;
103
+ } catch (err) {
104
+ return err instanceof LaunchPreconditionError && err.name === "LaunchPreconditionError";
105
+ }
106
+ })(),
107
+ );
108
+
109
+ // ── resolution: PATH, absolute, and never the cwd ────────────────────────────────
110
+ ok(
111
+ "resolve: the official runtime is found on PATH and returned as an absolute path",
112
+ resolvePiRuntime({ PATH: tmp }) === good,
113
+ );
114
+ ok(
115
+ "resolve: an empty PATH entry is skipped, never searched as cwd",
116
+ resolvePiRuntime({ PATH: `${path.delimiter}${tmp}` }) === good,
117
+ );
118
+ ok(
119
+ "resolve: a relative PATH entry is skipped — where the caller stood must not decide which binary launches",
120
+ resolvePiRuntime({ PATH: `.${path.delimiter}${tmp}` }) === good,
121
+ );
122
+ ok(
123
+ "resolve: a non-executable candidate does not shadow a later executable one",
124
+ (() => {
125
+ const shadow = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-mux-shadow-"));
126
+ try {
127
+ fs.writeFileSync(path.join(shadow, PI_RUNTIME_COMMAND), "x", { mode: 0o644 });
128
+ return resolvePiRuntime({ PATH: `${shadow}${path.delimiter}${tmp}` }) === good;
129
+ } finally {
130
+ fs.rmSync(shadow, { recursive: true, force: true });
131
+ }
132
+ })(),
133
+ );
134
+ // The refuse/skip split is a contract, not an accident of where the assertion sits. A
135
+ // candidate the shell ITSELF would skip (present but not executable) is skipped; a
136
+ // candidate the shell WOULD have picked (executable, but its path holds whitespace) is
137
+ // REFUSED. Falling through to a later PATH entry there would silently start a DIFFERENT
138
+ // install than the one the operator gets by typing `pi` — the drift this rail refuses —
139
+ // so which of the two a failure gets is pinned on its own claim. Moving
140
+ // `assertLaunchTarget` inside the loop's catch is exactly the refactor that flips it.
141
+ ok(
142
+ "[QK:MUX-LAUNCH-PATH-HIT-NOT-SKIPPED] a whitespace-bearing FIRST executable hit is REFUSED, never skipped to a later entry — the shell would have picked that one, so falling through would launch a different install than `pi` does",
143
+ refusalOf(() => resolvePiRuntime({ PATH: `${spaced}${path.delimiter}${tmp}` })) === "runtime-path-whitespace",
144
+ );
145
+ ok(
146
+ "resolve: no runtime anywhere on PATH refuses as runtime-unresolved, and a missing PATH is the same refusal",
147
+ refusalOf(() => resolvePiRuntime({ PATH: path.join(tmp, "empty") })) === "runtime-unresolved" &&
148
+ refusalOf(() => resolvePiRuntime({})) === "runtime-unresolved",
149
+ );
150
+ ok("resolve: the runtime name is the fixed official one", PI_RUNTIME_COMMAND === "pi");
151
+
152
+ // ── argv shape: the append shape plus the runtime, after `--` ────────────────────
153
+ // `--` is what keeps tmux from handing the target to a shell (measured: an argument of
154
+ // `x; touch <file>` created no file). It does NOT stop tmux re-splitting a spaced
155
+ // element, which is why the whitespace precondition above is separate and not cosmetic.
156
+ const args = buildLaunchArgs(SESSION, good);
157
+ ok(
158
+ "[QK:MUX-LAUNCH-NO-SHELL-ARGV] the fixed runtime is the LAST argv element and is preceded by `--`, so tmux execs it directly instead of running it through a shell",
159
+ args[args.length - 2] === "--" && args[args.length - 1] === good,
160
+ );
161
+ ok(
162
+ "[QK:MUX-LAUNCH-NO-CARRIER] launch argv is exactly the detached append shape plus the runtime — no -n/-c/-e/-b, no caller command, cwd, env or window name",
163
+ args.join(" ") === `new-window -d -a -t ${SESSION}:{end} -P -F ${APPEND_FORMAT} -- ${good}` &&
164
+ !["-n", "-c", "-e", "-b"].some((f) => args.includes(f)) &&
165
+ args.length === 10,
166
+ );
167
+ ok(
168
+ "argv: `-d` is kept — visible means a real window in the operator's session, not stolen focus",
169
+ args.includes("-d"),
170
+ );
171
+ ok(
172
+ "argv: the window is appended at {end} of the caller's own session, addressed by session id",
173
+ args.includes("-a") && args.includes(`${SESSION}:{end}`),
174
+ );
175
+ ok(
176
+ "argv: the printed handle format is the same stable @window/%pane row the leaf uses",
177
+ args.includes("-P") && args.includes(APPEND_FORMAT),
178
+ );
179
+ ok(
180
+ "argv: a non-native session selector is refused before tmux runs, exactly as the leaf refuses it",
181
+ ["$1; kill-server", "#{session_id}", "11", "@1", ""].every((s) => {
182
+ try {
183
+ buildLaunchArgs(s, good);
184
+ return false;
185
+ } catch {
186
+ return true;
187
+ }
188
+ }),
189
+ );
190
+ ok(
191
+ "argv: an unlaunchable runtime is refused by the builder too — the argv path cannot skip the precondition",
192
+ refusalOf(() => buildLaunchArgs(SESSION, dir)) === "runtime-not-regular-file" &&
193
+ refusalOf(() => buildLaunchArgs(SESSION, spacedPi)) === "runtime-path-whitespace",
194
+ );
195
+
196
+ // ── the import boundary (docs/mux-launch-rail.md §11) ───────────────────────────
197
+ // A new module is exactly when a forbidden line drifts, so it is asserted on source
198
+ // rather than trusted. Launch composition must not be reachable from the delivery
199
+ // core, and the launch module must not reach back into it.
200
+ //
201
+ // The docs' claim is NOT "these four files do not import it" — it is that NO production
202
+ // source imports it at all. An enumerated list proves the narrower thing and goes stale
203
+ // the moment a fifth delivery module is written, so the shipped source is WALKED. The
204
+ // corpus is asserted below to be the real one, because a scan is only as strong as what
205
+ // it covers: narrowing it later is how a generalized oracle turns into a false green.
206
+ const LAUNCH_MODULE = "pi-extensions/lib/mux-launch.ts";
207
+ const LAUNCH_SRC = fs.readFileSync(LAUNCH_MODULE, "utf8");
208
+
209
+ /** Module specifiers only — static `from "x"` and dynamic `import("x")`. A comment or a
210
+ * doc reference that merely NAMES a module is not an import, and the leaf's header now
211
+ * names the launch composition on purpose (see its export-seam note). */
212
+ function importsOf(src: string): string[] {
213
+ return [...src.matchAll(/(?:\bfrom|\bimport)\s*\(?\s*"([^"]+)"/g)].map((m) => m[1]);
214
+ }
215
+ const importsLaunch = (file: string): boolean =>
216
+ importsOf(fs.readFileSync(file, "utf8")).some((spec) => spec.includes("mux-launch"));
217
+
218
+ /** Every `.ts` under a shipped source root. `dist/` is a compiled copy of `src/` (a build
219
+ * artifact, not authored source — scanning it would double-count and drift), and
220
+ * `node_modules` is not ours. */
221
+ function walkTs(root: string): string[] {
222
+ const out: string[] = [];
223
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
224
+ const child = `${root}/${entry.name}`;
225
+ if (entry.isDirectory()) {
226
+ if (entry.name === "node_modules" || entry.name === "dist") continue;
227
+ out.push(...walkTs(child));
228
+ } else if (entry.isFile() && child.endsWith(".ts")) {
229
+ out.push(child);
230
+ }
231
+ }
232
+ return out;
233
+ }
234
+ const DELIVERY_MODULES = [
235
+ "pi-extensions/lib/entwurf-v2-contract.ts",
236
+ "pi-extensions/lib/entwurf-v2-decider.ts",
237
+ "pi-extensions/lib/entwurf-v2-runner.ts",
238
+ "pi-extensions/lib/entwurf-v2-production.ts",
239
+ ];
240
+ const PRODUCTION_SOURCES = [...walkTs("pi-extensions"), ...walkTs("mcp")].filter((f) => f !== LAUNCH_MODULE);
241
+ /** The allowed importers. T1-a shipped with none at all; the fresh-call composition was the
242
+ * first, and S1's resume-call composition is the second. Both are allowed BECAUSE they are
243
+ * the layer above — they reuse the launch preconditions rather than copying them, while
244
+ * delivery stays as far away as it ever was. The set is exact, not a prefix rule: "any
245
+ * mux-* file may import launch" would let a future module in without a decision. */
246
+ const FRESH_CALL_MODULE = "pi-extensions/lib/mux-fresh-call.ts";
247
+ const RESUME_CALL_MODULE = "pi-extensions/lib/mux-resume-call.ts";
248
+ const ALLOWED_LAUNCH_IMPORTERS = [FRESH_CALL_MODULE, RESUME_CALL_MODULE];
249
+ ok(
250
+ "corpus: the scanned corpus IS the shipped source — both roots, every delivery module and the bridge entrypoint present by name, no build artifacts, and the launch module itself the only exclusion",
251
+ PRODUCTION_SOURCES.length >= 40 &&
252
+ DELIVERY_MODULES.every((m) => PRODUCTION_SOURCES.includes(m)) &&
253
+ PRODUCTION_SOURCES.includes("pi-extensions/lib/mux-placement.ts") &&
254
+ PRODUCTION_SOURCES.includes("pi-extensions/entwurf-control.ts") &&
255
+ PRODUCTION_SOURCES.includes("mcp/entwurf-bridge/src/index.ts") &&
256
+ !PRODUCTION_SOURCES.includes(LAUNCH_MODULE) &&
257
+ PRODUCTION_SOURCES.every((f) => !f.includes("node_modules") && !f.includes("/dist/")),
258
+ );
259
+ ok(
260
+ "[QK:MUX-LAUNCH-CORE-IMPORT-FREE] launch is not a delivery or core dependency: the ONLY shipped files that may import mux-launch are the two compositions above it — fresh-call and resume-call — not the four delivery modules, not anything else, and mux-launch still imports no entwurf core",
261
+ PRODUCTION_SOURCES.filter((m) => !ALLOWED_LAUNCH_IMPORTERS.includes(m)).every((m) => !importsLaunch(m)) &&
262
+ ALLOWED_LAUNCH_IMPORTERS.every((m) => importsLaunch(m)) &&
263
+ !/from\s+"\.\/(entwurf|meta)-[^"]*"/.test(LAUNCH_SRC),
264
+ );
265
+ ok(
266
+ "boundary: mux-launch imports only node builtins and the placement leaf",
267
+ importsOf(LAUNCH_SRC).every((spec) => spec.startsWith("node:") || spec === "./mux-placement.ts"),
268
+ );
269
+ ok(
270
+ "boundary: the placement leaf does not import the launch module — the leaf stays deletable on its own",
271
+ !importsLaunch("pi-extensions/lib/mux-placement.ts"),
272
+ );
273
+ // Prose is not behaviour: the module header names identity vocabulary precisely to say
274
+ // it owns none of it, so this assertion reads CODE with the comments stripped. A check
275
+ // that failed on its own documentation would push the boundary out of the docs.
276
+ const LAUNCH_CODE = LAUNCH_SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
277
+ ok(
278
+ "boundary: the launch leaf stays identity-free — no code here reads a record store, a garden id, or a session-id carrier",
279
+ !/gardenId|nativeSessionId|session-id|meta-session|entwurf_v2/.test(LAUNCH_CODE),
280
+ );
281
+
282
+ console.log(`\ncheck-mux-launch: ${passed} checks passed`);
283
+ } finally {
284
+ fs.rmSync(tmp, { recursive: true, force: true });
285
+ }
286
+ }
287
+
288
+ main();