@junghanacs/entwurf 0.30.1 → 0.32.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 (76) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +194 -0
  3. package/DELIVERY.md +1 -0
  4. package/README.md +64 -14
  5. package/VERIFY.md +36 -1
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/setup-clean-host.md +121 -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/entwurf-deliverability.js +7 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +28 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +236 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +746 -0
  19. package/mcp/entwurf-bridge/src/index.ts +7 -7
  20. package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
  21. package/package.json +19 -19
  22. package/pi/entwurf-capabilities.json +1 -0
  23. package/pi/pi-durable/bootstrap.mjs +404 -0
  24. package/pi/pi-durable/carrier/LICENSE +21 -0
  25. package/pi/pi-durable/carrier/harness-setup.js +123 -0
  26. package/pi/pi-durable/carrier/prompt.js +74 -0
  27. package/pi/pi-durable/carrier/runtime.js +402 -0
  28. package/pi/pi-durable/carrier/sessions.js +56 -0
  29. package/pi/pi-durable/carrier/subagent.js +55 -0
  30. package/pi/pi-durable/carrier/tui.js +655 -0
  31. package/pi/pi-durable/carrier-relocation.mjs +160 -0
  32. package/pi/pi-durable/carrier-resolver.mjs +38 -0
  33. package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
  34. package/pi/pi-durable/overlay/upstream-pin.json +108 -0
  35. package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
  36. package/pi-extensions/lib/fresh-call-composition.ts +42 -7
  37. package/pi-extensions/lib/meta-sender-identity.ts +7 -1
  38. package/pi-extensions/lib/meta-session.ts +28 -2
  39. package/pi-extensions/lib/mux-fresh-call.ts +32 -2
  40. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
  41. package/pi-extensions/lib/pi-durable-runtime.ts +318 -0
  42. package/pi-extensions/meta-bridge-pi-durable.ts +1024 -0
  43. package/run.sh +624 -184
  44. package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
  45. package/scripts/check-fresh-call-dispatch.ts +18 -0
  46. package/scripts/check-gate-qualification.ts +87 -34
  47. package/scripts/check-herdr-activation.ts +5 -4
  48. package/scripts/check-pi-durable-contact.ts +767 -0
  49. package/scripts/check-pi-durable-receive.ts +1079 -0
  50. package/scripts/check-pi-durable-send.ts +1821 -0
  51. package/scripts/emit-pi-durable-carrier.ts +355 -0
  52. package/scripts/lib/mutation-qualify.ts +21 -45
  53. package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
  54. package/scripts/lib/pi-durable-operator-guard.ts +89 -0
  55. package/scripts/lib/pi-durable-r1.ts +100 -0
  56. package/scripts/lib/pi-durable-reap.ts +27 -0
  57. package/scripts/lib/pi-durable-receive.ts +137 -0
  58. package/scripts/lib/qualification-receipt.ts +7 -11
  59. package/scripts/mutants/control-send-receipt.json +1 -1
  60. package/scripts/mutants/copilot-birth.json +2 -2
  61. package/scripts/mutants/gate-qualification.json +13 -0
  62. package/scripts/mutants/meta-hook-session-switch.json +4 -2
  63. package/scripts/mutants/mux-fresh-call.json +69 -3
  64. package/scripts/mutants/omp-birth.json +2 -2
  65. package/scripts/mutants/omp-fresh.json +6 -4
  66. package/scripts/mutants/pack-install.json +3 -3
  67. package/scripts/mutants/pi-durable.json +723 -0
  68. package/scripts/mutants/pi-floor.json +24 -2
  69. package/scripts/pi-durable-contact-host.mjs +105 -0
  70. package/scripts/pi-durable-receive-host.mjs +127 -0
  71. package/scripts/pi-durable-send-host.mjs +284 -0
  72. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  73. package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
  74. package/scripts/smoke-setup-verdict.sh +22 -3
  75. package/scripts/smoke-user-scope-citizen.sh +1 -1
  76. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,624 @@
