@junghanacs/entwurf 0.20.1 → 0.21.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 (96) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +4 -3
  3. package/CHANGELOG.md +195 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +202 -60
  6. package/README.md +49 -22
  7. package/VERIFY.md +37 -7
  8. package/docs/acp-backend-rail.md +0 -1
  9. package/docs/external-mcp-host.md +54 -33
  10. package/docs/setup-clean-host.md +110 -17
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +101 -109
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +326 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +50 -15
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  25. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  26. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  27. package/mcp/entwurf-bridge/src/index.ts +115 -111
  28. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  29. package/package.json +13 -4
  30. package/pi-extensions/entwurf-control.ts +71 -19
  31. package/pi-extensions/lib/codex-fresh-preflight.ts +363 -0
  32. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  33. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  34. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  35. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  36. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  37. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  38. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  39. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  40. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  41. package/pi-extensions/lib/mux-fresh-call.ts +64 -19
  42. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  43. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  44. package/pi-extensions/lib/native-push/register.ts +7 -9
  45. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  46. package/run.sh +218 -28
  47. package/scripts/check-agy-sender-identity.ts +1 -1
  48. package/scripts/check-codex-birth-hook.ts +264 -0
  49. package/scripts/check-codex-bridge-identity.ts +179 -0
  50. package/scripts/check-codex-native-push.ts +386 -0
  51. package/scripts/check-codex-sender-identity.ts +495 -0
  52. package/scripts/check-compaction-send-guard.ts +130 -0
  53. package/scripts/check-copilot-receive-arm.ts +4 -1
  54. package/scripts/check-entwurf-fact-provider.ts +38 -0
  55. package/scripts/check-entwurf-peers-surface.ts +13 -1
  56. package/scripts/check-entwurf-self-address.ts +15 -16
  57. package/scripts/check-entwurf-v2-contract.ts +4 -3
  58. package/scripts/check-entwurf-v2-decider.ts +7 -5
  59. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  60. package/scripts/check-entwurf-v2-production.ts +203 -11
  61. package/scripts/check-entwurf-v2-runner.ts +1 -1
  62. package/scripts/check-entwurf-v2-surface.ts +1 -1
  63. package/scripts/check-gate-qualification.ts +6 -3
  64. package/scripts/check-harness-admission-parity.ts +0 -1
  65. package/scripts/check-mux-launch-tmux.ts +47 -2
  66. package/scripts/check-native-push-adapter.ts +20 -16
  67. package/scripts/check-native-push-register.ts +5 -1
  68. package/scripts/check-release-gate-outcomes.ts +47 -1
  69. package/scripts/check-setup-qualification.sh +3 -1
  70. package/scripts/codex-birth-doctor.sh +276 -0
  71. package/scripts/codex-birth-install.sh +414 -0
  72. package/scripts/codex-birth-uninstall.sh +170 -0
  73. package/scripts/codex-mcp-config.py +435 -0
  74. package/scripts/codex-statusline-config.py +434 -0
  75. package/scripts/codex_toml_io.py +532 -0
  76. package/scripts/lib/codex-fresh-live-protocol.ts +113 -0
  77. package/scripts/lib/codex-fresh-source-receipts.ts +399 -0
  78. package/scripts/lib/launch-receipt-windows.ts +46 -0
  79. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  80. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  81. package/scripts/mutants/codex-native.json +838 -0
  82. package/scripts/mutants/compaction-send-guard.json +103 -0
  83. package/scripts/mutants/entwurf-peers.json +19 -0
  84. package/scripts/mutants/mux-fresh-call.json +19 -8
  85. package/scripts/mutants/omp-fresh.json +6 -4
  86. package/scripts/mutants/release-gate.json +13 -0
  87. package/scripts/mutants/v2-surface.json +53 -1
  88. package/scripts/raw-async-delivery/README.md +2 -1
  89. package/scripts/raw-codex-measure/README.md +114 -46
  90. package/scripts/smoke-agy-native-push-live.ts +3 -1
  91. package/scripts/smoke-codex-birth.sh +347 -0
  92. package/scripts/smoke-codex-config-state.sh +511 -0
  93. package/scripts/smoke-codex-fresh-live.ts +1186 -0
  94. package/scripts/smoke-codex-native-push-live.ts +75 -0
  95. package/scripts/smoke-setup-verdict.sh +123 -10
  96. package/scripts/tsconfig.json +1 -0
@@ -31,7 +31,7 @@ import * as path from "node:path";
31
31
  import { fileURLToPath } from "node:url";
32
32
  import { type EntwurfFactsResult, recordLessSocketMessage } from "../pi-extensions/lib/entwurf-fact-provider.ts";
33
33
  import type { PeerFact } from "../pi-extensions/lib/entwurf-facts.ts";
34
- import { renderEntwurfPeers } from "../pi-extensions/lib/entwurf-peers-render.ts";
34
+ import { ENTWURF_PEERS_RENDER_LIMIT, renderEntwurfPeers } from "../pi-extensions/lib/entwurf-peers-render.ts";
35
35
  import type { FactLiveness } from "../pi-extensions/lib/entwurf-v2-contract.ts";
36
36
  import type { MetaCitizenBackend } from "../pi-extensions/lib/meta-session.ts";
37
37
  import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
@@ -199,6 +199,10 @@ function main(): void {
199
199
  );
200
200
  const bounded = renderEntwurfPeers({ facts: { peers: manyPeers, recordLessSockets: [] }, diagnostics: [] });
