@junghanacs/entwurf 0.12.6 → 0.12.7

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 (92) hide show
  1. package/AGENTS.md +31 -17
  2. package/BASELINE.md +42 -8
  3. package/CHANGELOG.md +22 -0
  4. package/DELIVERY.md +71 -14
  5. package/README.md +77 -45
  6. package/VERIFY.md +18 -13
  7. package/docs/setup-clean-host.md +80 -22
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
  30. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
  31. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
  33. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
  34. package/mcp/entwurf-bridge/src/index.ts +101 -67
  35. package/mcp/entwurf-bridge/test.sh +1 -1
  36. package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
  37. package/package.json +10 -5
  38. package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
  39. package/pi-extensions/lib/entwurf-self-address.ts +58 -15
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
  41. package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
  42. package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
  43. package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
  44. package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
  45. package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
  46. package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
  47. package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
  48. package/pi-extensions/lib/meta-sender-identity.ts +154 -0
  49. package/pi-extensions/lib/native-push/adapter.ts +255 -0
  50. package/pi-extensions/lib/native-push/register.ts +99 -0
  51. package/run.sh +756 -214
  52. package/scripts/agy-bridge-config.py +446 -0
  53. package/scripts/agy-bridge.sh +359 -0
  54. package/scripts/agy-hooks-bridge.sh +193 -0
  55. package/scripts/agy-hooks-config.py +257 -0
  56. package/scripts/agy-imprint.sh +28 -0
  57. package/scripts/agy-imprint.ts +193 -0
  58. package/scripts/agy-statusline-bridge.sh +176 -0
  59. package/scripts/agy-statusline-config.py +213 -0
  60. package/scripts/agy-statusline.sh +256 -0
  61. package/scripts/build-bridge.sh +20 -0
  62. package/scripts/check-agy-sender-identity.ts +364 -0
  63. package/scripts/check-entwurf-bridge-boot.ts +8 -2
  64. package/scripts/check-entwurf-deliverability.ts +34 -0
  65. package/scripts/check-entwurf-self-address.ts +78 -11
  66. package/scripts/check-entwurf-v2-contract.ts +136 -1
  67. package/scripts/check-entwurf-v2-decider.ts +95 -1
  68. package/scripts/check-entwurf-v2-matrix.ts +14 -3
  69. package/scripts/check-entwurf-v2-native-push.ts +193 -0
  70. package/scripts/check-entwurf-v2-production.ts +68 -1
  71. package/scripts/check-entwurf-v2-runner.ts +58 -0
  72. package/scripts/check-entwurf-v2-surface.ts +35 -0
  73. package/scripts/check-install-surface.ts +357 -0
  74. package/scripts/check-native-push-adapter.ts +319 -0
  75. package/scripts/check-native-push-register.ts +130 -0
  76. package/scripts/dev-bin.sh +195 -0
  77. package/scripts/doctor-pi-provider.ts +140 -0
  78. package/scripts/meta-bridge-doctor.sh +36 -2
  79. package/scripts/register-pi-package.py +37 -3
  80. package/scripts/register-pi-provider.py +287 -0
  81. package/scripts/smoke-agy-hooks-state.sh +172 -0
  82. package/scripts/smoke-agy-install-state.sh +660 -0
  83. package/scripts/smoke-agy-native-push-live.ts +243 -0
  84. package/scripts/smoke-agy-statusline-state.sh +300 -0
  85. package/scripts/smoke-meta-async-drift.sh +9 -2
  86. package/scripts/smoke-meta-install-state.sh +20 -0
  87. package/scripts/smoke-pi-provider-state.sh +182 -0
  88. package/scripts/smoke-user-scope-citizen.sh +62 -0
  89. package/scripts/with-dist-lock.sh +81 -0
  90. package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
  91. package/scripts/__pycache__/meta-bridge-state.cpython-313.pyc +0 -0
  92. package/scripts/__pycache__/register-pi-package.cpython-313.pyc +0 -0
@@ -46,11 +46,18 @@ import {
46
46
  factLivenessOf,
47
47
  isLivenessSupported,
48
48
  isPreProbeReject,
49
+ LIVENESS_DOMAIN_BACKENDS,
49
50
  makeRejectReceipt,
51
+ NATIVE_PUSH_BACKENDS,
52
+ NATIVE_PUSH_DISPATCH_TABLE,
53
+ NATIVE_PUSH_REJECT_REASONS,
54
+ type NativePushLiveness,
55
+ nativePushSupported,
50
56
  PRE_PROBE_REJECT_REASONS,
51
57
  RESOLVER_REJECT_REASONS,
52
58
  rejectObservedLivenessWellFormed,
53
59
  resolveDispatch,
60
+ resolveNativePushDispatch,
54
61
  UNSUPPORTED_DISPATCH_TABLE,
55
62
  } from "../pi-extensions/lib/entwurf-v2-contract.ts";
