@junghanacs/entwurf 0.30.0 → 0.31.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 (69) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +229 -0
  3. package/DELIVERY.md +1 -0
  4. package/README.md +49 -4
  5. package/VERIFY.md +23 -4
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +111 -18
  8. package/docs/setup-clean-host.md +104 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +43 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
  20. package/mcp/entwurf-bridge/src/index.ts +7 -7
  21. package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
  22. package/package.json +15 -12
  23. package/pi/entwurf-capabilities.json +1 -0
  24. package/pi/pi-durable/bootstrap.mjs +281 -0
  25. package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
  26. package/pi/pi-durable/overlay/upstream-pin.json +16 -0
  27. package/pi-extensions/lib/acp/backend-adapter.ts +68 -0
  28. package/pi-extensions/lib/acp/backend.ts +41 -5
  29. package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
  30. package/pi-extensions/lib/fresh-call-composition.ts +42 -7
  31. package/pi-extensions/lib/meta-sender-identity.ts +7 -1
  32. package/pi-extensions/lib/meta-session.ts +28 -2
  33. package/pi-extensions/lib/mux-fresh-call.ts +34 -2
  34. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
  35. package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
  36. package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
  37. package/run.sh +214 -36
  38. package/scripts/check-fresh-call-dispatch.ts +18 -0
  39. package/scripts/check-gate-qualification.ts +4 -2
  40. package/scripts/check-herdr-plugin-build.ts +159 -57
  41. package/scripts/check-pi-durable-contact.ts +834 -0
  42. package/scripts/check-pi-durable-receive.ts +1116 -0
  43. package/scripts/check-pi-durable-send.ts +1661 -0
  44. package/scripts/fixtures/herdr-supply.json +8 -8
  45. package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
  46. package/scripts/lib/pi-durable-r1.ts +100 -0
  47. package/scripts/lib/pi-durable-reap.ts +27 -0
  48. package/scripts/lib/pi-durable-receive.ts +137 -0
  49. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  50. package/scripts/mutants/acp-vendor-rejection.json +62 -0
  51. package/scripts/mutants/control-send-receipt.json +1 -1
  52. package/scripts/mutants/copilot-birth.json +2 -2
  53. package/scripts/mutants/herdr-plugin-build.json +11 -0
  54. package/scripts/mutants/herdr-supply.json +3 -3
  55. package/scripts/mutants/meta-hook-session-switch.json +4 -2
  56. package/scripts/mutants/mux-fresh-call.json +69 -3
  57. package/scripts/mutants/omp-birth.json +2 -2
  58. package/scripts/mutants/omp-fresh.json +6 -4
  59. package/scripts/mutants/pack-install.json +3 -3
  60. package/scripts/mutants/pi-durable.json +390 -0
  61. package/scripts/mutants/pi-floor.json +2 -2
  62. package/scripts/pi-durable-contact-host.mjs +105 -0
  63. package/scripts/pi-durable-receive-host.mjs +127 -0
  64. package/scripts/pi-durable-send-host.mjs +213 -0
  65. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  66. package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
  67. package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
  68. package/scripts/smoke-setup-verdict.sh +21 -2
  69. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,834 @@
