@junghanacs/entwurf 0.17.2 → 0.18.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 (44) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +127 -0
  3. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
  4. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
  5. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
  6. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
  7. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +6 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +38 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
  14. package/mcp/entwurf-bridge/src/index.ts +16 -8
  15. package/package.json +3 -2
  16. package/pi-extensions/lib/entwurf-deliverability.ts +104 -0
  17. package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
  18. package/pi-extensions/lib/entwurf-facts.ts +52 -3
  19. package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
  20. package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
  21. package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
  22. package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
  23. package/pi-extensions/lib/entwurf-v2-surface.ts +6 -0
  24. package/pi-extensions/lib/meta-session.ts +44 -0
  25. package/pi-extensions/meta-bridge-hook.ts +72 -3
  26. package/run.sh +21 -0
  27. package/scripts/check-bridge-delivery.ts +42 -1
  28. package/scripts/check-entwurf-fact-provider.ts +22 -1
  29. package/scripts/check-entwurf-facts.ts +17 -2
  30. package/scripts/check-entwurf-peers-surface.ts +20 -2
  31. package/scripts/check-entwurf-self-address.ts +20 -2
  32. package/scripts/check-entwurf-v2-decider.ts +6 -3
  33. package/scripts/check-entwurf-v2-matrix.ts +10 -5
  34. package/scripts/check-entwurf-v2-production.ts +72 -1
  35. package/scripts/check-gate-qualification.ts +1 -0
  36. package/scripts/check-meta-hook-session-switch.ts +560 -0
  37. package/scripts/meta-bridge-doctor.sh +29 -4
  38. package/scripts/mutants/meta-hook-session-switch.json +223 -0
  39. package/scripts/raw-claude-session-switch/README.md +170 -0
  40. package/scripts/smoke-acp-cortex-live.ts +22 -2
  41. package/scripts/smoke-acp-v2-send-live.ts +23 -2
  42. package/scripts/smoke-entwurf-chain-live.ts +85 -13
  43. package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
  44. package/scripts/smoke-mux-fresh-call-live.ts +13 -3
@@ -54,7 +54,15 @@ import fsp from "node:fs/promises";
54
54
  import os from "node:os";
55
55
  import path from "node:path";
56
56
  import { fileURLToPath } from "node:url";
