@junghanacs/entwurf 0.12.9 → 0.12.10

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 (69) hide show
  1. package/AGENTS.md +134 -248
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +19 -0
  4. package/DELIVERY.md +4 -3
  5. package/README.md +26 -13
  6. package/VERIFY.md +3 -2
  7. package/demo/demo.sh +7 -1
  8. package/docs/setup-clean-host.md +10 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +159 -66
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +5 -3
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +15 -17
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +9 -11
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +3 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +17 -20
  23. package/mcp/entwurf-bridge/src/index.ts +190 -82
  24. package/package.json +5 -3
  25. package/pi-extensions/entwurf-control.ts +69 -27
  26. package/pi-extensions/lib/acp/overlay.ts +5 -3
  27. package/pi-extensions/lib/entwurf-core.ts +26 -9
  28. package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
  29. package/pi-extensions/lib/entwurf-fact-provider.ts +15 -17
  30. package/pi-extensions/lib/entwurf-facts.ts +13 -13
  31. package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
  32. package/pi-extensions/lib/entwurf-self-address.ts +8 -9
  33. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
  34. package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
  35. package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
  36. package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
  37. package/pi-extensions/lib/entwurf-v2-production.ts +14 -11
  38. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +3 -1
  39. package/pi-extensions/lib/meta-session.ts +17 -20
  40. package/run.sh +48 -41
  41. package/scripts/agy-bridge-config.py +249 -49
  42. package/scripts/agy-bridge.sh +59 -14
  43. package/scripts/check-acp-carrier-augment.ts +34 -2
  44. package/scripts/check-acp-sdk-surface.ts +22 -11
  45. package/scripts/check-agy-permission-matrix.py +655 -0
  46. package/scripts/check-entwurf-deliverability.ts +9 -6
  47. package/scripts/check-entwurf-fact-provider.ts +7 -7
  48. package/scripts/check-entwurf-facts.ts +12 -12
  49. package/scripts/check-entwurf-resume-args.ts +22 -60
  50. package/scripts/check-entwurf-self-address.ts +187 -4
  51. package/scripts/check-entwurf-v2-contract.ts +2 -2
  52. package/scripts/check-entwurf-v2-production.ts +5 -5
  53. package/scripts/check-entwurf-v2-spawn-production.ts +6 -2
  54. package/scripts/check-entwurf-v2-surface.ts +327 -14
  55. package/scripts/check-gate-qualification.ts +785 -0
  56. package/scripts/check-meta-identity-consumers.ts +1 -1
  57. package/scripts/check-shell-quote.ts +2 -1
  58. package/scripts/lib/mutation-qualify.ts +794 -0
  59. package/scripts/mutants/acp-augment.json +30 -0
  60. package/scripts/mutants/agy-permission.json +144 -0
  61. package/scripts/mutants/meta-identity.json +17 -0
  62. package/scripts/mutants/self-address.json +59 -0
  63. package/scripts/mutants/v2-surface.json +87 -0
  64. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  65. package/scripts/smoke-acp-v2-send-live.ts +33 -18
  66. package/scripts/smoke-agy-install-state.sh +205 -20
  67. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
  68. package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
  69. package/scripts/check-entwurf-mailbox-guard.ts +0 -266
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * check-entwurf-deliverability — deterministic gate for the conversational-mailbox
3
- * deliverability predicate (SE-1/SE-2 slice 2c). This is the predicate the enqueue
4
- * sites (v1 fallback, MCP v1, pi-native v1, v2 decider/send-fallback) must consult in
5
- * slice 2d before writing a .msg, so a reply never rots in a dead session's mailbox
6
- * (SE-2) and is never enqueued for a backend that has no mailbox drain (SE-1, pi).
3
+ * deliverability predicate (SE-1/SE-2 slice 2c). This is the predicate an enqueue site
4
+ * must consult in slice 2d before writing a .msg, so a reply never rots in a dead
5
+ * session's mailbox (SE-2) and is never enqueued for a backend that has no mailbox drain
6
+ * (SE-1, pi). The SHIPPED sites are the v2 decider and its send-fallback re-resolve; the
7
+ * v1 fallback / MCP v1 / pi-native v1 sites were removed in the 0.12 cutover.
7
8
  *
8
9
  * Proves:
9
10
  * - computeMetaReceiverActive: the shared atom — active ⟺ recordBacked ∧ ownerAlive ∧
