@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
@@ -468,11 +468,18 @@ if diff:
468
468
  # template equality above, not from a loose per-field test. It says the wake wiring is
469
469
  # DECLARED as shipped; it does NOT say exit-2 → wake holds at runtime (#51 B2).
470
470
  bell = leaf("FileChanged")
471
+ # The two rewake strings are REPORTED, not optional: this line's whole job is to say what
472
+ # the wake wiring DECLARES, and since #98 the declaration includes what the operator and the
473
+ # model actually see when a letter lands. Omitting them would report a contract the manifest
474
+ # no longer fully states — the exact class of half-truth #98 exists to remove. They are safe
475
+ # to index: the exact-template equality above already ran, so a manifest missing either field
476
+ # never reaches here.
471
477
  doorbell = (
472
478
  f"ok FileChanged doorbell matches the shipped static contract exactly "
473
479
  f"(command={bell['command']}, args={bell['args']}, asyncRewake={bell['asyncRewake']}, "
474
- f"timeout={bell['timeout']}); runtime exit-2 wake was observed at Claude 2.1.217 (#51 B2) but "
475
- "is not re-proven by this static read"
480
+ f"timeout={bell['timeout']}, rewakeSummary={bell['rewakeSummary']!r}, "
481
+ f"rewakeMessage={bell['rewakeMessage']!r}); runtime exit-2 wake was observed at Claude "
482
+ "2.1.217 (#51 B2) but is not re-proven by this static read"
476
483
  )
477
484
 
478
485
  # Resolve the argv EXACTLY the way Claude does for an exec form: substitute the
@@ -563,7 +570,15 @@ PY
563
570
  bad "statusline bin not on PATH: $EXPECTED_STATUSLINE"
564
571
  fi
565
572
  SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
566
- if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q ' cc | s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
573
+ # The identity row may carry the #98 B unread badge between `cc` and the model letter:
574
+ # ` ✉N` when mail is waiting, nothing when the mailbox is empty, ` ✉?` when the count
575
+ # could not be taken. All three are healthy renders, so the shape test admits any of them.
576
+ if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -qE ' cc( ✉([0-9]+|\?))? \| s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
577
+ # The synthetic feeds a session_id with no meta-record, so the garden id does not
578
+ # resolve and there is no mailbox to count. `✉?` is the required answer: a statusline
579
+ # that drew nothing there would be claiming "no mail" about a mailbox it never opened —
580
+ # the exact silence #98 was opened to remove.
581
+ if printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '✉?'; then ok "statusline badge distinguishes 'could not count' (✉?) from zero on the no-record synthetic"; else bad "statusline badge omitted ✉? for the no-record synthetic (a false zero would be indistinguishable from an empty mailbox)"; fi
567
582
  else
568
583
  bad "cannot validate statusline without python3"
569
584
  fi
@@ -721,19 +736,43 @@ writeMetaReceiverMarker({
721
736
  receiversDir,
722
737
  });
723
738
 
724
- // RECEIVER — a DIFFERENT citizen, armed by a live owner pid (this process). An unarmed
725
- // target is correctly fail-closed as mailbox-undeliverable, so an unarmed probe could
726
- // never separate "the artifact cannot read its registry" from "nobody is listening".
739
+ // RECEIVER — a DIFFERENT citizen, armed by a DIFFERENT live owner pid. An unarmed target
740
+ // is correctly fail-closed as mailbox-undeliverable, so an unarmed probe could never
741
+ // separate "the artifact cannot read its registry" from "nobody is listening".
742
+ //
743
+ // The owner is a spawned idle process, not this one (#101): a claude-code watch owner
744
+ // serves exactly one garden at a time, and deliverability asks whether that owner's sender
745
+ // marker still names the garden being addressed. This probe's own pid already carries the
746
+ // SENDER citizen's marker, so arming the receiver under it would model a session switch —
747
+ // a retired watch — and the probe would report a delivery failure that is really the
748
+ // fixture. It cannot be the probe's parent either: the bridge resolves its own sender
749
+ // identity through its ancestry, and an ancestor holding two gardens is the "ambiguous
750
+ // sender identity" refusal. A live pid outside that ancestry is what a second native CLI
751
+ // actually looks like.
752
+ const receiverOwner = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 300000)'], { stdio: 'ignore' });
753
+ const receiverOwnerPid = receiverOwner.pid;
754
+ if (typeof receiverOwnerPid !== 'number') {
755
+ console.error('could not spawn the stand-in receiver owner process');
756
+ process.exit(1);
757
+ }
727
758
  const receiver = upsertMetaSession({
728
759
  input: { backend: 'claude-code', nativeSessionId: `doctor-delivery-receiver-${process.pid}`, cwd: tmp },
729
760
  dir: sessionsDir,
730
761
  });
