@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.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +248 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +14 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +24 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +61 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
- package/mcp/entwurf-bridge/src/index.ts +16 -8
- package/package.json +3 -2
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -1
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +13 -8
- package/pi-extensions/lib/entwurf-deliverability.ts +104 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
- package/pi-extensions/lib/entwurf-facts.ts +52 -3
- package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -6
- package/pi-extensions/lib/entwurf-v2-send.ts +26 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +24 -3
- package/pi-extensions/lib/meta-session.ts +76 -7
- package/pi-extensions/meta-bridge-hook.ts +72 -3
- package/run.sh +21 -0
- package/scripts/check-bridge-delivery.ts +42 -1
- package/scripts/check-entwurf-fact-provider.ts +22 -1
- package/scripts/check-entwurf-facts.ts +17 -2
- package/scripts/check-entwurf-peers-surface.ts +20 -2
- package/scripts/check-entwurf-self-address.ts +20 -2
- package/scripts/check-entwurf-v2-decider.ts +6 -3
- package/scripts/check-entwurf-v2-mailbox.ts +33 -0
- package/scripts/check-entwurf-v2-matrix.ts +10 -5
- package/scripts/check-entwurf-v2-production.ts +72 -1
- package/scripts/check-entwurf-v2-runner.ts +18 -0
- package/scripts/check-entwurf-v2-send.ts +13 -1
- package/scripts/check-entwurf-v2-surface.ts +75 -3
- package/scripts/check-gate-qualification.ts +1 -0
- package/scripts/check-hook-launch-topology.ts +70 -1
- package/scripts/check-mailbox-receipt-state.ts +6 -0
- package/scripts/check-meta-doctor-oracle.sh +73 -0
- package/scripts/check-meta-hook-session-switch.ts +560 -0
- package/scripts/check-meta-mailbox-state-write.ts +9 -2
- package/scripts/check-meta-manifest-schema.py +19 -1
- package/scripts/check-meta-session.ts +10 -2
- package/scripts/meta-bridge-doctor.sh +47 -7
- package/scripts/meta-bridge-state.py +23 -5
- package/scripts/meta-bridge-statusline.sh +65 -2
- package/scripts/mutants/meta-hook-session-switch.json +223 -0
- package/scripts/raw-async-delivery/README.md +280 -9
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +6 -2
- package/scripts/raw-async-delivery/lab-statusline.sh +63 -0
- package/scripts/raw-async-delivery/mailbox-watch.py +230 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +3 -1
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +13 -4
- package/scripts/raw-async-delivery/probe-delivery-transparency.sh +387 -0
- package/scripts/raw-claude-session-switch/README.md +170 -0
- package/scripts/smoke-acp-cortex-live.ts +22 -2
- package/scripts/smoke-acp-v2-send-live.ts +23 -2
- package/scripts/smoke-entwurf-chain-live.ts +85 -13
- package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
- package/scripts/smoke-meta-async-drift.sh +31 -8
- package/scripts/smoke-meta-install-state.sh +170 -11
- package/scripts/smoke-meta-keyset-guard.sh +4 -1
- 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']}
|
|
475
|
-
"
|
|
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
|
-
|
|
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
|
|
725
|
-
//
|
|
726
|
-
//
|
|
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:
|
|
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
|
|
70
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|