@junghanacs/entwurf 0.15.0 → 0.16.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 (90) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +299 -0
  3. package/DELIVERY.md +4 -3
  4. package/README.md +99 -67
  5. package/VERIFY.md +13 -7
  6. package/docs/acp-backend-rail.md +1 -1
  7. package/docs/external-mcp-host.md +147 -9
  8. package/docs/setup-clean-host.md +221 -16
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
  18. package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
  19. package/mcp/entwurf-bridge/src/index.ts +65 -19
  20. package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
  21. package/mcp/tsconfig.json +10 -0
  22. package/package.json +14 -4
  23. package/pi/entwurf-capabilities.json +1 -0
  24. package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
  25. package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
  26. package/pi-extensions/entwurf-control.ts +12 -9
  27. package/pi-extensions/lib/entwurf-self-address.ts +18 -7
  28. package/pi-extensions/lib/meta-sender-identity.ts +1 -1
  29. package/pi-extensions/lib/meta-session.ts +219 -5
  30. package/pi-extensions/lib/mux-fresh-call.ts +171 -17
  31. package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
  32. package/pi-extensions/meta-bridge-omp.ts +1244 -0
  33. package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
  34. package/run.sh +1041 -174
  35. package/scripts/agy-bridge.sh +4 -2
  36. package/scripts/check-copilot-birth-hook.ts +700 -23
  37. package/scripts/check-entwurf-self-address.ts +31 -0
  38. package/scripts/check-fresh-cut-gate.sh +12 -4
  39. package/scripts/check-gate-qualification.ts +8 -2
  40. package/scripts/check-harness-admission-parity.ts +143 -0
  41. package/scripts/check-meta-doctor-oracle.sh +20 -0
  42. package/scripts/check-omp-birth-hook.ts +1049 -0
  43. package/scripts/check-omp-fresh-preflight.ts +181 -0
  44. package/scripts/check-omp-receive-arm.ts +516 -0
  45. package/scripts/check-setup-qualification.sh +126 -0
  46. package/scripts/copilot-bridge-doctor.sh +104 -71
  47. package/scripts/copilot-bridge-install.sh +180 -20
  48. package/scripts/copilot-bridge-oracle.sh +223 -0
  49. package/scripts/copilot-bridge-uninstall.sh +137 -0
  50. package/scripts/dev-bin.sh +50 -9
  51. package/scripts/fake-copilot-vendor.sh +67 -0
  52. package/scripts/inventory-verification-surface.ts +1 -1
  53. package/scripts/meta-bridge-hook-log.sh +9 -1
  54. package/scripts/mutants/copilot-birth.json +96 -5
  55. package/scripts/mutants/mux-fresh-call.json +22 -22
  56. package/scripts/mutants/omp-birth.json +173 -0
  57. package/scripts/mutants/omp-fresh.json +300 -0
  58. package/scripts/mutants/omp-receive.json +135 -0
  59. package/scripts/mutants/pi-package-ownership.json +92 -0
  60. package/scripts/mutants/self-address.json +34 -0
  61. package/scripts/mutants/setup-verdict.json +148 -0
  62. package/scripts/mutants/source-install.json +32 -0
  63. package/scripts/omp-bridge-doctor.sh +315 -0
  64. package/scripts/omp-bridge-install.sh +221 -0
  65. package/scripts/omp-bridge-oracle.sh +154 -0
  66. package/scripts/omp-bridge-uninstall.sh +57 -0
  67. package/scripts/omp-mcp-bridge.sh +320 -0
  68. package/scripts/omp-mcp-config.py +392 -0
  69. package/scripts/omp-receive-doctor.sh +246 -0
  70. package/scripts/omp-receive-facts.ts +106 -0
  71. package/scripts/omp-receive-install.sh +228 -0
  72. package/scripts/omp-receive-uninstall.sh +60 -0
  73. package/scripts/omp-tool-surface.py +349 -0
  74. package/scripts/pi_settings_io.py +22 -0
  75. package/scripts/raw-omp-measure/README.md +420 -0
  76. package/scripts/raw-omp-measure/probe-extension.ts +76 -0
  77. package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
  78. package/scripts/raw-omp-measure/source-audit.md +414 -0
  79. package/scripts/register-pi-package.py +525 -7
  80. package/scripts/register-pi-provider.py +162 -12
  81. package/scripts/smoke-agy-install-state.sh +86 -30
  82. package/scripts/smoke-agy-statusline-state.sh +9 -6
  83. package/scripts/smoke-omp-bridge-state.sh +221 -0
  84. package/scripts/smoke-omp-fresh-live.ts +497 -0
  85. package/scripts/smoke-omp-mcp-state.sh +327 -0
  86. package/scripts/smoke-omp-receive-live.ts +340 -0
  87. package/scripts/smoke-omp-receive-state.sh +196 -0
  88. package/scripts/smoke-setup-verdict.sh +291 -0
  89. package/scripts/smoke-user-scope-citizen.sh +388 -1
  90. package/scripts/tsconfig.json +2 -0