731
762
  const gid = receiver.record.gardenId;
763
+ writeMetaSenderMarker({
764
+ backend: 'claude-code',
765
+ gardenId: gid,
766
+ nativeSessionId: receiver.record.nativeSessionId,
767
+ cwd: tmp,
768
+ ownerPid: receiverOwnerPid,
769
+ sendersDir,
770
+ });
732
771
  writeMetaReceiverMarker({
733
772
  gardenId: gid,
734
773
  backend: 'claude-code',
735
774
  nativeSessionId: receiver.record.nativeSessionId,
736
- ownerPid: process.pid,
775
+ ownerPid: receiverOwnerPid,
737
776
  armProvenance: 'session-start',
738
777
  receiversDir,
739
778
  });
@@ -830,6 +869,7 @@ try {
830
869
  bail(String(e instanceof Error ? e.message : e));
831
870
  } finally {
832
871
  try { child?.kill('SIGTERM'); } catch {}
872
+ try { receiverOwner.kill('SIGTERM'); } catch {}
833
873
  await fsp.rm(tmp, { recursive: true, force: true });
834
874
  }
835
875
  if (verdict === 0) console.log('delivered: one .msg landed + doorbell poked; seeded meta-sender identity joined');
@@ -66,16 +66,24 @@ LEGACY_PERMISSION_ALLOW = ["mcp__pi-tools-bridge__*"]
66
66
 
67
67
  # Claude Code single-driver policy scalars owned by entwurf for the native
68
68
  # meta-bridge install. These are not theming/personal hooks; they close background
69
- # autonomy/suggestion/compaction surfaces so Claude Code behaves like the same
70
- # single forged screwdriver that entwurf already enforces for ACP backends.
69
+ # autonomy/suggestion surfaces so Claude Code behaves like the same single forged
70
+ # screwdriver that entwurf already enforces for ACP backends.
71
+ #
72
+ # COMPACTION IS NOT ONE OF THEM (#94). entwurf neither enables nor disables it on
73
+ # any rail, and the ACP side reached that state first: 0.5.0 really did carry a
74
+ # pi-side compaction surface in CODE (`session_before_compact` cancelling by
75
+ # default, `assertLegacyCompactionKnobUnset`, `smoke-compaction-policy`), the v2
76
+ # subtraction removed it (`378c682`, 2026-06-16), and `623a4ea` (06-23) then
77
+ # removed the documentation that had outlived the code by a week. As of #94 the
78
+ # native rail stops owning `autoCompactEnabled` / `env.DISABLE_AUTOCOMPACT` too.
79
+ # Backend-native context management is allowed; we add no guard and only measure.
80
+ # Both keys live in RETIRED_SETTINGS_SCALARS below.
71
81
  MANAGED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
72
82
  ("cleanupPeriodDays", ["cleanupPeriodDays"], 365),
73
- ("env.DISABLE_AUTOCOMPACT", ["env", "DISABLE_AUTOCOMPACT"], "1"),
74
83
  ("promptSuggestionEnabled", ["promptSuggestionEnabled"], False),
75
84
  ("awaySummaryEnabled", ["awaySummaryEnabled"], False),
76
85
  ("autoMemoryEnabled", ["autoMemoryEnabled"], False),
77
86
  ("verbose", ["verbose"], False),
78
- ("autoCompactEnabled", ["autoCompactEnabled"], False),
79
87
  ("showTurnDuration", ["showTurnDuration"], False),
80
88
  ("terminalProgressBarEnabled", ["terminalProgressBarEnabled"], False),
81
89
  ("useAutoModeDuringPlan", ["useAutoModeDuringPlan"], False),
@@ -89,6 +97,14 @@ MANAGED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
89
97
  # exactly once, then uninstall can no longer restore over the operator's choice.