@@ -124,8 +125,10 @@ ok(
124
125
 
125
126
  // ── receiverMarkerMatchesIdentity: the marker ↔ identity SSOT (SE-2 2d-3) ───
126
127
  // A present marker only raises a receiver to active when it agrees with the record on
127
- // garden/backend/native id. The v1 mailbox guard AND the v2 production deliverability seam
128
- // both route through this one helper, so presence-only false-positives are closed in both.
128
+ // garden/backend/native id. The v2 production deliverability seam and the MCP bridge's
129
+ // entwurf_self both route through this one helper, so presence-only false-positives are
130
+ // closed on every shipped path — and there is no second implementation to drift from since
131
+ // entwurf-mailbox-guard.ts (production importers: 0) was deleted 2026-07-27.
129
132
  const ID = { gardenId: "20260612T100000-aaaaaa", backend: "claude-code", nativeSessionId: "n-1" };
130
133
  ok(
131
134
  "marker matches identity on all three axes → true",
@@ -8,7 +8,7 @@
8
8
  * into ONE `record-less-socket` diagnostic (#50 C4: a diagnostic subject,
9
9
  * never a listing section) whose message names the cause + fix (fresh-cut),
10
10
  * - corrupt record → meta-record-read-error diagnostic, listing NOT blinded,
11
- * - gardenId↔socket collision (non-pi citizen + same-gid socket) → BOTH sides
11
+ * - gardenId↔socket collision (out-of-socket-domain citizen + same-gid socket) → BOTH sides
12
12
  * quarantined (gid in neither peers nor socketOnly) + one
13
13
  * garden-id-socket-conflict diagnostic, and listEntwurfFacts does NOT throw
14
14
  * (expected external-state corruption → diagnostics, not a crash),
@@ -214,16 +214,16 @@ async function main(): Promise<void> {
214
214
  }
215
215
  }
216
216
 
217
- // ── non-pi citizen + SYMLINKED socket: the fact-provider:125 gap (closed) ───
218
- // A non-pi record sharing its gid with a *symlinked* `*.sock`. The symlink is
217
+ // ── out-of-socket-domain citizen + symlinked socket (closed gap) ──────────
218
+ // Such a record shares its gid with a *symlinked* `*.sock`. The symlink is
219
219
  // never probed, so its gid is absent from socketGids — the old socketGids-only
220
- // check missed this and the non-pi citizen survived as a clean PeerFact while
220
+ // check missed this and the out-of-domain citizen survived as a clean PeerFact while
221
221
  // the legacy send path still followed the symlink to a forged receiver. The
222
- // shared isNonPiGardenIdSocketConflict predicate unions socketGids with the
222
+ // shared isOutOfSocketDomainGardenIdConflict predicate unions socketGids with the
223
223
  // symlinked gids, so the citizen is now quarantined (gid in NEITHER peers nor
224
224
  // socketOnly) and the conflict diagnostic is raised — alongside the existing
225
225
  // socket-symlink-rejected one (both are honest: symlink-rejected = "not probed",
226
- // conflict = "non-pi address split").
226
+ // conflict = "out-of-socket-domain address split").
227
227
  {
228
228
  const r = await listEntwurfFacts(
229
229
  deps(
@@ -233,7 +233,7 @@ async function main(): Promise<void> {
233
233
  ),
234
234
  );
235
235
  ok(
236
- "symlink-collision: non-pi citizen quarantined (NOT in peers)",
236
+ "symlink-collision: out-of-socket-domain citizen quarantined (NOT in peers)",
237
237
  !r.facts.peers.some((p) => p.gardenId === GID_CONFLICT),
238
238
  );
239
239
  ok(
@@ -23,7 +23,7 @@
23
23
  import assert from "node:assert/strict";
24
24
  import {
25
25
  type FactList,
26
- isNonPiGardenIdSocketConflict,
26
+ isOutOfSocketDomainGardenIdConflict,
27
27
  type PeerFact,
28
28
  type RecordLessSocketFact,
29
29
  resolveFactList,
@@ -247,7 +247,7 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
247
247
  } catch {
248
248
  threw = true;
249
249
  }
250
- ok("dedup/authority: non-pi citizen + control socket at same gid → fail-loud", threw);
250
+ ok("dedup/authority: out-of-socket-domain citizen + same-gid socket → fail-loud", threw);
251
251
  }
252
252
 
253
253
  // ── dedup (동결3): pi citizen + its socket → PeerFact only, never both ────────
@@ -333,11 +333,11 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
333
333
  );
334
334
  }
335
335
 
336
- // ── isNonPiGardenIdSocketConflict: the SHARED record-side conflict predicate ──
336
+ // ── isOutOfSocketDomainGardenIdConflict: shared record-side predicate ──────
337
337
  // (fact-provider listing + v2 decider dispatch consume this same fn — 4c 재유도
338
338
  // 금지 동형). The union over socketGids ∪ symlinkedGardenIds is the fact-provider:125
339
339
  // gap closure: a symlinked socket is never probed, so its gid is absent from
340
- // socketGids; the predicate must STILL flag a non-pi citizen owning it.
340
+ // socketGids; the predicate must STILL flag an out-of-domain citizen owning it.
341
341
  {
342
342
  const G = "20260611T444444-dddddd";
343
343
  const realSockets = new Set([G]);
@@ -345,21 +345,21 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
345
345
  const empty = new Set<string>();
346
346
  for (const piBackend of IN_DOMAIN) {
347
347
  ok(
348
- `conflict-predicate: in-domain (${piBackend}) is NEVER a non-pi conflict (even with a colliding socket)`,
349
- !isNonPiGardenIdSocketConflict(piBackend, G, realSockets, symlinked),
348
+ `conflict-predicate: socket-domain (${piBackend}) is not a record-side conflict`,
349
+ !isOutOfSocketDomainGardenIdConflict(piBackend, G, realSockets, symlinked),
350
350
  );
351
351
  }
352
352
  ok(
353
- "conflict-predicate: non-pi + real socket → conflict",
354
- isNonPiGardenIdSocketConflict("claude-code", G, realSockets, empty),
353
+ "conflict-predicate: out-of-socket-domain + real socket → conflict",
354
+ isOutOfSocketDomainGardenIdConflict("claude-code", G, realSockets, empty),
355
355
  );
356
356
  ok(
357
- "conflict-predicate: non-pi + SYMLINKED socket only → conflict (the :125 gap)",
358
- isNonPiGardenIdSocketConflict("claude-code", G, empty, symlinked),
357
+ "conflict-predicate: out-of-socket-domain + symlinked socket → conflict",
358
+ isOutOfSocketDomainGardenIdConflict("claude-code", G, empty, symlinked),
359
359
  );
360
360
  ok(
361
- "conflict-predicate: non-pi + no socket of either kind → no conflict",
362
- !isNonPiGardenIdSocketConflict("claude-code", G, empty, empty),
361
+ "conflict-predicate: out-of-socket-domain + no socket → no conflict",
362
+ !isOutOfSocketDomainGardenIdConflict("claude-code", G, empty, empty),
363
363
  );
364
364
  }
365
365
 
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * check-entwurf-resume-args — deterministic gate for the 5c-3b resume-argv SSOT
3
- * (`buildResumePiArgs`). It pins the load-bearing A1 difference between the legacy one-shot
4
- * worker and the v2 spawn-bg RESIDENT citizen so the two launch shapes can never drift:
3
+ * (`buildResumePiArgs`). It pins the RESIDENT citizen launch shape so it cannot drift.
4
+ * This gate once carried a second `legacy` one-shot variant as a contrast; that launcher
5
+ * and its enum member were removed 2026-07-27, and the half of A1 that still ships is
6
+ * asserted directly instead — a resumed citizen stands its control socket up and never
7
+ * emits the one-shot `--no-extensions`:
5
8
  *
6
- * 1. legacy carries `--no-extensions` and NO `--entwurf-control`.
7
9
  * 2. v2-control carries `--entwurf-control` and NO `--no-extensions`.
8
- * 3. BOTH keep the headless prefix `--mode json -p` and run the prompt as the final
9
- * positional (the prompt-as-turn authority is unchanged in v2 — `-p` is NOT dropped).
10
- * 4. `explicitExtensionArgs` is preserved verbatim, exactly once, in BOTH variants
10
+ * 3. The headless prefix `--mode json -p` stays and the prompt is the final positional
11
+ * (the prompt-as-turn authority is unchanged in v2 — `-p` is NOT dropped).
12
+ * 4. `explicitExtensionArgs` is preserved verbatim, exactly once
11
13
  * (load-bearing for a recorded `provider=entwurf` resume; #29 footgun).
12
- * 5. v2-control includes `plan.launchArgs` (`--approve` / empty) as flags BEFORE the
13
- * prompt; legacy ignores launchArgs entirely.
14
- * 6. provider/model identity is laid out identically in both; a null/undefined provider
15
- * emits NO `--provider` flag; `--model <m>` and `<prompt>` are the last three tokens.
16
- * 7. no cross-contamination: the legacy-only and v2-only flags never leak into the other.
14
+ * 5. `plan.launchArgs` (`--approve` / empty) ride as flags BEFORE the prompt.
15
+ * 6. a null/undefined provider emits NO `--provider` flag; `--model <m>` and `<prompt>`
16
+ * are the last three tokens.
17
+ * 7. the resident posture is exact: `--entwurf-control` present, `--no-extensions` never.
17
18
  *
18
19
  * Pure string assembly — no IO, no spawn.
19
20
  */
@@ -45,34 +46,6 @@ function valueAfter(args: readonly string[], flag: string): string | undefined {
45
46
  }
46
47
 
47
48
  function main(): void {
48
- // ── legacy variant ──────────────────────────────────────────────────────────────
49
- {
50
- const args = buildResumePiArgs({
51
- variant: "legacy",
52
- sessionFile: SESSION_FILE,
53
- explicitExtensionArgs: EXT,
54
- provider: "entwurf",
55
- model: "claude-opus-5",
56
- prompt: "continue the task",
57
- });
58
- ok("1 legacy has --no-extensions", args.includes("--no-extensions"));
59
- ok("1 legacy has NO --entwurf-control", !args.includes("--entwurf-control"));
60
- ok("3 legacy headless prefix --mode json -p", args[0] === "--mode" && args[1] === "json" && args[2] === "-p");
61
- ok("3 legacy prompt is the final positional", args[args.length - 1] === "continue the task");
62
- ok("4 legacy keeps ext args exactly once", args.filter((a) => a === "-e").length === 1);
63
- ok("6 legacy provider laid out", valueAfter(args, "--provider") === "entwurf");
64
- ok("6 legacy model laid out", valueAfter(args, "--model") === "claude-opus-5");
65
- ok("6 legacy resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
66
- ok("6 legacy carries NO --session-id (the id is pi's own now)", !args.includes("--session-id"));
67
- // model + prompt are the last three tokens: --model <m> <prompt>
68
- ok(
69
- "6 legacy --model <m> <prompt> tail",
70
- args[args.length - 3] === "--model" &&
71
- args[args.length - 2] === "claude-opus-5" &&
72
- args[args.length - 1] === "continue the task",
73
- );
74
- }
75
-
76
49
  // ── v2-control variant ──────────────────────────────────────────────────────────
77
50
  {
78
51
  const args = buildResumePiArgs({
@@ -107,20 +80,6 @@ function main(): void {
107
80
  );
108
81
  }
109
82
 
110
- // ── 5. legacy IGNORES launchArgs (no --approve leaks in) ──────────────────────────
111
- {
112
- const args = buildResumePiArgs({
113
- variant: "legacy",
114
- sessionFile: SESSION_FILE,
115
- explicitExtensionArgs: [],
116
- provider: null,
117
- model: "m",
118
- prompt: "p",
119
- launchArgs: ["--approve"], // present in input but legacy must ignore it
120
- });
121
- ok("5 legacy ignores launchArgs (no --approve)", !args.includes("--approve"));
122
- }
123
-
124
83
  // ── 6. null/undefined provider emits NO --provider flag ───────────────────────────
125
84
  for (const provider of [null, undefined] as const) {
126
85
  const args = buildResumePiArgs({
@@ -136,20 +95,23 @@ function main(): void {
136
95
  ok(`6 provider=${provider}: --model <m> <prompt> still tail`, args.slice(-3).join(" ") === "--model m p");
137
96
  }
138
97
 
139
- // ── 7. no cross-contamination across variants over the same identity ──────────────
98
+ // ── 7. the resident posture is exact: the one-shot flag never reappears ───────────
99
+ // The `legacy` variant was removed 2026-07-27, so the old cross-contamination pair is
100
+ // gone. What must NOT rot is the A1 half that still ships: a resumed citizen stands its
101
+ // control socket up, which means `--no-extensions` (the one-shot flag that let `pi -p`
102
+ // exit) can never be emitted here again.
140
103
  {
141
- const base = {
104
+ const v2 = buildResumePiArgs({
142
105
  sessionFile: SESSION_FILE,
143
106
  explicitExtensionArgs: EXT,
144
107
  provider: "entwurf",
145
108
  model: "m",
146
109
  prompt: "p",
147
110
  launchArgs: ["--approve"],
148
- } as const;
149
- const legacy = buildResumePiArgs({ ...base, variant: "legacy" });
150
- const v2 = buildResumePiArgs({ ...base, variant: "v2-control" });
151
- ok("7 legacy-only flag absent from v2", !v2.includes("--no-extensions"));
152
- ok("7 v2-only flag absent from legacy", !legacy.includes("--entwurf-control") && !legacy.includes("--approve"));
111
+ variant: "v2-control",
112
+ });
113
+ ok("7 resident posture never emits the one-shot --no-extensions", !v2.includes("--no-extensions"));
114
+ ok("7 resident posture stands the control socket up", v2.includes("--entwurf-control"));
153
115
  }
154
116
 
155
117
  console.log(`\ncheck-entwurf-resume-args: ${passed} checks passed`);
@@ -7,8 +7,11 @@
7
7
  *
8
8
  * Proves:
9
9
  * - PURE truth table (computeSelfAddressability, facts injected): pi replyable ⟺
10
- * socketAlive; meta replyable recordBacked ownerAlive watchArmed; external
11
- * never replyable. socketState alive/expected/none is its own assertable field.
10
+ * socketAlive; external never replyable; and meta replyability splits by RAIL
11
+ * self-fetch recordBacked ownerAlive watchArmed, native-push ⟺ recordBacked ∧
12
+ * probeAlive (a separate axis: an agy citizen never arms a mailbox watch, so the
13
+ * self-fetch atom would make it un-replyable forever), and an unsupplied rail is
14
+ * fail-closed. socketState alive/expected/none is its own assertable field.
12
15
  * - The two REGRESSION-PROOF rows the lock requires (record-present, not all-absent):
13
16
  * (b) meta record present + owner-dead (start-key mismatch) → false
14
17
  * (c) meta record present + watch-unarmed → false
@@ -147,7 +150,7 @@ function row(facts: SelfAddressabilityFacts): { replyable: boolean; socketState:
147
150
 
148
151
  // A native-push citizen must NOT be able to buy replyability with mailbox facts.
149
152
  ok(
150
- "meta/native-push + owner-alive + watch-armed but probe-dead → NOT replyable (mailbox facts cannot rescue it)",
153
+ "meta/native-push + owner-alive + watch-armed but probe-dead → NOT replyable (mailbox facts cannot rescue it) [QK:SELFADDR-RAIL-FACT-LEAK]",
151
154
  row(push({ recordBacked: true, ownerAlive: true, watchArmed: true, probeAlive: false })).replyable === false,
152
155
  );
153
156
  }
@@ -223,7 +226,10 @@ const piBody = functionBody("buildStrictPiSenderEnvelope");
223
226
  ok("buildStrictPiSenderEnvelope calls computeSelfAddressability", /computeSelfAddressability\s*\(/.test(piBody));
224
227
  // Scoped to THIS function body (not a broad grep): the pi-session envelope must NOT
225
228
  // hardcode `replyable: true`; it must derive from the predicate result.
226
- ok("buildStrictPiSenderEnvelope no longer hardcodes `replyable: true`", !/replyable:\s*true/.test(piBody));
229
+ ok(
230
+ "buildStrictPiSenderEnvelope no longer hardcodes `replyable: true` [QK:SELFADDR-NO-HARDCODED-REPLYABLE]",
231
+ !/replyable:\s*true/.test(piBody),
232
+ );
227
233
  // existsSync alone is too loose — pin that it probes the CANONICAL socket path,
228
234
  // not some other file, so the honesty signal cannot drift to a path that does not
229
235
  // represent this session's socket.
@@ -246,6 +252,183 @@ ok(
246
252
  /existsSync\s*\(/.test(selfRegion),
247
253
  );
248
254
 
255
+ // ── SLICE INTEGRITY (before any claim is read off the slice) ─────────────────
256
+ // A source-slice assertion is only as good as its boundaries. `toolRegion` counts
257
+ // parens, so an unbalanced paren inside a string literal would silently widen the
258
+ // slice to a LATER tool and let a neighbour's text satisfy an assertion about this
259
+ // one. Pin both ends explicitly instead of trusting the counter: the slice must
260
+ // contain THIS tool's own error label and must NOT reach the next `server.tool(`.
261
+ ok("entwurf_self slice contains its own handler tail (start boundary real)", /entwurf_self error:/.test(selfRegion));
262
+ ok(
263
+ "entwurf_self slice stops before the next tool (end boundary real, no silent widen)",
264
+ !/entwurf_peers/.test(selfRegion) && !/entwurf_inbox_read/.test(selfRegion),
265
+ );
266
+
267
+ // ── F-1: entwurf_self must render the RAIL, never a universal mailbox ────────
268
+ // The defect: `origin === "meta-session"` unconditionally synthesized
269
+ // `<mailboxDir>/<gardenId>`. `origin` is sender PROVENANCE; the rail is the second
270
+ // axis. A native-push citizen (antigravity) has NO mailbox at all (AGENTS Hard Rule
271
+ // 10, VERIFY "No mailbox/receiver-marker evidence counts on this rail"), so that
272
+ // branch printed a path that will never exist and taught the model mailbox semantics
273
+ // its own rail does not have. Pinned PER RAIL, as one reusable predicate so the same
274
+ // judgement can be driven over negative controls below.
275
+ /** Brace-balanced body of the `if (<guard>) { … }` that follows `guard`, or null. */
276
+ function branchBody(region: string, guard: string): string | null {
277
+ const at = region.indexOf(guard);
278
+ if (at < 0) return null;
279
+ const open = region.indexOf("{", at);
280
+ if (open < 0) return null;
281
+ let depth = 0;
282
+ for (let i = open; i < region.length; i++) {
283
+ const c = region[i];
284
+ if (c === "{") depth++;
285
+ else if (c === "}") {
286
+ depth--;
287
+ if (depth === 0) return region.slice(open, i + 1);
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+
293
+ function metaRailRenderIsHonest(region: string): { honest: boolean; reason: string } {
294
+ // The mailbox path may be built at most once in the whole render branch…
295
+ const mailboxCalls = region.split("defaultMetaMailboxDir(").length - 1;
296
+ if (mailboxCalls !== 1) {
297
+ return { honest: false, reason: `expected exactly 1 defaultMetaMailboxDir() call, found ${mailboxCalls}` };
298
+ }
299
+ // …and that one call must sit INSIDE the self-fetch branch body. Ordering alone is not
300
+ // containment: an `if (rail === "self-fetch") {}` with an EMPTY body followed by an
301
+ // unconditional mailbox build satisfies "guard appears before mailbox" while still
302
+ // synthesizing a mailbox for every rail — the exact defect this predicate exists to
303
+ // reject. Extract the body by brace-counting and require the call to be in it.
304
+ const selfFetchBody = branchBody(region, 'rail === "self-fetch"');
305
+ if (selfFetchBody === null) {
306
+ return { honest: false, reason: 'no brace-balanced `rail === "self-fetch"` branch body' };
307
+ }
308
+ const inBranch = selfFetchBody.split("defaultMetaMailboxDir(").length - 1;
309
+ if (inBranch !== 1) {
310
+ return {
311
+ honest: false,
312
+ reason: `the self-fetch branch BODY must build the mailbox path exactly once, found ${inBranch} (ordering is not containment)`,
313
+ };
314
+ }
315
+ const pushBody = branchBody(region, 'rail === "native-push"');
316
+ if (pushBody === null) {
317
+ return { honest: false, reason: 'no brace-balanced `rail === "native-push"` branch body' };
318
+ }
319
+ // The native-push branch must deny an inbox AND keep the direct-inject claim conditional
320
+ // on the probe: a dead probe has no live conversation to inject into, so an unconditional
321
+ // "injects a reply into this live conversation" invents the very rail F-1 is about.
322
+ if (!/no inbox/.test(pushBody)) {
323
+ return { honest: false, reason: "the native-push branch does not state that there is no inbox" };
324
+ }
325
+ if (!/while the adapter probe is alive/.test(pushBody)) {
326
+ return {
327
+ honest: false,
328
+ reason: "the native-push branch states direct injection unconditionally (a dead probe has no live conversation)",
329
+ };
330
+ }
331
+ return {
332
+ honest: true,
333
+ reason:
334
+ "the sole mailbox build is INSIDE the self-fetch branch body; native-push denies an inbox and gates injection on the probe",
335
+ };
336
+ }
337
+
338
+ {
339
+ const verdict = metaRailRenderIsHonest(selfRegion);
340
+ ok(`entwurf_self renders the meta rail honestly [QK:SELFADDR-RAIL-RENDER] (${verdict.reason})`, verdict.honest);
341
+ ok(
342
+ "entwurf_self reads the rail from the builder (not re-derived at the render site)",
343
+ /self\.metaDeliveryDomain/.test(selfRegion),
344
+ );
345
+
346
+ // NEGATIVE CONTROLS — two KINDS, because a gate that only rejects the exact old
347
+ // sentence would pass a version where the branch is simply gone (and vice versa).
348
+ // Driving the same predicate over synthetic sources is what proves this assertion
349
+ // BLOCKS, not merely that it exists.
350
+ const retiredUnconditional = [
351
+ '} else if (sender.origin === "meta-session") {',
352
+ "\tconst mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
353
+ "\textra.mailboxPath = mailboxPath;",
354
+ "}",
355
+ ].join("\n");
356
+ ok(
357
+ "NEGATIVE 1/3 (exact false sentence): the retired unconditional meta-session mailbox is REJECTED",
358
+ metaRailRenderIsHonest(retiredUnconditional).honest === false,
359
+ );
360
+
361
+ const selfFetchOnlyNoPushBranch = [
362
+ "const rail = self.metaDeliveryDomain;",
363
+ 'if (rail === "self-fetch") {',
364
+ "\tconst mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
365
+ "\textra.mailboxPath = mailboxPath;",
366
+ "}",
367
+ ].join("\n");
368
+ ok(
369
+ "NEGATIVE 2/3 (omission): a self-fetch-guarded render with the native-push branch DELETED is REJECTED",
370
+ metaRailRenderIsHonest(selfFetchOnlyNoPushBranch).honest === false,
371
+ );
372
+
373
+ // NEGATIVE 3/3 — the form the FIRST version of this predicate wrongly accepted (caught in
374
+ // cross-review, 2026-07-27): every marker present, guard textually BEFORE the mailbox, yet
375
+ // the guard's body is empty and the mailbox is built unconditionally. Ordering satisfied,
376
+ // containment violated. This row is why the predicate brace-counts the branch body.
377
+ const guardedInNameOnly = [
378
+ "const rail = self.metaDeliveryDomain;",
379
+ 'if (rail === "self-fetch") { }',
380
+ "const mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
381
+ "extra.mailboxPath = mailboxPath;",
382
+ 'if (rail === "native-push") {',
383
+ '\tlines.push("mailbox: none — no inbox; direct-inject only while the adapter probe is alive");',
384
+ "}",
385
+ ].join("\n");
386
+ ok(
387
+ "NEGATIVE 3/3 (ordering-but-not-containment): an EMPTY self-fetch guard followed by an unconditional mailbox build is REJECTED",
388
+ metaRailRenderIsHonest(guardedInNameOnly).honest === false,
389
+ );
390
+ }
391
+
392
+ // ── F-1: the identity-wiring errors must not teach one backend's hook as THE hook ──
393
+ // Both native backends mint a garden id from their OWN hook (Claude Code SessionStart,
394
+ // Antigravity PreInvocation). Naming only SessionStart/Claude sent an agy operator
395
+ // looking for a hook its backend never runs.
396
+ {
397
+ /** Extract a `class NAME extends Error { ... }` body by brace-counting. */
398
+ const classBody = (name: string): string => {
399
+ const sig = `class ${name} extends Error {`;
400
+ const at = src.indexOf(sig);
401
+ assert.ok(at >= 0, `${name} present in MCP source`);
402
+ const open = src.indexOf("{", at);
403
+ let depth = 0;
404
+ for (let i = open; i < src.length; i++) {
405
+ const c = src[i];
406
+ if (c === "{") depth++;
407
+ else if (c === "}") {
408
+ depth--;
409
+ if (depth === 0) return src.slice(open, i + 1);
410
+ }
411
+ }
412
+ throw new Error(`class ${name} body never closed`);
413
+ };
414
+
415
+ // Anchored per class (not a repo-wide grep) so a mention somewhere else in the file
416
+ // cannot make either row vacuously green.
417
+ for (const cls of ["EntwurfEnvelopeWiringError", "EntwurfSenderIdentityError"]) {
418
+ const body = classBody(cls);
419
+ ok(
420
+ `${cls} names BOTH native hooks (SessionStart AND PreInvocation)`,
421
+ /SessionStart/.test(body) && /PreInvocation/.test(body),
422
+ );
423
+ ok(
424
+ `${cls} no longer presents SessionStart/Claude as THE marker writer`,
425
+ !/The native SessionStart hook writes that marker/.test(body) &&
426
+ !/whose SessionStart hook wrote a live/.test(body) &&
427
+ !/keyed by the\s+Claude Code parent pid/.test(body),
428
+ );
429
+ }
430
+ }
431
+
249
432
  // ── SE-2 2e-b: meta-session sender replyability from the receiver presence marker ──
250
433
  // Identity stays trusted (record-backed), but `replyable` is now derived from whether
251
434
  // THIS session's own receiver inbox can actually wake (slice-2 presence marker), not a
@@ -8,7 +8,7 @@
8
8
  * by prose. Pure; no backend, no socket, no API.
9
9
  *
10
10
  * Proves:
11
- * - R1 domain guard: only pi is in the liveness domain; claude-code / codex /
11
+ * - R1 domain guard: the control-socket domain currently contains pi; claude-code / codex /
12
12
  * antigravity are `unsupported` — NEVER folded into dead/indeterminate.
13
13
  * - the 6-cell table is exhaustive and every cell is a SINGLE verdict (Q2):
14
14
  * exactly two allow cells (ff+live=send, owned+dormant=resume), four reject.
@@ -92,7 +92,7 @@ function enumValues(schema: unknown): string[] {
92
92
 
93
93
  const SOCKET_LIVENESSES = ["alive", "dead", "indeterminate"] as const;
94
94
 
95
- // ── R1: backend liveness domain — only pi, others unsupported ──────────────
95
+ // ── R1: control-socket capability domain — currently pi ────────────────────
96
96
  eq("domain: pi is supported", isLivenessSupported("pi"), true);
97
97
  eq("domain: claude-code unsupported (self-fetch, no socket)", isLivenessSupported("claude-code"), false);
98
98
  eq("domain: codex unsupported (no probe surface yet)", isLivenessSupported("codex"), false);
@@ -271,15 +271,15 @@ async function main(): Promise<void> {
271
271
  ok("A: B1 — a pi target does NO pre-lock lstat (inspectPath unused)", spies.inspectPath.length === 0);
272
272
  }
273
273
 
274
- // ── A2: B1 — a NON-pi target DOES record-side lstat the conflict (pre-lock)
274
+ // ── A2: out-of-socket-domain target checks record-side conflict pre-lock ──
275
275
  {
276
276
  // claude-code (unsupported) + a symlink at the canonical path → record-side conflict →
277
277
  // the decider rejects target-address-conflict BEFORE acquiring (no lock for a quarantined
278
278
  // address). The lstat-only inspectPath ran exactly once; acquire never did.
279
279
  const { deps, spies } = makeSpiedFactory({ backend: "claude-code", inspectKind: "address-conflict" });
280
280
  const decision = await deps.decide({ target: GID, intent: "fire-and-forget", message: "m" });
281
- ok("A2: non-pi conflict → reject", decision.kind === "reject");
282
- ok("A2: non-pi target lstat'd the conflict exactly once", spies.inspectPath.length === 1);
281
+ ok("A2: out-of-socket-domain conflict → reject", decision.kind === "reject");
282
+ ok("A2: out-of-domain target lstat'd the conflict exactly once", spies.inspectPath.length === 1);
283
283
  ok("A2: a quarantined target is never lock-acquired", spies.acquire.length === 0);
284
284
  }
285
285
 
@@ -316,7 +316,7 @@ async function main(): Promise<void> {
316
316
  ok("A3b: no send attempted on a dead target", spies.nativePushSend.length === 0);
317
317
  }
318
318
 
319
- // ── A3: QB2 — a non-pi indeterminate lstat FAILS LOUD (never "no conflict") ─
319
+ // ── A3: out-of-domain indeterminate lstat fails loud ─────────────────────
320
320
  {
321
321
  const { deps } = makeSpiedFactory({ backend: "claude-code", inspectKind: "indeterminate" });
322
322
  let threw = false;
@@ -325,7 +325,7 @@ async function main(): Promise<void> {
325
325
  } catch {
326
326
  threw = true;
327
327
  }
328
- ok("A3: non-pi indeterminate lstat → decide throws (QB2 fail-loud)", threw);
328
+ ok("A3: out-of-domain indeterminate lstat → decide throws (QB2 fail-loud)", threw);
329
329
  }
330
330
 
331
331
  // ── B: control sendOverSocket builds RpcSendCommand + maps + lockDir release ─
@@ -499,9 +499,13 @@ async function main(): Promise<void> {
499
499
  // missing record → not a garden citizen (readMetaIdentityByGardenId fail-fast).
500
500
  rejects("20260101T000000-facade", "not a garden citizen", "9 recordless gid → not a citizen");
501
501
 
502
- // non-pi citizen spawn-bg resume is the pi rail.
502
+ // A citizen outside the spawn-bg capability domain cannot use that rail.
503
503
  const gidClaude = mintRecord("claude-native-1", null, "claude-code");
504
- rejects(gidClaude, "the pi rail", "9 non-pi citizen → refused (pi rail)");
504
+ rejects(
505
+ gidClaude,
506
+ "spawn-bg resume is a host-adapter capability",
507
+ "9 out-of-domain citizen → refused (spawn-bg rail)",
508
+ );
505
509
 
506
510
  // pi citizen with no recorded transcript (no turn yet) → nothing to resume.
507
511
  const gidNoFile = mintRecord("0199dddd-1111-4222-8333-444455556666", null);