@@ -0,0 +1,497 @@
1
+ /**
2
+ * smoke-omp-fresh-live — the LIVE acceptance for #87 Bundle C: OMP opened as ONE visible fresh
3
+ * sibling through the public `entwurf_fresh_call` surface, naming itself back by exact nonce, and
4
+ * then receiving an addressed message in that same native session.
5
+ *
6
+ * RELEASE MUST. Needs `LIVE=1`. Opens a real omp TUI and spends real model turns.
7
+ *
8
+ * ── Why this is a release MUST and not an on-demand axis ──
9
+ *
10
+ * The static gates admit omp to the fresh set and hold every public surface in parity, but a
11
+ * schema is not a product. `[측정]` omp 18.0.0's bootstrap-payload submission PARSES correctly,
12
+ * and parsing is exactly what pi got right while submitting no message at all
13
+ * (`mux-fresh-call.ts` — "Flag-first submitted no message"). For THIS backend that is not a
14
+ * hypothetical: `[LIVE 2026-08-30]` the retired positional candidate parsed perfectly, opened its
15
+ * window and minted its citizen, and the model still answered the literal text `ACK` with zero
16
+ * tool calls because the turn began before the callback tool existed. A release whose
17
+ * `entwurf_fresh_call(omp)` opens a window that never runs its turn, or whose model cannot see
18
+ * `mcp__entwurf_bridge_entwurf_v`, would pass every deterministic gate in this repo. So the
19
+ * admission contract's clause 7 receipt is wired here rather than left to an operator's memory:
20
+ * `docs/adding-a-harness.md` step 9 makes visible fresh the thing "supported" MEANS, and a MUST
21
+ * step is how that stops being prose. This is a NEW contract applied from omp onward; it does not
22
+ * retroactively redesign Copilot's operator-metered exclusion.
23
+ *
24
+ * ── It decides its own outcome, never a hardcoded verdict ──
25
+ *
26
+ * omp absent from FRESH_CALL_BACKENDS → protocol SKIP: the composition cannot open it, so
27
+ * there is no product to accept. `check-harness-admission-parity` is what makes that state
28
+ * a red release rather than a quiet one, and this step stays honest about having no subject.
29
+ * registry says omp has no drainable mailbox → protocol SKIP: clause 7 requires callback AND
30
+ * addressed receive, and half a contract is not an acceptance.
31
+ * admitted, and LIVE≠1 → protocol SKIP naming LIVE=1.
32
+ * admitted, and LIVE=1 → every missing prerequisite below is a FAIL, not a skip. Once the
33
+ * composition offers the backend, a host that cannot open it is a broken promise.
34
+ *
35
+ * ── Product surface only ──
36
+ *
37
+ * The sibling is opened by `tools/call entwurf_fresh_call` through the REAL bridge, in a private
38
+ * tmux server this step owns. A raw `tmux new-window omp` would prove the vendor and nothing about
39
+ * our product — that shortcut is what `smoke-omp-receive-live` uses deliberately, because ITS
40
+ * subject is receive and launch was not yet admitted. Here launch IS the subject, so the entry
41
+ * point is the one a caller reaches.
42
+ *
43
+ * ── What it writes ──
44
+ *
45
+ * The four meta roots are REAL, and that is stated rather than hidden. The birth and receiver
46
+ * extensions run INSIDE the launched omp process and resolve their own roots from the omp root
47
+ * policy; there is no env carrier on the fresh argv that could fence them, and inventing one to
48
+ * make a gate convenient would add a garden-root carrier the product refuses. So this step mints
49
+ * one real citizen, exactly as an operator's own `omp` would, and proves it minted only that.
50
+ * `smoke-omp-receive-live` set the precedent on the same lane.
51
+ */
52
+ import { type ChildProcess, execFileSync, spawn, spawnSync } from "node:child_process";
53
+ import * as fs from "node:fs";
54
+ import * as os from "node:os";
55
+ import * as path from "node:path";
56
+ import { FRESH_CALL_BACKENDS } from "../pi-extensions/lib/mux-fresh-call.ts";
57
+ import { ompFreshPreflight } from "../pi-extensions/lib/omp-fresh-preflight.ts";
58
+ import { buildOmpCallbackOnlyPrompt } from "../pi-extensions/meta-bridge-omp.ts";
59
+ import { skipLive } from "./lib/live-skip.ts";
60
+
61
+ const LABEL = "smoke-omp-fresh-live";
62
+ const REPO = path.resolve(import.meta.dirname, "..");
63
+ const DIST_ENTRY = path.join(REPO, "mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js");
64
+ /**
65
+ * The operator's configured omp model. Overridable because a model is a host fact, not a
66
+ * product one — but never OPTIONAL: step 9 clause 2 requires an explicit model.
67
+ *
68
+ * THE DEFAULT IS THE MODEL THE CALLBACK WAS MEASURED ON. `[측정 2026-08-30]` the reference
69
+ * callback-only run that proved the two-stage bootstrap end to end used
70
+ * `openai-codex/gpt-5.6-sol`: the tool appeared at +1105ms, the prompt went in at +1107ms and
71
+ * the sibling called `mcp__entwurf_bridge_entwurf_v` with the exact nonce
72
+ * (`omp-cb-btkvva4r87` -> `20260830T184054-1aa1f2`, `meta-mailbox → enqueued`). The earlier
73
+ * `-terra` default is deliberately NOT kept: the one run it was measured on is the run that
74
+ * answered `ACK` without calling anything, and while the tool was provably absent for that
75
+ * turn — so the model is not the established cause — a release MUST step should default to
76
+ * the configuration that has actually been observed to close, not to the one that has only
77
+ * been observed to fail.
78
+ */
79
+ const OMP_MODEL = process.env.ENTWURF_OMP_FRESH_MODEL ?? "openai-codex/gpt-5.6-sol";
80
+ const GARDEN_ID = /^\d{8}T\d{6}-[0-9a-f]{6}$/;
81
+ const CALLBACK_WAIT_MS = 240_000;
82
+ const READ_WAIT_MS = 240_000;
83
+
84
+ let passed = 0;
85
+ const receipts: Record<string, string> = {};
86
+ let socket = "";
87
+ let root = "";
88
+
89
+ function teardown(): void {
90
+ if (socket) {
91
+ spawnSync("tmux", ["-S", socket, "kill-server"], { stdio: "ignore" });
92
+ socket = "";
93
+ }
94
+ if (root) {
95
+ fs.rmSync(root, { recursive: true, force: true });
96
+ root = "";
97
+ }
98
+ }
99
+ function ok(label: string, cond: boolean, detail = ""): void {
100
+ if (!cond) {
101
+ console.error(` FAIL ${label}${detail ? `\n${detail}` : ""}`);
102
+ printReceipts();
103
+ teardown();
104
+ process.exit(1);
105
+ }
106
+ console.log(` ok ${label}`);
107
+ passed++;
108
+ }
109
+ function fail(message: string): never {
110
+ console.error(`[${LABEL}] ${message}`);
111
+ printReceipts();
112
+ teardown();
113
+ process.exit(1);
114
+ }
115
+ /** Receipts must travel: a host-local path is unreadable to whoever reads the cut record, so
116
+ * the decisive lines are printed into this run's own output. */
117
+ function printReceipts(): void {
118
+ if (Object.keys(receipts).length === 0) return;
119
+ console.log(`\n[${LABEL}] receipts`);
120
+ for (const [k, v] of Object.entries(receipts)) console.log(`--- ${k} ---\n${v}`);
121
+ }
122
+
123
+ async function until<T>(what: string, timeoutMs: number, probe: () => T | null): Promise<T> {
124
+ const deadline = Date.now() + timeoutMs;
125
+ for (;;) {
126
+ const got = probe();
127
+ if (got !== null && got !== undefined) return got;
128
+ if (Date.now() > deadline) fail(`timed out after ${timeoutMs}ms waiting for ${what}`);
129
+ await new Promise((r) => setTimeout(r, 500));
130
+ }
131
+ }
132
+
133
+ // ── 1. does this step have a subject at all? ─────────────────────────────────
134
+ const meta = await import("../pi-extensions/lib/meta-session.ts");
135
+
136
+ if (!(FRESH_CALL_BACKENDS as readonly string[]).includes("omp")) {
137
+ skipLive(
138
+ LABEL,
139
+ "omp is not in FRESH_CALL_BACKENDS, so the composition cannot open it and there is no visible-fresh product to accept. " +
140
+ "That state is not neutral: check-harness-admission-parity turns a citizen backend missing from the fresh set into a " +
141
+ "RED release package, because an `unsupported` note is not a partial-release permit (docs/adding-a-harness.md step 9).",
142
+ );
143
+ }
144
+ const wakeMode = (() => {
145
+ try {
146
+ return meta.loadMetaCapabilityRegistry().backends.omp?.wakeMode ?? "ABSENT";
147
+ } catch (err) {
148
+ fail(`capability registry unreadable: ${String(err)}`);
149
+ }
150
+ })();
151
+ if (wakeMode !== "self-fetch") {
152
+ skipLive(
153
+ LABEL,
154
+ `omp declares wakeMode=${wakeMode}, so it has no drainable mailbox. Step 9 clause 7 requires ONE receipt covering ` +
155
+ "callback AND addressed receive; half of that contract is not an acceptance.",
156
+ );
157
+ }
158
+ if (process.env.LIVE !== "1") {
159
+ skipLive(
160
+ LABEL,
161
+ "set LIVE=1 to run — this step opens a real omp TUI through entwurf_fresh_call and spends real model turns on the " +
162
+ "operator's configured omp model.",
163
+ );
164
+ }
165
+
166
+ // ── 2. prerequisites: FAIL, not skip ─────────────────────────────────────────
167
+ try {
168
+ execFileSync("bash", ["-c", "command -v omp"], { stdio: "ignore" });
169
+ } catch {
170
+ fail(
171
+ "the 'omp' CLI is not on PATH, but the composition offers omp as a fresh backend. entwurf never installs a harness.",
172
+ );
173
+ }
174
+ if (!fs.existsSync(DIST_ENTRY)) fail(`the built bridge is missing at ${DIST_ENTRY} — run 'pnpm run build-bridge'.`);
175
+
176
+ const missing = ompFreshPreflight(process.env);
177
+ if (missing) {
178
+ fail(
179
+ `the OMP fresh preflight refuses this host (${missing}). The composition offers omp, so a host that cannot open it is a ` +
180
+ "broken promise rather than an absent prerequisite. Repair the named unit and re-run.",
181
+ );
182
+ }
183
+ console.log(` ok the OMP fresh preflight passes on this host (all five axes)`);
184
+ passed++;
185
+
186
+ // The callback has to LAND. The caller citizen is minted under the default roots while the
187
+ // sibling's bridge child resolves the omp root policy; on a host where those disagree the
188
+ // callback would be enqueued into a mailbox this process never reads, and the failure would
189
+ // look like "the sibling never called back". Name it up front instead.
190
+ const ompRoots = meta.ompMetaRoots();
191
+ ok(
192
+ "the omp root policy and this process's default roots address the SAME mailbox — otherwise the callback lands somewhere this step cannot read and the failure would masquerade as a silent sibling",
193
+ ompRoots.mailboxDir === meta.defaultMetaMailboxDir() && ompRoots.sessionsDir === meta.defaultMetaSessionsDir(),
194
+ ` omp: ${ompRoots.mailboxDir}\n default: ${meta.defaultMetaMailboxDir()}`,
195
+ );
196
+
197
+ // ── 3. the caller: a citizen the bridge recognises as its own owner ──────────
198
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-omp-fresh-"));
199
+ process.on("exit", teardown);
200
+ const scratch = path.join(root, "scratch");
201
+ fs.mkdirSync(scratch, { recursive: true });
202
+
203
+ const nativeSessionId = `omp-fresh-live-${process.pid}`;
204
+ const caller = meta.upsertMetaSession({ input: { backend: "claude-code", nativeSessionId, cwd: scratch } });
205
+ const callerGid = caller.record.gardenId;
206
+ meta.writeMetaReceiverMarker({
207
+ gardenId: callerGid,
208
+ backend: "claude-code",
209
+ nativeSessionId,
210
+ ownerPid: process.pid,
211
+ armProvenance: "session-start",
212
+ });
213
+ const senderMarkerPath = meta.writeMetaSenderMarker({
214
+ backend: "claude-code",
215
+ gardenId: callerGid,
216
+ nativeSessionId,
217
+ cwd: scratch,
218
+ ownerPid: process.pid,
219
+ });
220
+ ok("the caller is a record-backed citizen with an armed mailbox and a sender marker", Boolean(callerGid));
221
+
222
+ const citizensBefore = new Set(
223
+ (
224
+ JSON.parse(execFileSync("bash", [path.join(REPO, "run.sh"), "meta-facts"], { encoding: "utf8" }))
225
+ .citizens as Array<{
226
+ gardenId: string;
227
+ }>
228
+ ).map((c) => c.gardenId),
229
+ );
230
+
231
+ // ── 4. a private tmux server, so placement never touches the operator's ──────
232
+ {
233
+ socket = path.join(root, "fresh.sock");
234
+ const serverEnv = { ...process.env } as NodeJS.ProcessEnv;
235
+ delete serverEnv.TMUX;
236
+ delete serverEnv.TMUX_PANE;
237
+ const started = spawnSync(
238
+ "tmux",
239
+ ["-S", socket, "new-session", "-d", "-s", "fixture", "-n", "anchor", "-c", scratch],
240
+ { env: serverEnv, encoding: "utf8" },
241
+ );
242
+ if (started.status !== 0) fail(`could not start the private tmux server at ${socket}: ${started.stderr}`);
243
+ }
244
+ const anchorPane = execFileSync("tmux", ["-S", socket, "display-message", "-p", "-t", "fixture:anchor", "#{pane_id}"], {
245
+ encoding: "utf8",
246
+ }).trim();
247
+ ok("the tmux anchor is a private socket, never the operator's", socket.startsWith(root) && anchorPane.startsWith("%"));
248
+
249
+ const bridgeEnv: NodeJS.ProcessEnv = {
250
+ ...process.env,
251
+ TMUX: `${socket},0,0`,
252
+ TMUX_PANE: anchorPane,
253
+ ENTWURF_META_SENDER_MARKER: senderMarkerPath,
254
+ };
255
+
256
+ // ── a persistent MCP client over the real bridge ─────────────────────────────
257
+ class BridgeClient {
258
+ private child: ChildProcess;
259
+ private buf = "";
260
+ private err = "";
261
+ private next = 2;
262
+ private waiters = new Map<number, (v: { text: string; isError: boolean }) => void>();
263
+ constructor(env: NodeJS.ProcessEnv) {
264
+ this.child = spawn(process.execPath, [DIST_ENTRY], { stdio: ["pipe", "pipe", "pipe"], env });
265
+ this.child.stderr?.on("data", (d) => {
266
+ this.err += d.toString();
267
+ });
268
+ this.child.stdout?.on("data", (d) => {
269
+ this.buf += d.toString();
270
+ const lines = this.buf.split("\n");
271
+ this.buf = lines.pop() ?? "";
272
+ for (const line of lines) {
273
+ if (!line.trim().startsWith("{")) continue;
274
+ try {
275
+ const msg = JSON.parse(line);
276
+ const w = this.waiters.get(msg.id);
277
+ if (!w) continue;
278
+ this.waiters.delete(msg.id);
279
+ w({
280
+ text: (msg.result?.content ?? []).map((c: { text?: string }) => c.text ?? "").join("\n"),
281
+ isError: msg.result?.isError === true,
282
+ });
283
+ } catch {
284
+ /* partial frame */
285
+ }
286
+ }
287
+ });
288
+ this.send({
289
+ jsonrpc: "2.0",
290
+ id: 1,
291
+ method: "initialize",
292
+ params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: LABEL, version: "0" } },
293
+ });
294
+ this.send({ jsonrpc: "2.0", method: "notifications/initialized" });
295
+ }
296
+ private send(msg: unknown): void {
297
+ this.child.stdin?.write(`${JSON.stringify(msg)}\n`);
298
+ }
299
+ stderrTail(): string {
300
+ return this.err.slice(-2000);
301
+ }
302
+ close(): void {
303
+ this.child.kill("SIGTERM");
304
+ }
305
+ call(name: string, args: Record<string, unknown>, timeoutMs = 60_000): Promise<{ text: string; isError: boolean }> {
306
+ const id = this.next++;
307
+ return new Promise((resolve, reject) => {
308
+ const timer = setTimeout(
309
+ () => reject(new Error(`${name} did not answer in ${timeoutMs}ms\n${this.err}`)),
310
+ timeoutMs,
311
+ );
312
+ this.waiters.set(id, (v) => {
313
+ clearTimeout(timer);
314
+ resolve(v);
315
+ });
316
+ this.send({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } });
317
+ });
318
+ }
319
+ }
320
+ const bridge = new BridgeClient(bridgeEnv);
321
+
322
+ // ── 5. open the sibling through the PUBLIC surface ───────────────────────────
323
+ const sceneFact = `TEAL-${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
324
+ const launch = await bridge.call("entwurf_fresh_call", {
325
+ backend: "omp",
326
+ model: OMP_MODEL,
327
+ task: `Answer with the single word ACK and then stop. Do not read files, do not run commands. (scene fact: ${sceneFact})`,
328
+ cwd: scratch,
329
+ });
330
+ receipts["1-launch"] = launch.text;
331
+ ok(
332
+ "tools/call entwurf_fresh_call(backend=omp) returned a LAUNCH receipt through the public surface",
333
+ !launch.isError,
334
+ `--- response ---\n${launch.text}\n--- bridge stderr ---\n${bridge.stderrTail()}`,
335
+ );
336
+ const nonce = /nonce:\s*(mux-fresh-call-[0-9a-f]+)/.exec(launch.text)?.[1];
337
+ ok("the launch receipt carries the correlation nonce", typeof nonce === "string", `--- receipt ---\n${launch.text}`);
338
+
339
+ // ── 6. the sibling names ITSELF, by exact nonce, from the sender envelope ────
340
+ const siblingGid = await until("the nonce callback", CALLBACK_WAIT_MS, () => {
341
+ let sender: string | null = null;
342
+ for (const msg of meta.readMetaInbox({ gardenId: callerGid }).messages) {
343
+ const from = /^\s*session:\s+(\S+)/m.exec(msg.body)?.[1] ?? "";
344
+ if (msg.body.includes(nonce as string) && from) sender = from;
345
+ }
346
+ return sender;
347
+ });
348
+ receipts["2-callback-identity"] = `${siblingGid} (sender envelope of ${nonce})`;
349
+ ok(
350
+ `the sibling called back with the exact nonce and its SENDER ENVELOPE names garden ${siblingGid}`,
351
+ GARDEN_ID.test(siblingGid),
352
+ );
353
+ ok(
354
+ "the launch receipt never named that garden id — the address came from the CALLBACK, not from the launch",
355
+ !launch.text.includes(siblingGid) && siblingGid !== callerGid,
356
+ );
357
+
358
+ // ── 7. that id is a real omp citizen, and the ONLY one this step minted ──────
359
+ const facts = JSON.parse(execFileSync("bash", [path.join(REPO, "run.sh"), "meta-facts"], { encoding: "utf8" }));
360
+ const citizen = (facts.citizens as Array<Record<string, unknown>>).find((c) => c.gardenId === siblingGid);
361
+ ok(
362
+ "the callback id resolves to a V3 record whose backend is omp",
363
+ citizen?.backend === "omp",
364
+ ` ${JSON.stringify(citizen)}`,
365
+ );
366
+ const minted = (facts.citizens as Array<{ gardenId: string }>)
367
+ .map((c) => c.gardenId)
368
+ .filter((g) => !citizensBefore.has(g) && g !== callerGid);
369
+ ok(
370
+ "exactly ONE new citizen was minted — the visible host, never a subagent (§3.5)",
371
+ minted.length === 1 && minted[0] === siblingGid,
372
+ ` new: ${minted.join(", ") || "(none)"}`,
373
+ );
374
+
375
+ // ── 8. addressed receive into that same native session ───────────────────────
376
+ ok(
377
+ "the sibling's own receiver extension armed its mailbox (a self-fetch citizen is deliverable)",
378
+ await until("the sibling's receiver marker", 120_000, () =>
379
+ meta.readMetaReceiverMarker({ gardenId: siblingGid }) ? true : null,
380
+ ),
381
+ );
382
+ const probe = `OMP-FRESH-${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
383
+ const send = await bridge.call("entwurf_v2", {
384
+ target: siblingGid,
385
+ intent: "fire-and-forget",
386
+ message: `Bundle C acceptance probe ${probe}. No action is requested; this message exists so an automated acceptance can confirm it reached the right mailbox.`,
387
+ });
388
+ receipts["3-addressed-send"] = send.text;
389
+ ok(
390
+ "tools/call entwurf_v2 to the fresh sibling chose the MAILBOX rail, not a refusal",
391
+ !send.isError && /mailbox|enqueued/i.test(send.text),
392
+ `--- response ---\n${send.text}\n--- bridge stderr ---\n${bridge.stderrTail()}`,
393
+ );
394
+
395
+ const statePath = path.join(ompRoots.mailboxDir, siblingGid, "state.json");
396
+ const state = await until("the sibling to record a read receipt (lastReadAt)", READ_WAIT_MS, () => {
397
+ if (!fs.existsSync(statePath)) return null;
398
+ try {
399
+ const s = JSON.parse(fs.readFileSync(statePath, "utf8"));
400
+ return s.lastReadAt ? s : null;
401
+ } catch {
402
+ return null;
403
+ }
404
+ });
405
+ receipts["4-read-receipt"] = JSON.stringify(state);
406
+ ok(
407
+ "lastReadAt moved AFTER lastEnqueuedAt — the sibling DRAINED its own inbox; a rung doorbell alone is only a wake attempt",
408
+ Date.parse(state.lastReadAt) >= Date.parse(state.lastEnqueuedAt),
409
+ ` ${JSON.stringify(state)}`,
410
+ );
411
+
412
+ // The drain must have happened INSIDE the session we opened. `entwurf_inbox_read` takes a
413
+ // CALLER-SUPPLIED garden id and does not verify it against the caller, so `lastReadAt` alone
414
+ // proves a read happened — not that THIS citizen performed it. The vendor's own transcript is
415
+ // the oracle, exactly as smoke-omp-receive-live established.
416
+ const transcriptPath = citizen?.transcriptPath as string | undefined;
417
+ ok("the citizen's V3 record names a transcript for this session", typeof transcriptPath === "string");
418
+ ok(
419
+ "the sibling's OWN transcript shows it calling the omp-dialect inbox tool for its OWN garden id — the launch, the callback and the drain are one session",
420
+ await until("the drain to appear in the sibling's transcript", 60_000, () => {
421
+ if (!fs.existsSync(transcriptPath as string)) return null;
422
+ const body = fs.readFileSync(transcriptPath as string, "utf8");
423
+ return body.includes("mcp__entwurf_bridge_entwurf_inbox_read") && body.includes(siblingGid) ? true : null;
424
+ }),
425
+ ` ${transcriptPath}`,
426
+ );
427
+
428
+ // ── 8. the TASK reached that same session, and only AFTER the callback ──────
429
+ //
430
+ // THIS IS THE STEP THE FIRST CANDIDATE COULD NOT HAVE PASSED. Its argv put the whole framing
431
+ // in a positional prompt and the sibling answered `ACK` without calling anything, so a
432
+ // "callback + receive" acceptance would still have shipped a sibling that never got its work.
433
+ // The two-stage bootstrap splits those into two messages, and the ORDER is the contract: the
434
+ // callback-only prompt is delivered first, and the task is released only by the exact
435
+ // successful callback result. The transcript is where both are visible, in order, in one
436
+ // native session.
437
+ const bootstrapLine = buildOmpCallbackOnlyPrompt({ target: callerGid, nonce: nonce as string })
438
+ .split("\n")
439
+ .find((l) => l.includes("FIRST AND ONLY ACTION")) as string;
440
+ const transcript = fs.readFileSync(transcriptPath as string, "utf8");
441
+ const callbackAt = transcript.indexOf(bootstrapLine);
442
+ const taskAt = transcript.indexOf(sceneFact);
443
+ receipts["5-two-stage-order"] = `callback-only prompt @${callbackAt}, task scene fact @${taskAt}`;
444
+ ok(
445
+ "stage one is in the sibling's own transcript: the extension delivered the callback-ONLY prompt, carrying this call's target and nonce",
446
+ callbackAt >= 0,
447
+ ` expected line: ${bootstrapLine}`,
448
+ );
449
+ ok(
450
+ "[QK:OMP-FRESH-LIVE-NO-POSITIONAL] the four-backend positional framing appears NOWHERE in this session — omp's first turn came from the extension, not from argv",
451
+ !transcript.includes("After the tool receipt, carry out this task:"),
452
+ );
453
+ ok(
454
+ "stage two: the caller's TASK reached the same native session as a separate later message",
455
+ taskAt >= 0,
456
+ ` scene fact ${sceneFact} not found in ${transcriptPath}`,
457
+ );
458
+ ok(
459
+ "[QK:OMP-FRESH-LIVE-CALLBACK-BEFORE-TASK] the callback-only prompt precedes the task — the task is armed by the callback RESULT and sent at the turn_end after it, never raced against it",
460
+ callbackAt >= 0 && taskAt > callbackAt,
461
+ ` callback @${callbackAt} vs task @${taskAt}`,
462
+ );
463
+ // ARRIVAL IS NOT THE SAME AS A TURN, and #87 paid for that distinction. `[LIVE 2026-08-30]` the
464
+ // first stage-two attempt queued the task with an explicit delivery option and it never became a
465
+ // message at all; a weaker assertion than "a user message carrying the task, followed by an
466
+ // assistant message" would have called a queued-and-forgotten task delivered.
467
+ const entries = transcript
468
+ .trim()
469
+ .split("\n")
470
+ .map((l) => {
471
+ try {
472
+ return JSON.parse(l) as { type?: string; message?: { role?: string; content?: unknown[] } };
473
+ } catch {
474
+ return null;
475
+ }
476
+ });
477
+ const taskEntry = entries.findIndex(
478
+ (e) =>
479
+ e?.type === "message" && e.message?.role === "user" && JSON.stringify(e.message?.content ?? []).includes(sceneFact),
480
+ );
481
+ ok(
482
+ "the task arrived as a USER message in that session, not as a queued payload nobody drained",
483
+ taskEntry >= 0,
484
+ ` no user message carrying ${sceneFact} in ${transcriptPath}`,
485
+ );
486
+ ok(
487
+ "[QK:OMP-FRESH-LIVE-TASK-STARTS-A-TURN] and it STARTED A TURN — an assistant message follows it, which is the only proof the omitted-option send did what the source says it does on an idle session",
488
+ entries.slice(taskEntry + 1).some((e) => e?.type === "message" && e.message?.role === "assistant"),
489
+ ` task entry #${taskEntry} of ${entries.length}, no assistant message after it`,
490
+ );
491
+
492
+ bridge.close();
493
+ printReceipts();
494
+ console.log(
495
+ `[${LABEL}] ${passed} assertions ok — visible fresh + two-stage bootstrap + addressed receive accepted for omp (step 9 clause 7)`,
496
+ );
497
+ teardown();