90
98
  RETIRED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
91
99
  ("skipDangerousModePermissionPrompt", ["skipDangerousModePermissionPrompt"], True),
100
+ # #94 — compaction returns to the operator. RETIRED, never deleted outright:
101
+ # a deleted entry would be ORPHANED in an existing install-state, and
102
+ # uninstall() walks every ledger entry through restore_entry(), so it would
103
+ # put the suppression back over an operator who had since enabled compaction.
104
+ # Only this list's relinquish path pops the entry so uninstall can no longer
105
+ # reach it.
106
+ ("env.DISABLE_AUTOCOMPACT", ["env", "DISABLE_AUTOCOMPACT"], "1"),
107
+ ("autoCompactEnabled", ["autoCompactEnabled"], False),
92
108
  ]
93
109
 
94
110
  _managed_scalar_names = {name for name, _path, _desired in MANAGED_SETTINGS_SCALARS}
@@ -607,7 +623,9 @@ def check(repo: Path, asm: Path) -> None:
607
623
  if existed and type(value) is type(last_managed_value) and value == last_managed_value:
608
624
  print(
609
625
  f"NOTE: settings {name}={json.dumps(last_managed_value)} is operator-owned; "
610
- f"entwurf no longer suppresses or restores this warning choice"
626
+ f"entwurf no longer sets or restores this value. It still carries the value "
627
+ f"entwurf last managed, which is not drift and is not a defect — a retired "
628
+ f"key has no desired value to drift from."
611
629
  )
612
630
 
613
631
  checks = [
@@ -7,6 +7,11 @@
7
7
  # garden id is looked up by scanning meta-record BODIES via the native Claude
8
8
  # `session_id`. No cache, no filename authority, no DB.
9
9
  #
10
+ # It also draws the #98 B unread badge: `✉N` when the garden's mailbox holds N
11
+ # messages the read tool would still return, nothing when it holds none, and `✉?` when
12
+ # the count could NOT be taken. That third state is the point — a statusline that silently
13
+ # renders 0 on a broken read would say "no mail" about a mailbox it never looked in.
14
+ #
10
15
  # Runtime dependency: python3 (already gated by install-meta-bridge/doctor).
11
16
  set -euo pipefail
12
17
 
@@ -18,7 +23,9 @@ input=$(cat)
18
23
  # wrote. doctor/smoke are the fail-LOUD surfaces; this surface stays silent.
19
24
  if ! command -v python3 >/dev/null 2>&1; then
20
25
  device="$(cat "$HOME/.current-device" 2>/dev/null || echo UNKNOWN)"
21
- printf '%s ?\n🪛 ? cc' "$device"
26
+ # ✉? not a missing badge: with no python3 the unread count was not TAKEN. Drawing
27
+ # nothing here would be indistinguishable from "no mail" (#98 B).
28
+ printf '%s ?\n🪛 ? cc ✉?' "$device"
22
29
  exit 0
23
30
  fi
24
31
  STATUSLINE_INPUT="$input" python3 - <<'PY' || true
@@ -166,6 +173,60 @@ def garden_lookup(native_session_id: str) -> str:
166
173
  return "?"
167
174
 
168
175
 
176
+ def meta_mailbox_dir() -> Path:
177
+ """Mirror of `defaultMetaMailboxDir()` in meta-session.ts, including the env
178
+ precedence. Kept as a mirror rather than a shell-out: the statusline runs on every
179
+ render (measured 4-10 times per doorbell rewake turn, #98 Phase 1 P2a), so it must not
180
+ spawn a node process to count files."""
181
+ override = os.environ.get("ENTWURF_META_MAILBOX_DIR")
182
+ if override:
183
+ return Path(override).expanduser().resolve()
184
+ agent = os.environ.get("PI_CODING_AGENT_DIR")
185
+ if agent:
186
+ return Path(agent).expanduser().resolve() / "meta-mailbox"
187
+ return Path.home() / ".pi" / "agent" / "meta-mailbox"
188
+
189
+
190
+ def unread_badge(garden: str) -> str:
191
+ """`✉N` / `""` / `✉?` — the #98 B badge.
192
+
193
+ The counted set is EXACTLY what `entwurf_inbox_read` would return: `*.msg` (arrived,
194
+ doorbell has not rung yet) PLUS `*.msg.delivered` (doorbell rang, model has not read).
195
+ `readMetaInbox` uses that same union (meta-session.ts), and `.read` is excluded by
196
+ both — so the badge clears on the read, on the statusline's next execution.
197
+
198
+ Counting only `.msg.delivered` would under-report a letter that landed between two
199
+ doorbells; counting `.read` too would never clear.
200
+
201
+ Three distinct returns, deliberately:
202
+ - `""` the mailbox was read and is empty. Silence is the quiet common case.
203
+ - `✉N` N letters the read tool will hand back.
204
+ - `✉?` the count could not be taken (unusable garden id, unreadable dir).
205
+ NEVER collapsed into `""` — a false zero is the failure this whole issue
206
+ is about.
207
+
208
+ A garden id that is `?`/`!`/`ready` means the lookup itself did not land, so there is
209
+ no mailbox to count and no claim to make: `✉?`, not silence.
210
+ """
211
+ if not garden or garden in ("?", "!"):
212
+ return "✉?"
213
+ if garden == "ready":
214
+ # No session_id yet (a fresh render before the hook minted a record). Nothing has
215
+ # been addressed to this session, so an empty badge is TRUE, not a guess.
216
+ return ""
217
+ if "/" in garden or garden.startswith("."):
218
+ return "✉?"
219
+ try:
220
+ box = meta_mailbox_dir() / garden
221
+ if not box.is_dir():
222
+ # A citizen with no mailbox dir has received nothing. Honest empty.
223
+ return ""
224
+ n = sum(1 for f in box.iterdir() if f.name.endswith(".msg") or f.name.endswith(".msg.delivered"))
225
+ except Exception:
226
+ return "✉?"
227
+ return f"✉{n}" if n > 0 else ""
228
+
229
+
169
230
  def main() -> None:
170
231
  data = load_input()
171
232
  raw_cwd = dig(data, "workspace", "current_dir") or data.get("cwd") or "?"
@@ -183,7 +244,9 @@ def main() -> None:
183
244
  f"{CYAN_BOLD}{cwd_tail}{RESET}"
184
245
  f"{DIM}{git_branch(str(raw_cwd))}{RESET}"
185
246
  )
