@junghanacs/entwurf 0.31.0 → 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 (56) hide show
  1. package/AGENTS.md +3 -2
  2. package/CHANGELOG.md +82 -0
  3. package/README.md +46 -36
  4. package/VERIFY.md +35 -16
  5. package/demo/README.md +1 -1
  6. package/docs/acp-backend-rail.md +1 -1
  7. package/docs/setup-clean-host.md +112 -95
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +1 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +1 -1
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +8 -10
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +3 -3
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +153 -205
  13. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +6 -4
  14. package/mcp/entwurf-bridge/src/index.ts +1 -1
  15. package/package.json +14 -17
  16. package/pi/pi-durable/bootstrap.mjs +142 -19
  17. package/pi/pi-durable/carrier/LICENSE +21 -0
  18. package/pi/pi-durable/carrier/harness-setup.js +123 -0
  19. package/pi/pi-durable/carrier/prompt.js +74 -0
  20. package/pi/pi-durable/carrier/runtime.js +402 -0
  21. package/pi/pi-durable/carrier/sessions.js +56 -0
  22. package/pi/pi-durable/carrier/subagent.js +55 -0
  23. package/pi/pi-durable/carrier/tui.js +655 -0
  24. package/pi/pi-durable/carrier-relocation.mjs +160 -0
  25. package/pi/pi-durable/carrier-resolver.mjs +38 -0
  26. package/pi/pi-durable/overlay/upstream-pin.json +103 -11
  27. package/pi-extensions/lib/fresh-call-composition.ts +1 -1
  28. package/pi-extensions/lib/mux-fresh-call.ts +8 -10
  29. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +3 -3
  30. package/pi-extensions/lib/pi-durable-runtime.ts +197 -263
  31. package/pi-extensions/meta-bridge-pi-durable.ts +11 -7
  32. package/run.sh +508 -235
  33. package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
  34. package/scripts/check-fresh-call-dispatch.ts +1 -1
  35. package/scripts/check-gate-qualification.ts +86 -34
  36. package/scripts/check-herdr-activation.ts +5 -4
  37. package/scripts/check-pi-durable-contact.ts +26 -93
  38. package/scripts/check-pi-durable-receive.ts +22 -59
  39. package/scripts/check-pi-durable-send.ts +235 -75
  40. package/scripts/emit-pi-durable-carrier.ts +355 -0
  41. package/scripts/lib/mutation-qualify.ts +21 -45
  42. package/scripts/lib/pi-durable-operator-guard.ts +89 -0
  43. package/scripts/lib/qualification-receipt.ts +7 -11
  44. package/scripts/mutants/control-send-receipt.json +1 -1
  45. package/scripts/mutants/gate-qualification.json +13 -0
  46. package/scripts/mutants/mux-fresh-call.json +13 -13
  47. package/scripts/mutants/pack-install.json +3 -3
  48. package/scripts/mutants/pi-durable.json +442 -109
  49. package/scripts/mutants/pi-floor.json +24 -2
  50. package/scripts/pi-durable-contact-host.mjs +3 -3
  51. package/scripts/pi-durable-receive-host.mjs +3 -3
  52. package/scripts/pi-durable-send-host.mjs +76 -5
  53. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  54. package/scripts/smoke-pi-durable-fresh-live.ts +3 -3
  55. package/scripts/smoke-setup-verdict.sh +15 -15
  56. package/scripts/smoke-user-scope-citizen.sh +1 -1
@@ -23,7 +23,7 @@
23
23
  * reports. None imports the adapter's expected constants.
24
24
  *
25
25
  * Inputs as check-pi-durable-contact (missing → SKIP 97, never a pass):
