@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.
- package/AGENTS.md +31 -17
- package/BASELINE.md +42 -8
- package/CHANGELOG.md +22 -0
- package/DELIVERY.md +71 -14
- package/README.md +77 -45
- package/VERIFY.md +18 -13
- package/docs/setup-clean-host.md +80 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
- package/mcp/entwurf-bridge/src/index.ts +101 -67
- package/mcp/entwurf-bridge/test.sh +1 -1
- package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
- package/package.json +10 -5
- package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
- package/pi-extensions/lib/entwurf-self-address.ts +58 -15
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
- package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
- package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
- package/pi-extensions/lib/meta-sender-identity.ts +154 -0
- package/pi-extensions/lib/native-push/adapter.ts +255 -0
- package/pi-extensions/lib/native-push/register.ts +99 -0
- package/run.sh +756 -214
- package/scripts/agy-bridge-config.py +446 -0
- package/scripts/agy-bridge.sh +359 -0
- package/scripts/agy-hooks-bridge.sh +193 -0
- package/scripts/agy-hooks-config.py +257 -0
- package/scripts/agy-imprint.sh +28 -0
- package/scripts/agy-imprint.ts +193 -0
- package/scripts/agy-statusline-bridge.sh +176 -0
- package/scripts/agy-statusline-config.py +213 -0
- package/scripts/agy-statusline.sh +256 -0
- package/scripts/build-bridge.sh +20 -0
- package/scripts/check-agy-sender-identity.ts +364 -0
- package/scripts/check-entwurf-bridge-boot.ts +8 -2
- package/scripts/check-entwurf-deliverability.ts +34 -0
- package/scripts/check-entwurf-self-address.ts +78 -11
- package/scripts/check-entwurf-v2-contract.ts +136 -1
- package/scripts/check-entwurf-v2-decider.ts +95 -1
- package/scripts/check-entwurf-v2-matrix.ts +14 -3
- package/scripts/check-entwurf-v2-native-push.ts +193 -0
- package/scripts/check-entwurf-v2-production.ts +68 -1
- package/scripts/check-entwurf-v2-runner.ts +58 -0
- package/scripts/check-entwurf-v2-surface.ts +35 -0
- package/scripts/check-install-surface.ts +357 -0
- package/scripts/check-native-push-adapter.ts +319 -0
- package/scripts/check-native-push-register.ts +130 -0
- package/scripts/dev-bin.sh +195 -0
- package/scripts/doctor-pi-provider.ts +140 -0
- package/scripts/meta-bridge-doctor.sh +36 -2
- package/scripts/register-pi-package.py +37 -3
- package/scripts/register-pi-provider.py +287 -0
- package/scripts/smoke-agy-hooks-state.sh +172 -0
- package/scripts/smoke-agy-install-state.sh +660 -0
- package/scripts/smoke-agy-native-push-live.ts +243 -0
- package/scripts/smoke-agy-statusline-state.sh +300 -0
- package/scripts/smoke-meta-async-drift.sh +9 -2
- package/scripts/smoke-meta-install-state.sh +20 -0
- package/scripts/smoke-pi-provider-state.sh +182 -0
- package/scripts/smoke-user-scope-citizen.sh +62 -0
- package/scripts/with-dist-lock.sh +81 -0
- package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
- package/scripts/__pycache__/meta-bridge-state.cpython-313.pyc +0 -0
- 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 [
|
|
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 +
|
|
401
|
-
// reject, or the "matrix is closed" claim is a lie. A dropped decider cell
|
|
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 = {
|
|
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" });
|