186
- line2 = f"{DIM}🪛 {garden} cc | {model}{vterm}{context_info(data)}{RESET}"
247
+ badge = unread_badge(garden)
248
+ badge_text = f" {badge}" if badge else ""
249
+ line2 = f"{DIM}🪛 {garden} cc{badge_text} | {model}{vterm}{context_info(data)}{RESET}"
187
250
  sys.stdout.write(f"{line1}\n{line2}")
188
251
 
189
252
 
@@ -0,0 +1,223 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "meta-hook-session-switch",
4
+ "mutants": [
5
+ {
6
+ "claim": "MHSS-RETIRE-PREV-GARDEN",
7
+ "title": "a hook that never retires the garden its pid stopped serving leaves the abandoned startup session advertising a doorbell nobody holds — the abandoned-session registration measured on oracle, where a real message sat unread in its mailbox for an hour",
8
+ "subject": "pi-extensions/meta-bridge-hook.ts",
9
+ "find": ["\t\tif (previous && previous.gardenId !== gardenId) {"],
10
+ "replace": ["\t\tif (false && previous && previous.gardenId !== gardenId) {"],
11
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
12
+ "timeoutSeconds": 300,
13
+ "signature": "[QK:MHSS-RETIRE-PREV-GARDEN]",
14
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
15
+ },
16
+ {
17
+ "claim": "MHSS-RETIRE-KEEPS-CURRENT",
18
+ "title": "retiring on ANY previous sender marker instead of a CHANGED one disarms the live session's own inbox on its next keystroke — the repair eating the citizen it exists to protect, which is worse than the defect",
19
+ "subject": "pi-extensions/meta-bridge-hook.ts",
20
+ "find": ["\t\tif (previous && previous.gardenId !== gardenId) {"],
21
+ "replace": ["\t\tif (previous) {"],
22
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
23
+ "timeoutSeconds": 300,
24
+ "signature": "[QK:MHSS-RETIRE-KEEPS-CURRENT]",
25
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
26
+ },
27
+ {
28
+ "claim": "MHSS-RETIRE-ONLY-OWN-OWNER",
29
+ "title": "dropping the owner-pid guard lets one session's SessionStart delete a marker another LIVE process holds — a silent cross-session disarm, not a stale-marker cleanup",
30
+ "subject": "pi-extensions/lib/meta-session.ts",
31
+ "find": ["\t\tif (!marker || marker.ownerPid !== opts.ownerPid) return false;"],
32
+ "replace": ["\t\tif (!marker) return false;"],
33
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
34
+ "timeoutSeconds": 300,
35
+ "signature": "[QK:MHSS-RETIRE-ONLY-OWN-OWNER]",
36
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
37
+ },
38
+ {
39
+ "claim": "MHSS-SOURCE-LOGGED",
40
+ "title": "dropping the vendor's `source` from the log leaves this host with no receipt for WHY a SessionStart fired — the exact evidence the #101 diagnosis had to inherit from vendor docs because nothing here recorded it",
41
+ "subject": "pi-extensions/meta-bridge-hook.ts",
42
+ "find": [
43
+ "\tconst source = typeof env.source === \"string\" && env.source.length > 0 ? env.source : \"(unset)\";"
44
+ ],
45
+ "replace": ["\tconst source = \"(unset)\";"],
46
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
47
+ "timeoutSeconds": 300,
48
+ "signature": "[QK:MHSS-SOURCE-LOGGED]",
49
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
50
+ },
51
+ {
52
+ "claim": "MHSS-SENDER-JOIN-MEASURED",
53
+ "title": "a join that always answers yes turns the receiver marker back into proof of something it never proved — that the process holding it is still serving THIS garden",
54
+ "subject": "pi-extensions/lib/entwurf-deliverability.ts",
55
+ "find": [
56
+ "\treturn !!senderMarker && senderMarker.gardenId === marker.gardenId && senderMarker.backend === marker.backend;"
57
+ ],
58
+ "replace": ["\treturn true;"],
59
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
60
+ "timeoutSeconds": 300,
61
+ "signature": "[QK:MHSS-SENDER-JOIN-MEASURED]",
62
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
63
+ },
64
+ {
65
+ "claim": "MHSS-SEAM-USES-JOIN",
66
+ "title": "restoring `watchArmed` as a copy of the identity match at the production seam re-ships the false deliverable even with the join intact — the shipped line that let a message be enqueued into a void",
67
+ "subject": "pi-extensions/lib/entwurf-v2-production.ts",
68
+ "find": [
69
+ "\t\tconst { ownerAlive, watchArmed } = resolveMailboxReceiverFacts(identity, {\n\t\t\treadReceiverMarker: io.readReceiverMarker,\n\t\t\treadSenderMarker: io.readSenderMarker,\n\t\t});"
70
+ ],
71
+ "replace": [
72
+ "\t\tconst { ownerAlive } = resolveMailboxReceiverFacts(identity, {\n\t\t\treadReceiverMarker: io.readReceiverMarker,\n\t\t\treadSenderMarker: io.readSenderMarker,\n\t\t});\n\t\tconst watchArmed = ownerAlive;"
73
+ ],
74
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
75
+ "timeoutSeconds": 300,
76
+ "signature": "[QK:MHSS-SEAM-USES-JOIN]",
77
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
78
+ },
79
+ {
80
+ "claim": "MHSS-REJECT-CARRIES-REASON",
81
+ "title": "dropping the deliverability reason at the decider boundary hands the caller a verdict with no cause — the bare `mailbox-undeliverable` that sent a sibling looking for a dead session when the session was alive and had moved",
82
+ "subject": "pi-extensions/lib/entwurf-v2-decider.ts",
83
+ "find": [
84
+ "\t\t\treturn deliverability.deliverable\n\t\t\t\t? reject(receipt)\n\t\t\t\t: reject(receipt, { kind: \"mailbox-undeliverable\", reason: deliverability.reason });"
85
+ ],
86
+ "replace": ["\t\t\treturn reject(receipt);"],
87
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
88
+ "timeoutSeconds": 300,
89
+ "signature": "[QK:MHSS-REJECT-CARRIES-REASON]",
90
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
91
+ },
92
+ {
93
+ "claim": "MHSS-REASON-RENDERED",
94
+ "title": "a surface that carries the diagnostic and prints nothing is the same silence one layer up — the operator still reads only `mailbox-undeliverable`",
95
+ "subject": "pi-extensions/lib/entwurf-v2-surface.ts",
96
+ "find": ["\t\t\t\ttext += `\\n mailbox-undeliverable: ${result.diagnostic.reason}`;"],
97
+ "replace": ["\t\t\t\ttext += \"\";"],
98
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
99
+ "timeoutSeconds": 300,
100
+ "signature": "[QK:MHSS-REASON-RENDERED]",
101
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
102
+ },
103
+ {
104
+ "claim": "MHSS-PEERS-RECEIVER-OBSERVED",
105
+ "title": "collapsing `inactive` into `none` erases the distinction a reader needs — a watch that was armed and is no longer valid looks exactly like one that was never armed, which is the listing telling a caller nothing again",
106
+ "subject": "pi-extensions/lib/entwurf-peer-observe.ts",
107
+ "find": ["\treturn fs.existsSync(metaReceiverMarkerPath(identity.gardenId)) ? \"inactive\" : \"none\";"],
108
+ "replace": ["\treturn \"none\";"],
109
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
110
+ "timeoutSeconds": 300,
111
+ "signature": "[QK:MHSS-PEERS-RECEIVER-OBSERVED]",
112
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
113
+ },
114
+ {
115
+ "claim": "MHSS-PEERS-TRANSCRIPT-OBSERVED",
116
+ "title": "reporting every transcript as present hides exactly the phantom this column exists to show: a registration minted at SessionStart with no conversation ever written behind it",
117
+ "subject": "pi-extensions/lib/entwurf-peer-observe.ts",
118
+ "find": [
119
+ "\t\treturn identity.transcriptPath && fs.existsSync(identity.transcriptPath) ? \"exists\" : \"absent\";"
120
+ ],
121
+ "replace": ["\t\treturn \"exists\";"],
122
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
123
+ "timeoutSeconds": 300,
124
+ "signature": "[QK:MHSS-PEERS-TRANSCRIPT-OBSERVED]",
125
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
126
+ },
127
+ {
128
+ "claim": "MHSS-PEERS-COLUMNS-RENDERED",
129
+ "title": "measuring both axes and printing neither returns the listing to two identical rows for a live citizen and a phantom — the surface gap that made a sibling dispatch into a void",
130
+ "subject": "pi-extensions/lib/entwurf-peers-render.ts",
131
+ "find": [
132
+ "\t\t`- ${p.gardenId} backend=${p.backend} liveness=${p.liveness} receiver=${p.receiver} ` +\n\t\t`transcript=${p.transcript} cwd=${p.cwd} model=${model}`"
133
+ ],
134
+ "replace": [
135
+ "\t\t`- ${p.gardenId} backend=${p.backend} liveness=${p.liveness} ` + `cwd=${p.cwd} model=${model}`"
136
+ ],
137
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
138
+ "timeoutSeconds": 300,
139
+ "signature": "[QK:MHSS-PEERS-COLUMNS-RENDERED]",
140
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
141
+ },
142
+ {
143
+ "claim": "MHSS-RETIRE-ARM-CAPABLE-ONLY",
144
+ "title": "retiring from an event that cannot arm subtracts a doorbell with nothing in the same run able to restore one — UserPromptSubmit cannot emit watchPaths, so a retirement reached from there is a one-way disarm",
145
+ "subject": "pi-extensions/meta-bridge-hook.ts",
146
+ "find": [
147
+ "\t\tconst previous =\n\t\t\tarmProvenanceFor(eventName) !== null\n\t\t\t\t? readMetaSenderMarker({ backend: \"claude-code\", ownerPid, verifyOwner: false })\n\t\t\t\t: null;"
148
+ ],
149
+ "replace": [
150
+ "\t\tconst previous = readMetaSenderMarker({ backend: \"claude-code\", ownerPid, verifyOwner: false });"
151
+ ],
152
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
153
+ "timeoutSeconds": 300,
154
+ "signature": "[QK:MHSS-RETIRE-ARM-CAPABLE-ONLY]",
155
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
156
+ },
157
+ {
158
+ "claim": "MHSS-RECORDS-SURVIVE",
159
+ "title": "a retirement that takes the RECORD with the marker deletes identity to clean up presence — the store-certification and fresh-cut rules exist because a record is the only address a citizen has",
160
+ "subject": "pi-extensions/meta-bridge-hook.ts",
161
+ "find": ["\t\t\tconst retired = removeMetaReceiverMarker({ gardenId: previous.gardenId, ownerPid });"],
162
+ "replace": [
163
+ "\t\t\tconst retired = removeMetaReceiverMarker({ gardenId: previous.gardenId, ownerPid });\n\t\t\tfs.rmSync(path.join(defaultMetaSessionsDir(), `${previous.gardenId}.meta.json`), { force: true });"
164
+ ],
165
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
166
+ "timeoutSeconds": 300,
167
+ "signature": "[QK:MHSS-RECORDS-SURVIVE]",
168
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
169
+ },
170
+ {
171
+ "claim": "MHSS-EXACTLY-ONE-DELIVERABLE",
172
+ "title": "forcing the watch fact closed refuses the citizen the operator is actually in — fail-closed is not free when it closes on every live session too",
173
+ "subject": "pi-extensions/lib/entwurf-v2-production.ts",
174
+ "find": ["\t\t\townerAlive,\n\t\t\twatchArmed,\n\t\t});"],
175
+ "replace": ["\t\t\townerAlive,\n\t\t\twatchArmed: false,\n\t\t});"],
176
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
177
+ "timeoutSeconds": 300,
178
+ "signature": "[QK:MHSS-EXACTLY-ONE-DELIVERABLE]",
179
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
180
+ },
181
+ {
182
+ "claim": "MHSS-JOIN-SCOPE-OWNERKIND",
183
+ "title": "admitting copilot-extension into the sender-marker join makes every Copilot citizen permanently mailbox-undeliverable: its watch owner is a forked child whose pid never carries a sender marker, so the join can only ever answer no",
184
+ "subject": "pi-extensions/lib/entwurf-deliverability.ts",
185
+ "find": ["export const SENDER_JOINED_RECEIVER_OWNER_KINDS: readonly string[] = [\"claude-code-cli\"];"],
186
+ "replace": [
187
+ "export const SENDER_JOINED_RECEIVER_OWNER_KINDS: readonly string[] = [\"claude-code-cli\", \"copilot-extension\"];"
188
+ ],
189
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
190
+ "timeoutSeconds": 300,
191
+ "signature": "[QK:MHSS-JOIN-SCOPE-OWNERKIND]",
192
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
193
+ },
194
+ {
195
+ "claim": "MHSS-SENDER-START-KEY-VERIFIED",
196
+ "title": "reading the sender marker without the pid-reuse guard lets a dead session's leftover file answer \"which garden does this pid serve now?\" — the join then vouches for a watch nobody holds",
197
+ "subject": "pi-extensions/lib/entwurf-v2-production.ts",
198
+ "find": [
199
+ "\t\t\t((backend: string, ownerPid: number) => readMetaSenderMarker({ backend: requireBackend(backend), ownerPid })),"
200
+ ],
201
+ "replace": [
202
+ "\t\t\t((backend: string, ownerPid: number) =>\n\t\t\t\treadMetaSenderMarker({ backend: requireBackend(backend), ownerPid, verifyOwner: false })),"
203
+ ],
204
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
205
+ "timeoutSeconds": 300,
206
+ "signature": "[QK:MHSS-SENDER-START-KEY-VERIFIED]",
207
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
208
+ },
209
+ {
210
+ "claim": "MHSS-JOIN-BACKEND-EQUALITY",
211
+ "title": "comparing only the garden id lets a marker written by another backend under the same pid vouch for this receiver — two harnesses can key markers under one process",
212
+ "subject": "pi-extensions/lib/entwurf-deliverability.ts",
213
+ "find": [
214
+ "\treturn !!senderMarker && senderMarker.gardenId === marker.gardenId && senderMarker.backend === marker.backend;"
215
+ ],
216
+ "replace": ["\treturn !!senderMarker && senderMarker.gardenId === marker.gardenId;"],
217
+ "gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
218
+ "timeoutSeconds": 300,
219
+ "signature": "[QK:MHSS-JOIN-BACKEND-EQUALITY]",
220
+ "signatureSource": "scripts/check-meta-hook-session-switch.ts"
221
+ }
222
+ ]
223
+ }