@junghanacs/entwurf 0.13.0 → 0.13.1

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 (72) hide show
  1. package/AGENTS.md +5 -3
  2. package/BASELINE.md +57 -203
  3. package/CHANGELOG.md +19 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +117 -261
  6. package/README.md +68 -408
  7. package/VERIFY.md +58 -87
  8. package/demo/README.md +1 -1
  9. package/docs/acp-backend-rail.md +231 -0
  10. package/docs/external-mcp-host.md +132 -0
  11. package/docs/fresh-cut-policy.md +99 -0
  12. package/docs/setup-clean-host.md +123 -328
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +21 -48
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +3 -3
  20. package/mcp/entwurf-bridge/src/index.ts +21 -48
  21. package/package.json +12 -9
  22. package/pi-extensions/entwurf-control.ts +20 -49
  23. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  24. package/pi-extensions/lib/acp/augment.ts +42 -3
  25. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  26. package/pi-extensions/lib/acp/backend.ts +305 -36
  27. package/pi-extensions/lib/acp/config.ts +1 -1
  28. package/pi-extensions/lib/acp/engraving.ts +43 -1
  29. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  30. package/pi-extensions/lib/acp/models.ts +4 -4
  31. package/pi-extensions/lib/acp/overlay.ts +3 -3
  32. package/prompts/engraving.md +13 -5
  33. package/run.sh +244 -108
  34. package/scripts/check-acp-carrier-augment.ts +246 -10
  35. package/scripts/check-acp-cortex.ts +5 -5
  36. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  37. package/scripts/check-acp-sdk-surface.ts +60 -21
  38. package/scripts/check-acp-stop-reason.ts +342 -0
  39. package/scripts/check-entwurf-v2-surface.ts +41 -0
  40. package/scripts/check-gate-qualification.ts +6 -3
  41. package/scripts/check-probe-ordering.ts +39 -7
  42. package/scripts/check-release-gate-outcomes.ts +332 -0
  43. package/scripts/lib/live-skip.ts +33 -0
  44. package/scripts/lib/probe-acp-turn.ts +33 -12
  45. package/scripts/lib/step-outcome.sh +88 -0
  46. package/scripts/mutants/acp-augment.json +106 -0
  47. package/scripts/mutants/acp-cortex.json +2 -2
  48. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  49. package/scripts/mutants/acp-stop-reason.json +80 -0
  50. package/scripts/mutants/probe-ordering.json +27 -5
  51. package/scripts/mutants/release-gate.json +105 -0
  52. package/scripts/mutants/v2-surface.json +26 -15
  53. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  54. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  55. package/scripts/smoke-acp-cortex-live.ts +8 -14
  56. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  57. package/scripts/smoke-acp-mcp-live.ts +2 -2
  58. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  59. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  60. package/scripts/smoke-acp-overlay-live.ts +2 -2
  61. package/scripts/smoke-acp-provider-live.ts +2 -2
  62. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  63. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  64. package/scripts/smoke-acp-skill-live.ts +2 -2
  65. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  66. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  67. package/scripts/smoke-agy-native-push-live.ts +4 -3
  68. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  69. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  70. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  71. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
  72. package/scripts/smoke-meta-install-state.sh +4 -0