1
+ /**
2
+ * smoke-pi-durable-fresh-live — the first-admission LIVE acceptance for the native durable host (#129):
3
+ * `docs/adding-a-harness.md` step 9 clause 7, plus the two cross-harness dispatch receipts the release
4
+ * stop requires, all through the public product surface.
5
+ *
6
+ * RELEASE MUST. Needs `LIVE=1`. Opens a real visible Pi and a real visible pi-durable host and spends
7
+ * real model turns on both.
8
+ *
9
+ * ── The sequence, on a private tmux server this step owns ──
10
+ *
11
+ * fixture a record-backed self-fetch COLLECTOR (claude-code backend, armed mailbox, sender marker).
12
+ * It only launches and collects; it never stands in for the existing citizen.
13
+ * 1. fixture → `entwurf_fresh_call(pi)` → a real visible Pi; Pi's first action calls back (nonce A).
14
+ * 2. Pi's live turn → `entwurf_fresh_call(pi-durable, <model>)` → the managed `entwurf pi-durable`
15
+ * verb opens the durable host; ITS first action calls back into Pi (nonce B, read from Pi's
16
+ * own transcript). Clauses 1, 2, 5 and 6 through the product surface; no raw tmux launch.
17
+ * 3. fixture → Pi (one instruction). Pi's live turn → `entwurf_v2` → pd: receipt 1, existing → new.
18
+ * pd's doorbell admits the notice; pd's live turn drains it with `entwurf_inbox_read` (addressed
19
+ * receive, clause 7) and follows the one instruction it read:
20
+ * 4. pd's live turn → `entwurf_v2` → Pi: receipt 2, new → existing, with the expected payload, from its authoritative sender. Pi relays it to the fixture, which must read it.
21
+ *
22
+ * ── Oracles (screen text and model-reformatted reports are never delivery evidence) ──
23
+ *
24
+ * Pi its own JSONL toolCall/toolResult rows and `entwurf-message` custom rows.
25
+ * pd its NATIVE store, read from a consistent snapshot taken with node:sqlite's online
26
+ * `backup` while the host is live (a raw copy of a live WAL set is not consistent):
27
+ * the `pi.system` entries' `toolsAdded` (the actual offering — task-wide means the six
28
+ * contact tools AND the native tools, no filter), the generation checkpoint's
29
+ * `request.model` and the assistant messages' provider/model (the actual model), the
30
+ * contact tool calls, and the doorbell submission. The live database and its transcript
31
+ * are only read, never written or removed.
32
+ * records V3 records, receiver markers and mailbox state files, read raw.
33
+ * identity the garden id on the pd pane is OBSERVED (capture-pane) as a visible-identity fact
34
+ * only — never as evidence that anything was delivered.
35
+ *
36
+ * ── What it writes ──
37
+ *
38
+ * The meta roots are REAL, as for smoke-omp-fresh-live: the birth runs inside the launched processes
39
+ * under their own root policy, and no env carrier exists to fence it. This step mints exactly one Pi
40
+ * and one pi-durable citizen (asserted) plus its own fixture record. It kills only its private tmux
41
+ * server, removes only its own fixture markers, and leaves every record, transcript and durable
42
+ * session database in place. Raw receipts are exported to a directory it prints.
43
+ */
44
+ import { type ChildProcess, execFileSync, spawn, spawnSync } from "node:child_process";
45
+ import { createHash } from "node:crypto";
46
+ import * as fs from "node:fs";
47
+ import * as os from "node:os";
48
+ import * as path from "node:path";
49
+ import { FRESH_CALL_BACKENDS } from "../pi-extensions/lib/mux-fresh-call.ts";
50
+ import { splitPiDurableModel } from "../pi-extensions/lib/pi-durable-fresh-preflight.ts";
51
+ import { checkPiDurableRuntime, piDurablePackageLayout } from "../pi-extensions/lib/pi-durable-runtime.ts";
52
+ import {
53
+ mailboxCallbacks,
54
+ parseJsonLines,
55
+ parseMetaMailboxBody,
56
+ piCallbackSenders,
57
+ piSourceToolReceipts,
58
+ type SourceToolReceipt,
59
+ } from "./lib/codex-fresh-source-receipts.ts";
60
+ import { skipLive } from "./lib/live-skip.ts";
61
+ import {
62
+ type PdNativeRow,
63
+ type PdNativeSubmissionRow,
64
+ pdDoorbellSubmission,
65
+ pdRootView,
66
+ piExactDeliveryRow,
67
+ } from "./lib/pi-durable-fresh-live-receipts.ts";
68
+
69
+ const LABEL = "smoke-pi-durable-fresh-live";
70
+ const REPO = path.resolve(import.meta.dirname, "..");
71
+ const DIST_ENTRY = path.join(REPO, "mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js");
72
+ /** The documented Codex-subscription selector (MODELS.md) — an exact id in the pinned catalog. A model
73
+ * is a host fact, so both are overridable, and both are recorded in the receipt as given. */
74
+ const PD_MODEL = process.env.ENTWURF_PI_DURABLE_FRESH_MODEL?.trim() || "openai-codex/gpt-6.1-sol";
75
+ const PI_MODEL = process.env.ENTWURF_PI_DURABLE_FRESH_PI_MODEL?.trim() || "openai-codex/gpt-6.1-sol";
76
+ const WIDTH = "task-wide";
77
+ /** Independent of the adapter's own constant: the six contact tools step 9 expects the host to offer. */
78
+ const CONTACT_TOOLS = [
79
+ "entwurf_self",
80
+ "entwurf_peers",
81
+ "entwurf_v2",
82
+ "entwurf_inbox_read",
83
+ "entwurf_callback",
84
+ "entwurf_fresh_call",
85
+ ];
86
+ /** The native app's own tools; task-wide = no filter, so they must be offered beside the contact. */
87
+ const NATIVE_TOOLS = ["bash", "read", "edit", "write", "subagent"];
88
+ const GARDEN_ID = /^\d{8}T\d{6}-[0-9a-f]{6}$/;
89
+ const NONCE = /nonce:\s+(mux-fresh-call-[0-9a-f]+)/;
90
+ const TURN_WAIT_MS = 300_000;
91
+
92
+ let passed = 0;
93
+ let root = "";
94
+ let socket = "";
95
+ const receipts: Record<string, string> = {};
96
+ const cleanups: Array<() => void> = [];
97
+
98
+ function teardown(): void {
99
+ for (const step of cleanups.splice(0).reverse()) {
100
+ try {
101
+ step();
102
+ } catch {
103
+ // Bounded cleanup of this step's own artifacts; a failure here cannot change the verdict.
104
+ }
105
+ }
106
+ if (socket) {
107
+ spawnSync("tmux", ["-S", socket, "kill-server"], { stdio: "ignore" });
108
+ socket = "";
109
+ }
110
+ if (root) {
111
+ fs.rmSync(root, { recursive: true, force: true });
112
+ root = "";
113
+ }
114
+ }
115
+ function printReceipts(): void {
116
+ if (Object.keys(receipts).length === 0) return;
117
+ console.log(`\n[${LABEL}] receipts`);
118
+ for (const [k, v] of Object.entries(receipts)) console.log(`--- ${k} ---\n${v}`);
119
+ }
120
+ function fail(message: string): never {
121
+ console.error(`[${LABEL}] ${message}`);
122
+ printReceipts();
123
+ exportReceipts();
124
+ teardown();
125
+ process.exit(1);
126
+ }
127
+ function ok(label: string, cond: unknown, detail = ""): asserts cond {
128
+ if (!cond) fail(`FAIL ${label}${detail ? `\n${detail}` : ""}`);
129
+ console.log(` ok ${label}`);
130
+ passed++;
131
+ }
132
+ async function until<T>(what: string, timeoutMs: number, probe: () => T | null | undefined): Promise<T> {
133
+ const deadline = Date.now() + timeoutMs;
134
+ for (;;) {
135
+ let got: T | null | undefined = null;
136
+ try {
137
+ got = probe();
138
+ } catch {
139
+ got = null; // a half-written file is "not yet", never a verdict
140
+ }
141
+ if (got !== null && got !== undefined) return got;
142
+ if (Date.now() > deadline) fail(`timed out after ${timeoutMs}ms waiting for ${what}`);
143
+ await new Promise((r) => setTimeout(r, 1000));
144
+ }
145
+ }
146
+
147
+ // Raw receipts travel: a directory outside the scratch root, printed, never removed by this step.
148
+ const receiptsDir =
149
+ process.env.ENTWURF_PI_DURABLE_FRESH_RECEIPTS?.trim() ||
150
+ path.join(os.tmpdir(), `${LABEL}-${new Date().toISOString().replace(/[:.]/g, "-")}`);
151
+ function exportFile(name: string, bytes: string | Buffer): void {
152
+ fs.mkdirSync(receiptsDir, { recursive: true });
153
+ fs.writeFileSync(path.join(receiptsDir, name), bytes);
154
+ }
155
+ function exportReceipts(): void {
156
+ try {
157
+ fs.mkdirSync(receiptsDir, { recursive: true });
158
+ fs.writeFileSync(path.join(receiptsDir, "receipts.json"), `${JSON.stringify(receipts, null, "\t")}\n`);
159
+ console.log(`[${LABEL}] raw receipts: ${receiptsDir}`);
160
+ } catch {
161
+ // Exporting is evidence hygiene; it cannot change the verdict.
162
+ }
163
+ }
164
+
165
+ // ── 1. does this step have a subject? ────────────────────────────────────────
166
+ const meta = await import("../pi-extensions/lib/meta-session.ts");
167
+ if (!(FRESH_CALL_BACKENDS as readonly string[]).includes("pi-durable")) {
168
+ skipLive(
169
+ LABEL,
170
+ "pi-durable is not in FRESH_CALL_BACKENDS, so there is no visible-fresh product to accept; check-harness-admission-parity owns that state.",
171
+ );
172
+ }
173
+ const wakeMode = (() => {
174
+ try {
175
+ return meta.loadMetaCapabilityRegistry().backends["pi-durable"]?.wakeMode ?? "ABSENT";
176
+ } catch (err) {
177
+ fail(`capability registry unreadable: ${String(err)}`);
178
+ }
179
+ })();
180
+ if (wakeMode !== "self-fetch") {
181
+ skipLive(
182
+ LABEL,
183
+ `pi-durable declares wakeMode=${wakeMode}; clause 7 needs a drainable mailbox for addressed receive.`,
184
+ );
185
+ }
186
+ if (process.env.LIVE !== "1") {
187
+ skipLive(
188
+ LABEL,
189
+ "set LIVE=1 to run — this opens a real visible Pi and a real visible pi-durable host and spends model turns on " +
190
+ `${PI_MODEL} (Pi) and ${PD_MODEL} (pi-durable). Override with ENTWURF_PI_DURABLE_FRESH_PI_MODEL / ENTWURF_PI_DURABLE_FRESH_MODEL.`,
191
+ );
192
+ }
193
+
194
+ // ── 2. prerequisites: FAIL, not skip ─────────────────────────────────────────
195
+ // The release gate strips ambient identity; a standalone run must not leak this shell's either.
196
+ for (const name of [
197
+ "PI_SESSION_ID",
198
+ "PI_AGENT_ID",
199
+ "ENTWURF_CALLBACK_TARGET",
200
+ "ENTWURF_CALLBACK_NONCE",
201
+ "ENTWURF_META_SENDER_MARKER",
202
+ ]) {
203
+ delete process.env[name];
204
+ }
205
+ receipts["0-selectors"] =
206
+ `pd: ${PD_MODEL} width=${WIDTH}\npi: ${PI_MODEL}\n(as requested; the actual model is measured below)`;
207
+ const pdModel = splitPiDurableModel(PD_MODEL);
208
+ ok("the pi-durable model is provider-qualified (`<provider>/<id>`)", pdModel !== null, PD_MODEL);
209
+ ok("the built bridge exists", fs.existsSync(DIST_ENTRY), DIST_ENTRY);
210
+ for (const bin of ["pi", "entwurf", "tmux"]) {
211
+ const found = spawnSync("bash", ["-c", `command -v ${bin}`], { encoding: "utf8" });
212
+ ok(`'${bin}' resolves on PATH`, found.status === 0, `missing ${bin}`);
213
+ receipts[`0-path-${bin}`] = found.stdout.trim();
214
+ }
215
+ const layout = piDurablePackageLayout();
216
+ ok("this package closure carries the pi-durable units", layout !== null);
217
+ const runtime = checkPiDurableRuntime(layout);
218
+ ok(
219
+ "this package's pi-durable carrier and its declared SDK set verify (the 0.32.0 supply contract)",
220
+ runtime.ok,
221
+ runtime.ok ? "" : `${runtime.reason}: ${runtime.detail}`,
222
+ );
223
+ receipts["0-runtime"] = runtime.ok ? JSON.stringify(runtime) : "";
224
+
225
+ // ── 3. the fixture collector, and a private tmux server ──────────────────────
226
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-pd-fresh-"));
227
+ process.on("exit", teardown);
228
+ fs.mkdirSync(path.join(root, "scratch"));
229
+ // The native app keys its session directory by the REALPATH of the cwd; use that same string everywhere.
230
+ const scratch = fs.realpathSync(path.join(root, "scratch"));
231
+ const fixtureNative = `pd-fresh-live-${process.pid}`;
232
+ const fixture = meta.upsertMetaSession({
233
+ input: { backend: "claude-code", nativeSessionId: fixtureNative, cwd: scratch },
234
+ });
235
+ const fixtureGid = fixture.record.gardenId;
236
+ meta.writeMetaReceiverMarker({
237
+ gardenId: fixtureGid,
238
+ backend: "claude-code",
239
+ nativeSessionId: fixtureNative,
240
+ ownerPid: process.pid,
241
+ armProvenance: "session-start",
242
+ });
243
+ cleanups.push(() => meta.removeMetaReceiverMarker({ gardenId: fixtureGid, ownerPid: process.pid }));
244
+ const senderMarkerPath = meta.writeMetaSenderMarker({
245
+ backend: "claude-code",
246
+ gardenId: fixtureGid,
247
+ nativeSessionId: fixtureNative,
248
+ cwd: scratch,
249
+ ownerPid: process.pid,
250
+ });
251
+ cleanups.push(() => fs.rmSync(senderMarkerPath, { force: true }));
252
+ ok("the fixture is a record-backed collector with an armed mailbox and a sender marker", GARDEN_ID.test(fixtureGid));
253
+ receipts["1-fixture"] = `gardenId=${fixtureGid} backend=claude-code (collector only) cwd=${scratch}`;
254
+
255
+ const citizensBefore = new Set(
256
+ (
257
+ JSON.parse(execFileSync("bash", [path.join(REPO, "run.sh"), "meta-facts"], { encoding: "utf8" })) as {
258
+ citizens: Array<{ gardenId: string }>;
259
+ }
260
+ ).citizens.map((c) => c.gardenId),
261
+ );
262
+
263
+ socket = path.join(root, "pd.sock");
264
+ {
265
+ const serverEnv = { ...process.env } as NodeJS.ProcessEnv;
266
+ delete serverEnv.TMUX;
267
+ delete serverEnv.TMUX_PANE;
268
+ const started = spawnSync(
269
+ "tmux",
270
+ ["-S", socket, "new-session", "-d", "-s", "fixture", "-n", "anchor", "-c", scratch],
271
+ {
272
+ env: serverEnv,
273
+ encoding: "utf8",
274
+ },
275
+ );
276
+ if (started.status !== 0) fail(`could not start the private tmux server at ${socket}: ${started.stderr}`);
277
+ }
278
+ const anchorPane = execFileSync("tmux", ["-S", socket, "display-message", "-p", "-t", "fixture:anchor", "#{pane_id}"], {
279
+ encoding: "utf8",
280
+ }).trim();
281
+ ok("placement is a private tmux socket this step owns, never the operator's", anchorPane.startsWith("%"));
282
+
283
+ // ── a persistent MCP client over the real compiled bridge ────────────────────
284
+ class BridgeClient {
285
+ private child: ChildProcess;
286
+ private buf = "";
287
+ private err = "";
288
+ private next = 2;
289
+ private waiters = new Map<number, (v: { raw: unknown; text: string; isError: boolean }) => void>();
290
+ constructor(env: NodeJS.ProcessEnv) {
291
+ this.child = spawn(process.execPath, [DIST_ENTRY], { stdio: ["pipe", "pipe", "pipe"], env });
292
+ this.child.stderr?.on("data", (d) => {
293
+ this.err += d.toString();
294
+ });
295
+ this.child.stdout?.on("data", (d) => {
296
+ this.buf += d.toString();
297
+ const lines = this.buf.split("\n");
298
+ this.buf = lines.pop() ?? "";
299
+ for (const line of lines) {
300
+ if (!line.trim().startsWith("{")) continue;
301
+ try {
302
+ const msg = JSON.parse(line);
303
+ const w = this.waiters.get(msg.id);
304
+ if (!w) continue;
305
+ this.waiters.delete(msg.id);
306
+ w({
307
+ raw: msg.result,
308
+ text: (msg.result?.content ?? []).map((c: { text?: string }) => c.text ?? "").join("\n"),
309
+ isError: msg.result?.isError === true,
310
+ });
311
+ } catch {
312
+ /* partial frame */
313
+ }
314
+ }
315
+ });
316
+ this.send({
317
+ jsonrpc: "2.0",
318
+ id: 1,
319
+ method: "initialize",
320
+ params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: LABEL, version: "0" } },
321
+ });
322
+ this.send({ jsonrpc: "2.0", method: "notifications/initialized" });
323
+ }
324
+ private send(msg: unknown): void {
325
+ this.child.stdin?.write(`${JSON.stringify(msg)}\n`);
326
+ }
327
+ stderrTail(): string {
328
+ return this.err.slice(-2000);
329
+ }
330
+ close(): void {
331
+ this.child.kill("SIGTERM");
332
+ }
333
+ rpc(method: string, params: unknown, timeoutMs = 60_000): Promise<{ raw: unknown; text: string; isError: boolean }> {
334
+ const id = this.next++;
335
+ return new Promise((resolve, reject) => {
336
+ const timer = setTimeout(
337
+ () => reject(new Error(`${method} did not answer in ${timeoutMs}ms\n${this.err}`)),
338
+ timeoutMs,
339
+ );
340
+ this.waiters.set(id, (v) => {
341
+ clearTimeout(timer);
342
+ resolve(v);
343
+ });
344
+ this.send({ jsonrpc: "2.0", id, method, params });
345
+ });
346
+ }
347
+ call(name: string, args: Record<string, unknown>, timeoutMs = 60_000) {
348
+ return this.rpc("tools/call", { name, arguments: args }, timeoutMs);
349
+ }
350
+ }
351
+ const bridge = new BridgeClient({
352
+ ...process.env,
353
+ TMUX: `${socket},0,0`,
354
+ TMUX_PANE: anchorPane,
355
+ ENTWURF_META_SENDER_MARKER: senderMarkerPath,
356
+ });
357
+ cleanups.push(() => bridge.close());
358
+
359
+ // The compiled bridge this candidate ships is the schema authority; pi-durable must be openable there.
360
+ const listed = await bridge.rpc("tools/list", {});
361
+ const freshSchema = (
362
+ listed.raw as { tools?: Array<{ name: string; inputSchema?: { properties?: { backend?: { enum?: string[] } } } }> }
363
+ ).tools?.find((t) => t.name === "entwurf_fresh_call")?.inputSchema?.properties?.backend?.enum;
364
+ ok(
365
+ "the current compiled bridge offers entwurf_fresh_call with pi-durable in its backend enum",
366
+ Array.isArray(freshSchema) && freshSchema.includes("pi-durable"),
367
+ JSON.stringify(freshSchema),
368
+ );
369
+ receipts["1-bridge-fresh-enum"] = JSON.stringify(freshSchema);
370
+
371
+ // ── 4. hop 1: the fixture opens a REAL visible Pi through the public surface ──
372
+ const tokenT1 = `PD-T1-${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
373
+ const tokenT2 = `PD-T2-${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
374
+ const pdTask =
375
+ "You are an automated acceptance probe. After your callback, do nothing until an entwurf inbox notice arrives. " +
376
+ "When it does, call entwurf_inbox_read with your own garden id, then carry out exactly the one instruction in the " +
377
+ "message you read. Do not read files and do not run commands.";
378
+ const piPhaseOne =
379
+ "You are an automated acceptance probe. Call the tool entwurf_fresh_call exactly once with these exact arguments: " +
380
+ `backend "pi-durable", model "${PD_MODEL}", cwd "${scratch}", task ${JSON.stringify(pdTask)}. ` +
381
+ "Then stop and wait; a callback will arrive by itself. Do not read files and do not run commands.";
382
+ const launchPi = await bridge.call(
383
+ "entwurf_fresh_call",
384
+ { backend: "pi", model: PI_MODEL, task: piPhaseOne, cwd: scratch },
385
+ 120_000,
386
+ );
387
+ receipts["2-launch-pi"] = launchPi.text;
388
+ ok(
389
+ "tools/call entwurf_fresh_call(pi) returned a launch receipt",
390
+ !launchPi.isError,
391
+ `${launchPi.text}\n${bridge.stderrTail()}`,
392
+ );
393
+ const nonceA = NONCE.exec(launchPi.text)?.[1];
394
+ ok("the Pi launch receipt carries a nonce", typeof nonceA === "string", launchPi.text);
395
+
396
+ const piGid = await until("Pi's callback into the fixture", TURN_WAIT_MS, () => {
397
+ const bodies = meta.readMetaInbox({ gardenId: fixtureGid }).messages.map((m) => m.body);
398
+ const found = mailboxCallbacks(bodies, nonceA as string);
399
+ return found.length === 1 ? found[0]?.sender : null;
400
+ });
401
+ ok("Pi called back with the exact nonce, and its sender envelope names its garden id", GARDEN_ID.test(piGid));
402
+ receipts["2-pi-callback"] = `${piGid} (sender of ${nonceA})`;
403
+ const piRecord = () => meta.readMetaIdentityByGardenId(piGid);
404
+ ok("the Pi callback id is a pi record", piRecord()?.backend === "pi", JSON.stringify(piRecord()));
405
+
406
+ const piEntries = (): unknown[] => {
407
+ const transcript = (piRecord() as { transcriptPath?: string | null } | null)?.transcriptPath ?? "";
408
+ if (!transcript || !fs.existsSync(transcript)) return [];
409
+ return parseJsonLines(fs.readFileSync(transcript, "utf8"));
410
+ };
411
+ const piTools = (): SourceToolReceipt[] => piSourceToolReceipts(piEntries());
412
+
413
+ // ── 5. hop 2: Pi's live turn opens pi-durable; pd's first action calls back into Pi ──
414
+ const pdLaunch = await until("Pi's own entwurf_fresh_call(pi-durable) result", TURN_WAIT_MS, () =>
415
+ piTools().find(
416
+ (t) => t.toolName === "entwurf_fresh_call" && t.arguments.backend === "pi-durable" && t.status !== "pending",
417
+ ),
418
+ );
419
+ receipts["3-pi-fresh-call-pd"] = `${JSON.stringify(pdLaunch.arguments)}\n${pdLaunch.text}`;
420
+ ok(
421
+ "Pi's live turn called entwurf_fresh_call(backend pi-durable) with the requested model and got a launch receipt",
422
+ pdLaunch.status === "completed" && pdLaunch.arguments.model === PD_MODEL,
423
+ pdLaunch.text,
424
+ );
425
+ const nonceB = NONCE.exec(pdLaunch.text)?.[1];
426
+ ok("the pi-durable launch receipt carries a nonce", typeof nonceB === "string", pdLaunch.text);
427
+ const pdPane = /pane:\s+(%\d+)/.exec(pdLaunch.text)?.[1];
428
+ ok("the pi-durable launch receipt names its pane", typeof pdPane === "string", pdLaunch.text);
429
+
430
+ const pdGid = await until("pi-durable's callback into Pi (Pi's own transcript)", TURN_WAIT_MS, () => {
431
+ const senders = piCallbackSenders(piEntries(), nonceB as string);
432
+ return senders.length === 1 ? senders[0] : null;
433
+ });
434
+ ok(
435
+ "pi-durable's FIRST action called back into Pi with the exact nonce; the sender envelope names its garden id",
436
+ GARDEN_ID.test(pdGid),
437
+ );
438
+ ok("the address came from the callback, not the launch receipt", !pdLaunch.text.includes(pdGid));
439
+ receipts["3-pd-callback"] = `${pdGid} (sender of ${nonceB}, read from Pi's transcript)`;
440
+ const pdRecord = meta.readMetaIdentityByGardenId(pdGid) as unknown as Record<string, unknown> | null;
441
+ ok("the pd callback id is a pi-durable V3 record", pdRecord?.backend === "pi-durable", JSON.stringify(pdRecord));
442
+ receipts["3-pd-record"] = JSON.stringify(pdRecord);
443
+ const pdMarker = await until("pi-durable's receiver marker (armed doorbell)", 120_000, () =>
444
+ meta.readMetaReceiverMarker({ gardenId: pdGid }),
445
+ );
446
+ ok(
447
+ "pi-durable's receiver is armed and owned by the durable host",
448
+ (pdMarker as { ownerKind?: string }).ownerKind === "pi-durable-host",
449
+ JSON.stringify(pdMarker),
450
+ );
451
+
452
+ const facts = JSON.parse(execFileSync("bash", [path.join(REPO, "run.sh"), "meta-facts"], { encoding: "utf8" })) as {
453
+ citizens: Array<{ gardenId: string }>;
454
+ };
455
+ // Other siblings may mint citizens in the operator's garden meanwhile; this step's are the ones in its scratch cwd.
456
+ const minted = facts.citizens
457
+ .map((c) => c.gardenId)
458
+ .filter((g) => !citizensBefore.has(g) && g !== fixtureGid && meta.readMetaIdentityByGardenId(g)?.cwd === scratch);
459
+ ok(
460
+ "exactly two citizens were minted in this step's cwd — the visible Pi and the visible pi-durable host, never a subagent",
461
+ minted.length === 2 && minted.includes(piGid) && minted.includes(pdGid),
462
+ `new: ${minted.join(", ")}`,
463
+ );
464
+
465
+ // Visible identity: OBSERVED on the pd pane (an identity fact, not delivery evidence).
466
+ const pdScreen = await until("the garden id on the pi-durable pane", 60_000, () => {
467
+ const shot = spawnSync("tmux", ["-S", socket, "capture-pane", "-p", "-J", "-t", pdPane as string, "-S", "-200"], {
468
+ encoding: "utf8",
469
+ });
470
+ return shot.status === 0 && shot.stdout.includes(pdGid) ? shot.stdout : null;
471
+ });
472
+ ok("the pi-durable host shows its own garden id on its visible surface", pdScreen.includes(pdGid));
473
+ exportFile("pd-pane-identity.txt", pdScreen);
474
+
475
+ // ── 6. receipt 1: Pi's live turn → pd; pd drains it in a live turn ───────────
476
+ const m1 =
477
+ `Acceptance probe ${tokenT1}. Reply by calling entwurf_v2 exactly once with target "${piGid}", ` +
478
+ `intent "fire-and-forget", message "${tokenT2}". Then stop.`;
479
+ const piPhaseTwo =
480
+ `Call entwurf_v2 exactly once with target "${pdGid}", intent "fire-and-forget", message ${JSON.stringify(m1)}. ` +
481
+ `Then stop. Later, when a message from ${pdGid} containing "${tokenT2}" arrives, call entwurf_v2 exactly once with ` +
482
+ `target "${fixtureGid}", intent "fire-and-forget", message "${tokenT2} RELAYED", then stop. Do nothing else.`;
483
+ const toPi = await bridge.call("entwurf_v2", { target: piGid, intent: "fire-and-forget", message: piPhaseTwo });
484
+ receipts["4-fixture-to-pi"] = toPi.text;
485
+ ok("the fixture's instruction reached Pi on its control socket", !toPi.isError, toPi.text);
486
+
487
+ const piToPd = await until("Pi's entwurf_v2 → pi-durable result", TURN_WAIT_MS, () =>
488
+ piTools().find((t) => t.toolName === "entwurf_v2" && t.arguments.target === pdGid && t.status !== "pending"),
489
+ );
490
+ receipts["4-receipt-1-pi-to-pd"] = `${JSON.stringify(piToPd.arguments)}\n${piToPd.text}`;
491
+ ok(
492
+ "RECEIPT 1 (existing → new): Pi's live turn dispatched to pi-durable and the bridge answered with a mailbox ENQUEUE",
493
+ piToPd.status === "completed" && piToPd.arguments.message === m1 && /meta-mailbox → enqueued/.test(piToPd.text),
494
+ piToPd.text,
495
+ );
496
+ const pdState = await until("pi-durable's read receipt (lastReadAt)", TURN_WAIT_MS, () => {
497
+ const file = path.join(meta.defaultMetaMailboxDir(), pdGid, "state.json");
498
+ if (!fs.existsSync(file)) return null;
499
+ const s = JSON.parse(fs.readFileSync(file, "utf8")) as { lastReadAt?: string; lastEnqueuedAt?: string };
500
+ return s.lastReadAt && s.lastEnqueuedAt && Date.parse(s.lastReadAt) >= Date.parse(s.lastEnqueuedAt) ? s : null;
501
+ });
502
+ receipts["4-pd-mailbox-state"] = JSON.stringify(pdState);
503
+ ok("pi-durable drained its own inbox after the enqueue (lastReadAt ≥ lastEnqueuedAt)", true);
504
+
505
+ // ── 7. receipt 2: pd's live turn → Pi; Pi relays to the fixture ──────────────
506
+ // Exact payload + ONE anchored terminal sender envelope naming pd. A row that merely mentions the
507
+ // token and pd's id is not a delivery: the fixture's own instruction to Pi did exactly that.
508
+ const landed = await until("pi-durable's message landing in Pi's transcript", TURN_WAIT_MS, () =>
509
+ piExactDeliveryRow(piEntries(), tokenT2, pdGid),
510
+ );
511
+ receipts["5-receipt-2-landed-in-pi"] = `row ${landed.index}: ${JSON.stringify(landed.row)}`;
512
+ ok(
513
+ "RECEIPT 2 (new → existing): exactly one Pi row carries the expected payload with pi-durable as its authoritative sender",
514
+ true,
515
+ );
516
+ const relay = await until("Pi's relay into the fixture", TURN_WAIT_MS, () => {
517
+ for (const m of meta.readMetaInbox({ gardenId: fixtureGid }).messages) {
518
+ const parsed = parseMetaMailboxBody(m.body);
519
+ if (parsed?.sender === piGid && parsed.payload === `${tokenT2} RELAYED`) return m.body;
520
+ }
521
+ return null;
522
+ });
523
+ receipts["5-relay-to-fixture"] = relay;
524
+ ok("Pi relayed the token to the fixture, which read it", true);
525
+
526
+ // ── 8. pd's NATIVE store: a consistent online snapshot, read-only ────────────
527
+ const agentDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
528
+ const sessionDir = path.join(
529
+ agentDir,
530
+ "experimental",
531
+ "durable-sessions",
532
+ createHash("sha256").update(scratch).digest("hex").slice(0, 24),
533
+ String(pdRecord?.nativeSessionId ?? ""),
534
+ );
535
+ const liveDb = path.join(sessionDir, "session.sqlite");
536
+ ok(
537
+ "the pi-durable record's native session database exists where the native app keeps it",
538
+ fs.existsSync(liveDb),
539
+ liveDb,
540
+ );
541
+ const snapshot = path.join(receiptsDir, "pd-session.snapshot.sqlite");
542
+ fs.mkdirSync(receiptsDir, { recursive: true });
543
+ const sqlite = await import("node:sqlite");
544
+ {
545
+ const source = new sqlite.DatabaseSync(liveDb, { readOnly: true });
546
+ try {
547
+ await sqlite.backup(source, snapshot);
548
+ } finally {
549
+ source.close();
550
+ }
551
+ }
552
+ receipts["6-native-db"] = `live=${liveDb} (read-only)\nsnapshot=${snapshot} (node:sqlite online backup)`;
553
+ const db = new sqlite.DatabaseSync(snapshot, { readOnly: true });
554
+ const roots = db
555
+ .prepare("SELECT id FROM conversations WHERE owner_conversation_id IS NULL AND owner_task_id IS NULL")
556
+ .all() as Array<{ id: number }>;
557
+ const nativeEntries = db.prepare("SELECT id, conversation_id, record FROM entries").all() as unknown as PdNativeRow[];
558
+ const nativeSubmissions = db
559
+ .prepare("SELECT conversation_id, status, record FROM submissions")
560
+ .all() as unknown as PdNativeSubmissionRow[];
561
+ db.close();
562
+ ok("pd's native store has exactly one root conversation", roots.length === 1, JSON.stringify(roots));
563
+ const rootId = (roots[0] as { id: number }).id;
564
+ const view = pdRootView(nativeEntries, rootId);
565
+ receipts["6-native-offering"] = view.offering.join(" ") || "(no pi.system offering)";
566
+ ok(
567
+ "ACTUAL OFFERING (root pi.system, toolsAdded/toolsRemoved applied): the six contact tools AND the native tools — task-wide, no filter",
568
+ CONTACT_TOOLS.every((t) => view.offering.includes(t)) && NATIVE_TOOLS.every((t) => view.offering.includes(t)),
569
+ `offered: ${view.offering.join(", ") || "(none)"}`,
570
+ );
571
+ receipts["6-native-model"] = `root pi.assistant provider/model: ${view.models.join(" ") || "(none)"}`;
572
+ ok(
573
+ `ACTUAL MODEL (root pi.assistant rows): every turn answered as ${PD_MODEL}`,
574
+ view.models.length === 1 && view.models[0] === PD_MODEL,
575
+ receipts["6-native-model"],
576
+ );
577
+ receipts["6-native-tool-calls"] = JSON.stringify(view.calls);
578
+ ok(
579
+ "pd's FIRST root tool call is entwurf_callback, and it is the only one",
580
+ view.calls[0]?.name === "entwurf_callback" && view.calls.filter((c) => c.name === "entwurf_callback").length === 1,
581
+ receipts["6-native-tool-calls"],
582
+ );
583
+ ok(
584
+ "pd's root entwurf_inbox_read is for its OWN garden id and settled without error",
585
+ view.calls.some(
586
+ (c) => c.name === "entwurf_inbox_read" && c.arguments.gardenId === pdGid && c.result?.isError === false,
587
+ ),
588
+ receipts["6-native-tool-calls"],
589
+ );
590
+ const pdToPi = view.calls.find(
591
+ (c) => c.name === "entwurf_v2" && c.arguments.target === piGid && c.arguments.message === tokenT2,
592
+ );
593
+ ok(
594
+ "pd's root entwurf_v2 to Pi carries the expected payload and SETTLED as a control-socket dispatch (native tool-result, joined by toolCallId)",
595
+ pdToPi?.result?.isError === false &&
596
+ /^entwurf_v2 control-socket → (sent|queued-steer|queued-follow-up|accepted-unknown-boundary)/.test(
597
+ pdToPi.result.text,
598
+ ),
599
+ JSON.stringify(pdToPi),
600
+ );
601
+ receipts["6-pd-to-pi-settled"] = JSON.stringify(pdToPi);
602
+ const msgName = /meta-mailbox → enqueued \(([^)]+)\)/.exec(piToPd.text)?.[1] ?? "";
603
+ const doorbell = pdDoorbellSubmission(nativeSubmissions, rootId, `entwurf-doorbell:${pdGid}:${msgName}`);
604
+ receipts["6-native-doorbell-submission"] = JSON.stringify(doorbell);
605
+ ok(
606
+ "the doorbell admitted THIS message's notice to the root conversation and the submission is done",
607
+ msgName !== "" && doorbell?.status === "done",
608
+ `${msgName} → ${receipts["6-native-doorbell-submission"]}`,
609
+ );
610
+ exportFile("pi-transcript.jsonl", fs.readFileSync((piRecord() as { transcriptPath: string }).transcriptPath));
611
+
612
+ // ── decisive lines, for the record that crosses hosts ────────────────────────
613
+ console.log(`\n[${LABEL}] decisive`);
614
+ console.log(` pi-durable ${pdGid} native=${pdRecord?.nativeSessionId} model=${PD_MODEL} width=${WIDTH}`);
615
+ console.log(` pi ${piGid} model=${PI_MODEL}`);
616
+ console.log(` fixture ${fixtureGid} (collector)`);
617
+ console.log(` offering ${receipts["6-native-offering"]}`);
618
+ console.log(` ${receipts["6-native-model"].replace(/\n/g, "\n ")}`);
619
+ console.log(` receipt-1 ${piToPd.text.split("\n")[0]}`);
620
+ console.log(` receipt-2 ${pdToPi?.result?.text} → Pi row ${landed.index} (exact payload ${tokenT2}, sender ${pdGid})`);
621
+ console.log(` root meta=${meta.defaultMetaSessionsDir()} agent=${agentDir}`);
622
+ exportReceipts();
623
+ console.log(`\n[${LABEL}] PASS (${passed} assertions)`);
624
+ teardown();