@junghanacs/entwurf 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +248 -0
  3. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
  4. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
  5. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
  6. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
  7. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +14 -2
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +24 -3
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +61 -7
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
  17. package/mcp/entwurf-bridge/src/index.ts +16 -8
  18. package/package.json +3 -2
  19. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -1
  20. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +13 -8
  21. package/pi-extensions/lib/entwurf-deliverability.ts +104 -0
  22. package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
  23. package/pi-extensions/lib/entwurf-facts.ts +52 -3
  24. package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
  25. package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
  26. package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
  27. package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
  28. package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
  29. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -6
  30. package/pi-extensions/lib/entwurf-v2-send.ts +26 -3
  31. package/pi-extensions/lib/entwurf-v2-surface.ts +24 -3
  32. package/pi-extensions/lib/meta-session.ts +76 -7
  33. package/pi-extensions/meta-bridge-hook.ts +72 -3
  34. package/run.sh +21 -0
  35. package/scripts/check-bridge-delivery.ts +42 -1
  36. package/scripts/check-entwurf-fact-provider.ts +22 -1
  37. package/scripts/check-entwurf-facts.ts +17 -2
  38. package/scripts/check-entwurf-peers-surface.ts +20 -2
  39. package/scripts/check-entwurf-self-address.ts +20 -2
  40. package/scripts/check-entwurf-v2-decider.ts +6 -3
  41. package/scripts/check-entwurf-v2-mailbox.ts +33 -0
  42. package/scripts/check-entwurf-v2-matrix.ts +10 -5
  43. package/scripts/check-entwurf-v2-production.ts +72 -1
  44. package/scripts/check-entwurf-v2-runner.ts +18 -0
  45. package/scripts/check-entwurf-v2-send.ts +13 -1
  46. package/scripts/check-entwurf-v2-surface.ts +75 -3
  47. package/scripts/check-gate-qualification.ts +1 -0
  48. package/scripts/check-hook-launch-topology.ts +70 -1
  49. package/scripts/check-mailbox-receipt-state.ts +6 -0
  50. package/scripts/check-meta-doctor-oracle.sh +73 -0
  51. package/scripts/check-meta-hook-session-switch.ts +560 -0
  52. package/scripts/check-meta-mailbox-state-write.ts +9 -2
  53. package/scripts/check-meta-manifest-schema.py +19 -1
  54. package/scripts/check-meta-session.ts +10 -2
  55. package/scripts/meta-bridge-doctor.sh +47 -7
  56. package/scripts/meta-bridge-state.py +23 -5
  57. package/scripts/meta-bridge-statusline.sh +65 -2
  58. package/scripts/mutants/meta-hook-session-switch.json +223 -0
  59. package/scripts/raw-async-delivery/README.md +280 -9
  60. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +6 -2
  61. package/scripts/raw-async-delivery/lab-statusline.sh +63 -0
  62. package/scripts/raw-async-delivery/mailbox-watch.py +230 -0
  63. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +3 -1
  64. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +13 -4
  65. package/scripts/raw-async-delivery/probe-delivery-transparency.sh +387 -0
  66. package/scripts/raw-claude-session-switch/README.md +170 -0
  67. package/scripts/smoke-acp-cortex-live.ts +22 -2
  68. package/scripts/smoke-acp-v2-send-live.ts +23 -2
  69. package/scripts/smoke-entwurf-chain-live.ts +85 -13
  70. package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
  71. package/scripts/smoke-meta-async-drift.sh +31 -8
  72. package/scripts/smoke-meta-install-state.sh +170 -11
  73. package/scripts/smoke-meta-keyset-guard.sh +4 -1
  74. package/scripts/smoke-mux-fresh-call-live.ts +13 -3
@@ -54,7 +54,15 @@ import fsp from "node:fs/promises";
54
54
  import os from "node:os";
55
55
  import path from "node:path";
56
56
  import { fileURLToPath } from "node:url";