@@ -0,0 +1,352 @@
1
+ // P3 — the cross-harness delivery CHAIN, on real authenticated rails.
2
+ //
3
+ // LIVE=1 ./run.sh smoke-entwurf-chain-live
4
+ //
5
+ // WHY THIS EXISTS. Every v2 delivery proof we had covered ONE hop between TWO
6
+ // citizens on ONE rail: matrix-live dispatches programmatically, v2-send-live has
7
+ // an ACP model send into a mailbox, bundled-mcp-live only reads identity. The
8
+ // claim the project actually makes — that independent harnesses address each
9
+ // other by garden id — is a CHAIN claim, and nothing exercised a chain. A gate
10
+ // that never links three harnesses cannot catch the failure where hop 2 loses the
11
+ // sender identity hop 1 carried, or where a rail change breaks only the seam
12
+ // between two backends.
13
+ //
14
+ // THE CHAIN (each arrow is a real model turn on a real authenticated backend):
15
+ //
16
+ // A native Claude Code --entwurf_v2--> B pi / openai-codex (GPT)
17
+ // B --entwurf_v2--> C pi / entwurf claude-sonnet-5 (ACP)
18
+ // C --entwurf_v2--> D mailbox-backed self-fetch citizen
19
+ //
20
+ // Three harnesses, three rails: A is a native self-fetch citizen born by its own
21
+ // SessionStart hook, B and C are live control-socket citizens, and D is the
22
+ // mailbox terminus. The terminus is what makes the READ-RECEIPT axis observable
23
+ // at all — a control-socket delivery has no receipt to stamp, so a chain that
24
+ // ended at C could not produce one. Draining D through the production inbox
25
+ // reader stamps `lastReadAt`, and that is the receipt.
26
+ //
27
+ // WHAT IS REAL AND WHAT IS SMOKE-OWNED (state this honestly or the evidence is
28
+ // worth nothing):
29
+ // - REAL: A's meta-record and its first arming are minted by the operator's own
30
+ // Claude Code SessionStart hook; all three deliveries are production
31
+ // `entwurf_v2` dispatches chosen by the decider; all three model turns are
32
+ // real authenticated calls; every sender envelope is the real one.
33
+ // - SMOKE-OWNED: the isolated world (store / mailbox / receivers under one temp
34
+ // root) so the operator's live garden is never paged, and D's arming marker.
35
+ // D is a seeded citizen exactly as smoke-acp-v2-send-live seeds its peer.
36
+ // - NOT CLAIMED: this proves the chain LINKS, not that any model chooses the
37
+ // entwurf surface on its own. Each hop is TOLD which tool to call, which is
38
+ // why this is a MUST-tier delivery gate and not a BEHAVIOR-lane signal (the
39
+ // lane note in run.sh draws exactly that line).
40
+ //
41
+ // The nonce is the thread: it is minted here, embedded in A's instruction, and
42
+ // must arrive at D having been re-sent by B and again by C. A nonce at D proves
43
+ // the payload traversed every hop — no hop can fabricate it.
44
+ //
45
+ // Sender identity is proven the same way v2-send-live proves it: no hop is ever
46
+ // told its OWN garden id, so a gid can only appear downstream because the real
47
+ // envelope carried it.
48
+
49
+ import { strict as assert } from "node:assert";
50
+ import { type ChildProcess, spawn, spawnSync } from "node:child_process";
51
+ import crypto from "node:crypto";
52
+ import { existsSync } from "node:fs";
53
+ import fsp from "node:fs/promises";
54
+ import os from "node:os";
55
+ import path from "node:path";
56
+ import { fileURLToPath } from "node:url";
57
+ import { readMetaInbox, upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
58
+ import { terminateChild } from "./lib/acp-child-cleanup.ts";
59
+ import { skipLive } from "./lib/live-skip.ts";
60
+
61
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
62
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
63
+ const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
64
+ const SOCKET_SUFFIX = ".sock";
65
+
66
+ const GPT_TARGET = process.env.ENTWURF_CHAIN_GPT_TARGET?.trim() || "openai-codex/gpt-5.4";
67
+ const ACP_TARGET = process.env.ENTWURF_CHAIN_ACP_TARGET?.trim() || "entwurf/claude-sonnet-5";
68
+ const BOOT_TIMEOUT_MS = 45_000;
69
+ const CLAUDE_TURN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_CLAUDE_TIMEOUT_MS) || 240_000;
70
+ const CHAIN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_TIMEOUT_MS) || 420_000;
71
+ const POLL_MS = 250;
72
+
73
+ let passed = 0;
74
+ function ok(label: string, cond: boolean): void {
75
+ assert.ok(cond, `smoke-entwurf-chain-live: ${label}`);
76
+ console.log(` ok ${label}`);
77
+ passed++;
78
+ }
79
+
80
+ function splitTarget(t: string): [provider: string, model: string] {
81
+ const i = t.indexOf("/");
82
+ assert.ok(i > 0, `target must be <provider>/<model>, got ${t}`);
83
+ return [t.slice(0, i), t.slice(i + 1)];
84
+ }
85
+
86
+ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
87
+
88
+ async function readRecords(dir: string): Promise<Array<{ gardenId: string; backend: string }>> {
89
+ const out: Array<{ gardenId: string; backend: string }> = [];
90
+ for (const f of await fsp.readdir(dir).catch(() => [] as string[])) {
91
+ if (!f.endsWith(".meta.json")) continue;
92
+ try {
93
+ const r = JSON.parse(await fsp.readFile(path.join(dir, f), "utf8")) as Record<string, unknown>;
94
+ if (typeof r.gardenId === "string" && typeof r.backend === "string") {
95
+ out.push({ gardenId: r.gardenId, backend: r.backend });
96
+ }
97
+ } catch {
98
+ /* a half-written record on the next poll is not an error */
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** Wait for ONE new record of `backend` that is not already in `known`. */
105
+ async function waitForNewRecord(dir: string, backend: string, known: Set<string>, ms: number): Promise<string | null> {
106
+ const until = Date.now() + ms;
107
+ while (Date.now() < until) {
108
+ for (const r of await readRecords(dir)) {
109
+ if (r.backend === backend && !known.has(r.gardenId)) return r.gardenId;
110
+ }
111
+ await sleep(POLL_MS);
112
+ }
113
+ return null;
114
+ }
115
+
116
+ async function waitForSocket(p: string, ms: number): Promise<boolean> {
117
+ const until = Date.now() + ms;
118
+ while (Date.now() < until) {
119
+ if (existsSync(p)) return true;
120
+ await sleep(POLL_MS);
121
+ }
122
+ return false;
123
+ }
124
+
125
+ async function main(): Promise<void> {
126
+ if (process.env.LIVE !== "1") {
127
+ skipLive(
128
+ "smoke-entwurf-chain-live",
129
+ "set LIVE=1 to run (drives three real authenticated model turns across Claude Code, pi GPT and pi ACP).",
130
+ );
131
+ }
132
+ if (spawnSync("bash", ["-lc", "command -v claude"], { encoding: "utf8" }).status !== 0) {
133
+ skipLive("smoke-entwurf-chain-live", "claude not on PATH — hop 1 is a real native Claude Code turn.");
134
+ }
135
+ const [gptProvider, gptModel] = splitTarget(GPT_TARGET);
136
+ const [acpProvider, acpModel] = splitTarget(ACP_TARGET);
137
+ // pi holds each backend's credentials in its own auth store; a missing one is a
138
+ // PREREQUISITE (the operator has not logged that backend in), never a defect.
139
+ const authPath = path.join(os.homedir(), ".pi", "agent", "auth.json");
140
+ let auth: Record<string, unknown> = {};
141
+ try {
142
+ auth = JSON.parse(await fsp.readFile(authPath, "utf8")) as Record<string, unknown>;
143
+ } catch {
144
+ skipLive("smoke-entwurf-chain-live", `no pi auth store at ${authPath} — log in the chain's backends first.`);
145
+ }
146
+ if (!(gptProvider in auth)) {
147
+ skipLive(
148
+ "smoke-entwurf-chain-live",
149
+ `pi has no '${gptProvider}' credentials (hop 2 target ${GPT_TARGET}); log that backend in, or point ` +
150
+ "ENTWURF_CHAIN_GPT_TARGET at one you have.",
151
+ );
152
+ }
153
+
154
+ const world = await fsp.mkdtemp(path.join(os.tmpdir(), "entwurf-chain-"));
155
+ const sessionsDir = path.join(world, "sessions");
156
+ const mailboxDir = path.join(world, "mailbox");
157
+ const receiversDir = path.join(world, "receivers");
158
+ for (const d of [sessionsDir, mailboxDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
159
+ const worldEnv = {
160
+ ENTWURF_META_SESSIONS_DIR: sessionsDir,
161
+ ENTWURF_META_MAILBOX_DIR: mailboxDir,
162
+ ENTWURF_META_RECEIVERS_DIR: receiversDir,
163
+ };
164
+
165
+ const nonce = `ENTWURF-CHAIN-${crypto.randomBytes(5).toString("hex").toUpperCase()}`;
166
+ console.error(`[smoke-entwurf-chain-live] repo: ${REPO_ROOT}`);
167
+ console.error(`[smoke-entwurf-chain-live] world: ${world}`);
168
+ console.error(`[smoke-entwurf-chain-live] hop2: ${GPT_TARGET}`);
169
+ console.error(`[smoke-entwurf-chain-live] hop3: ${ACP_TARGET}`);
170
+ console.error(`[smoke-entwurf-chain-live] nonce: ${nonce}`);
171
+
172
+ // ── D: the mailbox terminus (seeded + armed by this process, like v2-send) ──
173
+ const terminus = upsertMetaSession({
174
+ input: { backend: "claude-code", nativeSessionId: `chain-terminus-${process.pid}`, cwd: world },
175
+ dir: sessionsDir,
176
+ });
177
+ const gidD = terminus.record.gardenId;
178
+ writeMetaReceiverMarker({
179
+ gardenId: gidD,
180
+ backend: "claude-code",
181
+ nativeSessionId: terminus.record.nativeSessionId,
182
+ ownerPid: process.pid,
183
+ armProvenance: "session-start",
184
+ receiversDir,
185
+ });
186
+ console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
187
+
188
+ const children: ChildProcess[] = [];
189
+ let streamB = "";
190
+ let streamC = "";
191
+ try {
192
+ // ── C: the ACP Claude Sonnet citizen (spawned first so B can be told its id) ──
193
+ const known = new Set((await readRecords(sessionsDir)).map((r) => r.gardenId));
194
+ const c = spawn(
195
+ "pi",
196
+ [...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", acpProvider, "--model", acpModel, "--mode", "rpc"],
197
+ { cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
198
+ );
199
+ children.push(c);
200
+ c.stdout?.on("data", (b: Buffer) => {
201
+ streamC = (streamC + b.toString()).slice(-200_000);
202
+ });
203
+ c.stderr?.on("data", (b: Buffer) => {
204
+ streamC = (streamC + b.toString()).slice(-200_000);
205
+ });
206
+ const gidC = await waitForNewRecord(sessionsDir, "pi", known, BOOT_TIMEOUT_MS);
207
+ ok(`C birthed its own pi record (${ACP_TARGET})`, gidC !== null);
208
+ known.add(gidC as string);
209
+ ok(
210
+ "C stood up a control socket",
211
+ await waitForSocket(path.join(REAL_CONTROL_DIR, `${gidC}${SOCKET_SUFFIX}`), BOOT_TIMEOUT_MS),
212
+ );
213
+
214
+ // ── B: the pi GPT citizen ────────────────────────────────────────────────
215
+ const b = spawn(
216
+ "pi",
217
+ [...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", gptProvider, "--model", gptModel, "--mode", "rpc"],
218
+ { cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
219
+ );
220
+ children.push(b);
221
+ b.stdout?.on("data", (x: Buffer) => {
222
+ streamB = (streamB + x.toString()).slice(-200_000);
223
+ });
224
+ b.stderr?.on("data", (x: Buffer) => {
225
+ streamB = (streamB + x.toString()).slice(-200_000);
226
+ });
227
+ const gidB = await waitForNewRecord(sessionsDir, "pi", known, BOOT_TIMEOUT_MS);
228
+ ok(`B birthed its own pi record (${GPT_TARGET})`, gidB !== null);
229
+ known.add(gidB as string);
230
+ ok(
231
+ "B stood up a control socket",
232
+ await waitForSocket(path.join(REAL_CONTROL_DIR, `${gidB}${SOCKET_SUFFIX}`), BOOT_TIMEOUT_MS),
233
+ );
234
+ ok("B, C and D are three distinct citizens", new Set([gidB, gidC, gidD]).size === 3);
235
+
236
+ // ── A: one real native Claude Code turn that opens the chain ─────────────
237
+ // The instruction each hop receives is the message the previous hop sends.
238
+ // No hop is told its own garden id — that is what makes the downstream
239
+ // sender assertions meaningful.
240
+ const hop3 = [
241
+ `${nonce} hop3. You are the third hop of a delivery chain.`,
242
+ `Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidD}, intent=fire-and-forget,`,
243
+ `and message=${nonce} terminus. Then reply with only the tool's outcome line.`,
244
+ ].join(" ");
245
+ const hop2 = [
246
+ `${nonce} hop2. You are the second hop of a delivery chain.`,
247
+ `Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidC}, intent=fire-and-forget,`,
248
+ "and message set to exactly the text between INNER-BEGIN and INNER-END.",
249
+ "Then reply with only the tool's outcome line.",
250
+ "INNER-BEGIN",
251
+ hop3,
252
+ "INNER-END",
253
+ ].join("\n");
254
+ const hop1Prompt = [
255
+ "You are the first hop of a delivery chain. Do exactly this and nothing else.",
256
+ `Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidB}, intent=fire-and-forget,`,
257
+ "and message set to exactly the text between BEGIN and END.",
258
+ "Then reply with only the tool's outcome line.",
259
+ "BEGIN",
260
+ hop2,
261
+ "END",
262
+ ].join("\n");
263
+
264
+ const claudeRun = spawnSync("claude", ["-p", "--output-format=json", hop1Prompt], {
265
+ cwd: world,
266
+ encoding: "utf8",
267
+ timeout: CLAUDE_TURN_TIMEOUT_MS,
268
+ env: { ...process.env, ...worldEnv },
269
+ });
270
+ const claudeOut = `${claudeRun.stdout ?? ""}\n${claudeRun.stderr ?? ""}`;
271
+ ok("A's native Claude Code turn ran", claudeRun.status === 0);
272
+
273
+ const gidA = await waitForNewRecord(sessionsDir, "claude-code", new Set([gidD]), BOOT_TIMEOUT_MS);
274
+ ok("A was born a garden citizen by its OWN SessionStart hook (not seeded here)", gidA !== null);
275
+ ok("A is distinct from every other citizen", !new Set([gidB, gidC, gidD]).has(gidA as string));
276
+ console.error(`[smoke-entwurf-chain-live] A: ${gidA} (native Claude Code)`);
277
+
278
+ // ── the chain runs on its own from here; wait for the terminus ───────────
279
+ const boxDir = path.join(mailboxDir, gidD);
280
+ const until = Date.now() + CHAIN_TIMEOUT_MS;
281
+ let msgs: string[] = [];
282
+ while (Date.now() < until) {
283
+ msgs = (await fsp.readdir(boxDir).catch(() => [] as string[])).filter(
284
+ (f) => f.endsWith(".msg") || f.endsWith(".msg.delivered"),
285
+ );
286
+ if (msgs.length > 0) break;
287
+ await sleep(POLL_MS);
288
+ }
289
+ if (msgs.length === 0) {
290
+ console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut.slice(0, 1500)}`);
291
+ console.error(`[smoke-entwurf-chain-live] B stream tail:\n${streamB.slice(-2000)}`);
292
+ console.error(`[smoke-entwurf-chain-live] C stream tail:\n${streamC.slice(-2000)}`);
293
+ }
294
+ ok(`the chain reached the mailbox terminus (${msgs.length} .msg)`, msgs.length === 1);
295
+ ok("the terminus doorbell was poked", existsSync(path.join(boxDir, "inbox.signal")));
296
+
297
+ const body = await fsp.readFile(path.join(boxDir, msgs[0] as string), "utf8");
298
+
299
+ // ── hop-by-hop evidence ─────────────────────────────────────────────────
300
+ // hop 1: B saw the payload AND the real sender identity of a native
301
+ // Claude Code citizen. A never learned its own gid, so its presence in B's
302
+ // stream can only come from the envelope entwurf_v2 rendered.
303
+ ok("hop 1 — B received the chain payload", streamB.includes(nonce));
304
+ ok("hop 1 — B saw A's real garden id as the sender", streamB.includes(gidA as string));
305
+ ok("hop 1 — the sender was rendered replyable to B", /replyable/i.test(streamB));
306
+
307
+ // hop 2: C saw the payload and B's identity, so the chain did not collapse
308
+ // into A talking to C directly.
309
+ ok("hop 2 — C received the chain payload", streamC.includes(nonce));
310
+ ok("hop 2 — C saw B's real garden id as the sender", streamC.includes(gidB as string));
311
+
312
+ // hop 3: the terminus body carries C's identity, its model, and replyable.
313
+ ok("hop 3 — the terminus payload is the original nonce", body.includes(nonce));
314
+ ok("hop 3 — the terminus names C as the sender", body.includes(gidC as string));
315
+ ok(`hop 3 — the terminus names C's backend model (${acpModel})`, body.includes(acpModel));
316
+ ok("hop 3 — the terminus sender is replyable", /replyable/i.test(body));
317
+ ok(
318
+ "the terminus was not reached by a shortcut — A and B are not its sender",
319
+ !body.includes(gidA as string) && !body.includes(gidB as string),
320
+ );
321
+
322
+ // ── the read receipt: the one axis only the mailbox rail can produce ─────
323
+ // The PRODUCTION reader, not a re-implementation: reading is what stamps the
324
+ // receipt, so a hand-rolled drain would prove nothing about the real path.
325
+ const statePath = path.join(mailboxDir, gidD, "state.json");
326
+ const beforeRaw = await fsp.readFile(statePath, "utf8").catch(() => "");
327
+ const drained = readMetaInbox({ gardenId: gidD, sessionsDir, mailboxDir });
328
+ ok(
329
+ "draining the terminus returned the chain payload",
330
+ drained.messages.some((m) => m.body.includes(nonce)),
331
+ );
332
+ ok(
333
+ "the drain stamped a read receipt (lastReadAt)",
334
+ typeof drained.readAt === "string" && drained.readAt.length > 0,
335
+ );
336
+ const afterRaw = await fsp.readFile(statePath, "utf8").catch(() => "");
337
+ ok(
338
+ "the receipt is durable on disk, not just in the return value",
339
+ /lastReadAt/.test(afterRaw) && afterRaw !== beforeRaw,
340
+ );
341
+
342
+ console.error(`[smoke-entwurf-chain-live] chain: ${gidA} → ${gidB} → ${gidC} → ${gidD}`);
343
+ } finally {
344
+ for (const ch of children) await terminateChild(ch).catch(() => undefined);
345
+ }
346
+
347
+ console.log(
348
+ `[smoke-entwurf-chain-live] ok — ${passed} assertions; three harnesses linked on real authenticated rails`,
349
+ );
350
+ }
351
+
352
+ await main();
@@ -64,6 +64,7 @@ import {
64
64
  writeMetaReceiverMarker,
65
65
  } from "../pi-extensions/lib/meta-session.ts";
66
66
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
67
+ import { skipLive } from "./lib/live-skip.ts";
67
68
  import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
68
69
 
69
70
  // pi's control socket lives at the canonical dir keyed by the RECORD's garden id (#50 C4: the
@@ -130,8 +131,7 @@ function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
130
131
 
131
132
  async function main(): Promise<void> {
132
133
  if (process.env.LIVE !== "1") {
133
- console.log("[smoke-entwurf-v2-matrix-live] skipped — set LIVE=1 to run (spawns a real pi + opens a real socket).");
134
- return;
134
+ skipLive("smoke-entwurf-v2-matrix-live", "set LIVE=1 to run (spawns a real pi + opens a real socket).");
135
135
  }
136
136
 
137
137
  const { provider, model } = resolveTarget();
@@ -59,6 +59,7 @@ import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
59
59
  import { resolveResumeLaunchIdentity } from "../pi-extensions/lib/entwurf-v2-spawn-production.ts";
60
60
  import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
61
61
  import { controlSocketPath } from "../pi-extensions/lib/socket-discovery.ts";
62
+ import { skipLive } from "./lib/live-skip.ts";
62
63
 
63
64
  // pi owns the canonical control-socket dir; the resume child opens its socket HERE and the
64
65
  // decider's expectedSocketPath is built from the SAME dir — they must coincide (a fresh gid
@@ -208,10 +209,7 @@ async function pollForNonce(
208
209
 
209
210
  async function main(): Promise<void> {
210
211
  if (process.env.LIVE !== "1") {
211
- console.log(
212
- "[smoke-entwurf-v2-spawn-resume-live] skipped — set LIVE=1 to run (spawns a real pi child + opens a real socket).",
213
- );
214
- return;
212
+ skipLive("smoke-entwurf-v2-spawn-resume-live", "set LIVE=1 to run (spawns a real pi child + opens a real socket).");
215
213
  }
216
214
 
217
215
  const { provider, model } = resolveTarget();
@@ -87,6 +87,10 @@ case "$1${2:+ $2}" in
87
87
  "mcp get")
88
88
  printf '%s\n' "Scope: User config" "Status: ✔ Connected"
89
89
  [ "${FAKE_MCP_RC:-0}" != 0 ] && exit "$FAKE_MCP_RC"
90
+ # Give grep -q a scheduling window to consume the match and close the pipe
91
+ # before the long tail starts. Without this, a fast host can buffer the whole
92
+ # tail before grep exits, making the old broken probe spuriously green.
93
+ sleep 0.05
90
94
  # Keep writing well past the pipe buffer: a reader that leaves early SIGPIPEs us.
91
95
  cat "$FAKE_MCP_TAIL"
92
96
  exit $? ;;