201
201
  ok("bounded text omits older entries when peer list is large", bounded.text.includes("older entries omitted"));
202
+ ok(
203
+ "#112: human row budget remains exactly 32",
204
+ ENTWURF_PEERS_RENDER_LIMIT === 32 && bounded.text.includes("(8 older entries omitted; showing latest 32)"),
205
+ );
202
206
  ok("bounded text shows latest entries", bounded.text.includes("20260612T000039-aaaaaa"));
203
207
  ok("bounded text omits oldest entry", !bounded.text.includes("20260612T000000-aaaaaa backend="));
204
208
  ok("bounded payload still carries every peer", bounded.payload.peers.length === 40);
@@ -255,6 +259,14 @@ function main(): void {
255
259
  ok("wiring: bridge calls renderEntwurfPeers(", bridgeSrc.includes("renderEntwurfPeers("));
256
260
  ok("wiring: native pi tool calls listEntwurfFacts(", nativeSrc.includes("listEntwurfFacts("));
257
261
  ok("wiring: native pi tool calls renderEntwurfPeers(", nativeSrc.includes("renderEntwurfPeers("));
262
+ ok(
263
+ "#112 wiring: MCP observation uses the render limit",
264
+ bridgeSrc.includes("observationLimit: ENTWURF_PEERS_RENDER_LIMIT"),
265
+ );
266
+ ok(
267
+ "#112 wiring: native pi observation uses the render limit",
268
+ nativeSrc.includes("observationLimit: render.ENTWURF_PEERS_RENDER_LIMIT"),
269
+ );
258
270
  ok(
259
271
  "wiring: bridge does not paste full JSON payload into human text",
260
272
  !bridgeSrc.includes("JSON.stringify(payload)"),
@@ -266,14 +266,14 @@ ok(
266
266
  /existsSync\s*\(/.test(piBody) && /controlSocketPathIn\s*\(\s*ENTWURF_DIR\s*,/.test(piBody),
267
267
  );
268
268
 
269
- const metaBuilder = functionBody("buildTrustedMetaSenderEnvelope");
269
+ const metaBuilder = functionBody("buildMetaSenderEnvelope");
270
270
  // FIRST of the two derivation claims so a binary-fallback mutant dies here, not at the seam pin.
271
271
  ok(
272
- "buildTrustedMetaSenderEnvelope does not fall back every non-native-push backend to self-fetch [QK:SELFADDR-NO-FALLBACK-SELF-FETCH]",
272
+ "buildMetaSenderEnvelope does not fall back every non-native-push backend to self-fetch [QK:SELFADDR-NO-FALLBACK-SELF-FETCH]",
273
273
  !/\?\s*"native-push"\s*:\s*"self-fetch"/.test(metaBuilder),
274
274
  );
275
275
  ok(
276
- "buildTrustedMetaSenderEnvelope derives self-fetch through resolveMailboxWakeModeCapability(identity), not a backend-name list [QK:SELFADDR-MAILBOX-WAKE-SEAM]",
276
+ "buildMetaSenderEnvelope derives self-fetch through resolveMailboxWakeModeCapability(identity), not a backend-name list [QK:SELFADDR-MAILBOX-WAKE-SEAM]",
277
277
  /resolveMailboxWakeModeCapability\s*\(\s*identity\s*\)/.test(metaBuilder),
278
278
  );
279
279
 
@@ -443,14 +443,13 @@ function metaRailRenderIsHonest(region: string): { honest: boolean; reason: stri
443
443
  throw new Error(`class ${name} body never closed`);
444
444
  };
445
445
 
446
- // Anchored per class (not a repo-wide grep) so a mention somewhere else in the file
447
- // cannot make either row vacuously green.
446
+ // Anchored per class (not a repo-wide grep) so a mention elsewhere cannot make
447
+ // either row vacuously green. Native hook mechanisms vary by backend; Codex
448
+ // carries request-scoped identity instead of a shared process marker.
448
449
  for (const cls of ["EntwurfEnvelopeWiringError", "EntwurfSenderIdentityError"]) {
449
450
  const body = classBody(cls);
450
- ok(
451
- `${cls} names BOTH native hooks (SessionStart AND PreInvocation)`,
452
- /SessionStart/.test(body) && /PreInvocation/.test(body),
453
- );
451
+ ok(`${cls} names a native sender marker path`, /native (sender )?(claim|hook)|sender marker/.test(body));
452
+ ok(`${cls} names the managed Codex request identity path`, /managed Codex/.test(body));
454
453
  ok(
455
454
  `${cls} no longer presents SessionStart/Claude as THE marker writer`,
456
455
  !/The native SessionStart hook writes that marker/.test(body) &&
@@ -465,33 +464,33 @@ function metaRailRenderIsHonest(region: string): { honest: boolean; reason: stri
465
464
  // THIS session's own receiver inbox can actually wake (slice-2 presence marker), not a
466
465
  // hardcoded true. An inactive receiver must still return the meta identity (replyable:false)
467
466
  // — degrading to null would erase who-sent and fall through to external-mcp.
468
- const metaBody = functionBody("buildTrustedMetaSenderEnvelope");
469
- ok("buildTrustedMetaSenderEnvelope calls computeSelfAddressability", /computeSelfAddressability\s*\(/.test(metaBody));
470
- ok("buildTrustedMetaSenderEnvelope no longer hardcodes `replyable: true`", !/replyable:\s*true/.test(metaBody));
467
+ const metaBody = functionBody("buildMetaSenderEnvelope");
468
+ ok("buildMetaSenderEnvelope calls computeSelfAddressability", /computeSelfAddressability\s*\(/.test(metaBody));
469
+ ok("buildMetaSenderEnvelope no longer hardcodes `replyable: true`", !/replyable:\s*true/.test(metaBody));
471
470
  ok(
472
471
  // #101: the identity match alone is no longer the whole answer. `entwurf_self` composes
473
472
  // the SAME `resolveMailboxReceiverFacts` the v2 dispatch seam uses — reading the receiver
474
473
  // marker AND, where the watch owner is the sender-marker process, the join that says that
475
474
  // owner is still serving this garden. A citizen's self-reported replyability and what
476
475
  // dispatch decides about it come from one measurement, so they cannot disagree.
477
- "buildTrustedMetaSenderEnvelope derives active-receiver from the SHARED receiver composition (both markers)",
476
+ "buildMetaSenderEnvelope derives active-receiver from the SHARED receiver composition (both markers)",
478
477
  /resolveMailboxReceiverFacts\s*\(/.test(metaBody) &&
479
478
  /readMetaReceiverMarker\s*\(/.test(metaBody) &&
480
479
  /readMetaSenderMarker\s*\(/.test(metaBody),
481
480
  );
482
481
  ok(
483
- "buildTrustedMetaSenderEnvelope no longer copies one match into both receiver facts",
482
+ "buildMetaSenderEnvelope no longer copies one match into both receiver facts",
484
483
  !/ownerAlive:\s*active/.test(metaBody) && !/watchArmed:\s*active/.test(metaBody),
485
484
  );
486
485
  ok(
487
486
  // #101 (cross-review): both markers are read with their liveness guards ON. A reader that
488
487
  // opted out would accept a dead session's leftover file as "which garden this pid serves
489
488
  // now", and every fixture pid in a gate is live, so nothing dynamic here could tell.
490
- "buildTrustedMetaSenderEnvelope reads neither marker with the owner guard disabled",
489
+ "buildMetaSenderEnvelope reads neither marker with the owner guard disabled",
491
490
  !/verifyOwner:\s*false/.test(metaBody),
492
491
  );
493
492
  ok(
494
- "buildTrustedMetaSenderEnvelope keeps meta identity + derived replyable (inactive → not null)",
493
+ "buildMetaSenderEnvelope keeps meta identity + derived replyable (inactive → not null)",
495
494
  /origin:\s*"meta-session"/.test(metaBody) && /replyable:\s*self\.replyable/.test(metaBody),
496
495
  );
497
496
 
@@ -280,8 +280,8 @@ ok(
280
280
  // ── native-push (봉인 1/2/4): a THIRD table, disjoint from pi socket + mailbox ──
281
281
  const NATIVE_PUSH_LIVENESSES = ["alive", "dead", "indeterminate"] as const;
282
282
 
283
- // domain: antigravity is native-push; pi/claude-code are NOT; the domain is DISJOINT
284
- // from the pi socket liveness domain (a backend is never in both rails).
283
+ // domain: antigravity and Codex are native-push; pi/claude-code are NOT; the domain
284
+ // is DISJOINT from pi socket liveness (a backend is never in both rails).
285
285
  eq("native-push domain: antigravity supported", nativePushSupported("antigravity"), true);
286
286
  eq("native-push domain: pi is NOT native-push (it is pi-socket)", nativePushSupported("pi"), false);
287
287
  eq(
@@ -289,7 +289,8 @@ eq(
289
289
  nativePushSupported("claude-code"),
290
290
  false,
291
291
  );
292
- eq("native-push domain: NATIVE_PUSH_BACKENDS == ['antigravity']", [...NATIVE_PUSH_BACKENDS], ["antigravity"]);
292
+ eq("native-push domain: Codex supported", nativePushSupported("codex"), true);
293
+ eq("native-push domain: NATIVE_PUSH_BACKENDS exact", [...NATIVE_PUSH_BACKENDS], ["antigravity", "codex"]);
293
294
  ok(
294
295
  "native-push ∩ pi-socket liveness domain = ∅ (separate rails)",
295
296
  !NATIVE_PUSH_BACKENDS.some((b) => (LIVENESS_DOMAIN_BACKENDS as readonly string[]).includes(b)),
@@ -381,7 +381,7 @@ async function main(): Promise<void> {
381
381
  // ── 8: unsupported + ff + seam says undeliverable → reject, no plan, no lock ─
382
382
  {
383
383
  const t = mkDeps({
384
- resolution: { identity: identity("codex"), preProbeAddressConflict: false },
384
+ resolution: { identity: identity("omp"), preProbeAddressConflict: false },
385
385
  mailboxDeliverable: false,
386
386
  });
387
387
  const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "mail" }, t.deps);
@@ -399,8 +399,7 @@ async function main(): Promise<void> {
399
399
  // reject, NO plan, NO lock, NO inspect/probe. This is the v2 closure of the gap slice
400
400
  // 2d-2 closed for v1: the decider no longer trusts wake-mode alone; the required seam's
401
401
  // active-receiver verdict governs, so a reply to a dead claude-code is refused, not
402
- // enqueued as mailbox garbage. (Indistinguishable here from the codex case at the
403
- // receipt level — that is the point: the seam, not the backend, decides.) ────────────
402
+ // enqueued as mailbox garbage. ───────────────────────────────────────────────────
404
403
  {
405
404
  const t = mkDeps({
406
405
  resolution: { identity: identity("claude-code"), preProbeAddressConflict: false },
@@ -581,7 +580,7 @@ async function main(): Promise<void> {
581
580
  // ff × alive → execute native-push send; plan carries route/backend/nativeSessionId; lock null.
582
581
  const t = mkDeps({
583
582
  resolution: npResolution,
584
- nativePush: { status: "alive", route: { lsAddress: "127.0.0.1:5599" } },
583
+ nativePush: { status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" } },
585
584
  });
586
585
  const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "yo" }, t.deps);
587
586
  ok("native-push ff+alive: execute", isExecute(d));
@@ -594,7 +593,10 @@ async function main(): Promise<void> {
594
593
  t.mailboxCalls.length === 0,
595
594
  );
596
595
  if (isExecute(d) && d.plan.transport === "native-push") {
597
- ok("native-push ff+alive: plan route = probed route", d.plan.route.lsAddress === "127.0.0.1:5599");
596
+ ok(
597
+ "native-push ff+alive: plan route = probed route",
598
+ d.plan.route.backend === "antigravity" && d.plan.route.lsAddress === "127.0.0.1:5599",
599
+ );
598
600
  ok("native-push ff+alive: plan backend = antigravity", d.plan.backend === "antigravity");
599
601
  ok("native-push ff+alive: plan nativeSessionId from identity", d.plan.nativeSessionId === `native-${GID}`);
600
602
  ok(
@@ -35,7 +35,11 @@ function ok(label: string, cond: boolean): void {
35
35
  }
36
36
 
37
37
  const CONV = "conv-xyz";
38
- const PLANTED: NativePushRoute = { lsAddress: "127.0.0.1:5600" };
38
+ const PLANTED: NativePushRoute = { backend: "antigravity", lsAddress: "127.0.0.1:5600" };
39
+
40
+ function agyAddress(route: NativePushRoute | undefined): string | undefined {
41
+ return route?.backend === "antigravity" ? route.lsAddress : undefined;
42
+ }
39
43
 
40
44
  interface SendCall {
41
45
  route: NativePushRoute;
@@ -48,6 +52,7 @@ interface FakeAdapterConfig {
48
52
  probes: NativePushProbeResult[];
49
53
  /** 0-based indices of send() calls that THROW. */
50
54
  sendFailAt?: number[];
55
+ retriable?: boolean;
51
56
  }
52
57
 
53
58
  function makeFakeAdapter(config: FakeAdapterConfig): {
@@ -60,6 +65,7 @@ function makeFakeAdapter(config: FakeAdapterConfig): {
60
65
  const sends: SendCall[] = [];
61
66
  const adapter: NativePushAdapter = {
62
67
  id: "antigravity",
68
+ retriable: config.retriable ?? true,
63
69
  async probe() {
64
70
  const p = config.probes[probeIdx] ?? { status: "dead", reason: "fake: probes exhausted" };
65
71
  probeIdx++;
@@ -81,31 +87,53 @@ async function main(): Promise<void> {
81
87
  const r = await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
82
88
  ok("success: retried:false", r.success === true && r.retried === false);
83
89
  ok("success: exactly ONE send", sends.length === 1);
84
- ok("success: send used the PLANTED route (no re-probe)", sends[0]?.route.lsAddress === "127.0.0.1:5600");
90
+ ok("success: send used the PLANTED route (no re-probe)", agyAddress(sends[0]?.route) === "127.0.0.1:5600");
85
91
  ok("success: ZERO re-probe", probeCount() === 0);
86
92
  }
87
93
 
94
+ // Codex's vendor queue may accept before its receipt fails: no re-probe, no replay.
95
+ {
96
+ const { adapter, sends, probeCount } = makeFakeAdapter({
97
+ probes: [{ status: "alive", route: { backend: "antigravity", lsAddress: "must-not-be-used" } }],
98
+ sendFailAt: [0, 1],
99
+ retriable: false,
100
+ });
101
+ await assert.rejects(() => deliverViaNativePush(adapter, PLANTED, CONV, "hi"), /fake send fail/);
102
+ ok("non-retriable failure makes exactly ONE send", sends.length === 1);
103
+ ok("non-retriable failure makes ZERO probes", probeCount() === 0);
104
+ }
105
+
88
106
  // ── fail → re-probe alive → re-send success: retried, fresh route ────────────
89
107
  {
90
108
  const { adapter, sends, probeCount } = makeFakeAdapter({
91
- probes: [{ status: "alive", route: { lsAddress: "127.0.0.1:5601" } }],
109
+ probes: [{ status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5601" } }],
92
110
  sendFailAt: [0],
93
111
  });
94
112
  const r = await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
95
113
  ok("retry: retried:true", r.success === true && r.retried === true);
96
114
  ok("retry: TWO sends (initial + one retry)", sends.length === 2);
97
115
  ok("retry: exactly ONE re-probe", probeCount() === 1);
98
- ok("retry: 1st send used the planted route", sends[0]?.route.lsAddress === "127.0.0.1:5600");
116
+ ok("retry: 1st send used the planted route", agyAddress(sends[0]?.route) === "127.0.0.1:5600");
99
117
  ok(
100
118
  "retry: 2nd send used the RE-DISCOVERED route (not the stale one)",
101
- sends[1]?.route.lsAddress === "127.0.0.1:5601",
119
+ agyAddress(sends[1]?.route) === "127.0.0.1:5601",
120
+ );
121
+ // The retry replays CONTENT, never re-derives it. The caller hands this leaf one already
122
+ // rendered string (the #95 sender envelope is rendered ONCE, upstream, with its own
123
+ // timestamp), so the only honest retry is the same bytes — a second render, or any
124
+ // per-attempt decoration here, would deliver a body that disagrees with the first
125
+ // attempt the target may already have accepted. Asserted at this leaf so the claim holds
126
+ // without coupling sender rendering into the adapter hand.
127
+ ok(
128
+ "[QK:NATIVE-PUSH-RETRY-BYTE-IDENTITY] the retry replays byte-identical content, never a re-derived body",
129
+ sends[0]?.content === sends[1]?.content,
102
130
  );
103
131
  }
104
132
 
105
133
  // ── fail → re-probe alive → re-send FAIL: throws (fail-loud, no 3rd attempt) ──
106
134
  {
107
135
  const { adapter, sends } = makeFakeAdapter({
108
- probes: [{ status: "alive", route: { lsAddress: "127.0.0.1:5601" } }],
136
+ probes: [{ status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5601" } }],
109
137
  sendFailAt: [0, 1],
110
138
  });
111
139
  let threw = false;
@@ -182,7 +210,7 @@ async function main(): Promise<void> {
182
210
  );
183
211
  ok(
184
212
  "makeNativePushSend: sent the plan message over the plan route",
185
- sends[0]?.content === "payload" && sends[0]?.route.lsAddress === "127.0.0.1:5600",
213
+ sends[0]?.content === "payload" && agyAddress(sends[0]?.route) === "127.0.0.1:5600",
186
214
  );
187
215
  ok("makeNativePushSend: lock IGNORED (lock-free — a bogus lock did not break delivery)", sends.length === 1);
188
216
  }
@@ -27,8 +27,14 @@ import {
27
27
  makeProductionEntwurfV2Deps,
28
28
  type ProductionEntwurfV2Opts,
29
29
  } from "../pi-extensions/lib/entwurf-v2-production.ts";
30
- import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
30
+ import {
31
+ type EntwurfV2RunDeps,
32
+ type EntwurfV2RunResult,
33
+ executeDispatch,
34
+ runEntwurfV2,
35
+ } from "../pi-extensions/lib/entwurf-v2-runner.ts";
31
36
  import type { ControlSocketPlan, MetaMailboxPlan } from "../pi-extensions/lib/entwurf-v2-send.ts";
37
+ import { formatMetaMailboxBody, type MailboxSenderEnvelope } from "../pi-extensions/lib/meta-mailbox-body.ts";
32
38
  import type { MetaIdentity, MetaReceiverMarker, MetaSenderMarker } from "../pi-extensions/lib/meta-session.ts";
33
39
  import type { NativePushAdapter, NativePushProbeResult } from "../pi-extensions/lib/native-push/adapter.ts";
34
40
  import type { TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
@@ -43,6 +49,13 @@ function ok(label: string, cond: boolean): void {
43
49
  const GID = "20260613T100000-aaaaaa";
44
50
  /** The garden a switched-away owner serves instead (#101). */
45
51
  const OTHER_GID = "20260613T100000-bbbbbb";
52
+ /** The one trusted sender every rail in this gate renders from. */
53
+ const SENDER: MailboxSenderEnvelope = {
54
+ sessionId: "self",
55
+ agentId: "pi/x",
56
+ cwd: "/cwd",
57
+ timestamp: "2026-06-13T00:00:00.000Z",
58
+ };
46
59
  const LOCK_DIR = "/fake/locks";
47
60
  const SESSIONS_DIR = "/fake/sessions";
48
61
  const MAILBOX_DIR = "/fake/mailbox";
@@ -132,7 +145,36 @@ interface Spies {
132
145
  /** which backend the native-push adapter resolver was asked for (decide + execute). */
133
146
  nativePushResolve: string[];
134
147
  nativePushProbe: { conv: string }[];
135
- nativePushSend: { lsAddress: string; conv: string; content: string }[];
148
+ nativePushSend: { route: string; conv: string; content: string }[];
149
+ /** every `senderProvider()` call — ONE provider feeds all three rails, so a hand that
150
+ * resolves the envelope twice (or not at all) is visible here. */
151
+ senderCalls: number[];
152
+ /** ORDERED trace of the native-push hand's observable steps. Counts alone cannot see
153
+ * ORDER, and order is the whole safety claim: the envelope must be resolved BEFORE the
154
+ * adapter is touched, so a provider throw leaves the target's conversation untouched
155
+ * rather than half-delivered. `runInTwoPhases` marks where the execute phase starts. */
156
+ events: ("sender" | "resolve" | "probe" | "send")[];
157
+ }
158
+
159
+ /**
160
+ * `runEntwurfV2` split at its own seam so the EXECUTE phase has a MEASURED boundary. The decide
161
+ * phase legitimately resolves the adapter and probes (봉인 4: one adapter for both hands), so an
162
+ * order claim about the send hand has to start after decide returns — but the boundary must be
163
+ * the index recorded between the two calls, never something inferred from the event VALUES. An
164
+ * earlier version sliced after the last `probe`, which a regression could defeat: an executor
165
+ * that resolved and probed before the sender would make its own probe the "last" one and hide
166
+ * the illegal prefix it just produced. This records the length instead, so ANY extra execute-side
167
+ * resolve or probe ahead of the sender stays inside the slice and fails.
168
+ */
169
+ async function runInTwoPhases(
170
+ input: DispatchInput,
171
+ deps: EntwurfV2RunDeps,
172
+ spies: Spies,
173
+ ): Promise<{ result: EntwurfV2RunResult; executeEvents: string[] }> {
174
+ const decision = await deps.decide(input);
175
+ const executeStart = spies.events.length;
176
+ const result = await executeDispatch(decision, deps.executor);
177
+ return { result, executeEvents: spies.events.slice(executeStart) };
136
178
  }
137
179
 
138
180
  /** Build a factory whose every leaf IO is a spy. `over` lets a case shape the decision
@@ -146,6 +188,8 @@ function makeSpiedFactory(over: {
146
188
  classifyDead?: boolean;
147
189
  /** #50 C3 — a caller with no authoritative sender (senderProvider → undefined). */
148
190
  noSender?: boolean;
191
+ /** #95 — the envelope source itself fails: the send must reject with NOTHING injected. */
192
+ senderThrows?: boolean;
149
193
  /** the native-push adapter probe result (only reached on a native-push backend). */
150
194
  nativePushProbe?: NativePushProbeResult;
151
195
  /** SE-2 2d-3 — the target's receiver presence marker: "active" (matches identity,
@@ -166,12 +210,16 @@ function makeSpiedFactory(over: {
166
210
  nativePushResolve: [],
167
211
  nativePushProbe: [],
168
212
  nativePushSend: [],
213
+ senderCalls: [],
214
+ events: [],
169
215
  };
170
216
  const opts: ProductionEntwurfV2Opts = {
171
- senderProvider: () =>
172
- over.noSender
173
- ? undefined
174
- : { sessionId: "self", agentId: "pi/x", cwd: "/cwd", timestamp: "2026-06-13T00:00:00.000Z" },
217
+ senderProvider: () => {
218
+ spies.senderCalls.push(spies.senderCalls.length + 1);
219
+ spies.events.push("sender");
220
+ if (over.senderThrows) throw new Error("fake: sender envelope unavailable");
221
+ return over.noSender ? undefined : SENDER;
222
+ },
175
223
  lockDir: LOCK_DIR,
176
224
  sessionsDir: SESSIONS_DIR,
177
225
  mailboxDir: MAILBOX_DIR,
@@ -231,14 +279,19 @@ function makeSpiedFactory(over: {
231
279
  // two hands never resolve different adapters.
232
280
  resolveNativePushAdapter: (backend: string): NativePushAdapter => {
233
281
  spies.nativePushResolve.push(backend);
282
+ spies.events.push("resolve");
234
283
  return {
235
- id: "antigravity",
284
+ id: backend as NativePushAdapter["id"],
285
+ retriable: backend === "antigravity",
236
286
  async probe(conv) {
237
287
  spies.nativePushProbe.push({ conv });
288
+ spies.events.push("probe");
238
289
  return over.nativePushProbe ?? { status: "dead", reason: "fake: no native-push probe configured" };
239
290
  },
240
291
  async send(route, conv, content) {
241
- spies.nativePushSend.push({ lsAddress: route.lsAddress, conv, content });
292
+ const routeLabel = route.backend === "antigravity" ? route.lsAddress : route.socketPath;
293
+ spies.nativePushSend.push({ route: routeLabel, conv, content });
294
+ spies.events.push("send");
242
295
  },
243
296
  };
244
297
  },
@@ -277,14 +330,14 @@ async function main(): Promise<void> {
277
330
  {
278
331
  const { deps, spies } = makeSpiedFactory({
279
332
  backend: "antigravity",
280
- nativePushProbe: { status: "alive", route: { lsAddress: "127.0.0.1:5599" } },
333
+ nativePushProbe: { status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" } },
281
334
  });
282
335
  const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi agy" }, deps);
283
336
  ok("A3: antigravity ff → native-push delivered", result.kind === "executed" && result.transport === "native-push");
284
337
  ok("A3: decider probed the native-push adapter once (decide side)", spies.nativePushProbe.length === 1);
285
338
  ok("A3: executor sent via the native-push adapter once (execute side)", spies.nativePushSend.length === 1);
286
- ok("A3: send used the DECIDER-probed route", spies.nativePushSend[0]?.lsAddress === "127.0.0.1:5599");
287
- ok("A3: send carried the dispatch message", spies.nativePushSend[0]?.content === "hi agy");
339
+ ok("A3: send used the DECIDER-probed route", spies.nativePushSend[0]?.route === "127.0.0.1:5599");
340
+ ok("A3: send carried the dispatch message", spies.nativePushSend[0]?.content.includes("hi agy") === true);
288
341
  ok(
289
342
  "A3: BOTH hands resolved the SAME adapter (for 'antigravity')",
290
343
  spies.nativePushResolve.length === 2 && spies.nativePushResolve.every((b) => b === "antigravity"),
@@ -292,6 +345,145 @@ async function main(): Promise<void> {
292
345
  ok("A3: native-push is LOCK-FREE (no acquire)", spies.acquire.length === 0);
293
346
  ok("A3: native-push did NOT enqueue a mailbox (not the unsupported path)", spies.enqueue.length === 0);
294
347
  }
348
+ {
349
+ const { deps, spies } = makeSpiedFactory({
350
+ backend: "codex",
351
+ nativePushProbe: {
352
+ status: "alive",
353
+ route: { backend: "codex", socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock" },
354
+ },
355
+ });
356
+ const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi codex" }, deps);
357
+ ok(
358
+ "[QK:CODEX-NATIVE-PRODUCTION-WIRING] Codex ff → native-push delivered",
359
+ result.kind === "executed" && result.transport === "native-push",
360
+ );
361
+ ok(
362
+ "A3c: both hands resolve the Codex adapter",
363
+ spies.nativePushResolve.length === 2 && spies.nativePushResolve.every((b) => b === "codex"),
364
+ );
365
+ ok(
366
+ "A3c: planted Codex UDS reaches the send hand",
367
+ spies.nativePushSend[0]?.route.endsWith("/app-server-control.sock") === true,
368
+ );
369
+ ok(
370
+ "A3c: Codex native-push remains lock-free and mailbox-free",
371
+ spies.acquire.length === 0 && spies.enqueue.length === 0,
372
+ );
373
+ }
374
+ // ── A3d (#95): the native-push rail injects the MAILBOX-SERIALIZED sender envelope ──
375
+ // Measured in the #95 A LIVE run: a nonce-only fresh-call callback direct-injected into a
376
+ // Codex thread arrived as bare text, so the receiving citizen could not name who called it
377
+ // and the callback was uncorrelatable. Direct injection has neither the control socket's RPC
378
+ // framing nor a mailbox file, so the envelope must ride IN the body — the same bytes
379
+ // `formatMetaMailboxBody` gives the mailbox rail. ONE rail: both native-push backends, no
380
+ // per-backend branch.
381
+ {
382
+ const route = { backend: "antigravity", lsAddress: "127.0.0.1:5599" } as const;
383
+ const { deps, spies } = makeSpiedFactory({
384
+ backend: "antigravity",
385
+ nativePushProbe: { status: "alive", route },
386
+ });
387
+ const { result, executeEvents } = await runInTwoPhases(
388
+ { target: GID, intent: "fire-and-forget", message: "hi agy", wantsReply: true },
389
+ deps,
390
+ spies,
391
+ );
392
+ // WIRING FIRST, and it is its own claim: the factory must hand the native-push hand the
393
+ // SAME `opts.senderProvider` the mailbox hand gets. A render that is reachable but never
394
+ // fed falls back to raw silently, so "the provider was consulted exactly once on this
395
+ // rail" is asserted BEFORE the body is compared — that way a dropped wiring fails here
396
+ // at its own QK instead of borrowing the envelope claim's failure.
397
+ ok(
398
+ "[QK:NATIVE-PUSH-SENDER-PROVIDER-WIRING] the factory fed the native-push hand its one senderProvider, exactly once",
399
+ result.kind === "executed" && spies.senderCalls.length === 1,
400
+ );
401
+ ok(
402
+ "[QK:NATIVE-PUSH-SENDER-ENVELOPE] agy direct injection carries the mailbox-serialized trusted envelope",
403
+ spies.nativePushSend[0]?.content === formatMetaMailboxBody(SENDER, "hi agy", true),
404
+ );
405
+ ok(
406
+ "A3d: the enveloped body is NOT the raw plan message (an envelope was actually added)",
407
+ spies.nativePushSend[0]?.content !== "hi agy",
408
+ );
409
+ ok(
410
+ "A3d: wants_reply rides the envelope on this rail",
411
+ spies.nativePushSend[0]?.content.includes("wants reply: yes") === true,
412
+ );
413
+ // ORDER, not counts. The envelope must be resolved BEFORE the adapter is resolved or
414
+ // touched. Counts would be identical if the hand resolved the adapter first and the
415
+ // provider second — but then a provider throw would land after the adapter was already
416
+ // in hand, and the safety claim ("a failing envelope source injects nothing") would
417
+ // rest on luck rather than sequence.
418
+ ok(
419
+ "[QK:NATIVE-PUSH-SENDER-BEFORE-ADAPTER] the send hand resolves the envelope, THEN the adapter, THEN sends",
420
+ JSON.stringify(executeEvents) === JSON.stringify(["sender", "resolve", "send"]),
421
+ );
422
+ }
423
+ {
424
+ // Same rail, other backend — codex renders identically. A backend branch would show here.
425
+ const route = {
426
+ backend: "codex",
427
+ socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock",
428
+ } as const;
429
+ const { deps, spies } = makeSpiedFactory({ backend: "codex", nativePushProbe: { status: "alive", route } });
430
+ const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi codex" }, deps);
431
+ ok(
432
+ "A3d: codex direct injection carries the SAME mailbox-serialized envelope (one rail, no backend branch)",
433
+ result.kind === "executed" &&
434
+ spies.nativePushSend[0]?.content === formatMetaMailboxBody(SENDER, "hi codex", false),
435
+ );
436
+ ok("A3d: the codex body is NOT the raw plan message either", spies.nativePushSend[0]?.content !== "hi codex");
437
+ ok("A3d: codex resolved the envelope EXACTLY once", spies.senderCalls.length === 1);
438
+ }
439
+ {
440
+ // #50 C3 parity: no authoritative sender → the raw message is preserved, never a
441
+ // fabricated envelope. This is the same fallback the mailbox hand has.
442
+ const { deps, spies } = makeSpiedFactory({
443
+ backend: "codex",
444
+ noSender: true,
445
+ nativePushProbe: {
446
+ status: "alive",
447
+ route: { backend: "codex", socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock" },
448
+ },
449
+ });
450
+ const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "raw only" }, deps);
451
+ ok(
452
+ "A3d: no authoritative sender → the RAW message is injected, no fabricated envelope",
453
+ result.kind === "executed" && spies.nativePushSend[0]?.content === "raw only",
454
+ );
455
+ }
456
+ {
457
+ // A throwing provider must reject the send with NOTHING injected — a half-delivered
458
+ // conversation is worse than a rejected dispatch. No new reject reason, no fallback.
459
+ const { deps, spies } = makeSpiedFactory({
460
+ backend: "codex",
461
+ senderThrows: true,
462
+ nativePushProbe: {
463
+ status: "alive",
464
+ route: { backend: "codex", socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock" },
465
+ },
466
+ });
467
+ const { result, executeEvents } = await runInTwoPhases(
468
+ { target: GID, intent: "fire-and-forget", message: "never sent" },
469
+ deps,
470
+ spies,
471
+ );
472
+ ok(
473
+ "A3d: a throwing sender provider fails the dispatch EXACTLY as execution-failed",
474
+ result.kind === "execution-failed",
475
+ );
476
+ ok("A3d: a throwing sender provider called its provider exactly once", spies.senderCalls.length === 1);
477
+ ok("A3d: a throwing sender provider never resolved the adapter", executeEvents.includes("resolve") === false);
478
+ ok("A3d: a throwing sender provider injected NOTHING", spies.nativePushSend.length === 0);
479
+ // The whole execute phase is ONE event: the envelope source was asked, it failed, and the
480
+ // target's conversation was never reached. No partial delivery, no adapter in hand.
481
+ ok(
482
+ "A3d: the failed execute phase is exactly ['sender'] — nothing downstream ran",
483
+ JSON.stringify(executeEvents) === JSON.stringify(["sender"]),
484
+ );
485
+ }
486
+
295
487
  {
296
488
  // antigravity + dead probe → reject native-push-target-dead, NO send.
297
489
  const { deps, spies } = makeSpiedFactory({
@@ -101,7 +101,7 @@ const NATIVE_PUSH_PLAN: NativePushPlan = {
101
101
  targetGardenId: GID,
102
102
  backend: "antigravity",
103
103
  nativeSessionId: "conv-xyz",
104
- route: { lsAddress: "127.0.0.1:5599" },
104
+ route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" },
105
105
  wantsReply: false,
106
106
  message: "m",
107
107
  };
@@ -729,7 +729,7 @@ async function main(): Promise<void> {
729
729
  v2Start !== -1 && after > v2Start,
730
730
  );
731
731
  const v2Block = src.slice(v2Start, after);
732
- ok("5: MCP — v2 handler builds buildSendSenderEnvelope()", /buildSendSenderEnvelope\(\)/.test(v2Block));
732
+ ok("5: MCP — v2 handler builds buildSendSenderEnvelope(context)", /buildSendSenderEnvelope\s*\(/.test(v2Block));
733
733
  ok("5: MCP — v2 handler passes senderProvider: () => sender", /senderProvider:\s*\(\)\s*=>\s*sender/.test(v2Block));
734
734
  ok(
735
735
  "5: MCP — v2 handler calls runAndRenderEntwurfV2FromSurface",
@@ -832,9 +832,12 @@ let manifestCount: number;
832
832
  "bridge-boot-resume": 3,
833
833
  "bridge-command-boot": 9,
834
834
  "capability-cache": 3,
835
+ "codex-native": 68,
836
+ "compaction-send-guard": 7,
835
837
  "copilot-birth": 19,
836
838
  "copilot-launch": 14,
837
839
  "copilot-receive": 20,
840
+ "entwurf-peers": 1,
838
841
  "fresh-cut": 3,
839
842
  "gate-qualification": 2,
840
843
  "meta-facts": 4,
@@ -842,7 +845,7 @@ let manifestCount: number;
842
845
  "meta-identity": 4,
843
846
  "meta-retire": 3,
844
847
  "mux-boundary": 16,
845
- "mux-fresh-call": 44,
848
+ "mux-fresh-call": 45,
846
849
  "mux-launcher-fence": 7,
847
850
  "mux-parent-artifact": 3,
848
851
  "pack-install": 2,
@@ -852,13 +855,13 @@ let manifestCount: number;
852
855
  "omp-fresh": 24,
853
856
  "omp-receive": 11,
854
857
  "probe-ordering": 1,
855
- "release-gate": 18,
858
+ "release-gate": 19,
856
859
  "resume-args": 6,
857
860
  "resume-launch-identity": 6,
858
861
  "self-address": 5,
859
862
  "setup-verdict": 14,
860
863
  "source-install": 2,
861
- "v2-surface": 7,
864
+ "v2-surface": 11,
862
865
  "v2-visible-resume": 17,
863
866
  };
864
867
  const laneTally: Record<string, number> = {};
@@ -81,7 +81,6 @@ const PRE_82_LEGACY: Partial<Record<MetaCitizenBackend, [file: string, sentence:
81
81
  "DELIVERY.md",
82
82
  "Admitted before the #82 step 9 contract and not re-evaluated under it, so it is legacy citizen evidence, not a step-9 supported harness",
83
83
  ],
84
- codex: ["DELIVERY.md", "No owned native-citizen install/invocation lane."],
85
84
  };
86
85
 
87
86
  const fresh = new Set<string>(FRESH_CALL_BACKENDS);