57
- import { readMetaInbox, upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
57
+ import { resolveMailboxReceiverFacts } from "../pi-extensions/lib/entwurf-deliverability.ts";
58
+ import {
59
+ readMetaInbox,
60
+ readMetaReceiverMarker,
61
+ readMetaSenderMarker,
62
+ upsertMetaSession,
63
+ writeMetaReceiverMarker,
64
+ writeMetaSenderMarker,
65
+ } from "../pi-extensions/lib/meta-session.ts";
58
66
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
59
67
  import { skipLive } from "./lib/live-skip.ts";
60
68
 
@@ -155,11 +163,13 @@ async function main(): Promise<void> {
155
163
  const sessionsDir = path.join(world, "sessions");
156
164
  const mailboxDir = path.join(world, "mailbox");
157
165
  const receiversDir = path.join(world, "receivers");
158
- for (const d of [sessionsDir, mailboxDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
166
+ const sendersDir = path.join(world, "senders");
167
+ for (const d of [sessionsDir, mailboxDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
159
168
  const worldEnv = {
160
169
  ENTWURF_META_SESSIONS_DIR: sessionsDir,
161
170
  ENTWURF_META_MAILBOX_DIR: mailboxDir,
162
171
  ENTWURF_META_RECEIVERS_DIR: receiversDir,
172
+ ENTWURF_META_SENDERS_DIR: sendersDir,
163
173
  };
164
174
  // The MCP bridge prefers PI_SESSION_ID+PI_AGENT_ID over a meta-sender marker when both
165
175
  // are present. A runner that is itself a pi --entwurf-control session (release-gate,
@@ -183,20 +193,46 @@ async function main(): Promise<void> {
183
193
  dir: sessionsDir,
184
194
  });
185
195
  const gidD = terminus.record.gardenId;
186
- writeMetaReceiverMarker({
187
- gardenId: gidD,
188
- backend: "claude-code",
189
- nativeSessionId: terminus.record.nativeSessionId,
190
- ownerPid: process.pid,
191
- armProvenance: "session-start",
192
- receiversDir,
193
- });
194
- console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
195
-
196
196
  const children: ChildProcess[] = [];
197
+ let terminusOwner: ChildProcess | null = null;
197
198
  let streamB = "";
198
199
  let streamC = "";
199
200
  try {
201
+ // The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli` watch
202
+ // owner is deliverable only while ITS OWN sender marker still names the garden it is armed
203
+ // for — that join is what refuses a session which switched away in place. A receiver marker
204
+ // with no sender marker beside it models a state the product now treats as retired, so this
205
+ // smoke would fail on its own fixture instead of on the rail it exists to prove.
206
+ //
207
+ // THE OWNER IS A SPAWNED IDLE PROCESS, NOT THIS ONE, and that is specific to this smoke:
208
+ // hop 1 is a native `claude` child of THIS process, and the bridge resolves a sender's
209
+ // identity through its ancestry. A terminus marker under this pid therefore puts TWO garden
210
+ // citizens on one host process, which the bridge refuses outright as ambiguous sender
211
+ // identity — measured on oracle 2026-09-04, where hop 1 came back "refused: ambiguous sender
212
+ // identity (…-f34713, …-912b42)" and the chain never started. The sibling smokes keep
213
+ // `process.pid` because their children are pi/ACP sessions carrying their own identity, or
214
+ // tmux siblings that are not descendants at all.
215
+ terminusOwner = spawn(process.execPath, ["-e", "setTimeout(() => {}, 900000)"], { stdio: "ignore" });
216
+ const terminusOwnerPid = terminusOwner.pid;
217
+ if (typeof terminusOwnerPid !== "number") throw new Error("could not spawn the terminus owner process");
218
+ writeMetaReceiverMarker({
219
+ gardenId: gidD,
220
+ backend: "claude-code",
221
+ nativeSessionId: terminus.record.nativeSessionId,
222
+ ownerPid: terminusOwnerPid,
223
+ armProvenance: "session-start",
224
+ receiversDir,
225
+ });
226
+ writeMetaSenderMarker({
227
+ backend: "claude-code",
228
+ gardenId: gidD,
229
+ nativeSessionId: terminus.record.nativeSessionId,
230
+ cwd: world,
231
+ ownerPid: terminusOwnerPid,
232
+ sendersDir,
233
+ });
234
+ console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
235
+
200
236
  // ── C: the ACP Claude Sonnet citizen (spawned first so B can be told its id) ──
201
237
  const known = new Set((await readRecords(sessionsDir)).map((r) => r.gardenId));
202
238
  const c = spawn(
@@ -283,6 +319,21 @@ async function main(): Promise<void> {
283
319
  ok("A is distinct from every other citizen", !new Set([gidB, gidC, gidD]).has(gidA as string));
284
320
  console.error(`[smoke-entwurf-chain-live] A: ${gidA} (native Claude Code)`);
285
321
 
322
+ // ── the fixture must still be a deliverable citizen when the chain arrives ──
323
+ // The terminus is only reachable while its owner is alive AND that owner's sender marker
324
+ // still names it (#101). Both are fixture state this smoke owns, and both can decay while
325
+ // three model turns run. Asserting it here separates "the chain did not arrive" from "the
326
+ // target stopped being addressable", which cost a LIVE run to tell apart once.
327
+ const terminusFacts = resolveMailboxReceiverFacts(terminus.record, {
328
+ readReceiverMarker: (gardenId: string) => readMetaReceiverMarker({ gardenId, receiversDir }),
329
+ readSenderMarker: (backend: string, pid: number) =>
330
+ readMetaSenderMarker({ backend: backend as "claude-code", ownerPid: pid, sendersDir }),
331
+ });
332
+ ok(
333
+ "fixture: the terminus is a deliverable citizen at the moment the chain starts",
334
+ terminusFacts.ownerAlive && terminusFacts.watchArmed,
335
+ );
336
+
286
337
  // ── the chain runs on its own from here; wait for the terminus ───────────
287
338
  const boxDir = path.join(mailboxDir, gidD);
288
339
  const until = Date.now() + CHAIN_TIMEOUT_MS;
@@ -295,7 +346,25 @@ async function main(): Promise<void> {
295
346
  await sleep(POLL_MS);
296
347
  }
297
348
  if (msgs.length === 0) {
298
- console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut.slice(0, 1500)}`);
349
+ let ownerAlive = false;
350
+ try {
351
+ process.kill(terminusOwnerPid, 0);
352
+ ownerAlive = true;
353
+ } catch {}
354
+ const facts = resolveMailboxReceiverFacts(terminus.record, {
355
+ readReceiverMarker: (gardenId: string) => readMetaReceiverMarker({ gardenId, receiversDir }),
356
+ readSenderMarker: (backend: string, pid: number) =>
357
+ readMetaSenderMarker({ backend: backend as "claude-code", ownerPid: pid, sendersDir }),
358
+ });
359
+ console.error(
360
+ `[smoke-entwurf-chain-live] terminus fixture at timeout: ownerPid=${terminusOwnerPid} alive=${ownerAlive} ` +
361
+ `ownerAlive=${facts.ownerAlive} watchArmed=${facts.watchArmed}`,
362
+ );
363
+ // The FULL turn, not a 1500-char head: that slice cut the JSON before its `result`
364
+ // field, so the one line that says what A actually did — the tool outcome it was told
365
+ // to echo back — was the first thing this diagnostic dropped. A failure that hides its
366
+ // own cause costs another LIVE run to re-learn.
367
+ console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut}`);
299
368
  console.error(`[smoke-entwurf-chain-live] B stream tail:\n${streamB.slice(-2000)}`);
300
369
  console.error(`[smoke-entwurf-chain-live] C stream tail:\n${streamC.slice(-2000)}`);
301
370
  }
@@ -350,6 +419,9 @@ async function main(): Promise<void> {
350
419
  console.error(`[smoke-entwurf-chain-live] chain: ${gidA} → ${gidB} → ${gidC} → ${gidD}`);
351
420
  } finally {
352
421
  for (const ch of children) await terminateChild(ch).catch(() => undefined);
422
+ try {
423
+ terminusOwner?.kill("SIGTERM");
424
+ } catch {}
353
425
  }
354
426
 
355
427
  console.log(
@@ -62,6 +62,7 @@ import {
62
62
  metaRecordExistsByGardenId,
63
63
  upsertMetaSession,
64
64
  writeMetaReceiverMarker,
65
+ writeMetaSenderMarker,
65
66
  } from "../pi-extensions/lib/meta-session.ts";
66
67
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
67
68
  import { skipLive } from "./lib/live-skip.ts";
@@ -187,10 +188,15 @@ async function main(): Promise<void> {
187
188
  const mailboxDir = path.join(tmp, "mailbox");
188
189
  const lockDir = path.join(tmp, "locks");
189
190
  const receiversDir = path.join(tmp, "receivers");
190
- for (const d of [sessionsDir, mailboxDir, lockDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
191
+ const sendersDir = path.join(tmp, "senders");
192
+ for (const d of [sessionsDir, mailboxDir, lockDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
191
193
  // The receiver marker is read through defaultMetaReceiversDir() (env-overridable); point it
192
194
  // (and the store/mailbox defaults, belt-and-suspenders to the opts dirs) at the temp world.
195
+ // The SENDER dir joins that list for #101: deliverability now also reads the receiver owner's
196
+ // sender marker, and leaving this one pointed at the operator's real root would have the
197
+ // fixture judged against markers this smoke never wrote.
193
198
  process.env.ENTWURF_META_RECEIVERS_DIR = receiversDir;
199
+ process.env.ENTWURF_META_SENDERS_DIR = sendersDir;
194
200
  process.env.ENTWURF_META_SESSIONS_DIR = sessionsDir;
195
201
  process.env.ENTWURF_META_MAILBOX_DIR = mailboxDir;
196
202
 
@@ -341,6 +347,14 @@ async function main(): Promise<void> {
341
347
  const gid = minted.record.gardenId;
342
348
  artifacts["C2.gid"] = gid;
343
349
  // armed = a presence marker owned by THIS (live) process → deliverable.
350
+ //
351
+ // The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli`
352
+ // watch owner is deliverable only while ITS OWN sender marker still names the garden it
353
+ // is armed for — that join is what refuses a session which switched away in place. A
354
+ // receiver marker with no sender marker beside it models a state the product now treats
355
+ // as retired, so this smoke would fail on its own fixture instead of on the rail it
356
+ // exists to prove. Same shape as smoke-mux-lifecycle-live / smoke-omp-fresh-live, which
357
+ // already seeded both.
344
358
  writeMetaReceiverMarker({
345
359
  gardenId: gid,
346
360
  backend: "claude-code",
@@ -349,6 +363,14 @@ async function main(): Promise<void> {
349
363
  armProvenance: "session-start",
350
364
  receiversDir,
351
365
  });
366
+ writeMetaSenderMarker({
367
+ backend: "claude-code",
368
+ gardenId: gid,
369
+ nativeSessionId: minted.record.nativeSessionId,
370
+ cwd: tmp,
371
+ ownerPid: process.pid,
372
+ sendersDir,
373
+ });
352
374
 
353
375
  const result: EntwurfV2RunResult = await runEntwurfV2(
354
376
  { target: gid, intent: "fire-and-forget", message: "matrix-live C2 meta-mailbox probe" },
@@ -6,13 +6,24 @@
6
6
  #
7
7
  # WHY THIS EXISTS
8
8
  # The whole Claude async-delivery path rides on UNDOCUMENTED Claude Code behavior:
9
- # `asyncRewake` force-prepends `Stop hook feedback:` and ignores `rewakeMessage`;
10
- # the payload channel is stderr-only; `watchPaths` can arm from only 3 hook events
11
- # (SessionStart / CwdChanged / FileChanged). Half the Gotchas in
12
- # scripts/raw-async-delivery/README.md are reverse-engineered. Claude ships ~weekly.
13
- # So the path can break SILENTLY on any upgrade. This gate makes it SCREAM instead —
14
- # direct lineage of the 0.8.x fail-fast tool-surface gates ("don't let a surface
15
- # break quietly, make the gate cry out").
9
+ # `asyncRewake` names the operator row and the model prefix from `rewakeSummary` /
10
+ # `rewakeMessage` (both read from a local plugin's hooks.json, both @internal, and
11
+ # the product doorbell now DEPENDS on both); the doorbell writes its notice on
12
+ # stderr; `watchPaths` can arm from only 3 hook events (SessionStart / CwdChanged /
13
+ # FileChanged). Half the Gotchas in scripts/raw-async-delivery/README.md are
14
+ # reverse-engineered. Claude ships ~weekly. So the path can break SILENTLY on any
15
+ # upgrade. This gate makes it SCREAM instead — direct lineage of the 0.8.x fail-fast
16
+ # tool-surface gates ("don't let a surface break quietly, make the gate cry out").
17
+ #
18
+ # THIS PARAGRAPH USED TO BE WRONG, AND THAT IS THE POINT (#98, corrected
19
+ # 2026-09-03). It said `asyncRewake` "force-prepends `Stop hook feedback:` and
20
+ # ignores `rewakeMessage`", and that the payload channel was "stderr-only". Both are
21
+ # FALSE — measured on 2.1.236/2.1.258/2.1.259: `rewakeMessage` REPLACES the model
22
+ # prefix, `rewakeSummary` REPLACES the operator row, and the model body is
23
+ # `stderr || stdout`. The marker strings below stayed green the whole time, because
24
+ # a marker pins a STRING and cannot see the sentence next to it rot. Nobody tried
25
+ # the two fields for months on the strength of this header. Receipts:
26
+ # scripts/raw-async-delivery/README.md "Inherited facts corrected".
16
27
  #
17
28
  # TWO TIERS (mirrors smoke-compaction-policy: deterministic default + LIVE=1 add-on)
18
29
  # DETERMINISTIC (default; free, offline, CI/pre-commit safe):
@@ -70,7 +81,19 @@ MARKERS=(
70
81
  flushPendingAsyncRewakeHooks # engine turn-boundary flush (edge-bound delivery)
71
82
  CLAUDE_CODE_STOP_HOOK_BLOCK_CAP # native re-wake cap
72
83
  FileChanged # idle-wake event
73
- rewakeMessage # the field asyncRewake IGNORES (doorbell-only proof)
84
+ # rewakeMessage/rewakeSummary CORRECTED 2026-09-03 (#98 Phase 1). The comment here used
85
+ # to read "the field asyncRewake IGNORES (doorbell-only proof)". That was FALSE, and it is
86
+ # the reason nobody tried the field for months: measured on 2.1.236/2.1.258/2.1.259, BOTH
87
+ # are read from a local plugin's hooks.json with no first-party gate. `rewakeMessage`
88
+ # REPLACES the model-facing `Stop hook blocking error from command "…":` prefix;
89
+ # `rewakeSummary` REPLACES the operator-visible row (default `Stop hook feedback`). Only
90
+ # the stdout-JSON form of rewakeSummary is first-party gated. The marker string was right
91
+ # while the reason next to it was dead — which is why these two are pinned HERE: the
92
+ # product doorbell now DEPENDS on both (pi/meta-bridge/.../hooks/hooks.json), the fields
93
+ # are @internal, and if either disappears the surface silently reverts to
94
+ # `Stop hook feedback` + "blocking error" with no other gate to notice.
95
+ rewakeMessage # model-facing prefix replacement (product depends on it)
96
+ rewakeSummary # operator-facing row replacement (product depends on it)
74
97
  hookSpecificOutput # the watchPaths emit envelope
75
98
  CwdChanged # the 2nd of the 3 watch-arming events
76
99
  )
@@ -317,6 +317,69 @@ def structural_cells():
317
317
  assert (broken, settings) == before
318
318
 
319
319
  cell("", "retired/managed sets stay disjoint and a corrupt key ledger fails closed", structural_cells)
320
+
321
+ def compaction_cells():
322
+ # #94 — the two compaction keys join the retired set. They exercise two axes
323
+ # the skipDangerous cells above never touch: a NESTED path and a STRING
324
+ # scalar. Both matter, because relinquish compares `type(value) is
325
+ # type(last_managed_value)` and walks the path through get_nested.
326
+ for name, path, last in [
327
+ ("autoCompactEnabled", ["autoCompactEnabled"], False),
328
+ ("env.DISABLE_AUTOCOMPACT", ["env", "DISABLE_AUTOCOMPACT"], "1"),
329
+ ]:
330
+ def build(current=MISSING, old=MISSING):
331
+ settings = {}
332
+ if current is not MISSING:
333
+ node = settings
334
+ for key in path[:-1]:
335
+ node = node.setdefault(key, {})
336
+ node[path[-1]] = current
337
+ keys = {} if old is MISSING else {name: copy.deepcopy(old)}
338
+ return {"files": {"settings": {"keys": keys}}}, settings
339
+
340
+ def led(existed, value):
341
+ return {"kind": "scalar", "path": path, "original": {"existed": existed, "value": value}}
342
+
343
+ def call(current=MISSING, old=MISSING):
344
+ state, settings = build(current, old)
345
+ m.relinquish_retired_scalar(state, settings, name, path, last)
346
+ return state, settings
347
+
348
+ # (a) still carrying what entwurf last managed, with provenance -> the
349
+ # pre-entwurf snapshot is restored, and the entry is consumed.
350
+ state, settings = call(last, led(True, last))
351
+ assert m.get_nested(settings, path) == (True, last), (name, settings)
352
+ assert name not in state["files"]["settings"]["keys"], name
353
+ # (a') the same, where the operator had NOTHING before entwurf: restoring
354
+ # the snapshot must DELETE the key rather than leave our value.
355
+ state, settings = call(last, led(False, None))
356
+ assert m.get_nested(settings, path)[0] is False, (name, settings)
357
+ # (b) the operator has since changed it -> already theirs, untouched.
358
+ other = True if last is False else "0"
359
+ state, settings = call(other, led(True, last))
360
+ assert m.get_nested(settings, path) == (True, other), (name, settings)
361
+ assert name not in state["files"]["settings"]["keys"], name
362
+ # (c) no ledger entry at all (fresh install) -> untouched, and a fresh
363
+ # install never creates one because the key is not managed any more.
364
+ for current in [last, other, MISSING]:
365
+ state, settings = call(current)
366
+ expected = (False, None) if current is MISSING else (True, current)
367
+ assert m.get_nested(settings, path) == expected, (name, current, settings)
368
+ # Type strictness, where a lookalike EXISTS. For the bool axis that is `0`
369
+ # (`0 == False` is True while `type(0) is not bool`) — NOT `1`, which is
370
+ # simply unequal to False and would pass the cell without the type guard
371
+ # ever being consulted. The string axis has no lookalike at all: no
372
+ # non-str value equals `"1"`, so equality alone already refuses and there
373
+ # is nothing for a type check to add. Asserted only where it can fail.
374
+ if last is False:
375
+ state, settings = call(0, led(True, last))
376
+ assert m.get_nested(settings, path) == (True, 0), (name, settings)
377
+
378
+ cell(
379
+ "",
380
+ "#94 compaction keys relinquish across nested/string axes: restore, delete, operator-changed, fresh, type-strict",
381
+ compaction_cells,
382
+ )
320
383
  sys.exit(failed)
321
384
  PY
322
385
  then ok "#71 retirement leaf: provenance, bool strictness, malformed refusal, one-shot convergence"
@@ -339,10 +402,12 @@ assert stat.S_IMODE(os.stat(path).st_mode) == 0o600
339
402
  s=json.load(open(path))
340
403
  assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['value'] is False
341
404
  assert s['files']['settings']['keys']['cleanupPeriodDays']['original']['value'] == 30
342
- assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['value'] == '0'
343
405
  assert s['files']['settings']['keys']['statusLine']['original']['value']['command'] == '/old/user/statusline.sh'
344
406
  assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['value'] is True
345
- assert s['files']['settings']['keys']['autoCompactEnabled']['original']['existed'] is False
407
+ # #94 case (c) — a FRESH install never captures a retired key, so entwurf holds no
408
+ # ownership evidence for compaction and uninstall can never restore a suppression.
409
+ assert 'autoCompactEnabled' not in s['files']['settings']['keys']
410
+ assert 'env.DISABLE_AUTOCOMPACT' not in s['files']['settings']['keys']
346
411
  assert 'skipDangerousModePermissionPrompt' not in s['files']['settings']['keys']
347
412
  assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['value']['command'] == 'old'
348
413
  PY
@@ -360,6 +425,21 @@ state['files']['settings']['keys']['skipDangerousModePermissionPrompt']={
360
425
  'kind':'scalar', 'path':['skipDangerousModePermissionPrompt'],
361
426
  'original':{'existed':True, 'value':False}
362
427
  }
428
+ # #94 — the same upgrade shape for the two compaction keys, planted at DRIVE level
429
+ # so apply() itself is the subject, not the leaf. Two branches that the unit cells
430
+ # cannot reach through the real ledger: a NESTED path whose original must come
431
+ # back, and a key whose original never existed and must therefore be DELETED
432
+ # rather than left carrying entwurf's value.
433
+ settings['autoCompactEnabled']=False
434
+ state['files']['settings']['keys']['autoCompactEnabled']={
435
+ 'kind':'scalar', 'path':['autoCompactEnabled'],
436
+ 'original':{'existed':False, 'value':None}
437
+ }
438
+ settings.setdefault('env',{})['DISABLE_AUTOCOMPACT']='1'
439
+ state['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']={
440
+ 'kind':'scalar', 'path':['env','DISABLE_AUTOCOMPACT'],
441
+ 'original':{'existed':True, 'value':'0'}
442
+ }
363
443
  json.dump(settings, open(sp,'w'), indent=2); open(sp,'a').write('\n')
364
444
  json.dump(state, open(stp,'w'), indent=2); open(stp,'a').write('\n')
365
445
  PY
@@ -373,14 +453,26 @@ assert settings['enabledPlugins']['entwurf-meta-receive@meta-bridge-local'] is T
373
453
  assert settings['enabledPlugins']['keep@user'] is True
374
454
  assert settings['extraKnownMarketplaces']['meta-bridge-local']['source']['path'] == os.environ['ASM']
375
455
  assert settings['cleanupPeriodDays'] == 365
376
- assert settings['env']['DISABLE_AUTOCOMPACT'] == '1'
456
+ # #94 - retired: apply neither writes nor rewrites it. The fixture's operator
457
+ # value ('0') survives untouched, and the scalar is never introduced at all.
458
+ assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
459
+ assert 'autoCompactEnabled' not in settings
377
460
  assert settings['env']['KEEP_ME'] == 'yes'
378
461
  assert settings['statusLine']['command'] == os.environ['REPO'] + '/scripts/meta-bridge-statusline.sh'
379
- for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','showTurnDuration','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
462
+ for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','showTurnDuration','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
380
463
  assert settings[key] is False, key
381
464
  assert settings['skipDangerousModePermissionPrompt'] is False
465
+ # #94 drive-level relinquishment, both branches:
466
+ # nested path -> the operator's pre-entwurf '0' is back, and the env map that
467
+ # carries it survives with the neighbour key untouched.
468
+ # absent original -> the key is REMOVED, not left holding entwurf's False.
469
+ assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
470
+ assert settings['env']['KEEP_ME'] == 'yes'
471
+ assert 'autoCompactEnabled' not in settings
382
472
  state=json.load(open(cfg + '/entwurf.install-state.json'))
383
473
  assert 'skipDangerousModePermissionPrompt' not in state['files']['settings']['keys']
474
+ assert 'autoCompactEnabled' not in state['files']['settings']['keys']
475
+ assert 'env.DISABLE_AUTOCOMPACT' not in state['files']['settings']['keys']
384
476
  for item in ['Bash','Read','Write','Edit','Grep','Glob','WebFetch','WebSearch','Skill','mcp__entwurf-bridge__*']:
385
477
  assert item in settings['permissions']['allow'], item
386
478
  assert settings['permissions']['allow'].count('Read') == 1
@@ -455,7 +547,6 @@ import json, os
455
547
  s=json.load(open(os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'))
456
548
  assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['value'] is False
457
549
  assert s['files']['settings']['keys']['cleanupPeriodDays']['original']['value'] == 30
458
- assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['value'] == '0'
459
550
  assert s['files']['settings']['keys']['statusLine']['original']['value']['command'] == '/old/user/statusline.sh'
460
551
  assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['value'] is True
461
552
  PY
@@ -479,13 +570,18 @@ root=json.load(open(os.environ['HOME'] + '/.claude.json'))
479
570
  assert settings['cleanupPeriodDays'] == 30
480
571
  assert settings['enabledPlugins']['entwurf-meta-receive@meta-bridge-local'] is False
481
572
  assert 'meta-bridge-local' not in settings.get('extraKnownMarketplaces', {})
573
+ # #94 - still the operator's '0'. NOT restored by uninstall (we never owned it):
574
+ # never written in the first place.
482
575
  assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
483
576
  assert settings['env']['KEEP_ME'] == 'yes'
484
577
  assert settings['statusLine']['command'] == '/old/user/statusline.sh'
485
578
  assert settings['promptSuggestionEnabled'] is True
486
579
  assert settings['skipDangerousModePermissionPrompt'] is False
487
580
  assert settings['showTurnDuration'] is True
488
- for key in ['awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
581
+ # One list, one meaning: these are the keys UNINSTALL removed. `autoCompactEnabled`
582
+ # is not among them post-#94 — that entwurf never introduced it is a different
583
+ # claim, pinned on the post-install assertion above.
584
+ for key in ['awaySummaryEnabled','autoMemoryEnabled','verbose','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
489
585
  assert key not in settings, key
490
586
  allow=settings['permissions']['allow']
491
587
  deny=settings['permissions']['deny']
@@ -667,7 +763,8 @@ s=json.load(open(os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
667
763
  assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['existed'] is False
668
764
  assert s['files']['settings']['keys']['extraKnownMarketplaces.meta-bridge-local']['original']['existed'] is False
669
765
  assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['existed'] is False
670
- assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['existed'] is True
766
+ # #94 - retired: prepare captures no ownership evidence for it any more.
767
+ assert 'env.DISABLE_AUTOCOMPACT' not in s['files']['settings']['keys']
671
768
  PY
672
769
  then ok "legacy exact plugin/marketplace/MCP migrate as pi-owned absent, policy keys remain user-owned"; else bad "legacy migration state check failed"; fi
673
770
  py uninstall >/dev/null
@@ -706,18 +803,80 @@ valid_record "20260606T000001-bbbbbb" "native-b" > "$STORE/20260606T000001-bbbbb
706
803
  STATUS_INPUT_MATCH='{"session_id":"native-a","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":2,"current_usage":{"input_tokens":10}}}'
707
804
  STATUS_INPUT_MISS='{"session_id":"native-missing","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-opus-5"}}'
708
805
  STATUS_INPUT_READY='{"workspace":{"current_dir":"/tmp"},"model":{"id":"claude-haiku-4-5"}}'
709
- STATUS_OUT_MATCH="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
806
+ # Pin the mailbox root on EVERY statusline render below, not just the badge cells: the
807
+ # #98 B badge reads it, so an unpinned call would let the operator's real ~/.pi mailbox
808
+ # decide whether these rows carry an envelope.
809
+ MBX="$TMP/mbx"; mkdir -p "$MBX/20260606T000000-aaaaaa"
810
+ STATUS_OUT_MATCH="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh")"
710
811
  if [ "$(printf '%s\n' "$STATUS_OUT_MATCH" | wc -l | tr -d ' ')" = "2" ]; then ok "statusline renders exactly two rows"; else bad "statusline should render two rows: $STATUS_OUT_MATCH"; fi
711
812
  if printf '%s\n' "$STATUS_OUT_MATCH" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$STATUS_OUT_MATCH" | sed -n '2p' | grep -q '🪛 20260606T000000-aaaaaa cc | s'; then ok "statusline keeps row-1 work context and maps native session_id to row-2 garden-id"; else bad "statusline did not show expected two-row content for native-a: $STATUS_OUT_MATCH"; fi
712
- STATUS_OUT_MISS="$(printf '%s' "$STATUS_INPUT_MISS" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
813
+ STATUS_OUT_MISS="$(printf '%s' "$STATUS_INPUT_MISS" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh")"
713
814
  if printf '%s' "$STATUS_OUT_MISS" | grep -q '🪛 ? cc'; then ok "statusline no-record fallback is ?"; else bad "statusline no-record fallback wrong: $STATUS_OUT_MISS"; fi
714
- STATUS_OUT_READY="$(printf '%s' "$STATUS_INPUT_READY" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
815
+ STATUS_OUT_READY="$(printf '%s' "$STATUS_INPUT_READY" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh")"
715
816
  if printf '%s' "$STATUS_OUT_READY" | grep -q '🪛 ready cc'; then ok "statusline no-session_id fallback is ready"; else bad "statusline ready fallback wrong: $STATUS_OUT_READY"; fi
716
817
  cp "$STORE/20260606T000000-aaaaaa.meta.json" "$STORE/20260606T000003-dddddd.meta.json"
717
- STATUS_OUT_DUP="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
818
+ STATUS_OUT_DUP="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh")"
718
819
  if printf '%s' "$STATUS_OUT_DUP" | grep -q '🪛 ! cc'; then ok "statusline duplicate nativeSessionId fallback is !"; else bad "statusline duplicate fallback wrong: $STATUS_OUT_DUP"; fi
719
820
  rm "$STORE/20260606T000003-dddddd.meta.json"
720
821
 
822
+ # --- #98 B: the unread badge -------------------------------------------------
823
+ # Three states that must LOOK different: N letters, none, and "could not count".
824
+ # The last one is the whole reason the badge exists — an unread letter that the
825
+ # operator never saw is exactly the failure #98 opened on, and a statusline that
826
+ # renders a confident 0 when its read failed reproduces it one layer down.
827
+ badge_line() { printf '%s' "$1" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh" | sed -n '2p'; }
828
+
829
+ # empty mailbox -> NO badge. Silence is the quiet common case; the badge is for mail.
830
+ B_EMPTY="$(badge_line "$STATUS_INPUT_MATCH")"
831
+ if ! printf '%s' "$B_EMPTY" | grep -q '✉'; then ok "badge: empty mailbox draws no envelope"; else bad "badge: empty mailbox drew one: $B_EMPTY"; fi
832
+
833
+ # The counted set is EXACTLY readMetaInbox's unread union: *.msg + *.msg.delivered.
834
+ # The three decoys below must all be excluded: a .read (already drained — a badge that
835
+ # counted it would never clear), the signal file, and state.json.
836
+ : > "$MBX/20260606T000000-aaaaaa/one.msg"
837
+ : > "$MBX/20260606T000000-aaaaaa/two.msg.delivered"
838
+ : > "$MBX/20260606T000000-aaaaaa/three.msg.delivered.read"
839
+ : > "$MBX/20260606T000000-aaaaaa/inbox.signal"
840
+ : > "$MBX/20260606T000000-aaaaaa/state.json"
841
+ B_TWO="$(badge_line "$STATUS_INPUT_MATCH")"
842
+ if printf '%s' "$B_TWO" | grep -q '✉2'; then ok "badge: .msg + .msg.delivered counted, .read/signal/state excluded (✉2)"; else bad "badge: expected ✉2, got: $B_TWO"; fi
843
+
844
+ # The drain clears it on the NEXT render — the property acceptance criterion 4 names.
845
+ mv "$MBX/20260606T000000-aaaaaa/one.msg" "$MBX/20260606T000000-aaaaaa/one.msg.read"
846
+ mv "$MBX/20260606T000000-aaaaaa/two.msg.delivered" "$MBX/20260606T000000-aaaaaa/two.msg.delivered.read"
847
+ B_DRAINED="$(badge_line "$STATUS_INPUT_MATCH")"
848
+ if ! printf '%s' "$B_DRAINED" | grep -q '✉'; then ok "badge: clears after the read archives both bodies to .read"; else bad "badge: stayed after drain: $B_DRAINED"; fi
849
+
850
+ # Could-not-count #1: no garden id resolved, so there is no mailbox to look in. The
851
+ # statusline must NOT quietly claim zero for a session it could not place.
852
+ B_NOGARDEN="$(badge_line "$STATUS_INPUT_MISS")"
853
+ if printf '%s' "$B_NOGARDEN" | grep -q '✉?'; then ok "badge: unresolved garden id renders ✉? (never a false zero)"; else bad "badge: unresolved garden id should be ✉?, got: $B_NOGARDEN"; fi
854
+
855
+ # Could-not-count #2: the mailbox dir exists but cannot be read. Same rule.
856
+ chmod 000 "$MBX/20260606T000000-aaaaaa"
857
+ B_UNREADABLE="$(badge_line "$STATUS_INPUT_MATCH")"
858
+ chmod 755 "$MBX/20260606T000000-aaaaaa"
859
+ if printf '%s' "$B_UNREADABLE" | grep -q '✉?'; then ok "badge: unreadable mailbox renders ✉? (measurement failure, not zero)"; else bad "badge: unreadable mailbox should be ✉?, got: $B_UNREADABLE"; fi
860
+
861
+ # Could-not-count #3: no python3 at all. That degrade path predates the badge and used
862
+ # to print a bare `🪛 ? cc`; without the ✉? it would read as "counted, none".
863
+ # The stand-in PATH carries `cat` and nothing else: the script reads its stdin with `cat`
864
+ # BEFORE it tests for python3, so a truly empty PATH would kill it under `set -e` and the
865
+ # cell would pass for the wrong reason.
866
+ mkdir -p "$TMP/emptybin"
867
+ ln -sf "$(command -v cat)" "$TMP/emptybin/cat"
868
+ if [ -n "$(command -v python3 2>/dev/null)" ] && PATH="$TMP/emptybin" command -v python3 >/dev/null 2>&1; then bad "no-python3 fixture is not actually python3-less"; else ok "no-python3 fixture really hides python3"; fi
869
+ # `$BASH` (an absolute path) rather than the name `bash`, so the interpreter is reachable
870
+ # without putting a second binary on the stand-in PATH the fixture is trying to empty.
871
+ B_NOPY="$(printf '%s' "$STATUS_INPUT_MATCH" | PATH="$TMP/emptybin" ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$BASH" "$REPO/scripts/meta-bridge-statusline.sh" | sed -n '2p')"
872
+ if printf '%s' "$B_NOPY" | grep -q '✉?'; then ok "badge: python3-less degrade line still says ✉? (not a silent zero)"; else bad "badge: python3-less degrade should carry ✉?, got: $B_NOPY"; fi
873
+
874
+ # A garden id that never renders a real mailbox: the pre-record "ready" state. Nothing
875
+ # has been addressed to this session yet, so an empty badge is TRUE, not a guess.
876
+ B_READY="$(badge_line "$STATUS_INPUT_READY")"
877
+ if ! printf '%s' "$B_READY" | grep -q '✉'; then ok "badge: pre-record 'ready' draws no envelope (nothing addressed yet)"; else bad "badge: ready state drew an envelope: $B_READY"; fi
878
+ rm -rf "$MBX"
879
+
721
880
  if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null; then ok "store doctor accepts valid records"; else bad "store doctor rejected valid records"; fi
722
881
  cp "$STORE/20260606T000000-aaaaaa.meta.json" "$STORE/20260606T000002-cccccc.meta.json"
723
882
  if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null 2>&1; then bad "store doctor missed body/filename drift + duplicate"; else ok "store doctor fails on body/filename drift and duplicate nativeSessionId"; fi
@@ -31,7 +31,10 @@ d=json.load(sys.stdin)
31
31
  s=d['settings']
32
32
  assert 'statusLine' in s['map-entry'], 'statusLine missing from SSOT map-entry'
33
33
  assert 'permissions.allow' in s['array-items'] and 'permissions.deny' in s['array-items'], 'permissions missing'
34
- assert 'verbose' in s['scalar'] and 'autoCompactEnabled' in s['scalar'], 'scalar policy missing'
34
+ assert 'verbose' in s['scalar'] and 'awaySummaryEnabled' in s['scalar'], 'scalar policy missing'
35
+ # #94: the compaction keys are RETIRED, so managed-keys must NOT claim them.
36
+ assert 'autoCompactEnabled' not in s['scalar'], 'retired compaction scalar still claimed as managed'
37
+ assert 'env.DISABLE_AUTOCOMPACT' not in s['scalar'], 'retired compaction env scalar still claimed as managed'
35
38
  assert any(k.startswith('enabledPlugins.') for k in s['map-entry']), 'plugin ref missing'
36
39
  assert d['claudeRoot']['map-entry'] == ['mcpServers.entwurf-bridge'], 'MCP key missing'
37
40
  " && ok "managed-keys SSOT is valid JSON with the expected owned keys" \
@@ -194,7 +194,10 @@ async function main(): Promise<void> {
194
194
  const callerGid = caller.record.gardenId;
195
195
  // A record proves identity; only an ARMED receiver marker makes the mailbox deliverable —
196
196
  // without it the callbacks would be refused as mailbox-undeliverable and this smoke would
197
- // measure nothing.
197
+ // measure nothing. Since #101 the marker is half of that: a `claude-code-cli` watch owner
198
+ // is deliverable only while ITS OWN sender marker still names the garden it is armed for,
199
+ // which is what refuses a session that switched away in place. Both, or the fixture models
200
+ // a retired watch — the same shape smoke-mux-lifecycle-live already seeds.
198
201
  meta.writeMetaReceiverMarker({
199
202
  gardenId: callerGid,
200
203
  backend: "claude-code",
@@ -202,9 +205,16 @@ async function main(): Promise<void> {
202
205
  ownerPid: process.pid,
203
206
  armProvenance: "session-start",
204
207
  });
208
+ const callerSenderMarker = meta.writeMetaSenderMarker({
209
+ backend: "claude-code",
210
+ gardenId: callerGid,
211
+ nativeSessionId,
212
+ cwd: scratch,
213
+ ownerPid: process.pid,
214
+ });
205
215
  ok(
206
- "fixture: the caller citizen was minted INSIDE the fixture store and its mailbox is armed",
207
- Boolean(callerGid) && caller.path.startsWith(root),
216
+ "fixture: the caller citizen was minted INSIDE the fixture store, with an armed mailbox and a sender marker",
217
+ Boolean(callerGid) && caller.path.startsWith(root) && callerSenderMarker.startsWith(root),
208
218
  );
209
219
 
210
220
  // ── One private tmux server per backend ──────────────────────────────────