26
- * ENTWURF_PI_DURABLE_RUNTIME, ENTWURF_PI_DURABLE_BRIDGE_ENTRY
26
+ * ENTWURF_PI_DURABLE_BRIDGE_ENTRY (the durable app is this checkout's verified carrier, #130)
27
27
  * Optional:
28
28
  * ENTWURF_PI_DURABLE_RECEIPTS a NEW absolute directory (refused if it exists) that receives the
29
29
  * cell's raw bytes: wire.json, bridge-tools.json, native-view.json (a
@@ -38,7 +38,11 @@
38
38
  * REQUIRED `intent`, and the app's own validation must settle it as
39
39
  * `invalid_arguments`; `reject-undeliverable` — valid arguments, a
40
40
  * target with no receiver marker, and the bridge must refuse by name
41
- * (`mailbox-undeliverable`), surfacing natively as `tool_error`.
41
+ * (`mailbox-undeliverable`), surfacing natively as `tool_error`;
42
+ * `native-module` (#130 P2) — no send: the packaged `main` runs with
43
+ * the synthetic operator module test/fixtures/pi-durable-native-module/
44
+ * mock.mjs, and ONE scripted answer calls its tool, the native `bash`
45
+ * and the contact's `entwurf_self`.
42
46
  * Unknown selector exits 2 before anything spawns.
43
47
  *
44
48
  * Both cells read the bridge child's kernel counters (/proc/<pid>/io `rchar`, `syscr`) at sampled
@@ -61,32 +65,39 @@ import * as path from "node:path";
61
65
  import { createInterface } from "node:readline";
62
66
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
63
67
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
68
+ import { CARRIER_SCHEME } from "../pi/pi-durable/carrier-relocation.mjs";
64
69
  import {
65
70
  metaReceiverMarkerPath,
66
71
  upsertMetaSession,
67
72
  writeMetaReceiverMarker,
68
73
  writeMetaSenderMarker,
69
74
  } from "../pi-extensions/lib/meta-session.ts";
70
- import {
71
- inspectPiDurableRuntime,
72
- PI_DURABLE_RESOLVER_RELATIVE,
73
- readPiDurablePin,
74
- } from "../pi-extensions/lib/pi-durable-runtime.ts";
75
+ import { checkPiDurableRuntime, piDurableLayoutAt } from "../pi-extensions/lib/pi-durable-runtime.ts";
75
76
  import { skipLive } from "./lib/live-skip.ts";
77
+ import {
78
+ describeOperatorSnapshot,
79
+ operatorPiDurableSnapshot,
80
+ operatorSnapshotUnchanged,
81
+ } from "./lib/pi-durable-operator-guard.ts";
76
82
  import { checkpointVerdict, intentFrameVerdict, preCopyVerdict } from "./lib/pi-durable-r1.ts";
77
83
  import { ownedReapVerdict } from "./lib/pi-durable-reap.ts";
78
84
 
79
85
  const LABEL = "check-pi-durable-send";
80
86
  const REPO = path.resolve(import.meta.dirname, "..");
81
- const OVERLAY = path.join(REPO, "pi", "pi-durable", "overlay");
82
87
  const HOST_DRIVER = path.join(REPO, "scripts", "pi-durable-send-host.mjs");
83
88
  const REPORT_TIMEOUT_MS = 120_000;
84
89
  const CLOSE_TIMEOUT_MS = 30_000;
85
90
  const REAP_TIMEOUT_MS = 5_000;
86
91
  const CELL = process.env.ENTWURF_PI_DURABLE_SEND_CELL?.trim() || "valid";
87
- if (CELL !== "valid" && CELL !== "invalid-args" && CELL !== "reject-undeliverable" && CELL !== "r1-unsafe-recovery") {
92
+ if (
93
+ CELL !== "valid" &&
94
+ CELL !== "invalid-args" &&
95
+ CELL !== "reject-undeliverable" &&
96
+ CELL !== "r1-unsafe-recovery" &&
97
+ CELL !== "native-module"
98
+ ) {
88
99
  console.error(
89
- `check-pi-durable-send: unknown ENTWURF_PI_DURABLE_SEND_CELL ${JSON.stringify(CELL)} (only "valid", "invalid-args", "reject-undeliverable", "r1-unsafe-recovery")`,
100
+ `check-pi-durable-send: unknown ENTWURF_PI_DURABLE_SEND_CELL ${JSON.stringify(CELL)} (only "valid", "invalid-args", "reject-undeliverable", "r1-unsafe-recovery", "native-module")`,
90
101
  );
91
102
  process.exit(2);
92
103
  }
@@ -106,9 +117,69 @@ const REJECT = CELL === "reject-undeliverable";
106
117
  * input, and the call must settle as interrupted without the tool implementation running again.
107
118
  */
108
119
  const R1 = CELL === "r1-unsafe-recovery";
109
- const QK = INVALID ? "PDI" : REJECT ? "PDR" : R1 ? "PR1" : "PDS";
110
- const CALL_ID = INVALID ? "call_pdi_1" : REJECT ? "call_pdr_1" : R1 ? "call_pr1_1" : "call_pds_1";
111
- const FINAL_TEXT = INVALID ? "invalid noted." : REJECT ? "reject noted." : R1 ? "recovered." : "sent.";
120
+ /**
121
+ * native-module (#130 P2): the host runs the whole packaged `main` with ONE operator module — the
122
+ * synthetic fixture, node:* only — and the scripted first answer calls the module's tool, the native
123
+ * `bash` and the contact's `entwurf_self` in one assistant message. An observing resolve hook,
124
+ * preloaded after the carrier resolver and before the host driver, appends the first resolution of
125
+ * every `@earendil-works/*` and carrier-scheme (CARRIER_SCHEME) specifier to the same file the module's
126
+ * initialization appends `init` to: one file, so its line order IS the event order. The bridge's
127
+ * environment is not judged here — the beside QK PI-DURABLE-MODULE-BRIDGE-ENV-SPEC owns that spec.
128
+ */
129
+ const NATIVE = CELL === "native-module";
130
+ const QK = INVALID ? "PDI" : REJECT ? "PDR" : R1 ? "PR1" : NATIVE ? "PDM" : "PDS";
131
+ const CALL_ID = INVALID
132
+ ? "call_pdi_1"
133
+ : REJECT
134
+ ? "call_pdr_1"
135
+ : R1
136
+ ? "call_pr1_1"
137
+ : NATIVE
138
+ ? "call_pdm_1"
139
+ : "call_pds_1";
140
+ const FINAL_TEXT = INVALID
141
+ ? "invalid noted."
142
+ : REJECT
143
+ ? "reject noted."
144
+ : R1
145
+ ? "recovered."
146
+ : NATIVE
147
+ ? "module noted."
148
+ : "sent.";
149
+ const MOCK_MODULE = path.join(REPO, "test", "fixtures", "pi-durable-native-module", "mock.mjs");
150
+ /** What the fixture's initialization sets — typed here, not imported from it. */
151
+ const MOCK_MARK = "mock-native-mark-0f5c";
152
+ const NATIVE_CALLS = [
153
+ { id: CALL_ID, name: "mock_native_echo", arguments: { text: "pdm" } },
154
+ { id: "call_pdm_2", name: "bash", arguments: { command: 'printf %s "$MOCK_NATIVE_MARK"' } },
155
+ { id: "call_pdm_3", name: "entwurf_self", arguments: {} },
156
+ ];
157
+ /** The contact's six tool names, typed here (this gate imports no adapter constant). */
158
+ const CONTACT_TOOLS = [
159
+ "entwurf_self",
160
+ "entwurf_peers",
161
+ "entwurf_v2",
162
+ "entwurf_inbox_read",
163
+ "entwurf_callback",
164
+ "entwurf_fresh_call",
165
+ ];
166
+ const IMPORT_OBSERVER = `data:text/javascript,${encodeURIComponent(
167
+ [
168
+ 'import { appendFileSync } from "node:fs";',
169
+ 'import { registerHooks } from "node:module";',
170
+ "const file = process.env.MOCK_NATIVE_RECEIPT;",
171
+ "const seen = new Set();",
172
+ "registerHooks({",
173
+ "\tresolve(specifier, context, nextResolve) {",
174
+ `\t\tif (file && !seen.has(specifier) && (specifier.startsWith("@earendil-works/") || specifier.startsWith(${JSON.stringify(CARRIER_SCHEME)}))) {`,
175
+ "\t\t\tseen.add(specifier);",
176
+ '\t\t\tappendFileSync(file, "resolve " + specifier + "\\n");',
177
+ "\t\t}",
178
+ "\t\treturn nextResolve(specifier, context);",
179
+ "\t},",
180
+ "});",
181
+ ].join("\n"),
182
+ )}`;
112
183
  /** The committed-intent frame must arrive within this of `go` — a validity cutoff under the MCP
113
184
  * SDK's 60s request timeout, never an elapsed-time success inference. */
114
185
  const INTENT_DEADLINE_MS = 30_000;
@@ -116,17 +187,14 @@ const INTERRUPTED_MESSAGE = "Tool entwurf_v2 was interrupted and may have partia
116
187
  /** One sampled idle window between two counter reads, before the input. Not a quiescence proof. */
117
188
  const IDLE_SAMPLE_MS = 300;
118
189
 
119
- const runtimeInput = process.env.ENTWURF_PI_DURABLE_RUNTIME?.trim();
120
190
  const bridgeInput = process.env.ENTWURF_PI_DURABLE_BRIDGE_ENTRY?.trim();
121
- if (!runtimeInput || !bridgeInput) {
191
+ if (!bridgeInput) {
122
192
  skipLive(
123
193
  LABEL,
124
- "set ENTWURF_PI_DURABLE_RUNTIME=<overlay checkout at the pin, patch applied, deps and model data installed> and " +
125
- "ENTWURF_PI_DURABLE_BRIDGE_ENTRY=<bridge index.js emitted from this checkout into a private bundle>; " +
126
- "this gate provisions neither",
194
+ "set ENTWURF_PI_DURABLE_BRIDGE_ENTRY=<bridge index.js emitted from this checkout into a private bundle>; " +
195
+ "this gate does not provision it",
127
196
  );
128
197
  }
129
- const runtime = path.resolve(runtimeInput);
130
198
  const bridgeEntry = path.resolve(bridgeInput);
131
199
  const FAULT = process.env.ENTWURF_PI_DURABLE_SEND_FAULT?.trim() || undefined;
132
200
  if (FAULT !== undefined && FAULT !== "malformed-report") {
@@ -175,13 +243,12 @@ function canonicalJson(value: unknown): string {
175
243
  }
176
244
 
177
245
  // ---------------------------------------------------------------------------
178
- // 0. Inputs — the same pinned overlay / model data / bundle registry facts as G-contact.
246
+ // 0. Inputs — the same verified carrier / bundle registry facts as G-contact.
179
247
  // ---------------------------------------------------------------------------
180
- // The pin and the runtime facts come from the ONE verifier the managed launch, setup and the fresh
181
- // preflight use (pi-extensions/lib/pi-durable-runtime.ts).
182
- const { pin } = readPiDurablePin(OVERLAY);
183
- const facts = inspectPiDurableRuntime(runtime, OVERLAY);
184
- const resolver = path.join(runtime, PI_DURABLE_RESOLVER_RELATIVE);
248
+ // The carrier and its SDK set are verified by the ONE verifier the managed launch, setup and the fresh
249
+ // preflight use (pi-extensions/lib/pi-durable-runtime.ts), from this checkout's layout.
250
+ const carrier = checkPiDurableRuntime(piDurableLayoutAt(REPO));
251
+ const resolver = carrier.ok ? carrier.resolver : "";
185
252
  const bundleRegistry = path.join(
186
253
  path.resolve(bridgeEntry, "..", "..", "..", ".."),
187
254
  "pi-extensions",
@@ -189,16 +256,8 @@ const bundleRegistry = path.join(
189
256
  );
190
257
  console.log("0. inputs");
191
258
  ok(
192
- `[QK:${QK}-INPUTS] runtime is the pin with exactly the tracked overlay (index == HEAD, nothing staged), the pin's ignored model data, the resolver, and a bridge bundle whose registry matches this checkout`,
193
- facts.gitAnswered &&
194
- facts.head === pin.commit &&
195
- facts.indexMatchesHead &&
196
- facts.diffEqualsPatch &&
197
- facts.untracked === "" &&
198
- facts.modelDataFiles === pin.modelData.files &&
199
- facts.modelDataAggregate === pin.modelData.aggregateSha256 &&
200
- facts.manifestSha256 === pin.modelData.manifestSha256 &&
201
- facts.resolverPresent &&
259
+ `[QK:${QK}-INPUTS] this checkout's pi-durable carrier and the pi SDK set it resolves are the pin (${carrier.ok ? "verified" : `${carrier.reason}: ${carrier.detail}`}), and the bridge bundle's registry matches this checkout`,
260
+ carrier.ok &&
202
261
  fs.existsSync(bridgeEntry) &&
203
262
  fs.existsSync(bundleRegistry) &&
204
263
  fs.readFileSync(bundleRegistry).equals(fs.readFileSync(path.join(REPO, "pi", "entwurf-capabilities.json"))),
@@ -218,35 +277,11 @@ ok(
218
277
  `[QK:${QK}-BUNDLE-OWNER-JOIN] the bridge bundle's compiled owner join admits pi-durable-host (${joinLine || "not found"})`,
219
278
  joinLine.includes('"pi-durable-host"'),
220
279
  );
221
- if (failed > 0)
222
- fatal("inputs are not the pinned overlay, its model data, this checkout's registry and owner join — refusing to run");
280
+ if (failed > 0) fatal("inputs are not the verified carrier, this checkout's registry and owner join — refusing to run");
223
281
 
224
282
  // Coarse pi-durable-specific operator guard (as G-contact): no operator-byte claim beyond it.
225
283
  const operatorAgent = path.join(os.homedir(), ".pi", "agent");
226
- function operatorPiDurableFacts(): string {
227
- const sessions = path.join(operatorAgent, "meta-sessions");
228
- const durableRecords = fs.existsSync(sessions)
229
- ? fs
230
- .readdirSync(sessions)
231
- .filter((n) => n.endsWith(".meta.json"))
232
- .filter((n) => {
233
- try {
234
- return (
235
- (JSON.parse(fs.readFileSync(path.join(sessions, n), "utf8")) as { backend?: unknown }).backend ===
236
- "pi-durable"
237
- );
238
- } catch {
239
- return false;
240
- }
241
- })
242
- : [];
243
- return JSON.stringify({
244
- durableRecords,
245
- senders: fs.existsSync(path.join(operatorAgent, "meta-senders", "pi-durable")),
246
- durableSessions: fs.existsSync(path.join(operatorAgent, "experimental", "durable-sessions")),
247
- });
248
- }
249
- const operatorBefore = operatorPiDurableFacts();
284
+ const operatorBefore = operatorPiDurableSnapshot(operatorAgent);
250
285
 
251
286
  // ---------------------------------------------------------------------------
252
287
  // Sandbox: one private world — HOME/XDG/agent, the four garden roots and ENTWURF_DIR.
@@ -290,6 +325,7 @@ const env: Record<string, string> = {
290
325
  const hostEnv: Record<string, string> = {
291
326
  ...env,
292
327
  ...(FAULT === undefined ? {} : { ENTWURF_PI_DURABLE_SEND_FAULT: FAULT }),
328
+ ...(NATIVE ? { MOCK_NATIVE_RECEIPT: path.join(dirs.tmp, "native-module.receipt") } : {}),
293
329
  };
294
330
 
295
331
  // ---------------------------------------------------------------------------
@@ -346,18 +382,17 @@ const server = http.createServer((req, res) => {
346
382
  wire.push({ method: req.method ?? "", url: req.url ?? "", body });
347
383
  const n = wire.length;
348
384
  if (req.method === "POST" && req.url === "/v1/chat/completions" && n === 1) {
385
+ const calls = NATIVE ? NATIVE_CALLS : [{ id: CALL_ID, name: "entwurf_v2", arguments: scriptedArgs }];
349
386
  sse(res, [
350
387
  chunk(
351
388
  {
352
389
  role: "assistant",
353
- tool_calls: [
354
- {
355
- index: 0,
356
- id: CALL_ID,
357
- type: "function",
358
- function: { name: "entwurf_v2", arguments: JSON.stringify(scriptedArgs) },
359
- },
360
- ],
390
+ tool_calls: calls.map((call, index) => ({
391
+ index,
392
+ id: call.id,
393
+ type: "function",
394
+ function: { name: call.name, arguments: JSON.stringify(call.arguments) },
395
+ })),
361
396
  },
362
397
  null,
363
398
  ),
@@ -1137,8 +1172,9 @@ interface LaunchedHost {
1137
1172
  stderr(): string;
1138
1173
  }
1139
1174
 
1140
- async function launchHost(args: string[], what: string): Promise<LaunchedHost> {
1141
- const child = spawn(process.execPath, ["--import", resolver, HOST_DRIVER, bridgeEntry, ...args], {
1175
+ async function launchHost(args: string[], what: string, imports: string[] = []): Promise<LaunchedHost> {
1176
+ const preload = ["--import", resolver, ...imports.flatMap((url) => ["--import", url])];
1177
+ const child = spawn(process.execPath, [...preload, HOST_DRIVER, bridgeEntry, ...args], {
1142
1178
  cwd: dirs.project,
1143
1179
  env: hostEnv,
1144
1180
  stdio: ["pipe", "pipe", "pipe"],
@@ -1606,9 +1642,131 @@ function exportR1Receipts(dir: string, ctx: R1Context | undefined): void {
1606
1642
  );
1607
1643
  }
1608
1644
 
1645
+ interface NativeContext {
1646
+ report: Report & { native?: boolean };
1647
+ receipt: string[];
1648
+ hostGardenId: string | undefined;
1649
+ }
1650
+ let nativeContext: NativeContext | undefined;
1651
+
1652
+ /**
1653
+ * #130 P2: one operator module through the packaged `main`, one native turn. The order oracle is the
1654
+ * receipt file (module `init` vs the observer's first SDK/carrier resolutions); the registration,
1655
+ * coexistence and child-environment oracles are the native view's three settled results and the
1656
+ * host's own record; the wire is judged after the run (finalWireNative).
1657
+ */
1658
+ async function nativeModuleCell(): Promise<void> {
1659
+ console.log("1. native module: the packaged main with one operator module, then one native turn");
1660
+ const host = await launchHost(["use the native module", "--native-module", MOCK_MODULE], "host", [IMPORT_OBSERVER]);
1661
+ const report = (await host.next(REPORT_TIMEOUT_MS)) as unknown as NativeContext["report"];
1662
+ if (report.error !== undefined) throw new Error(`host: ${report.error}`);
1663
+ // The host is alive and holding: own its bridge child from /proc now, as the other cells do.
1664
+ if (!observeBridges(host.owned))
1665
+ throw new Error(`GAP: host ${host.owned.pid}'s children were not observed coherently`);
1666
+ const receipt = fs
1667
+ .readFileSync(hostEnv.MOCK_NATIVE_RECEIPT as string, "utf8")
1668
+ .split("\n")
1669
+ .filter(Boolean);
1670
+ const firstResolve = receipt.findIndex((line) => line.startsWith("resolve "));
1671
+ ok(
1672
+ `[QK:PDM-INIT-BEFORE-SDK] the module's initialization completed before the first @earendil-works / carrier-scheme resolution in the host, and the TUI's and the runtime's own imports came after it (${receipt.length} receipt lines; first: ${receipt.slice(0, 3).join(" | ")})`,
1673
+ receipt[0] === "init" &&
1674
+ receipt.filter((line) => line === "init").length === 1 &&
1675
+ firstResolve === 1 &&
1676
+ receipt.includes("resolve @earendil-works/pi-tui") &&
1677
+ receipt.includes(`resolve ${CARRIER_SCHEME}core/model-runtime.js`),
1678
+ );
1679
+ const records = fs
1680
+ .readdirSync(dirs.sessions)
1681
+ .filter((name) => name.endsWith(".meta.json"))
1682
+ .map(
1683
+ (name) =>
1684
+ JSON.parse(fs.readFileSync(path.join(dirs.sessions, name), "utf8")) as { backend?: string; gardenId?: string },
1685
+ );
1686
+ const hostRecords = records.filter((r) => r.backend === "pi-durable");
1687
+ const hostGardenId = hostRecords.length === 1 ? hostRecords[0]?.gardenId : undefined;
1688
+ const calls = report.entries.flatMap((e) => e.toolCalls);
1689
+ const resultOf = (id: string) => report.entries.find((e) => e.kind === "pi.tool-result" && e.toolCallId === id);
1690
+ ok(
1691
+ `[QK:PDM-NATIVE-CALLS] the native view records the scripted assistant message's three calls — the module's tool, the native bash and the contact's entwurf_self — each with a settled, non-error result (${calls.map((c) => `${c.id}:${c.name}`).join(", ")})`,
1692
+ calls.length === 3 &&
1693
+ NATIVE_CALLS.every((want) => calls.some((c) => c.id === want.id && c.name === want.name)) &&
1694
+ NATIVE_CALLS.every((want) => resultOf(want.id)?.isError === false),
1695
+ );
1696
+ ok(
1697
+ `[QK:PDM-MODULE-TOOL-RAN] the module's own tool ran natively and answered its text (got ${JSON.stringify(resultOf(CALL_ID)?.text)})`,
1698
+ resultOf(CALL_ID)?.text === "mock:pdm",
1699
+ );
1700
+ ok(
1701
+ `[QK:PDM-BASH-CHILD-ENV] the native bash child process saw the value the module's initialization set (got ${JSON.stringify(resultOf("call_pdm_2")?.text)})`,
1702
+ resultOf("call_pdm_2")?.text === MOCK_MARK,
1703
+ );
1704
+ ok(
1705
+ `[QK:PDM-CONTACT-COEXISTS] beside the module, the contact still answers natively: entwurf_self names the host's own record (${hostGardenId ?? `${hostRecords.length} pi-durable records`})`,
1706
+ hostGardenId !== undefined && (resultOf("call_pdm_3")?.text ?? "").includes(hostGardenId),
1707
+ );
1708
+ const lastAssistant = report.entries.filter((e) => e.kind === "pi.assistant").at(-1);
1709
+ ok(
1710
+ `[QK:PDM-TURN-SETTLED] the turn ends on the scripted text and the conversation is no longer busy (last: ${JSON.stringify(lastAssistant?.text)}, busy ${report.busy})`,
1711
+ lastAssistant?.text === FINAL_TEXT && report.busy === false,
1712
+ );
1713
+ console.log(` notices: ${report.notices.join(" | ") || "none"}`);
1714
+ nativeContext = { report, receipt, hostGardenId };
1715
+ host.child.stdin?.write("close\n");
1716
+ const closed = await host.next(CLOSE_TIMEOUT_MS);
1717
+ await Promise.race([host.exited, sleep(CLOSE_TIMEOUT_MS)]);
1718
+ ok(
1719
+ `[QK:PDM-CLOSE] the host closes and exits 0 (closed ${String(closed.closed)}, exit ${host.exit().code})`,
1720
+ closed.closed === true && host.exit().code === 0,
1721
+ );
1722
+ }
1723
+
1724
+ /** The native-module cell's wire, judged on the closed run's final snapshot. */
1725
+ function finalWireNative(ctx: NativeContext): void {
1726
+ console.log("3. wire, final snapshot (host exited, endpoint closed)");
1727
+ const first = wire[0]?.body ?? {};
1728
+ const offered = ((first.tools as { function?: { name?: string } }[] | undefined) ?? []).map(
1729
+ (t) => t.function?.name ?? "",
1730
+ );
1731
+ ok(
1732
+ `[QK:PDM-WIRE-TOOLS-OFFERED] the app's first model request offers the module's tool, the native bash and all six contact tools together (offered: ${offered.join(", ")})`,
1733
+ ["mock_native_echo", "bash", ...CONTACT_TOOLS].every((name) => offered.includes(name)),
1734
+ );
1735
+ ok(
1736
+ `[QK:PDM-WIRE-TWO-REQUESTS] over the whole closed run the endpoint saw exactly the two scripted requests, nothing unexpected (${wire.map((w) => `${w.method} ${w.url}`).join(" | ")})`,
1737
+ !server.listening &&
1738
+ wire.length === 2 &&
1739
+ wire.every((w) => w.method === "POST" && w.url === "/v1/chat/completions"),
1740
+ );
1741
+ const second =
1742
+ (wire[1]?.body?.messages as { role?: string; tool_call_id?: string; content?: unknown }[] | undefined) ?? [];
1743
+ const toolText = (id: string) => messageText(second.find((m) => m.role === "tool" && m.tool_call_id === id)?.content);
1744
+ ok(
1745
+ `[QK:PDM-RESULTS-REACH-MODEL] the second model request carries all three results as the native view recorded them (module ${JSON.stringify(toolText(CALL_ID))})`,
1746
+ NATIVE_CALLS.every(
1747
+ (call) =>
1748
+ toolText(call.id) !== "" &&
1749
+ toolText(call.id) ===
1750
+ ctx.report.entries.find((e) => e.kind === "pi.tool-result" && e.toolCallId === call.id)?.text,
1751
+ ),
1752
+ );
1753
+ }
1754
+
1755
+ /** The native-module cell's raw bytes. */
1756
+ function exportNativeReceipts(dir: string, ctx: NativeContext | undefined): void {
1757
+ fs.writeFileSync(path.join(dir, "wire.json"), `${JSON.stringify(wire, null, 1)}\n`);
1758
+ if (ctx === undefined) return;
1759
+ fs.writeFileSync(path.join(dir, "native-view.json"), `${JSON.stringify(ctx.report, null, 1)}\n`);
1760
+ fs.writeFileSync(path.join(dir, "module-receipt.log"), `${ctx.receipt.join("\n")}\n`);
1761
+ fs.writeFileSync(
1762
+ path.join(dir, "correlation.json"),
1763
+ `${JSON.stringify({ cell: CELL, module: MOCK_MODULE, hostGardenId: ctx.hostGardenId, calls: NATIVE_CALLS, wireRequests: wire.length }, null, 1)}\n`,
1764
+ );
1765
+ }
1766
+
1609
1767
  let primary: unknown;
1610
1768
  try {
1611
- await (R1 ? r1Cell() : cell());
1769
+ await (R1 ? r1Cell() : NATIVE ? nativeModuleCell() : cell());
1612
1770
  } catch (error) {
1613
1771
  primary = error;
1614
1772
  }
@@ -1634,20 +1792,22 @@ ok(
1634
1792
  );
1635
1793
  if (R1) {
1636
1794
  if (r1Context !== undefined) finalWireR1(r1Context);
1795
+ } else if (NATIVE) {
1796
+ if (nativeContext !== undefined) finalWireNative(nativeContext);
1637
1797
  } else if (context !== undefined) finalWire(context);
1638
1798
  if (receiptsInput !== undefined) {
1639
1799
  try {
1640
1800
  if (R1) exportR1Receipts(receiptsInput, r1Context);
1801
+ else if (NATIVE) exportNativeReceipts(receiptsInput, nativeContext);
1641
1802
  else exportReceipts(receiptsInput, context);
1642
1803
  ok(`[QK:${QK}-RECEIPTS-EXPORTED] the cell's raw bytes and correlation are in ${receiptsInput}`, true);
1643
1804
  } catch (error) {
1644
1805
  ok(`[QK:${QK}-RECEIPTS-EXPORTED] receipt export failed: ${(error as Error).message}`, false);
1645
1806
  }
1646
1807
  }
1647
- const before = JSON.parse(operatorBefore) as { durableRecords: string[]; senders: boolean };
1648
1808
  ok(
1649
- `[QK:${QK}-OPERATOR-UNTOUCHED] coarse pi-durable guard: the operator's own roots are unchanged by this run and hold no pi-durable record or sender dir (${operatorBefore})`,
1650
- operatorPiDurableFacts() === operatorBefore && before.durableRecords.length === 0 && !before.senders,
1809
+ `[QK:${QK}-OPERATOR-UNTOUCHED] coarse pi-durable registration guard: this run left the operator's pi-durable registrations exactly as it found them (${describeOperatorSnapshot(operatorBefore)} — same names, same bytes; existing state is not a red; no auth or session content read)`,
1810
+ operatorSnapshotUnchanged(operatorBefore, operatorPiDurableSnapshot(operatorAgent)),
1651
1811
  );
1652
1812
  if (alive.length > 0) console.log(`sandbox kept because owned processes are still alive: ${root}`);
1653
1813
  else if (process.env.ENTWURF_PI_DURABLE_KEEP === "1") console.log(`sandbox kept: ${root}`);