57
- import { readMetaInbox, upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
57
+ import { resolveMailboxReceiverFacts } from "../pi-extensions/lib/entwurf-deliverability.ts";
58
+ import {
59
+ readMetaInbox,
60
+ readMetaReceiverMarker,
61
+ readMetaSenderMarker,
62
+ upsertMetaSession,
63
+ writeMetaReceiverMarker,
64
+ writeMetaSenderMarker,
65
+ } from "../pi-extensions/lib/meta-session.ts";
58
66
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
59
67
  import { skipLive } from "./lib/live-skip.ts";
60
68
 
@@ -155,11 +163,13 @@ async function main(): Promise<void> {
155
163
  const sessionsDir = path.join(world, "sessions");
156
164
  const mailboxDir = path.join(world, "mailbox");
157
165
  const receiversDir = path.join(world, "receivers");
158
- for (const d of [sessionsDir, mailboxDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
166
+ const sendersDir = path.join(world, "senders");
167
+ for (const d of [sessionsDir, mailboxDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
159
168
  const worldEnv = {
160
169
  ENTWURF_META_SESSIONS_DIR: sessionsDir,
161
170
  ENTWURF_META_MAILBOX_DIR: mailboxDir,
162
171
  ENTWURF_META_RECEIVERS_DIR: receiversDir,
172
+ ENTWURF_META_SENDERS_DIR: sendersDir,
163
173
  };
164
174
  // The MCP bridge prefers PI_SESSION_ID+PI_AGENT_ID over a meta-sender marker when both
165
175
  // are present. A runner that is itself a pi --entwurf-control session (release-gate,
@@ -183,20 +193,46 @@ async function main(): Promise<void> {
183
193
  dir: sessionsDir,
184
194
  });
185
195
  const gidD = terminus.record.gardenId;
186
- writeMetaReceiverMarker({
187
- gardenId: gidD,
188
- backend: "claude-code",
189
- nativeSessionId: terminus.record.nativeSessionId,
190
- ownerPid: process.pid,
191
- armProvenance: "session-start",
192
- receiversDir,
193
- });
194
- console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
195
-
196
196
  const children: ChildProcess[] = [];
197
+ let terminusOwner: ChildProcess | null = null;
197
198
  let streamB = "";
198
199
  let streamC = "";
199
200
  try {
201
+ // The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli` watch
202
+ // owner is deliverable only while ITS OWN sender marker still names the garden it is armed
203
+ // for — that join is what refuses a session which switched away in place. A receiver marker
204
+ // with no sender marker beside it models a state the product now treats as retired, so this
205
+ // smoke would fail on its own fixture instead of on the rail it exists to prove.
206
+ //
207
+ // THE OWNER IS A SPAWNED IDLE PROCESS, NOT THIS ONE, and that is specific to this smoke:
208
+ // hop 1 is a native `claude` child of THIS process, and the bridge resolves a sender's
209
+ // identity through its ancestry. A terminus marker under this pid therefore puts TWO garden
210
+ // citizens on one host process, which the bridge refuses outright as ambiguous sender
211
+ // identity — measured on oracle 2026-09-04, where hop 1 came back "refused: ambiguous sender
212
+ // identity (…-f34713, …-912b42)" and the chain never started. The sibling smokes keep
213
+ // `process.pid` because their children are pi/ACP sessions carrying their own identity, or
214
+ // tmux siblings that are not descendants at all.
215
+ terminusOwner = spawn(process.execPath, ["-e", "setTimeout(() => {}, 900000)"], { stdio: "ignore" });
216
+ const terminusOwnerPid = terminusOwner.pid;
217
+ if (typeof terminusOwnerPid !== "number") throw new Error("could not spawn the terminus owner process");
218
+ writeMetaReceiverMarker({
219
+ gardenId: gidD,
220
+ backend: "claude-code",
221
+ nativeSessionId: terminus.record.nativeSessionId,
222
+ ownerPid: terminusOwnerPid,
223
+ armProvenance: "session-start",
224
+ receiversDir,
225
+ });
226
+ writeMetaSenderMarker({
227
+ backend: "claude-code",
228
+ gardenId: gidD,
229
+ nativeSessionId: terminus.record.nativeSessionId,
230
+ cwd: world,
231
+ ownerPid: terminusOwnerPid,
232
+ sendersDir,
233
+ });
234
+ console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
235
+
200
236
  // ── C: the ACP Claude Sonnet citizen (spawned first so B can be told its id) ──
201
237
  const known = new Set((await readRecords(sessionsDir)).map((r) => r.gardenId));
202
238
  const c = spawn(
@@ -283,6 +319,21 @@ async function main(): Promise<void> {
283
319
  ok("A is distinct from every other citizen", !new Set([gidB, gidC, gidD]).has(gidA as string));
284
320
  console.error(`[smoke-entwurf-chain-live] A: ${gidA} (native Claude Code)`);
285
321
 
322
+ // ── the fixture must still be a deliverable citizen when the chain arrives ──
323
+ // The terminus is only reachable while its owner is alive AND that owner's sender marker
324
+ // still names it (#101). Both are fixture state this smoke owns, and both can decay while
325
+ // three model turns run. Asserting it here separates "the chain did not arrive" from "the
326
+ // target stopped being addressable", which cost a LIVE run to tell apart once.
327
+ const terminusFacts = resolveMailboxReceiverFacts(terminus.record, {
328
+ readReceiverMarker: (gardenId: string) => readMetaReceiverMarker({ gardenId, receiversDir }),
329
+ readSenderMarker: (backend: string, pid: number) =>
330
+ readMetaSenderMarker({ backend: backend as "claude-code", ownerPid: pid, sendersDir }),
331
+ });
332
+ ok(
333
+ "fixture: the terminus is a deliverable citizen at the moment the chain starts",
334
+ terminusFacts.ownerAlive && terminusFacts.watchArmed,
335
+ );
336
+
286
337
  // ── the chain runs on its own from here; wait for the terminus ───────────
287
338
  const boxDir = path.join(mailboxDir, gidD);
288
339
  const until = Date.now() + CHAIN_TIMEOUT_MS;
@@ -295,7 +346,25 @@ async function main(): Promise<void> {
295
346
  await sleep(POLL_MS);
296
347
  }
297
348
  if (msgs.length === 0) {
298
- console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut.slice(0, 1500)}`);
349
+ let ownerAlive = false;
350
+ try {
351
+ process.kill(terminusOwnerPid, 0);
352
+ ownerAlive = true;
353
+ } catch {}
354
+ const facts = resolveMailboxReceiverFacts(terminus.record, {
355
+ readReceiverMarker: (gardenId: string) => readMetaReceiverMarker({ gardenId, receiversDir }),
356
+ readSenderMarker: (backend: string, pid: number) =>
357
+ readMetaSenderMarker({ backend: backend as "claude-code", ownerPid: pid, sendersDir }),
358
+ });
359
+ console.error(
360
+ `[smoke-entwurf-chain-live] terminus fixture at timeout: ownerPid=${terminusOwnerPid} alive=${ownerAlive} ` +
361
+ `ownerAlive=${facts.ownerAlive} watchArmed=${facts.watchArmed}`,
362
+ );
363
+ // The FULL turn, not a 1500-char head: that slice cut the JSON before its `result`
364
+ // field, so the one line that says what A actually did — the tool outcome it was told
365
+ // to echo back — was the first thing this diagnostic dropped. A failure that hides its
366
+ // own cause costs another LIVE run to re-learn.
367
+ console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut}`);
299
368
  console.error(`[smoke-entwurf-chain-live] B stream tail:\n${streamB.slice(-2000)}`);
300
369
  console.error(`[smoke-entwurf-chain-live] C stream tail:\n${streamC.slice(-2000)}`);
301
370
  }
@@ -350,6 +419,9 @@ async function main(): Promise<void> {
350
419
  console.error(`[smoke-entwurf-chain-live] chain: ${gidA} → ${gidB} → ${gidC} → ${gidD}`);
351
420
  } finally {
352
421
  for (const ch of children) await terminateChild(ch).catch(() => undefined);
422
+ try {
423
+ terminusOwner?.kill("SIGTERM");
424
+ } catch {}
353
425
  }
354
426
 
355
427
  console.log(
@@ -62,6 +62,7 @@ import {
62
62
  metaRecordExistsByGardenId,
63
63
  upsertMetaSession,
64
64
  writeMetaReceiverMarker,
65
+ writeMetaSenderMarker,
65
66
  } from "../pi-extensions/lib/meta-session.ts";
66
67
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
67
68
  import { skipLive } from "./lib/live-skip.ts";
@@ -187,10 +188,15 @@ async function main(): Promise<void> {
187
188
  const mailboxDir = path.join(tmp, "mailbox");
188
189
  const lockDir = path.join(tmp, "locks");
189
190
  const receiversDir = path.join(tmp, "receivers");
190
- for (const d of [sessionsDir, mailboxDir, lockDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
191
+ const sendersDir = path.join(tmp, "senders");
192
+ for (const d of [sessionsDir, mailboxDir, lockDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
191
193
  // The receiver marker is read through defaultMetaReceiversDir() (env-overridable); point it
192
194
  // (and the store/mailbox defaults, belt-and-suspenders to the opts dirs) at the temp world.
195
+ // The SENDER dir joins that list for #101: deliverability now also reads the receiver owner's
196
+ // sender marker, and leaving this one pointed at the operator's real root would have the
197
+ // fixture judged against markers this smoke never wrote.
193
198
  process.env.ENTWURF_META_RECEIVERS_DIR = receiversDir;
199
+ process.env.ENTWURF_META_SENDERS_DIR = sendersDir;
194
200
  process.env.ENTWURF_META_SESSIONS_DIR = sessionsDir;
195
201
  process.env.ENTWURF_META_MAILBOX_DIR = mailboxDir;
196
202
 
@@ -341,6 +347,14 @@ async function main(): Promise<void> {
341
347
  const gid = minted.record.gardenId;
342
348
  artifacts["C2.gid"] = gid;
343
349
  // armed = a presence marker owned by THIS (live) process → deliverable.
350
+ //
351
+ // The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli`
352
+ // watch owner is deliverable only while ITS OWN sender marker still names the garden it
353
+ // is armed for — that join is what refuses a session which switched away in place. A
354
+ // receiver marker with no sender marker beside it models a state the product now treats
355
+ // as retired, so this smoke would fail on its own fixture instead of on the rail it
356
+ // exists to prove. Same shape as smoke-mux-lifecycle-live / smoke-omp-fresh-live, which
357
+ // already seeded both.
344
358
  writeMetaReceiverMarker({
345
359
  gardenId: gid,
346
360
  backend: "claude-code",
@@ -349,6 +363,14 @@ async function main(): Promise<void> {
349
363
  armProvenance: "session-start",
350
364
  receiversDir,
351
365
  });
366
+ writeMetaSenderMarker({
367
+ backend: "claude-code",
368
+ gardenId: gid,
369
+ nativeSessionId: minted.record.nativeSessionId,
370
+ cwd: tmp,
371
+ ownerPid: process.pid,
372
+ sendersDir,
373
+ });
352
374
 
353
375
  const result: EntwurfV2RunResult = await runEntwurfV2(
354
376
  { target: gid, intent: "fire-and-forget", message: "matrix-live C2 meta-mailbox probe" },
@@ -194,7 +194,10 @@ async function main(): Promise<void> {
194
194
  const callerGid = caller.record.gardenId;
195
195
  // A record proves identity; only an ARMED receiver marker makes the mailbox deliverable —
196
196
  // without it the callbacks would be refused as mailbox-undeliverable and this smoke would
197
- // measure nothing.
197
+ // measure nothing. Since #101 the marker is half of that: a `claude-code-cli` watch owner
198
+ // is deliverable only while ITS OWN sender marker still names the garden it is armed for,
199
+ // which is what refuses a session that switched away in place. Both, or the fixture models
200
+ // a retired watch — the same shape smoke-mux-lifecycle-live already seeds.
198
201
  meta.writeMetaReceiverMarker({
199
202
  gardenId: callerGid,
200
203
  backend: "claude-code",
@@ -202,9 +205,16 @@ async function main(): Promise<void> {
202
205
  ownerPid: process.pid,
203
206
  armProvenance: "session-start",
204
207
  });
208
+ const callerSenderMarker = meta.writeMetaSenderMarker({
209
+ backend: "claude-code",
210
+ gardenId: callerGid,
211
+ nativeSessionId,
212
+ cwd: scratch,
213
+ ownerPid: process.pid,
214
+ });
205
215
  ok(
206
- "fixture: the caller citizen was minted INSIDE the fixture store and its mailbox is armed",
207
- Boolean(callerGid) && caller.path.startsWith(root),
216
+ "fixture: the caller citizen was minted INSIDE the fixture store, with an armed mailbox and a sender marker",
217
+ Boolean(callerGid) && caller.path.startsWith(root) && callerSenderMarker.startsWith(root),
208
218
  );
209
219
 
210
220
  // ── One private tmux server per backend ──────────────────────────────────