1
+ /**
2
+ * G-contact (#129) — the real overlay durable app, the real compiled bridge, one host.
3
+ * CHECKOUT-ONLY dev gate: an installed package refuses it in `run_ts` before it reads an input.
4
+ *
5
+ * LABEL: adapter-direct, harness-bypassed. The durable app is opened through the production
6
+ * bootstrap composition against a provisioned overlay checkout, the contact is born, and its
7
+ * registered tools are executed DIRECTLY in the host (scripts/pi-durable-contact-host.mjs). No
8
+ * model turn, no ToolTask: this gate does NOT prove that the harness selects, validates or
9
+ * replays these tools, that a model accepts their schema, or anything about send / delivery /
10
+ * fresh / resume / visible identity. Of receive it observes only the ARM — the raw receiver marker,
11
+ * the contact's own `receiver()` state and the bridge's replyability verdict — never a doorbell, an
12
+ * admission or a drain; those are check-pi-durable-receive's.
13
+ *
14
+ * Inputs, both required, neither provisioned here (missing → SKIP, never a pass):
15
+ * ENTWURF_PI_DURABLE_RUNTIME an upstream checkout at pi/pi-durable/overlay's pin,
16
+ * with exactly the overlay patch applied, its deps installed
17
+ * and the pin's git-ignored model data in place
18
+ * ENTWURF_PI_DURABLE_BRIDGE_ENTRY <bundle>/mcp/entwurf-bridge/src/index.js emitted from THIS
19
+ * checkout into a private bundle (a shared/installed bridge
20
+ * carries the old backend list and is refused as stale) — the
21
+ * bundle must carry the `pi-durable-host` owner join
22
+ *
23
+ * Matrix: garden roots {default (PI_CODING_AGENT_DIR), explicit (contact options, distinct from
24
+ * the default)} × open {fresh, --continue}. The explicit cells are the root-coupling proof: the
25
+ * child inherits an environment that points at the DEFAULT roots, so it can only find the birth
26
+ * if the contact bound it to the roots it wrote.
27
+ *
28
+ * Oracles are independent of the subject and of the bridge's sender resolver: records and
29
+ * markers are read as raw JSON from roots this gate built, the durable session directory is
30
+ * located by hashing the project path here, the bridge child is found by scanning /proc for a
31
+ * process whose PPID is the host pid this gate spawned, and peers is asked through a SECOND bridge
32
+ * whose parent is this gate — also the negative control that the host's identity does not leak
33
+ * to a process it did not spawn. The host runs with poisoned identity carriers; the cell is green
34
+ * only if the contact keeps them from its bridge child. That is proven at the SDK CONNECTOR BOUNDARY
35
+ * — the spawn spec the adapter hands the production stdio connector, recorded by the host driver
36
+ * (identity carrier names and the four root values only) — plus the bridge's own behaviour
37
+ * (entwurf_self, the raw records/markers, peers). It is NOT a kernel child-environ observation:
38
+ * this gate reads no /proc/<pid>/environ.
39
+ *
40
+ * After a host is spawned, every failure is THROWN, and the gate reaps only the processes it
41
+ * owns — the host it spawned and the bridges observed as that host's children (pid + start key) —
42
+ * before reporting the cause and any cleanup failure. `ENTWURF_PI_DURABLE_CONTACT_FAULT` is the
43
+ * gate's own control for that path: `after-birth` throws right after the first birth;
44
+ * `malformed-report` makes the gate's host driver send a non-JSON report line, which arrives while
45
+ * the gate is already waiting for it (the pending-line path of `Host.next`).
46
+ */
47
+ import { type ChildProcess, spawn, spawnSync } from "node:child_process";
48
+ import { createHash } from "node:crypto";
49
+ import * as fs from "node:fs";
50
+ import * as os from "node:os";
51
+ import * as path from "node:path";
52
+ import { createInterface } from "node:readline";
53
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
54
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
55
+ import {
56
+ inspectPiDurableRuntime,
57
+ PI_DURABLE_RESOLVER_RELATIVE,
58
+ readPiDurablePin,
59
+ } from "../pi-extensions/lib/pi-durable-runtime.ts";
60
+ import { skipLive } from "./lib/live-skip.ts";
61
+
62
+ const LABEL = "check-pi-durable-contact";
63
+ const REPO = path.resolve(import.meta.dirname, "..");
64
+ const OVERLAY = path.join(REPO, "pi", "pi-durable", "overlay");
65
+ const HOST_DRIVER = path.join(REPO, "scripts", "pi-durable-contact-host.mjs");
66
+ const REPORT_TIMEOUT_MS = 90_000;
67
+ const CLOSE_TIMEOUT_MS = 30_000;
68
+ const REAP_TIMEOUT_MS = 5_000;
69
+ const FAULT = process.env.ENTWURF_PI_DURABLE_CONTACT_FAULT?.trim() || undefined;
70
+ if (FAULT !== undefined && FAULT !== "after-birth" && FAULT !== "malformed-report") {
71
+ console.error(
72
+ `${LABEL}: unknown ENTWURF_PI_DURABLE_CONTACT_FAULT ${JSON.stringify(FAULT)} (only "after-birth", "malformed-report")`,
73
+ );
74
+ process.exit(2);
75
+ }
76
+
77
+ const runtimeInput = process.env.ENTWURF_PI_DURABLE_RUNTIME?.trim();
78
+ const bridgeInput = process.env.ENTWURF_PI_DURABLE_BRIDGE_ENTRY?.trim();
79
+ if (!runtimeInput || !bridgeInput) {
80
+ skipLive(
81
+ LABEL,
82
+ "set ENTWURF_PI_DURABLE_RUNTIME=<overlay checkout at the pin, patch applied, deps and model data installed> and " +
83
+ "ENTWURF_PI_DURABLE_BRIDGE_ENTRY=<bridge index.js emitted from this checkout into a private bundle>; " +
84
+ "this gate provisions neither",
85
+ );
86
+ }
87
+ const runtime = path.resolve(runtimeInput);
88
+ const bridgeEntry = path.resolve(bridgeInput);
89
+
90
+ let passed = 0;
91
+ let failed = 0;
92
+ function ok(label: string, cond: boolean): void {
93
+ if (cond) {
94
+ console.log(` ok ${label}`);
95
+ passed++;
96
+ } else {
97
+ console.log(` FAIL ${label}`);
98
+ failed++;
99
+ }
100
+ }
101
+ /** Pre-spawn only: nothing is owned yet, so exiting leaves nothing behind. */
102
+ function fatal(message: string): never {
103
+ console.error(`${LABEL}: ${message}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ const sha256 = (data: string | Buffer): string => createHash("sha256").update(data).digest("hex");
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // 0. Inputs. A provisioned-but-wrong input is broken, not absent: these FAIL, they do not skip.
111
+ // ---------------------------------------------------------------------------
112
+ // The pin and the runtime facts come from the ONE verifier the managed launch, setup and the fresh
113
+ // preflight use (pi-extensions/lib/pi-durable-runtime.ts); this gate keeps its finer-grained QKs over
114
+ // those facts and adds the reverse-apply check only it makes.
115
+ const { pin, patchBytes } = readPiDurablePin(OVERLAY);
116
+ const patchFile = path.join(OVERLAY, pin.patches[0] as string);
117
+ const facts = inspectPiDurableRuntime(runtime, OVERLAY);
118
+ const reverse = spawnSync("git", ["-C", runtime, "apply", "-R", "--check", patchFile]);
119
+ console.log("0. inputs: tracked overlay, ignored model data, bridge bundle registry");
120
+ ok(
121
+ `[QK:PDC-PIN] runtime HEAD is the overlay pin ${pin.commit} (got ${facts.head || "no answer"})`,
122
+ facts.gitAnswered && facts.head === pin.commit,
123
+ );
124
+ ok(
125
+ `[QK:PDC-TRACKED-OVERLAY-EXACT] the TRACKED tree is exactly pin + overlay patch and no non-ignored file is untracked (index ${facts.indexMatchesHead ? "==" : "!="} HEAD, diff ${facts.diffEqualsPatch ? "==" : "!="} patch ${sha256(patchBytes).slice(0, 12)}, untracked ${facts.untracked || "none"}) — git-ignored runtime inputs are outside this assertion`,
126
+ facts.gitAnswered && facts.indexMatchesHead && facts.diffEqualsPatch && facts.untracked === "",
127
+ );
128
+ ok("[QK:PDC-OVERLAY-REVERSE] the patch reverse-applies cleanly", reverse.status === 0);
129
+ ok(
130
+ `[QK:PDC-MODEL-DATA-DIGEST] the git-ignored model data is the pin's recorded bytes (${facts.modelDataFiles} files, aggregate ${facts.modelDataAggregate.slice(0, 12)}, manifest ${facts.manifestSha256.slice(0, 12)})`,
131
+ facts.modelDataFiles === pin.modelData.files &&
132
+ facts.modelDataAggregate === pin.modelData.aggregateSha256 &&
133
+ facts.manifestSha256 === pin.modelData.manifestSha256,
134
+ );
135
+ const resolver = path.join(runtime, PI_DURABLE_RESOLVER_RELATIVE);
136
+ ok("[QK:PDC-RESOLVER] the checkout carries the experimental source resolver", facts.resolverPresent);
137
+ const bundleRoot = path.resolve(bridgeEntry, "..", "..", "..", "..");
138
+ const bundleRegistry = path.join(bundleRoot, "pi-extensions", "entwurf-capabilities.json");
139
+ const repoRegistry = path.join(REPO, "pi", "entwurf-capabilities.json");
140
+ ok(
141
+ `[QK:PDC-BUNDLE-REGISTRY-MATCH] the bridge bundle carries a capability registry byte-identical to this checkout's (${bundleRegistry}) — a registry fact only; the bundle's code provenance is its build receipt, not this assertion`,
142
+ fs.existsSync(bridgeEntry) &&
143
+ fs.existsSync(bundleRegistry) &&
144
+ fs.readFileSync(bundleRegistry).equals(fs.readFileSync(repoRegistry)),
145
+ );
146
+ // The compiled bundle's owner join is what decides the armed host's replyability below; a bundle
147
+ // emitted before the join would answer for the wrong source. Read from the emitted bytes, not inferred.
148
+ const bundleDeliverability = path.join(bundleRoot, "pi-extensions", "lib", "entwurf-deliverability.js");
149
+ const joinLine = fs.existsSync(bundleDeliverability)
150
+ ? (/SENDER_JOINED_RECEIVER_OWNER_KINDS\s*=\s*(\[[^\]]*\])/.exec(fs.readFileSync(bundleDeliverability, "utf8"))?.[1] ??
151
+ "")
152
+ : "";
153
+ ok(
154
+ `[QK:PDC-BUNDLE-OWNER-JOIN] the bridge bundle's compiled owner join admits pi-durable-host (${joinLine || "not found"})`,
155
+ joinLine.includes('"pi-durable-host"'),
156
+ );
157
+ if (failed > 0)
158
+ fatal("inputs are not the pinned overlay, its model data, this checkout's registry and owner join — refusing to run");
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Operator guard — COARSE and pi-durable-specific: the operator's own roots gain no pi-durable
162
+ // record, sender dir or durable session tree. Not a proof about any other operator byte.
163
+ // ---------------------------------------------------------------------------
164
+ const operatorAgent = path.join(os.homedir(), ".pi", "agent");
165
+ function operatorPiDurableFacts(): string {
166
+ const sessions = path.join(operatorAgent, "meta-sessions");
167
+ const durableRecords = fs.existsSync(sessions)
168
+ ? fs
169
+ .readdirSync(sessions)
170
+ .filter((n) => n.endsWith(".meta.json"))
171
+ .filter((n) => {
172
+ try {
173
+ return (
174
+ (JSON.parse(fs.readFileSync(path.join(sessions, n), "utf8")) as { backend?: unknown }).backend ===
175
+ "pi-durable"
176
+ );
177
+ } catch {
178
+ return false;
179
+ }
180
+ })
181
+ : [];
182
+ const senders = fs.existsSync(path.join(operatorAgent, "meta-senders", "pi-durable"));
183
+ const durableSessions = fs.existsSync(path.join(operatorAgent, "experimental", "durable-sessions"));
184
+ const receiversDir = path.join(operatorAgent, "meta-receivers");
185
+ const durableReceivers = fs.existsSync(receiversDir)
186
+ ? fs
187
+ .readdirSync(receiversDir)
188
+ .filter((n) => n.endsWith(".json"))
189
+ .filter((n) => {
190
+ try {
191
+ return (
192
+ (JSON.parse(fs.readFileSync(path.join(receiversDir, n), "utf8")) as { backend?: unknown }).backend ===
193
+ "pi-durable"
194
+ );
195
+ } catch {
196
+ return false;
197
+ }
198
+ })
199
+ : [];
200
+ return JSON.stringify({ durableRecords, senders, durableSessions, durableReceivers });
201
+ }
202
+ const operatorBefore = operatorPiDurableFacts();
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Sandbox: private root, env built from nothing, children at umask 022.
206
+ // ---------------------------------------------------------------------------
207
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-durable-contact-"));
208
+ const dirs = {
209
+ home: path.join(root, "home"),
210
+ agent: path.join(root, "agent"),
211
+ tmp: path.join(root, "tmp"),
212
+ projectDefault: path.join(root, "project-default"),
213
+ projectExplicit: path.join(root, "project-explicit"),
214
+ explicitSessions: path.join(root, "explicit-roots", "sessions"),
215
+ explicitSenders: path.join(root, "explicit-roots", "senders"),
216
+ explicitMailbox: path.join(root, "explicit-roots", "mailbox"),
217
+ explicitReceivers: path.join(root, "explicit-roots", "receivers"),
218
+ xdgConfig: path.join(root, "xdg", "config"),
219
+ xdgCache: path.join(root, "xdg", "cache"),
220
+ xdgData: path.join(root, "xdg", "data"),
221
+ xdgState: path.join(root, "xdg", "state"),
222
+ };
223
+ for (const dir of Object.values(dirs)) fs.mkdirSync(dir, { recursive: true });
224
+ process.umask(0o022);
225
+ const sandboxEnv: Record<string, string> = {
226
+ PATH: process.env.PATH ?? "/run/current-system/sw/bin",
227
+ HOME: dirs.home,
228
+ TMPDIR: dirs.tmp,
229
+ XDG_CONFIG_HOME: dirs.xdgConfig,
230
+ XDG_CACHE_HOME: dirs.xdgCache,
231
+ XDG_DATA_HOME: dirs.xdgData,
232
+ XDG_STATE_HOME: dirs.xdgState,
233
+ PI_CODING_AGENT_DIR: dirs.agent,
234
+ PI_OFFLINE: "1",
235
+ NOSYSBASHRC: "1",
236
+ TERM: "dumb",
237
+ };
238
+ const poisoned = {
239
+ PI_SESSION_ID: "20260101T000000-000000",
240
+ PI_AGENT_ID: "pi/poisoned",
241
+ ENTWURF_META_SENDER_MARKER: path.join(root, "poisoned-marker.json"),
242
+ ENTWURF_BRIDGE_NATIVE_HOST: "codex",
243
+ };
244
+ const hostEnv: Record<string, string> = {
245
+ ...sandboxEnv,
246
+ ...poisoned,
247
+ ...(FAULT === "malformed-report" ? { ENTWURF_PI_DURABLE_CONTACT_FAULT: FAULT } : {}),
248
+ };
249
+
250
+ interface RootsCase {
251
+ name: "default" | "explicit";
252
+ project: string;
253
+ sessionsDir: string;
254
+ sendersDir: string;
255
+ mailboxDir: string;
256
+ receiversDir: string;
257
+ /** Extra host-driver argv: explicit roots go to the contact; default passes none. */
258
+ hostArgs: string[];
259
+ }
260
+ const defaultRoots: RootsCase = {
261
+ name: "default",
262
+ project: dirs.projectDefault,
263
+ sessionsDir: path.join(dirs.agent, "meta-sessions"),
264
+ sendersDir: path.join(dirs.agent, "meta-senders"),
265
+ mailboxDir: path.join(dirs.agent, "meta-mailbox"),
266
+ receiversDir: path.join(dirs.agent, "meta-receivers"),
267
+ hostArgs: [],
268
+ };
269
+ const explicitRoots: RootsCase = {
270
+ name: "explicit",
271
+ project: dirs.projectExplicit,
272
+ sessionsDir: dirs.explicitSessions,
273
+ sendersDir: dirs.explicitSenders,
274
+ mailboxDir: dirs.explicitMailbox,
275
+ receiversDir: dirs.explicitReceivers,
276
+ hostArgs: [dirs.explicitSessions, dirs.explicitSenders, dirs.explicitMailbox, dirs.explicitReceivers],
277
+ };
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // /proc helpers
281
+ // ---------------------------------------------------------------------------
282
+ function procStat(pid: number): { ppid: number; start: string } | null {
283
+ try {
284
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
285
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
286
+ return { ppid: Number(fields[1]), start: fields[19] as string };
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
291
+
292
+ function procArgv(pid: number): string[] {
293
+ try {
294
+ return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
295
+ } catch {
296
+ return [];
297
+ }
298
+ }
299
+
300
+ /** Every process whose PPID is `parent` and whose argv runs `entry`. */
301
+ function childrenRunning(parent: number, entry: string): number[] {
302
+ return fs
303
+ .readdirSync("/proc")
304
+ .filter((n) => /^\d+$/.test(n))
305
+ .map(Number)
306
+ .filter((pid) => procStat(pid)?.ppid === parent && procArgv(pid).includes(entry));
307
+ }
308
+
309
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
310
+
311
+ /** Wait (bounded) until `pid` with start key `start` no longer exists. */
312
+ async function goneWithin(pid: number, start: string, ms: number): Promise<boolean> {
313
+ const deadline = Date.now() + ms;
314
+ while (Date.now() < deadline) {
315
+ if (procStat(pid)?.start !== start) return true;
316
+ await sleep(50);
317
+ }
318
+ return procStat(pid)?.start !== start;
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // Owned hosts: spawned here, reaped here — nothing else is touched.
323
+ // ---------------------------------------------------------------------------
324
+ interface OwnedProcess {
325
+ pid: number;
326
+ start: string;
327
+ }
328
+
329
+ interface Host {
330
+ child: ChildProcess;
331
+ pid: number;
332
+ start: string;
333
+ /** Bridges observed as this host's children, recorded while the host was alive. */
334
+ bridges: OwnedProcess[];
335
+ next(timeoutMs: number): Promise<Record<string, unknown>>;
336
+ stderr(): string;
337
+ exitCode: number | null | undefined;
338
+ exited: Promise<number | null>;
339
+ }
340
+
341
+ const owned: Host[] = [];
342
+
343
+ function observeBridges(host: Host): void {
344
+ for (const pid of childrenRunning(host.pid, bridgeEntry)) {
345
+ const start = procStat(pid)?.start;
346
+ if (start !== undefined && !host.bridges.some((b) => b.pid === pid)) host.bridges.push({ pid, start });
347
+ }
348
+ }
349
+
350
+ function startHost(roots: RootsCase, mode: "new" | "continue"): Host {
351
+ const child = spawn(process.execPath, ["--import", resolver, HOST_DRIVER, bridgeEntry, mode, ...roots.hostArgs], {
352
+ cwd: roots.project,
353
+ env: hostEnv,
354
+ stdio: ["pipe", "pipe", "pipe"],
355
+ });
356
+ // Stream and process errors are recorded and surface through next()/exit — never as an
357
+ // uncaught event that would skip the single cleanup (a write to an exited host's stdin is EPIPE).
358
+ let streamError = "";
359
+ child.on("error", (error) => {
360
+ streamError += ` [host process error: ${error.message}]`;
361
+ });
362
+ child.stdin?.on("error", (error) => {
363
+ streamError += ` [host stdin error: ${error.message}]`;
364
+ });
365
+ const pid = child.pid;
366
+ if (pid === undefined) throw new Error(`the host did not spawn${streamError}`);
367
+ let stderr = "";
368
+ child.stderr?.on("data", (chunk: Buffer) => {
369
+ stderr = (stderr + chunk.toString("utf8")).slice(-8192);
370
+ });
371
+ const queue: string[] = [];
372
+ const waiters: ((line: string | null) => void)[] = [];
373
+ let ended = false;
374
+ createInterface({ input: child.stdout as NodeJS.ReadableStream })
375
+ .on("line", (line) => {
376
+ const waiter = waiters.shift();
377
+ if (waiter) waiter(line);
378
+ else queue.push(line);
379
+ })
380
+ .on("close", () => {
381
+ ended = true;
382
+ for (const waiter of waiters.splice(0)) waiter(null);
383
+ });
384
+ const host: Host = {
385
+ child,
386
+ pid,
387
+ start: procStat(pid)?.start ?? "",
388
+ bridges: [],
389
+ stderr: () => stderr,
390
+ exitCode: undefined,
391
+ exited: new Promise<number | null>((resolve) =>
392
+ child.on("exit", (code) => {
393
+ host.exitCode = code;
394
+ resolve(code);
395
+ }),
396
+ ),
397
+ next(timeoutMs) {
398
+ const parse = (line: string, path: "queued" | "pending"): Record<string, unknown> => {
399
+ try {
400
+ return JSON.parse(line) as Record<string, unknown>;
401
+ } catch (error) {
402
+ throw new Error(
403
+ `host sent a malformed line on the ${path}-line path (${(error as Error).message}): ${line.slice(0, 200)}`,
404
+ );
405
+ }
406
+ };
407
+ return new Promise((resolve, reject) => {
408
+ const queued = queue.shift();
409
+ if (queued !== undefined) return resolve(parse(queued, "queued"));
410
+ if (ended) return reject(new Error(`host ended without a line${streamError}; stderr: ${stderr.trim()}`));
411
+ const timer = setTimeout(
412
+ () => reject(new Error(`host gave no line within ${timeoutMs}ms${streamError}; stderr: ${stderr.trim()}`)),
413
+ timeoutMs,
414
+ );
415
+ // Runs in the readline callback, not the executor: a throw here would escape as an
416
+ // uncaught exception, so every outcome is settled explicitly.
417
+ waiters.push((line) => {
418
+ clearTimeout(timer);
419
+ if (line === null) {
420
+ reject(new Error(`host ended without a line${streamError}; stderr: ${stderr.trim()}`));
421
+ return;
422
+ }
423
+ try {
424
+ resolve(parse(line, "pending"));
425
+ } catch (error) {
426
+ reject(error);
427
+ }
428
+ });
429
+ });
430
+ },
431
+ };
432
+ owned.push(host);
433
+ return host;
434
+ }
435
+
436
+ /** Ask the host to close; it must answer and exit 0 within the bound. */
437
+ async function closeHost(host: Host): Promise<{ closed: boolean; code: number | null | undefined }> {
438
+ host.child.stdin?.write("close\n");
439
+ const line = await host.next(CLOSE_TIMEOUT_MS);
440
+ await Promise.race([host.exited, sleep(CLOSE_TIMEOUT_MS)]);
441
+ return { closed: line.closed === true, code: host.exitCode };
442
+ }
443
+
444
+ /**
445
+ * Reap one owned host and the bridges observed under it: TERM, bounded wait, KILL, bounded wait.
446
+ * A pid is signalled only while its start key still matches what was recorded.
447
+ */
448
+ async function reap(host: Host): Promise<string[]> {
449
+ const problems: string[] = [];
450
+ if (host.exitCode === undefined && procStat(host.pid)?.start === host.start) observeBridges(host);
451
+ const targets: (OwnedProcess & { what: string })[] = [
452
+ { pid: host.pid, start: host.start, what: "host" },
453
+ ...host.bridges.map((b) => ({ ...b, what: "bridge" })),
454
+ ];
455
+ for (const target of targets) {
456
+ if (procStat(target.pid)?.start !== target.start) continue;
457
+ try {
458
+ process.kill(target.pid, "SIGTERM");
459
+ } catch (error) {
460
+ problems.push(`${target.what} ${target.pid}: SIGTERM failed (${(error as Error).message})`);
461
+ }
462
+ if (await goneWithin(target.pid, target.start, REAP_TIMEOUT_MS)) continue;
463
+ try {
464
+ process.kill(target.pid, "SIGKILL");
465
+ } catch (error) {
466
+ problems.push(`${target.what} ${target.pid}: SIGKILL failed (${(error as Error).message})`);
467
+ }
468
+ if (!(await goneWithin(target.pid, target.start, REAP_TIMEOUT_MS))) {
469
+ problems.push(`${target.what} ${target.pid} is still alive after SIGKILL`);
470
+ }
471
+ }
472
+ return problems;
473
+ }
474
+
475
+ // ---------------------------------------------------------------------------
476
+ // Independent oracles
477
+ // ---------------------------------------------------------------------------
478
+ function recordsIn(dir: string): Record<string, unknown>[] {
479
+ if (!fs.existsSync(dir)) return [];
480
+ return fs
481
+ .readdirSync(dir)
482
+ .filter((n) => n.endsWith(".meta.json"))
483
+ .map((n) => JSON.parse(fs.readFileSync(path.join(dir, n), "utf8")) as Record<string, unknown>);
484
+ }
485
+
486
+ /**
487
+ * Durable session directories for a project, located by hashing its real path here. Only names in
488
+ * the vendor's session grammar count: proper-lockfile keeps a `<session>.lock` DIRECTORY beside a
489
+ * locked session (measured, first G-contact run), and that is not a session.
490
+ */
491
+ const DURABLE_SESSION_NAME = /^\d{13}-[0-9a-f-]{36}$/;
492
+ function durableSessionDirs(project: string): string[] {
493
+ const dir = path.join(
494
+ dirs.agent,
495
+ "experimental",
496
+ "durable-sessions",
497
+ createHash("sha256").update(fs.realpathSync(project)).digest("hex").slice(0, 24),
498
+ );
499
+ return fs.existsSync(dir)
500
+ ? fs.readdirSync(dir).filter((n) => DURABLE_SESSION_NAME.test(n) && fs.statSync(path.join(dir, n)).isDirectory())
501
+ : [];
502
+ }
503
+
504
+ function lastJsonLine(text: string): Record<string, unknown> {
505
+ const line = text.trim().split("\n").at(-1) ?? "";
506
+ try {
507
+ return JSON.parse(line) as Record<string, unknown>;
508
+ } catch {
509
+ return {};
510
+ }
511
+ }
512
+
513
+ /** A bridge spawned by THIS gate on the case's roots — its parent is not the host. */
514
+ async function outsideBridge(roots: RootsCase): Promise<{ peers: string; self: { text: string; isError: boolean } }> {
515
+ const transport = new StdioClientTransport({
516
+ command: process.execPath,
517
+ args: [bridgeEntry],
518
+ env: {
519
+ ...sandboxEnv,
520
+ ENTWURF_META_SESSIONS_DIR: roots.sessionsDir,
521
+ ENTWURF_META_SENDERS_DIR: roots.sendersDir,
522
+ ENTWURF_META_MAILBOX_DIR: roots.mailboxDir,
523
+ ENTWURF_META_RECEIVERS_DIR: roots.receiversDir,
524
+ },
525
+ cwd: roots.project,
526
+ stderr: "pipe",
527
+ });
528
+ transport.stderr?.on("data", () => {});
529
+ const client = new Client({ name: "check-pi-durable-contact", version: "1" });
530
+ try {
531
+ await client.connect(transport);
532
+ } catch (error) {
533
+ // A failed connect may already have spawned the bridge: close the transport (which ends the
534
+ // child it spawned) before rethrowing, so no unowned bridge outlives the gate; keep the cause.
535
+ try {
536
+ await transport.close();
537
+ } catch (closeError) {
538
+ throw new AggregateError([error, closeError], `outside bridge did not connect: ${(error as Error).message}`);
539
+ }
540
+ throw error;
541
+ }
542
+ try {
543
+ const text = (r: Awaited<ReturnType<Client["callTool"]>>): string =>
544
+ (Array.isArray(r.content) ? r.content : [])
545
+ .flatMap((b) => (b.type === "text" && typeof b.text === "string" ? [b.text] : []))
546
+ .join("\n");
547
+ const peers = text(await client.callTool({ name: "entwurf_peers", arguments: {} }));
548
+ const selfResult = await client.callTool({ name: "entwurf_self", arguments: {} });
549
+ return { peers, self: { text: text(selfResult), isError: selfResult.isError === true } };
550
+ } finally {
551
+ await client.close();
552
+ }
553
+ }
554
+
555
+ // ---------------------------------------------------------------------------
556
+ // The cell
557
+ // ---------------------------------------------------------------------------
558
+ interface HostReport {
559
+ hostPid: number;
560
+ session: { id: string; directory: string; cwd: string };
561
+ attachment: {
562
+ gardenId: string;
563
+ action: string;
564
+ recordPath: string;
565
+ markerPath: string;
566
+ roots: { sessionsDir: string; sendersDir: string; mailboxDir: string; receiversDir: string };
567
+ bridgePid: number | null;
568
+ };
569
+ receiver: { status: string; gardenId?: string; markerPath?: string; admissions?: unknown[] };
570
+ toolNames: string[];
571
+ /** Spawn specs the adapter handed the production stdio connector, as the host driver recorded them. */
572
+ bridgeSpecs?: {
573
+ command: string;
574
+ args: string[];
575
+ cwd: string;
576
+ identityCarrierPresent: Record<string, boolean>;
577
+ rootCarriers: Record<string, string | null>;
578
+ }[];
579
+ self: string;
580
+ peers: string;
581
+ notices: string[];
582
+ error?: string;
583
+ }
584
+
585
+ /**
586
+ * The SDK connector-boundary QKs. They need only the case's roots and project, so they are judged on a
587
+ * host's failure report too — before the gate throws that report's error, which stays the cause.
588
+ */
589
+ function judgeBridgeSpec(
590
+ report: { hostPid?: number; bridgeSpecs?: HostReport["bridgeSpecs"] },
591
+ host: Host,
592
+ roots: RootsCase,
593
+ projectReal: string,
594
+ ): void {
595
+ // SDK connector boundary, not kernel environ: the ONE spawn spec the adapter handed the production
596
+ // stdio connector, as the host driver recorded it. Absent, repeated, malformed or from another pid is red.
597
+ const spec = report.hostPid === host.pid && report.bridgeSpecs?.length === 1 ? report.bridgeSpecs[0] : undefined;
598
+ const identityNames = Object.keys(poisoned);
599
+ ok(
600
+ `[QK:PDC-BRIDGE-ENV-SCRUBBED] at the SDK connector boundary (the one spawn spec the adapter handed the production stdio connector — node on this gate's bridge entry in the project; not a kernel child-environ read), the bridge's environment carries none of the host's four poisoned identity carrier names (${report.bridgeSpecs?.length ?? "no"} spec(s), ${JSON.stringify(spec?.identityCarrierPresent ?? null)})`,
601
+ spec !== undefined &&
602
+ spec.command === process.execPath &&
603
+ JSON.stringify(spec.args) === JSON.stringify([bridgeEntry]) &&
604
+ spec.cwd === projectReal &&
605
+ JSON.stringify(Object.keys(spec.identityCarrierPresent ?? {})) === JSON.stringify(identityNames) &&
606
+ identityNames.every((name) => spec.identityCarrierPresent[name] === false),
607
+ );
608
+ ok(
609
+ `[QK:PDC-BRIDGE-ROOTS-BOUND] at the same SDK connector boundary, the bridge's four root carriers are the case's literal roots, whatever the host inherited (${JSON.stringify(spec?.rootCarriers ?? null)})`,
610
+ spec !== undefined &&
611
+ Object.keys(spec.rootCarriers ?? {}).length === 4 &&
612
+ spec.rootCarriers.ENTWURF_META_SESSIONS_DIR === roots.sessionsDir &&
613
+ spec.rootCarriers.ENTWURF_META_SENDERS_DIR === roots.sendersDir &&
614
+ spec.rootCarriers.ENTWURF_META_MAILBOX_DIR === roots.mailboxDir &&
615
+ spec.rootCarriers.ENTWURF_META_RECEIVERS_DIR === roots.receiversDir,
616
+ );
617
+ }
618
+
619
+ async function birthCell(
620
+ step: string,
621
+ roots: RootsCase,
622
+ mode: "new" | "continue",
623
+ expectGardenId: string | null,
624
+ ): Promise<string> {
625
+ console.log(`${step}. roots=${roots.name}, ${mode === "new" ? "fresh host" : "reopen with --continue"} (input 0)`);
626
+ const projectReal = fs.realpathSync(roots.project);
627
+ const defaultStoreBefore = recordsIn(defaultRoots.sessionsDir).length;
628
+ const host = startHost(roots, mode);
629
+ const report = (await host.next(REPORT_TIMEOUT_MS)) as unknown as HostReport;
630
+ if (report.error !== undefined) {
631
+ judgeBridgeSpec(report, host, roots, projectReal);
632
+ throw new Error(`host failed: ${report.error}; stderr: ${host.stderr().trim()}`);
633
+ }
634
+ observeBridges(host);
635
+ const gardenId = report.attachment.gardenId;
636
+ if (FAULT === "after-birth") {
637
+ console.log(
638
+ ` owned: host=${host.pid} bridges=${host.bridges.map((b) => b.pid).join(",") || "none"} garden=${gardenId}`,
639
+ );
640
+ throw new Error("[controlled fault] after-birth: the gate fails on purpose with its host and bridge alive");
641
+ }
642
+
643
+ ok(
644
+ `[QK:PDC-HOST-IS-SPAWNED] the reporting host is the process this gate spawned (${report.hostPid})`,
645
+ report.hostPid === host.pid,
646
+ );
647
+ ok(
648
+ `[QK:PDC-TOOLS] the registered extension exposes exactly entwurf_self, entwurf_peers, entwurf_v2, entwurf_inbox_read, entwurf_callback, entwurf_fresh_call, in that order (${report.toolNames.join(", ")})`,
649
+ JSON.stringify(report.toolNames) ===
650
+ JSON.stringify([
651
+ "entwurf_self",
652
+ "entwurf_peers",
653
+ "entwurf_v2",
654
+ "entwurf_inbox_read",
655
+ "entwurf_callback",
656
+ "entwurf_fresh_call",
657
+ ]),
658
+ );
659
+
660
+ const records = recordsIn(roots.sessionsDir);
661
+ const record = records.find((r) => r.nativeSessionId === report.session.id) ?? {};
662
+ ok(
663
+ `[QK:PDC-ONE-RECORD] the case's store (${roots.sessionsDir}) holds exactly one record (got ${records.length})`,
664
+ records.length === 1,
665
+ );
666
+ ok(
667
+ `[QK:PDC-RECORD-BINDING] that record is pi-durable, keyed by the durable session id, at the project's real cwd (${String(record.backend)}, ${String(record.nativeSessionId)})`,
668
+ record.backend === "pi-durable" && record.cwd === projectReal && record.gardenId === gardenId,
669
+ );
670
+ const sessionDirs = durableSessionDirs(roots.project);
671
+ ok(
672
+ `[QK:PDC-NATIVE-ID-IS-DIRECTORY] the native id is the one durable session directory under the project's hash (${sessionDirs.join(", ")})`,
673
+ sessionDirs.length === 1 && sessionDirs[0] === report.session.id,
674
+ );
675
+ if (expectGardenId === null) {
676
+ ok(
677
+ `[QK:PDC-CREATE] a fresh host creates its record (action ${report.attachment.action})`,
678
+ report.attachment.action === "create",
679
+ );
680
+ } else {
681
+ ok(
682
+ `[QK:PDC-REOPEN-SAME-GARDEN] --continue reattaches the same garden id (${gardenId} vs ${expectGardenId}, action ${report.attachment.action})`,
683
+ gardenId === expectGardenId && report.attachment.action === "attach",
684
+ );
685
+ }
686
+ ok(
687
+ `[QK:PDC-ROOTS-REPORTED] the contact reports the case's four roots (${Object.values(report.attachment.roots).join(", ")})`,
688
+ report.attachment.roots.sessionsDir === roots.sessionsDir &&
689
+ report.attachment.roots.sendersDir === roots.sendersDir &&
690
+ report.attachment.roots.mailboxDir === roots.mailboxDir &&
691
+ report.attachment.roots.receiversDir === roots.receiversDir,
692
+ );
693
+ if (roots.name === "explicit") {
694
+ const defaultRecords = recordsIn(defaultRoots.sessionsDir);
695
+ ok(
696
+ `[QK:PDC-EXPLICIT-NO-DEFAULT-WRITE] an explicit-root birth leaves the default roots alone (default store ${defaultStoreBefore} → ${defaultRecords.length} records, none for this session; no default sender marker, receiver marker or mailbox for it)`,
697
+ defaultRecords.length === defaultStoreBefore &&
698
+ !defaultRecords.some((r) => r.nativeSessionId === report.session.id) &&
699
+ !fs.existsSync(path.join(defaultRoots.sendersDir, "pi-durable", `${host.pid}.json`)) &&
700
+ !fs.existsSync(path.join(defaultRoots.receiversDir, `${gardenId}.json`)) &&
701
+ !fs.existsSync(path.join(defaultRoots.mailboxDir, gardenId)),
702
+ );
703
+ }
704
+
705
+ const markerFile = path.join(roots.sendersDir, "pi-durable", `${host.pid}.json`);
706
+ const marker = fs.existsSync(markerFile)
707
+ ? (JSON.parse(fs.readFileSync(markerFile, "utf8")) as Record<string, unknown>)
708
+ : {};
709
+ ok(
710
+ `[QK:PDC-SENDER-MARKER] the sender marker under the case's sender root is keyed to the spawned host pid with its /proc start-key and names the record`,
711
+ marker.gardenId === gardenId &&
712
+ marker.ownerPid === host.pid &&
713
+ marker.ownerStartKey === `linux:${host.start}` &&
714
+ marker.nativeSessionId === report.session.id,
715
+ );
716
+
717
+ const bridgePid = host.bridges[0]?.pid ?? -1;
718
+ ok(
719
+ `[QK:PDC-PPID-DIRECT] /proc shows exactly one bridge child whose PPID is the host and whose argv[0] is node — no shell between (pids ${host.bridges.map((b) => b.pid).join(", ") || "none"}, reported ${report.attachment.bridgePid})`,
720
+ host.bridges.length === 1 &&
721
+ bridgePid === report.attachment.bridgePid &&
722
+ procArgv(bridgePid)[0] === process.execPath,
723
+ );
724
+ judgeBridgeSpec(report, host, roots, projectReal);
725
+
726
+ const self = lastJsonLine(report.self);
727
+ ok(
728
+ `[QK:PDC-SELF] entwurf_self through the host's bridge names the record's garden id as a meta-session pi-durable sender (${String(self.sessionId)}, ${String(self.agentId)})`,
729
+ self.sessionId === gardenId &&
730
+ self.agentId === "meta-session/pi-durable" &&
731
+ self.origin === "meta-session" &&
732
+ self.cwd === projectReal,
733
+ );
734
+ // The arm, read raw: a path proves nothing, so the marker's owner is checked against the host this
735
+ // gate spawned (pid + /proc start key), and its join partner is the sender marker read above.
736
+ const receiverFile = path.join(roots.receiversDir, `${gardenId}.json`);
737
+ const receiverMarker = fs.existsSync(receiverFile)
738
+ ? (JSON.parse(fs.readFileSync(receiverFile, "utf8")) as Record<string, unknown>)
739
+ : {};
740
+ ok(
741
+ `[QK:PDC-RECEIVER-ARMED] the case's receiver root holds this garden's marker, owned by the spawned host (pid + /proc start key) as pi-durable-host on the durable session, with the mailbox signal beside it; the host's sender marker names the same garden, which is the #101 join (${receiverFile})`,
742
+ receiverMarker.gardenId === gardenId &&
743
+ receiverMarker.backend === "pi-durable" &&
744
+ receiverMarker.nativeSessionId === report.session.id &&
745
+ receiverMarker.ownerPid === host.pid &&
746
+ receiverMarker.ownerStartKey === `linux:${host.start}` &&
747
+ receiverMarker.ownerKind === "pi-durable-host" &&
748
+ receiverMarker.armProvenance === "session-start" &&
749
+ marker.gardenId === gardenId &&
750
+ marker.ownerPid === host.pid &&
751
+ fs.existsSync(path.join(roots.mailboxDir, gardenId, "inbox.signal")),
752
+ );
753
+ ok(
754
+ `[QK:PDC-RECEIVER-STATE] the contact's own receiver() says armed for that garden at that marker (${report.receiver.status}, ${String(report.receiver.markerPath)})`,
755
+ report.receiver.status === "armed" &&
756
+ report.receiver.gardenId === gardenId &&
757
+ report.receiver.markerPath === receiverFile,
758
+ );
759
+ ok(
760
+ `[QK:PDC-REPLYABLE] with that receiver armed and joined, the bridge's own entwurf_self reports the citizen replyable (replyable ${String(self.replyable)})`,
761
+ self.replyable === true,
762
+ );
763
+ ok(`[QK:PDC-PEERS-HOST] entwurf_peers through the host's bridge lists the citizen`, report.peers.includes(gardenId));
764
+
765
+ const outside = await outsideBridge(roots);
766
+ ok(
767
+ `[QK:PDC-PEERS-OUTSIDE] a bridge this gate spawned on the case's roots (not the host's child) lists the citizen as pi-durable`,
768
+ outside.peers.includes(gardenId) && outside.peers.includes("pi-durable"),
769
+ );
770
+ ok(
771
+ `[QK:PDC-IDENTITY-HOST-SCOPED] that outside bridge is NOT the citizen — the marker joins only the host's own child (isError ${outside.self.isError})`,
772
+ outside.self.isError && !outside.self.text.includes(gardenId),
773
+ );
774
+
775
+ const closed = await closeHost(host);
776
+ ok(
777
+ `[QK:PDC-CLOSE] the host closes and exits 0 (closed ${closed.closed}, exit ${closed.code})`,
778
+ closed.closed && closed.code === 0,
779
+ );
780
+ ok(`[QK:PDC-BRIDGE-GONE] the bridge child is gone after close`, procStat(bridgePid) === null);
781
+ ok(
782
+ `[QK:PDC-RECEIVER-RETIRED-ON-CLOSE] the host's close removed the receiver marker it owned (${receiverFile})`,
783
+ !fs.existsSync(receiverFile),
784
+ );
785
+ console.log(` notices: ${report.notices.join(" | ") || "none"}`);
786
+ return gardenId;
787
+ }
788
+
789
+ let primary: unknown;
790
+ try {
791
+ const defaultGarden = await birthCell("1", defaultRoots, "new", null);
792
+ await birthCell("2", defaultRoots, "continue", defaultGarden);
793
+ const explicitGarden = await birthCell("3", explicitRoots, "new", null);
794
+ await birthCell("4", explicitRoots, "continue", explicitGarden);
795
+ } catch (error) {
796
+ primary = error;
797
+ }
798
+
799
+ // Cleanup runs on every path: reap what this gate owns, then the operator guard, then the roots.
800
+ const cleanupProblems: string[] = [];
801
+ for (const host of owned) cleanupProblems.push(...(await reap(host)));
802
+ const stillAlive = owned.flatMap((h) =>
803
+ [{ pid: h.pid, start: h.start }, ...h.bridges].filter((p) => procStat(p.pid)?.start === p.start).map((p) => p.pid),
804
+ );
805
+ console.log("5. cleanup and operator guard");
806
+ const ownedSummary = owned
807
+ .map((h) => `host ${h.pid}${h.bridges.length > 0 ? ` bridges ${h.bridges.map((b) => b.pid).join(",")}` : ""}`)
808
+ .join("; ");
809
+ ok(
810
+ `[QK:PDC-OWNED-REAPED] every host this gate spawned and every bridge observed under it is gone (${ownedSummary || "none spawned"}; alive ${stillAlive.join(",") || "none"})`,
811
+ stillAlive.length === 0 && cleanupProblems.length === 0,
812
+ );
813
+ const before = JSON.parse(operatorBefore) as {
814
+ durableRecords: string[];
815
+ senders: boolean;
816
+ durableReceivers: string[];
817
+ };
818
+ ok(
819
+ `[QK:PDC-OPERATOR-UNTOUCHED] coarse pi-durable guard: the operator's own roots are unchanged by this run and hold no pi-durable record, sender dir or receiver marker (${operatorBefore})`,
820
+ operatorPiDurableFacts() === operatorBefore &&
821
+ before.durableRecords.length === 0 &&
822
+ !before.senders &&
823
+ before.durableReceivers.length === 0,
824
+ );
825
+ if (stillAlive.length > 0) console.log(`sandbox kept because owned processes are still alive: ${root}`);
826
+ else if (process.env.ENTWURF_PI_DURABLE_KEEP === "1") console.log(`sandbox kept: ${root}`);
827
+ else fs.rmSync(root, { recursive: true, force: true });
828
+
829
+ if (primary !== undefined) {
830
+ console.error(`${LABEL}: FAILED — ${primary instanceof Error ? primary.message : String(primary)}`);
831
+ }
832
+ for (const problem of cleanupProblems) console.error(`${LABEL}: cleanup — ${problem}`);
833
+ console.log(`\n${LABEL}: ${passed} passed, ${failed} failed — adapter-direct, harness-bypassed, checkout-only`);
834
+ process.exit(primary === undefined && failed === 0 && cleanupProblems.length === 0 ? 0 : 1);