56
63
  import {
@@ -291,6 +298,128 @@ ok(
291
298
  (RESOLVER_REJECT_REASONS as readonly string[]).includes("mailbox-undeliverable"),
292
299
  );
293
300
 
301
+ // ── native-push (봉인 1/2/4): a THIRD table, disjoint from pi socket + mailbox ──
302
+ const NATIVE_PUSH_LIVENESSES = ["alive", "dead", "indeterminate"] as const;
303
+
304
+ // domain: antigravity is native-push; pi/claude-code are NOT; the domain is DISJOINT
305
+ // from the pi socket liveness domain (a backend is never in both rails).
306
+ eq("native-push domain: antigravity supported", nativePushSupported("antigravity"), true);
307
+ eq("native-push domain: pi is NOT native-push (it is pi-socket)", nativePushSupported("pi"), false);
308
+ eq(
309
+ "native-push domain: claude-code is NOT native-push (self-fetch mailbox)",
310
+ nativePushSupported("claude-code"),
311
+ false,
312
+ );
313
+ eq("native-push domain: NATIVE_PUSH_BACKENDS == ['antigravity']", [...NATIVE_PUSH_BACKENDS], ["antigravity"]);
314
+ ok(
315
+ "native-push ∩ pi-socket liveness domain = ∅ (separate rails)",
316
+ !NATIVE_PUSH_BACKENDS.some((b) => (LIVENESS_DOMAIN_BACKENDS as readonly string[]).includes(b)),
317
+ );
318
+
319
+ // transport present + stays OUT of the pi 6-cell table (like meta-mailbox).
320
+ ok(
321
+ "transport: native-push present (direct-inject into a live app-server conversation)",
322
+ (ENTWURF_V2_TRANSPORTS as readonly string[]).includes("native-push"),
323
+ );
324
+ for (const intent of ENTWURF_INTENTS) {
325
+ for (const dl of DISPATCH_LIVENESSES) {
326
+ const cell = DISPATCH_TABLE[intent][dl as DispatchLiveness];
327
+ ok(
328
+ `guard: DISPATCH_TABLE[${intent}][${dl}] is NOT native-push (native-push stays out of the pi 6-cell table)`,
329
+ cell.action === "reject" || cell.transport !== "native-push",
330
+ );
331
+ }
332
+ }
333
+
334
+ // NATIVE_PUSH_DISPATCH_TABLE: 6 cells (2 intents × 3 liveness), single verdict each.
335
+ {
336
+ let allow = 0;
337
+ let reject = 0;
338
+ for (const intent of ENTWURF_INTENTS) {
339
+ for (const lv of NATIVE_PUSH_LIVENESSES) {
340
+ const cell = NATIVE_PUSH_DISPATCH_TABLE[intent]?.[lv as NativePushLiveness];
341
+ ok(`native-push table[${intent}][${lv}] present`, cell != null);
342
+ if (cell.action === "reject") {
343
+ reject++;
344
+ ok(
345
+ `native-push table[${intent}][${lv}] reason in taxonomy`,
346
+ (ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(cell.reason),
347
+ );
348
+ } else {
349
+ allow++;
350
+ }
351
+ }
352
+ }
353
+ eq("native-push table has 6 cells (2 intents × 3 liveness)", allow + reject, 6);
354
+ eq("native-push: exactly 1 allow cell (fire-and-forget × alive)", allow, 1);
355
+ eq("native-push: exactly 5 reject cells", reject, 5);
356
+ }
357
+ // the one allow cell + the intended reject reasons.
358
+ eq("native-push allow: ff+alive = send/native-push/ack-only", NATIVE_PUSH_DISPATCH_TABLE["fire-and-forget"].alive, {
359
+ action: "send",
360
+ transport: "native-push",
361
+ ownership: "ack-only",
362
+ });
363
+ eq("native-push: ff+dead = reject(native-push-target-dead)", NATIVE_PUSH_DISPATCH_TABLE["fire-and-forget"].dead, {
364
+ action: "reject",
365
+ reason: "native-push-target-dead",
366
+ });
367
+ eq(
368
+ "native-push: ff+indeterminate = reject(native-push-probe-indeterminate)",
369
+ NATIVE_PUSH_DISPATCH_TABLE["fire-and-forget"].indeterminate,
370
+ { action: "reject", reason: "native-push-probe-indeterminate" },
371
+ );
372
+ // owned-outcome × * = single, state-independent no-resume-authority reject.
373
+ for (const lv of NATIVE_PUSH_LIVENESSES) {
374
+ eq(
375
+ `native-push: owned+${lv} = reject(native-push-no-resume-authority)`,
376
+ NATIVE_PUSH_DISPATCH_TABLE["owned-outcome"][lv],
377
+ {
378
+ action: "reject",
379
+ reason: "native-push-no-resume-authority",
380
+ },
381
+ );
382
+ }
383
+ // `backend-liveness-unsupported` is NOT reused for a native-push backend (봉인 1).
384
+ ok(
385
+ "native-push: reject reasons never reuse backend-liveness-unsupported (agy IS measured)",
386
+ !(NATIVE_PUSH_REJECT_REASONS as readonly string[]).includes("backend-liveness-unsupported"),
387
+ );
388
+ // resolveNativePushDispatch round-trip: observedLiveness === probed value, verdict === cell.
389
+ for (const intent of ENTWURF_INTENTS) {
390
+ for (const lv of NATIVE_PUSH_LIVENESSES as readonly NativePushLiveness[]) {
391
+ const cell = NATIVE_PUSH_DISPATCH_TABLE[intent][lv];
392
+ const receipt = resolveNativePushDispatch(intent, lv);
393
+ ok(
394
+ `native-push round-trip ${intent}/${lv}: observedLiveness === probed value (post-probe stamp)`,
395
+ receipt.observedLiveness === lv,
396
+ );
397
+ if (cell.action === "reject") {
398
+ ok(
399
+ `native-push round-trip ${intent}/${lv}: receipt reject matches cell`,
400
+ receipt.ok === false && receipt.reason === cell.reason,
401
+ );
402
+ } else {
403
+ ok(
404
+ `native-push round-trip ${intent}/${lv}: receipt allow matches cell (native-push send)`,
405
+ receipt.ok === true &&
406
+ receipt.action === "send" &&
407
+ receipt.transport === "native-push" &&
408
+ receipt.ownership === "ack-only",
409
+ );
410
+ }
411
+ }
412
+ }
413
+ // every native-push reject reason is post-probe (non-null) and in the master taxonomy.
414
+ for (const r of NATIVE_PUSH_REJECT_REASONS) {
415
+ ok(`native-push taxonomy: '${r}' is in the enum`, (ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(r));
416
+ ok(`native-push: '${r}' is post-probe (non-null observedLiveness required)`, !isPreProbeReject(r));
417
+ ok(
418
+ `native-push: '${r}' is NOT a pi/mailbox resolver reason (separate table)`,
419
+ !(RESOLVER_REJECT_REASONS as readonly string[]).includes(r),
420
+ );
421
+ }
422
+
294
423
  // ── R5: taxonomy coverage + pre-claims + pre-dispatch scope ────────────────
295
424
  for (const r of RESOLVER_REJECT_REASONS) {
296
425
  ok(`taxonomy: resolver reason '${r}' is in the enum`, (ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(r));
@@ -377,6 +506,7 @@ for (const r of PRE_PROBE_REJECT_REASONS) {
377
506
  }
378
507
  for (const r of [
379
508
  ...RESOLVER_REJECT_REASONS,
509
+ ...NATIVE_PUSH_REJECT_REASONS,
380
510
  "untrusted-fail-fast" as const,
381
511
  "socket-only-no-resume-authority" as const,
382
512
  ]) {
@@ -440,7 +570,12 @@ for (const r of PRE_PROBE_REJECT_REASONS) {
440
570
  passed++;
441
571
  }
442
572
  }
443
- for (const r of [...RESOLVER_REJECT_REASONS, "untrusted-fail-fast" as const]) {
573
+ for (const r of [
574
+ ...RESOLVER_REJECT_REASONS,
575
+ ...NATIVE_PUSH_REJECT_REASONS,
576
+ "untrusted-fail-fast" as const,
577
+ "socket-only-no-resume-authority" as const,
578
+ ]) {
444
579
  assert.throws(
445
580
  () => makeRejectReceipt(r, null),
446
581
  /ill-formed reject/,
@@ -48,6 +48,7 @@ import {
48
48
  } from "../pi-extensions/lib/entwurf-v2-decider.ts";
49
49
  import type { AcquireLockResult, LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
50
50
  import type { MetaBackendV2, MetaCapability, MetaIdentity } from "../pi-extensions/lib/meta-session.ts";
51
+ import type { NativePushProbeResult } from "../pi-extensions/lib/native-push/adapter.ts";
51
52
  import { controlSocketPath, type TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
52
53
  import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
53
54
 
@@ -131,6 +132,9 @@ interface ScenarioOpts {
131
132
  * unsupported path. Default false (fail-closed) — the decider trusts the seam, never
132
133
  * wake-mode alone, so this is how a test asserts both the deliverable and inactive cells. */
133
134
  mailboxDeliverable?: boolean;
135
+ /** 봉인 4: the native-push adapter probe result the injected nativePushProbe seam returns
136
+ * (only reached on a nativePushSupported backend, e.g. antigravity). */
137
+ nativePush?: NativePushProbeResult;
134
138
  }
135
139
 
136
140
  interface Tracked {
@@ -139,6 +143,7 @@ interface Tracked {
139
143
  releaseCalls: LockClaim[];
140
144
  inspectCalls: string[];
141
145
  mailboxCalls: MetaIdentity[];
146
+ nativePushCalls: MetaIdentity[];
142
147
  }
143
148
 
144
149
  // Build injected deps with call tracking. A lock "conflict" returns a failed
@@ -149,6 +154,7 @@ function mkDeps(opts: ScenarioOpts): Tracked {
149
154
  const releaseCalls: LockClaim[] = [];
150
155
  const inspectCalls: string[] = [];
151
156
  const mailboxCalls: MetaIdentity[] = [];
157
+ const nativePushCalls: MetaIdentity[] = [];
152
158
  const deps: DispatchDeciderDeps = {
153
159
  resolveTarget: () => opts.resolution,
154
160
  acquireLock: (gardenId: string): AcquireLockResult => {
@@ -179,10 +185,14 @@ function mkDeps(opts: ScenarioOpts): Tracked {
179
185
  mailboxCalls.push(identity);
180
186
  return { deliverable: opts.mailboxDeliverable ?? false, reason: "fake-deliverability" };
181
187
  },
188
+ nativePushProbe: (identity: MetaIdentity): NativePushProbeResult => {
189
+ nativePushCalls.push(identity);
190
+ return opts.nativePush ?? { status: "dead", reason: "fake: no native-push probe configured" };
191
+ },
182
192
  mailboxDir: "/fake/mailbox",
183
193
  sessionsDir: "/fake/sessions",
184
194
  };
185
- return { deps, acquireCalls, releaseCalls, inspectCalls, mailboxCalls };
195
+ return { deps, acquireCalls, releaseCalls, inspectCalls, mailboxCalls, nativePushCalls };
186
196
  }
187
197
 
188
198
  function isExecute(d: DispatchDecision): d is Extract<DispatchDecision, { kind: "execute" }> {
@@ -265,6 +275,9 @@ async function main(): Promise<void> {
265
275
  mailboxDeliverabilityFor: () => {
266
276
  throw new Error("target-locked(corrupt): in-domain pi never consults the mailbox seam");
267
277
  },
278
+ nativePushProbe: () => {
279
+ throw new Error("target-locked(corrupt): in-domain pi never consults the native-push seam");
280
+ },
268
281
  },
269
282
  );
270
283
  ok("target-locked(corrupt): reject", d.kind === "reject" && d.receipt.reason === "target-locked");
@@ -670,6 +683,9 @@ async function main(): Promise<void> {
670
683
  probeSocket: async () => "dead",
671
684
  preflightForCwd: () => APPROVE,
672
685
  mailboxDeliverabilityFor: () => ({ deliverable: false, reason: "in-domain pi: seam unused" }),
686
+ nativePushProbe: () => {
687
+ throw new Error("lock-leak: in-domain pi never consults the native-push seam");
688
+ },
673
689
  ...over,
674
690
  },
675
691
  );
@@ -724,6 +740,9 @@ async function main(): Promise<void> {
724
740
  probeSocket: async () => "alive",
725
741
  preflightForCwd: () => APPROVE,
726
742
  mailboxDeliverabilityFor: () => ({ deliverable: false, reason: "in-domain pi: seam unused" }),
743
+ nativePushProbe: () => {
744
+ throw new Error("reject-release-throw: in-domain pi never consults the native-push seam");
745
+ },
727
746
  },
728
747
  );
729
748
  } catch (e) {
@@ -763,6 +782,9 @@ async function main(): Promise<void> {
763
782
  mailboxDeliverabilityFor: () => {
764
783
  throw new Error("invalid-gid: mailboxDeliverabilityFor must not be reached");
765
784
  },
785
+ nativePushProbe: () => {
786
+ throw new Error("invalid-gid: nativePushProbe must not be reached");
787
+ },
766
788
  };
767
789
  try {
768
790
  await decideDispatch({ target: "../etc/passwd", intent: "owned-outcome", message: "x" }, deps);
@@ -772,6 +794,78 @@ async function main(): Promise<void> {
772
794
  ok("invalid-gid: throws", threw);
773
795
  ok("invalid-gid: throws BEFORE resolveTarget (no path/lookup built)", !resolveCalled);
774
796
  }
797
+
798
+ // ── native-push rail (봉인 4): antigravity is intercepted BEFORE the unsupported ──
799
+ // mailbox branch, routed by nativePushProbe + the NATIVE_PUSH table, LOCK-FREE.
800
+ const npResolution = { identity: identity("antigravity"), preProbeAddressConflict: false };
801
+ {
802
+ // ff × alive → execute native-push send; plan carries route/backend/nativeSessionId; lock null.
803
+ const t = mkDeps({
804
+ resolution: npResolution,
805
+ nativePush: { status: "alive", route: { lsAddress: "127.0.0.1:5599" } },
806
+ });
807
+ const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "yo" }, t.deps);
808
+ ok("native-push ff+alive: execute", isExecute(d));
809
+ ok("native-push ff+alive: transport native-push", isExecute(d) && d.plan.transport === "native-push");
810
+ ok("native-push ff+alive: LOCK-FREE (decision lock null)", isExecute(d) && d.lock === null);
811
+ ok("native-push ff+alive: acquireLock NOT called (lock-free rail)", t.acquireCalls.length === 0);
812
+ ok("native-push ff+alive: nativePushProbe consulted once", t.nativePushCalls.length === 1);
813
+ ok(
814
+ "native-push ff+alive: mailbox seam NOT consulted (intercepts before unsupported branch)",
815
+ t.mailboxCalls.length === 0,
816
+ );
817
+ if (isExecute(d) && d.plan.transport === "native-push") {
818
+ ok("native-push ff+alive: plan route = probed route", d.plan.route.lsAddress === "127.0.0.1:5599");
819
+ ok("native-push ff+alive: plan backend = antigravity", d.plan.backend === "antigravity");
820
+ ok("native-push ff+alive: plan nativeSessionId from identity", d.plan.nativeSessionId === `native-${GID}`);
821
+ ok(
822
+ "native-push ff+alive: plan keyset exact",
823
+ planKeys(d.plan).join(",") ===
824
+ "action,backend,message,nativeSessionId,route,targetGardenId,transport,wantsReply",
825
+ );
826
+ }
827
+ ok(
828
+ "native-push ff+alive: receipt success transport native-push, observedLiveness alive",
829
+ isExecute(d) && d.receipt.ok && d.receipt.transport === "native-push" && d.receipt.observedLiveness === "alive",
830
+ );
831
+ }
832
+ {
833
+ // ff × dead → reject native-push-target-dead (post-probe, dead), lock-free.
834
+ const t = mkDeps({ resolution: npResolution, nativePush: { status: "dead", reason: "no host" } });
835
+ const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "yo" }, t.deps);
836
+ ok(
837
+ "native-push ff+dead: reject native-push-target-dead",
838
+ d.kind === "reject" && d.receipt.reason === "native-push-target-dead",
839
+ );
840
+ ok(
841
+ "native-push ff+dead: observedLiveness dead (post-probe stamp)",
842
+ d.kind === "reject" && d.receipt.observedLiveness === "dead",
843
+ );
844
+ ok("native-push ff+dead: acquireLock NOT called", t.acquireCalls.length === 0);
845
+ }
846
+ {
847
+ // ff × indeterminate → reject native-push-probe-indeterminate.
848
+ const t = mkDeps({ resolution: npResolution, nativePush: { status: "indeterminate", reason: "no port served" } });
849
+ const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "yo" }, t.deps);
850
+ ok(
851
+ "native-push ff+indeterminate: reject native-push-probe-indeterminate",
852
+ d.kind === "reject" && d.receipt.reason === "native-push-probe-indeterminate",
853
+ );
854
+ }
855
+ {
856
+ // owned × alive → reject native-push-no-resume-authority (state-independent), lock-free.
857
+ const t = mkDeps({
858
+ resolution: npResolution,
859
+ nativePush: { status: "alive", route: { lsAddress: "127.0.0.1:5599" } },
860
+ });
861
+ const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "yo" }, t.deps);
862
+ ok(
863
+ "native-push owned+alive: reject native-push-no-resume-authority",
864
+ d.kind === "reject" && d.receipt.reason === "native-push-no-resume-authority",
865
+ );
866
+ ok("native-push owned+alive: acquireLock NOT called (lock-free)", t.acquireCalls.length === 0);
867
+ }
868
+
775
869
  // The wake-mode capability HELPER stays gate-pinned (renamed; the decider no longer
776
870
  // calls it directly — deliverability flows through the required seam). It answers the
777
871
  // capability HALF only; the active-receiver half lives in the production seam.
@@ -154,6 +154,13 @@ function mkDeps(opts: ScenarioOpts): Tracked {
154
154
  mailboxCalls.push(id);
155
155
  return { deliverable: opts.mailboxDeliverable ?? false, reason: "fake-deliverability" };
156
156
  },
157
+ // The matrix rows are pi / claude-code targets — the native-push branch (antigravity)
158
+ // is NOT exercised here (it is a SEPARATE table, covered by check-entwurf-v2-contract's
159
+ // NATIVE_PUSH round-trip + check-entwurf-v2-decider's branch scenarios). A tripwire so a
160
+ // future antigravity row that forgets to wire this fails loud instead of misrouting.
161
+ nativePushProbe: () => {
162
+ throw new Error("matrix: native-push branch must not be reached (no antigravity row wired)");
163
+ },
157
164
  mailboxDir: "/fake/mailbox",
158
165
  sessionsDir: "/fake/sessions",
159
166
  };
@@ -397,9 +404,13 @@ async function main(): Promise<void> {
397
404
  await runRow(row);
398
405
  }
399
406
 
400
- // ── COVERAGE: the table must span every transport + lock class + pre-probe ──
401
- // reject, or the "matrix is closed" claim is a lie. A dropped decider cell makes
402
- // one of these sets shrink → fail (not a silent green).
407
+ // ── COVERAGE: the table must span every matrix-owned transport + lock class + ──
408
+ // pre-probe reject, or the "matrix is closed" claim is a lie. A dropped decider cell
409
+ // makes one of these sets shrink → fail (not a silent green). NOTE: native-push is NOT
410
+ // a matrix-owned transport — it is a SEPARATE NATIVE_PUSH_DISPATCH_TABLE (check-entwurf-
411
+ // v2-contract round-trip + check-entwurf-v2-decider branch), so it is deliberately absent
412
+ // from this coverage set (adding an antigravity row would blur the matrix's lock-class /
413
+ // transport semantics — GPT R8).
403
414
  console.log("\n── coverage (table completeness)");
404
415
  const transports = new Set<string>();
405
416
  const lockClasses = new Set<LockClass>();
@@ -0,0 +1,193 @@
1
+ /**
2
+ * check-entwurf-v2-native-push — deterministic gate for the native-push SEND hand
3
+ * (봉인 3/4), the executor half of the native-push rail. This is where the 1-shot retry
4
+ * lives (moved out of the adapter leaf per the review): the decider planted a fresh route,
5
+ * the hand sends over it, and on failure re-probes ONCE and re-sends.
6
+ *
7
+ * Proves (deliverViaNativePush, fake adapter):
8
+ * - success first try → {retried:false}, ONE send, ZERO re-probe (the planted route is used).
9
+ * - fail → re-probe alive → re-send success → {retried:true}, TWO sends, ONE re-probe, and
10
+ * the second send uses the RE-DISCOVERED route (not the stale planted one).
11
+ * - fail → re-probe alive → re-send FAIL → THROWS (fail-loud, no third attempt).
12
+ * - fail → re-probe dead/indeterminate → THROWS (not retried), NO second send.
13
+ * Proves (makeNativePushSend):
14
+ * - resolves the adapter from plan.backend, delivers via deliverViaNativePush, and IGNORES
15
+ * the lock (native-push is lock-free, 봉인 4).
16
+ *
17
+ * Pure; no backend, no socket, no real process.
18
+ */
19
+
20
+ import assert from "node:assert/strict";
21
+ import type { LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
22
+ import type { NativePushPlan } from "../pi-extensions/lib/entwurf-v2-native-push.ts";
23
+ import { deliverViaNativePush, makeNativePushSend } from "../pi-extensions/lib/entwurf-v2-native-push.ts";
24
+ import type {
25
+ NativePushAdapter,
26
+ NativePushProbeResult,
27
+ NativePushRoute,
28
+ } from "../pi-extensions/lib/native-push/adapter.ts";
29
+
30
+ let passed = 0;
31
+ function ok(label: string, cond: boolean): void {
32
+ assert.ok(cond, label);
33
+ console.log(` ok ${label}`);
34
+ passed++;
35
+ }
36
+
37
+ const CONV = "conv-xyz";
38
+ const PLANTED: NativePushRoute = { lsAddress: "127.0.0.1:5600" };
39
+
40
+ interface SendCall {
41
+ route: NativePushRoute;
42
+ nativeSessionId: string;
43
+ content: string;
44
+ }
45
+
46
+ interface FakeAdapterConfig {
47
+ /** probe() results consumed in order (each re-probe pops the next). */
48
+ probes: NativePushProbeResult[];
49
+ /** 0-based indices of send() calls that THROW. */
50
+ sendFailAt?: number[];
51
+ }
52
+
53
+ function makeFakeAdapter(config: FakeAdapterConfig): {
54
+ adapter: NativePushAdapter;
55
+ sends: SendCall[];
56
+ probeCount: () => number;
57
+ } {
58
+ let probeIdx = 0;
59
+ let sendIdx = 0;
60
+ const sends: SendCall[] = [];
61
+ const adapter: NativePushAdapter = {
62
+ id: "antigravity",
63
+ async probe() {
64
+ const p = config.probes[probeIdx] ?? { status: "dead", reason: "fake: probes exhausted" };
65
+ probeIdx++;
66
+ return p;
67
+ },
68
+ async send(route, nativeSessionId, content) {
69
+ const i = sendIdx++;
70
+ sends.push({ route, nativeSessionId, content });
71
+ if (config.sendFailAt?.includes(i)) throw new Error(`fake send fail #${i}`);
72
+ },
73
+ };
74
+ return { adapter, sends, probeCount: () => probeIdx };
75
+ }
76
+
77
+ async function main(): Promise<void> {
78
+ // ── success first try: ONE send over the planted route, ZERO re-probe ────────
79
+ {
80
+ const { adapter, sends, probeCount } = makeFakeAdapter({ probes: [] });
81
+ const r = await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
82
+ ok("success: retried:false", r.success === true && r.retried === false);
83
+ 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");
85
+ ok("success: ZERO re-probe", probeCount() === 0);
86
+ }
87
+
88
+ // ── fail → re-probe alive → re-send success: retried, fresh route ────────────
89
+ {
90
+ const { adapter, sends, probeCount } = makeFakeAdapter({
91
+ probes: [{ status: "alive", route: { lsAddress: "127.0.0.1:5601" } }],
92
+ sendFailAt: [0],
93
+ });
94
+ const r = await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
95
+ ok("retry: retried:true", r.success === true && r.retried === true);
96
+ ok("retry: TWO sends (initial + one retry)", sends.length === 2);
97
+ 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");
99
+ ok(
100
+ "retry: 2nd send used the RE-DISCOVERED route (not the stale one)",
101
+ sends[1]?.route.lsAddress === "127.0.0.1:5601",
102
+ );
103
+ }
104
+
105
+ // ── fail → re-probe alive → re-send FAIL: throws (fail-loud, no 3rd attempt) ──
106
+ {
107
+ const { adapter, sends } = makeFakeAdapter({
108
+ probes: [{ status: "alive", route: { lsAddress: "127.0.0.1:5601" } }],
109
+ sendFailAt: [0, 1],
110
+ });
111
+ let threw = false;
112
+ try {
113
+ await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
114
+ } catch {
115
+ threw = true;
116
+ }
117
+ ok("double-fail: THROWS (fail-loud)", threw);
118
+ ok("double-fail: exactly TWO sends, no third attempt", sends.length === 2);
119
+ }
120
+
121
+ // ── fail → re-probe dead: throws (not retried), NO second send ───────────────
122
+ {
123
+ const { adapter, sends, probeCount } = makeFakeAdapter({
124
+ probes: [{ status: "dead", reason: "host gone" }],
125
+ sendFailAt: [0],
126
+ });
127
+ let msg = "";
128
+ try {
129
+ await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
130
+ } catch (err) {
131
+ msg = (err as Error).message;
132
+ }
133
+ ok("reprobe-dead: THROWS", msg.length > 0);
134
+ ok("reprobe-dead: error names the dead re-probe + 'not retried'", /dead/.test(msg) && /not retried/.test(msg));
135
+ ok("reprobe-dead: exactly ONE send (no re-send into a dead conversation)", sends.length === 1);
136
+ ok("reprobe-dead: re-probe was attempted once", probeCount() === 1);
137
+ }
138
+
139
+ // ── fail → re-probe indeterminate: throws (not retried) ──────────────────────
140
+ {
141
+ const { adapter, sends } = makeFakeAdapter({
142
+ probes: [{ status: "indeterminate", reason: "no port" }],
143
+ sendFailAt: [0],
144
+ });
145
+ let threw = false;
146
+ try {
147
+ await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
148
+ } catch {
149
+ threw = true;
150
+ }
151
+ ok("reprobe-indeterminate: THROWS (never coerced to a retry)", threw);
152
+ ok("reprobe-indeterminate: exactly ONE send", sends.length === 1);
153
+ }
154
+
155
+ // ── makeNativePushSend: resolves adapter from plan.backend, ignores lock ─────
156
+ {
157
+ const plan: NativePushPlan = {
158
+ transport: "native-push",
159
+ action: "send",
160
+ targetGardenId: "20260704T000000-abcdef",
161
+ backend: "antigravity",
162
+ nativeSessionId: CONV,
163
+ route: PLANTED,
164
+ wantsReply: false,
165
+ message: "payload",
166
+ };
167
+ const { adapter, sends } = makeFakeAdapter({ probes: [] });
168
+ const resolvedBackends: string[] = [];
169
+ const send = makeNativePushSend({
170
+ resolveAdapter: (backend) => {
171
+ resolvedBackends.push(backend);
172
+ return adapter;
173
+ },
174
+ });
175
+ // pass a NON-null lock to prove it is ignored (lock-free rail).
176
+ const bogusLock = { gardenId: "x" } as unknown as LockClaim;
177
+ const r = await send(plan, bogusLock);
178
+ ok("makeNativePushSend: delivered success", r.success === true && r.retried === false);
179
+ ok(
180
+ "makeNativePushSend: resolved the adapter from plan.backend",
181
+ resolvedBackends.length === 1 && resolvedBackends[0] === "antigravity",
182
+ );
183
+ ok(
184
+ "makeNativePushSend: sent the plan message over the plan route",
185
+ sends[0]?.content === "payload" && sends[0]?.route.lsAddress === "127.0.0.1:5600",
186
+ );
187
+ ok("makeNativePushSend: lock IGNORED (lock-free — a bogus lock did not break delivery)", sends.length === 1);
188
+ }
189
+
190
+ console.log(`\ncheck-entwurf-v2-native-push: ${passed} assertions passed`);
191
+ }
192
+
193
+ void main();
@@ -27,9 +27,11 @@ 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
31
  import type { ControlSocketPlan, MetaMailboxPlan } from "../pi-extensions/lib/entwurf-v2-send.ts";
31
32
  import type { SpawnBgPlan } from "../pi-extensions/lib/entwurf-v2-spawn.ts";
32
33
  import type { MetaIdentity, MetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
34
+ import type { NativePushAdapter, NativePushProbeResult } from "../pi-extensions/lib/native-push/adapter.ts";
33
35
  import type { TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
34
36
 
35
37
  let passed = 0;
@@ -125,6 +127,10 @@ interface Spies {
125
127
  enqueue: { gardenId: string; sessionsDir?: string; mailboxDir?: string }[];
126
128
  rpc: { socketPath: string; command: Record<string, unknown> }[];
127
129
  inspectPath: { socketPath: string }[];
130
+ /** which backend the native-push adapter resolver was asked for (decide + execute). */
131
+ nativePushResolve: string[];
132
+ nativePushProbe: { conv: string }[];
133
+ nativePushSend: { lsAddress: string; conv: string; content: string }[];
128
134
  }
129
135
 
130
136
  /** Build a factory whose every leaf IO is a spy. `over` lets a case shape the decision
@@ -137,11 +143,22 @@ function makeSpiedFactory(over: {
137
143
  rpc?: "success" | "dead-throw";
138
144
  classifyDead?: boolean;
139
145
  spawnChildThrows?: boolean;
146
+ /** the native-push adapter probe result (only reached on a native-push backend). */
147
+ nativePushProbe?: NativePushProbeResult;
140
148
  /** SE-2 2d-3 — the target's receiver presence marker: "active" (matches identity,
141
149
  * default), "absent" (terminated/never-armed), or "mismatch" (drifted native id). */
142
150
  receiverMarker?: "active" | "absent" | "mismatch";
143
151
  }) {
144
- const spies: Spies = { acquire: [], release: [], enqueue: [], rpc: [], inspectPath: [] };
152
+ const spies: Spies = {
153
+ acquire: [],
154
+ release: [],
155
+ enqueue: [],
156
+ rpc: [],
157
+ inspectPath: [],
158
+ nativePushResolve: [],
159
+ nativePushProbe: [],
160
+ nativePushSend: [],
161
+ };
145
162
  const opts: ProductionEntwurfV2Opts = {
146
163
  senderProvider: () => ({ sessionId: "self", agentId: "pi/x", cwd: "/cwd", timestamp: "2026-06-13T00:00:00.000Z" }),
147
164
  lockDir: LOCK_DIR,
@@ -194,6 +211,23 @@ function makeSpiedFactory(over: {
194
211
  spies.enqueue.push({ gardenId: o.gardenId, sessionsDir: o.sessionsDir, mailboxDir: o.mailboxDir });
195
212
  return { gardenId: o.gardenId, recordPath: "r", messagePath: "m", signalPath: "s" };
196
213
  },
214
+ // ONE native-push adapter resolver (봉인 4): the SAME injected fake feeds the
215
+ // decider's nativePushProbe AND the executor's sendNativePush — so a single dispatch
216
+ // records both a probe (decide) and a send (execute) on this one adapter, proving the
217
+ // two hands never resolve different adapters.
218
+ resolveNativePushAdapter: (backend: string): NativePushAdapter => {
219
+ spies.nativePushResolve.push(backend);
220
+ return {
221
+ id: "antigravity",
222
+ async probe(conv) {
223
+ spies.nativePushProbe.push({ conv });
224
+ return over.nativePushProbe ?? { status: "dead", reason: "fake: no native-push probe configured" };
225
+ },
226
+ async send(route, conv, content) {
227
+ spies.nativePushSend.push({ lsAddress: route.lsAddress, conv, content });
228
+ },
229
+ };
230
+ },
197
231
  spawnOverrides: {
198
232
  spawnChild: over.spawnChildThrows
199
233
  ? () => {
@@ -232,6 +266,39 @@ async function main(): Promise<void> {
232
266
  ok("A2: a quarantined target is never lock-acquired", spies.acquire.length === 0);
233
267
  }
234
268
 
269
+ // ── A3: native-push wiring — ONE injected adapter feeds BOTH decider + executor ─
270
+ {
271
+ const { deps, spies } = makeSpiedFactory({
272
+ backend: "antigravity",
273
+ nativePushProbe: { status: "alive", route: { lsAddress: "127.0.0.1:5599" } },
274
+ });
275
+ const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi agy" }, deps);
276
+ ok("A3: antigravity ff → native-push delivered", result.kind === "executed" && result.transport === "native-push");
277
+ ok("A3: decider probed the native-push adapter once (decide side)", spies.nativePushProbe.length === 1);
278
+ ok("A3: executor sent via the native-push adapter once (execute side)", spies.nativePushSend.length === 1);
279
+ ok("A3: send used the DECIDER-probed route", spies.nativePushSend[0]?.lsAddress === "127.0.0.1:5599");
280
+ ok("A3: send carried the dispatch message", spies.nativePushSend[0]?.content === "hi agy");
281
+ ok(
282
+ "A3: BOTH hands resolved the SAME adapter (for 'antigravity')",
283
+ spies.nativePushResolve.length === 2 && spies.nativePushResolve.every((b) => b === "antigravity"),
284
+ );
285
+ ok("A3: native-push is LOCK-FREE (no acquire)", spies.acquire.length === 0);
286
+ ok("A3: native-push did NOT enqueue a mailbox (not the unsupported path)", spies.enqueue.length === 0);
287
+ }
288
+ {
289
+ // antigravity + dead probe → reject native-push-target-dead, NO send.
290
+ const { deps, spies } = makeSpiedFactory({
291
+ backend: "antigravity",
292
+ nativePushProbe: { status: "dead", reason: "no host" },
293
+ });
294
+ const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "x" }, deps);
295
+ ok(
296
+ "A3b: antigravity ff + dead → rejected native-push-target-dead",
297
+ result.kind === "rejected" && result.receipt.reason === "native-push-target-dead",
298
+ );
299
+ ok("A3b: no send attempted on a dead target", spies.nativePushSend.length === 0);
300
+ }
301
+
235
302
  // ── A3: QB2 — a non-pi indeterminate lstat FAILS LOUD (never "no conflict") ─
236
303
  {
237
304
  const { deps } = makeSpiedFactory({ backend: "claude-code", inspectKind: "indeterminate" });