v0.10.0
The first meta-bridge release (#30): garden-native async delivery into already-running, already-authenticated native coding-agent sessions — Claude Code only. Built bottom-up — a deterministic drift sentinel protecting the (then-undocumented) async-delivery path, the backend-agnostic meta-record authority (schema + pure functions), the idempotent fs upsert, the Claude SessionStart create/attach hook, and entwurf_send mailbox delivery + read-receipt — then the sender/addressee identity path (a native session becomes both wakeable AND a trusted, replyable sender) — and on top of it the operator surfaces: honesty gate (Phase 1), stateful install/uninstall/doctor (Phase 2), the garden-identity statusline (Phase 3), the listing-only meta-store prune janitor (Phase 4), and native sender identity + addressee delivery (Phase 5). A final cut-time fix closed the last identity asymmetry: entwurf_self now resolves trusted meta-session identity too, so a native Claude Code garden citizen can self-report the same replyable garden-id envelope it uses for entwurf_send. Hardening landed in the same cut: the doctor's drift surface is fail-loud again (a set -e early-death regression closed + gated), and #34's level-triggered body-drain robustness basis is now an asserted gate, not just a doc claim. The 0.9.0 evidence-closure entries below also strengthen two live gates so 0.9.0's guarantees are proven directly rather than indirectly and trim a stale follow-up. Scope stays Claude Code only; agy/Codex remain proven future adapters, not shipped surfaces.
Added (0.10.0 meta-bridge — Phase 5 native sender identity + addressee delivery)
entwurf_sendlearns WHO a native session is — authoritatively, not by cwd guess. Phases 1–4 made a native Claude session addressable (the receiver half: wakeable by garden-id, inbox-readable). Phase 5 closes the sender half. A native session sending through the user-scopepi-tools-bridgeMCP has noPI_SESSION_ID, so the bridge resolves identity from a sender marker theSessionStarthook writes, keyed by the shared Claude parent pid (process.ppidof the MCP child IS the Claude process the hook ran under — never cwd inference;PI_META_SENDER_MARKERoverrides for tests). A trusted marker promotes the send to a replyablemeta-sessionsender addressed by its garden-id. The marker carriesownerPid+ a boot-uniqueownerStartKey, so a pid-reuse by an unrelated process fails the guard instead of granting a wrong-identity send, and a marker with no backing meta-record is refused.PI_TOOLS_BRIDGE_REQUIRE_META_SENDER=1(set by the Claude user-scope install) closes the anonymous-send hole: a send with neither pi-session identity nor a trusted marker is refused rather than going out as anonymousexternal-mcp— "if we don't know who sent it, we don't send it." The sender envelopeorigingains"meta-session"; the receiver render shows a[meta-session]badge. Deterministic E2E insmoke-meta-sender-identity(A→B garden-id sender, B→A reply, PPID path, unbacked-record reject, pid-reuse mismatch reject, anonymous reject); the mailbox round-trip (send → enqueue + signal →entwurf_inbox_read→.readarchive +lastReadAtreceipt) is covered E2E bysmoke-meta-mailbox. Both gates are inpnpm check.entwurf_resumeasync-capability discriminant fixed:origin === "pi-session", notreplyablealone. A meta-session isentwurf_send-replyable (it owns a garden-id mailbox) but has no pi control socket, so it cannot host an async-resumefollowUpany more than an external host can. The old auto-resolve keyed onreplyableand so routed a meta-session resume into a control-socket lookup that always fails. The async discriminant is nowasyncCapable = replyable && origin === "pi-session": a meta-session (like an external host) auto-resolves to sync, and an explicitmode="async"from either is rejected with the canonical reason text.check-async-resume-gateadds the meta-session cases (now 19 assertions).- Preventive keyset-overlap guard (
check-keyset-overlap+smoke-meta-keyset-guard). The meta-bridge install owns a fixed set of~/.claude/settings.json(+~/.claude.json) keys (SSOT:meta-bridge-state.py managed-keys); agent-config and any future consumer merge their own fragment into the same file. This is the preventive half (doctor'sstate.py checkis the after-the-fact survival half): it fails loud when a consumer fragment collides with a pi-owned key — exact match OR ancestor/descendant — so a lateragent-configjq merge (.[0] * .[1], which replaces arrays and overwrites scalars) cannot silently clobber pi-owned policy or vice-versa.smoke-meta-keyset-guardproves a disjoint fragment passes and exact/array/parent-child collisions fail loud while unrelated sibling keys (permissions.defaultMode,language) stay clean; inpnpm check.
Fixed (0.10.0 meta-bridge — cut-time identity symmetry)
entwurf_selfcloses the last pi-only identity gate for garden-native meta-sessions. Phase 5 made native Claude Code sends replyable by garden id, but the introspection twin still used the old strict pi-env path and threw whenPI_SESSION_ID/PI_AGENT_IDwere absent — even when the same MCP process had a trusted sender marker.entwurf_selfnow uses the same authoritative identity resolver asentwurf_send: pi sessions returnorigin="pi-session"+socketPath; trusted meta-sessions returnorigin="meta-session",agentId="meta-session/claude-code",replyable=true, the garden id, andmailboxPath; plain anonymous external hosts still fail because they have no authoritative reply address. This closes the cut-time asymmetry: a garden-native Claude Code session can now both send as and self-identify as a replyable garden citizen. Regression coverage lives insmoke-meta-sender-identity, and the fix was live-confirmed fromagent-configvia native Claude Code (entwurf_selfreturned the session's garden id andreplyable: true).
Fixed (0.10.0 meta-bridge — doctor fail-loud drift detail)
doctor-meta-bridgesurfaces a managed-config drift instead of dying silently. The[managed config state]section captured thestate.py checkoutput with a bareCHECK_ERR="$(… check …)"assignment underset -euo pipefail. The instantcheckexited nonzero (drift), the assignment trippedset -eand the doctor exited 1 after printing only the section header — the very "which key drifted" detail the section exists to print was lost, and every later section ([plugin install], [meta-record store], the SILENT-MISS guard) never ran. The substitution is now the condition of anif, so its nonzero status is consumed (not a death) and the drift detail + the entire rest of the chain always print. A hermetic regression insmoke-meta-install-stateforces a real managed-config drift behind a fully-faked claude toolchain (no real claude, no real install) and proves the doctor (a) still exits 1, (b) printsDrift detail:, (c) names the concrete drifted key, and (d) runs to its final summary line — i.e. no earlyset -edeath anywhere. Negative-tested: re-introducing the bare assignment fails the regression on exactly those assertions.
Added (0.10.0 meta-bridge — #34 D8 level-triggered drain gate)
check-meta-sessionasserts the level-triggered body-drain robustness basis (#34). The wake signal (inbox.signal) is edge-triggered — rapid pokes can coalesce into oneFileChanged, or a signal can be lost entirely — but each message body is a separate level-state file on disk, so onereadMetaInboxdrains the whole directory. #34 names this theD8robustness basis and asks the gate to assert it, not just letDELIVERY.mdclaim it. A new case enqueues three messages (two fresh.msg+ one doorbell-rung.msg.delivered) and proves a single drain returns all of them, in deterministic chronological (sorted) order, with onelastReadAtreceipt for the batch, every body archived to.read, and a re-read empty. Negative-tested: an edge-triggered "drain one per signal" regression fails on the all-three assertion while the single-message cases stay green.
Added (0.10.0 meta-bridge — Phase 4 prune listing)
./run.sh meta-bridge-prune— listing-only meta-store janitor.doctor-meta-bridgereds on corrupt JSON / duplicatenativeSessionId/ body↔filename drift, but it intentionally does not fail on transcript-gone records — so a green store still silently bloats with abandoned meta-records as native sessions come and go. This separate hygiene surface scansdefaultMetaSessionsDir()(override with a positional dir; stale window via--ttl-days, default 30) and classifies every record into orphan (parse OK buttranscriptPathno longer exists — a strong abandonment signal, not proof: a backend path migration / cleanup / config-dir change can also vacate it), stale (parse OK, transcript present,lastSeenolder than the ttl), ambiguous (corrupt / drift / duplicate — manual-only: the operator decides which authority survives, never a blind rm of a duplicate pair), and keep (live transcript + recent). It prints the exact manualrmcommands for orphan/stale candidates and deletes nothing — no--applyin 0.10.0 (this is the conservative "list, the operator removes" scope; actual GC / TTL automation / a global agent-skill wrapper are deferred). Exit 0 on any scannable store (corrupt records are classified, not fatal); a missing store is a clean 0-record listing. The offlinesmoke-meta-prunegate builds a synthetic store covering every class and proves correct classification, exit 0, and the no-deletion invariant (wording + on-disk file count unchanged); it is inpnpm check.
Added (0.10.0 meta-bridge — Phase 3 statusline)
- Repo-owned Claude Code statusline with garden identity.
scripts/meta-bridge-statusline.shpreserves GLG's existing Claude statusline data (device, shortened cwd with highlighted tail, git branch, model letter, context usage) and renders it as a documented Claude Code multi-line status area: row 1 is<device> <cwd> [branch], row 2 is the meta-bridge truth surface plus runtime summary,🪛 <garden-id> cc | <model> | <context>. The garden id is resolved on every render by scanning meta-record bodies for the native Claudesession_id(nativeSessionId), never by filename, cache, or DB; no match falls back to?, missingsession_idtoready, and duplicate matches to!while doctor remains the fail-loud surface. Before implementation, the join key was live-measured: a real Claude statusline input carriedsession_id=f232cc4a-29a9-42d9-8295-e4e3707c0c40, which matched meta-record20260606T133915-418d94.meta.jsonby body. - Install/doctor now own and verify
statusLine. The Phase-2 state manager snapshots/restoressettings.json.statusLine, applies the repo-owned statusline command, and checks for drift.doctor-meta-bridgeverifies the command path, executability, and a synthetic two-row statusline run;smoke-meta-install-statecovers state capture/restore plus statusline garden-id / no-record / no-session / duplicate fallbacks and exact two-row output.
Added (0.10.0 meta-bridge — Phase 2 stateful install/doctor)
- Stateful Claude Code install/uninstall for the meta-bridge.
install-meta-bridgenow gates onpython3as a first-class runtime dependency (the FileChanged doorbell parses hook JSON with it), snapshots the operator's pre-install values into${CLAUDE_CONFIG_DIR:-~/.claude}/pi-shell-acp.install-state.json(mode0600), then asserts only the repo-owned keyset:enabledPlugins["entwurf-meta-receive@meta-bridge-local"],extraKnownMarketplaces["meta-bridge-local"], USER-scopepi-tools-bridgeMCP, the single-driverpermissions.allow/denyadditions,env.DISABLE_AUTOCOMPACT, and the Claude single-driver scalar policy (cleanupPeriodDays=365, prompt/away/memory/auto-compact/progress/plan-mode toggles pinned off,skipDangerousModePermissionPrompt=true,verbose=false). Newuninstall-meta-bridgeis the honest inverse: it preflights the state before touching Claude plugin/MCP registrations; scalar/map keys restore their original value or disappear if originally absent; permission arrays remove only the items pi-shell-acp added and preserve user additions. Without state it refuses to guess and performs zero Claude-side removals. A legacy-migration path treats exact pre-Phase-2 plugin/marketplace/MCP values as pi-owned absent values so GLG's already-dogfooded install can uninstall cleanly, while policy keys remain user-owned. - Doctor now consumes the Phase-1/2 blockers fail-loud.
doctor-meta-bridgevalidates the state file + managed keyset, fails whenmeta-bridge-hook.logshows an unrecoveredERROR(only a laterINFO armed watchclears a transient miss; degradedUserPromptSubmitrecord backfill does not; a store-blocked miss that keeps re-logging ERROR stays red — the append-only log never goes stale-red on a one-time, since-healed failure), checkspython3, and runs a full meta-record store scan viameta-bridge-store-doctor.tsfor corrupt JSON/schema, duplicatenativeSessionId, body↔filenamegardenIddrift, and backend↔wakeMode contradiction. The offlinesmoke-meta-install-stategate covers state capture/apply/uninstall, no-state refusal, legacy migration, and store-doctor failure modes, and is now inpnpm check.
Added (0.10.0 meta-bridge — step 3 fs upsert)
upsertMetaSession— the idempotent filesystem upsert (#30 step 3). Wraps the step-2 pure core with the real filesystem:mkdir -pthe store →readdir→scanByNativeId→decideUpsert→ atomic write (tmp file + rename, mode0600) so a crash never leaves a half-written record (the #30 "write the record before the session takes over" crash-safety gate). Idempotent end to end: the second call for the samenativeSessionIdattaches the same file/garden-id (lastSeen refreshed, no shadow record), and a duplicatenativeSessionIdalready on disk throws rather than silently picking one.defaultMetaSessionsDir()resolves to<pi-agent-dir>/meta-sessions— honoringPI_CODING_AGENT_DIR(overridePI_META_SESSIONS_DIR) so an isolated install/test isolates its meta-records exactly like pi's own sessions, rather than a bare~/.pi/meta-sessions. The function lives insidemeta-session.ts(not a sibling*-store.ts): the typecheck fence forbids a root-config lib importing another.tslib via a.tsspecifier while the.jsspecifier is unresolvable undernode --experimental-strip-types, so a separate store file could not be exercised by the deterministic strip-types gate; only node builtins were added, keepingcheck-meta-sessionstrip-types clean (now 38 assertions, 5 real-fs temp-dir). The thin CLI/argv shell that invokes this is deferred to step 4, where its stdin contract couples to the ClaudeSessionStartpayload.
Changed (0.10.0 meta-bridge — drift sentinel pin policy)
smoke-meta-async-driftpins the backend MAJOR.MINOR line, not the exact patch. Claude ships ~weekly (observed 2026-06-05: 2.1.163 → 2.1.165 the same day, all 9 binary markers unchanged), so an exact-patch pin screamed on every bump and the signal was lost. The version check (A) now compares onlymajor.minor(Claude 2.1.x / codex-cli 0.136.x / agy 1.0.x) and a minor/major move is the real "re-verify markers + Gotchas + raw/LIVE probes" trigger; the binary-marker cross-validation (B) now resolves and scans the actually installed patch binary rather than a hardcoded version path. Patch drift within a pinned minor passes; a minor/major move still screams with exit 1 (negative-tested).
Added (0.10.0 meta-bridge — step 2 record authority)
pi-extensions/lib/meta-session.ts+./run.sh check-meta-session— the meta-record authority (#30 step 2, "record authority FIRST, hook LAST"). A meta-session is the bib card for a native backend session (Claude Code / Antigravity / Codex) that has no pi JSONL of its own: an opaque.meta.jsonpointer that makes the native session a garden citizen — addressable + wakeable by a garden id — without pretending pi owns its transcript (Hard Rule #8). This step is pure functions + types only (no fs authority, no hook, no CLI — those are steps 3/4), so the schema and the per-backend adapter seam get cut backend-agnostically before any "hook = Claude Code" assumption can ossify.mintMetaRecordstamps a fresh garden id +createdAt==lastSeen+ a delivery slot seeded from the backend descriptor;serializeMetaRecordis deterministic (stable key order, 2-space, trailing newline);parseMetaRecordcrashes-not-warns on every malformed shape (MetaRecordError), including a backend↔wakeMode contradiction (a Claude record claimingdirect-inject, or vice-versa, is corrupt — delivery mode is backend-determined);scanByNativeIdis THE lookup authority — it scans record bodies by top-levelnativeSessionId(the.meta.jsonanalog of 0.9.0findSessionFileById), proven against a decoy filename in a real temp dir so it can never regress to filename-parse or a derived index, and it scans to completion and throws on a duplicatenativeSessionId(authority ambiguity is fail-fast, never silently pick one);decideUpsertkeys on record existence (idempotent create-then-attach, never a second id) and refuses backend/identity drift. The read-receipt aspect is pre-drilled (delivery.lastEnqueuedAt/lastDeliveredAt/lastReadAt+markEnqueued/markDelivered/markRead) so the later mailbox/send path never touches the schema twice (bbot review #4); the three-backend seam is declared up front (META_BACKENDS+META_BACKEND_DESCRIPTORSwith honestwakeModeself-fetch-vs-direct-inject /deliveryLevel). Deterministic gate: 33 assertions, wired into thepnpm checkstatic floor.- Garden-id grammar consolidated into a real
.jsleaf (pi-extensions/lib/session-id.js).SESSION_ID_RE/formatSessionTimestamp/generateSessionId/isValidSessionIdmoved out ofentwurf-core.tsinto a dependency-free.jsleaf, following theprotocol.jspattern (resolvable identically from both the tsc-emit path and thenode --experimental-strip-typespath — a literal.jsspecifier that pure unit gates can import, which a.tssibling import cannot satisfy under strip-types without breaking the root tsc emit).entwurf-coreimports and re-exports them, so existing importers are untouched and the id grammar is now a true single source instead of one-copy-per-importer.check-entwurf-session-identitystays 158/158 (no regression).
Added (0.10.0 meta-bridge — step 1 drift sentinel)
./run.sh smoke-meta-async-drift— drift sentinel + capability gate (#30 step 1). The Claude async-delivery path rides on undocumented Claude Code behavior (asyncRewakeforce-prependsStop hook feedback:and ignoresrewakeMessage; the payload channel is stderr-only;watchPathsarms from onlySessionStart/CwdChanged/FileChanged). Claude ships ~weekly, so the path can break silently on any bump. This gate makes it scream instead — direct lineage of the 0.8.x fail-fast tool-surface gates. Two tiers, mirroringsmoke-compaction-policy: the deterministic default (offline, free, CI/pre-commit safe) asserts (A) the three backends are on their pinned major.minor lines — Claude 2.1.x / codex-cli 0.136.x / agy 1.0.x (patch is intentionally not pinned: Claude ships ~weekly — observed 2.1.163 → 2.1.165 same day with all 9 markers unchanged — so an exact-patch pin screams every bump and loses the signal; a minor/major move is the real re-verify trigger and does scream; the#30prose "agy 0.136" was a conflation with codex's version) — and (B) nine undocumented-behavior marker strings are still present in the actually installed Claude binary (binary cross-validation; a marker dropping to zero = the behavior was renamed/removed = the delivery path is dead). LIVE=1 adds (C) the pluginSessionStartwatch-arm probe (repro-plugin-idle-wake.sh probe, one meteredclaude -p). Negative-tested: a moved pin or a vanished marker yieldsDRIFT DETECTED+ exit 1. Not yet wired intorelease-gate— it asserts on the host's installed Claude binary (environment-dependent), so it stays out of the hermeticpnpm checkfloor; promotion into the aggregate gate waits for the 0.10.0 cut.
Added (verification docs)
DELIVERY.mddefines native async-delivery capability levels (D0–D8) for live external sessions. This gives Claude Code, Antigravity/agy, Codex, and pi-native Entwurf a shared diagnostic coordinate system for "can an already-running session receive async work?" without collapsing transport-specific facts into a vague works/doesn't-work claim. Companion raw probes live underscripts/raw-async-delivery/; current evidence records Claude CodeFileChanged/watchPaths/asyncRewakeidle wake, agy nativesend-message, and Codex direct-TUI vs app-server split.
Changed (test harness — evidence closure)
cross-cwd-resume-smokenow asserts append-not-recreate at the file/id level (T5), not just by recall. The cross-cwd resume gate (verify-resumePhase 2) proved the issue-#9 fix semantically — the sentinel was recalled across the cwd boundary — but never directly asserted that the resume appended to the one true session file rather than silently minting a shadow session in the resumer's cwd. Around the existing recall, the smoke now captures a structural baseline after spawn and re-checks it after resume: (a) exactly one session file carries the header id before and after (no shadow minted anywhere), (b) it is the same file, appended in place (turn count grew), (c) the header id and cwd never drifted (resume authority stays = header, never the resumer's process cwd), and (d) no session for that id exists under the resumer's (wrong) cwd session dir. Live-verified: spawn at a scratch project dir, resume from$HOME, same file appended (turns 1→2), header id/cwd stable, no shadow under the resumer's ($HOME) session dir.smoke-resident-garden-guardnow directly proves the resume-into-uuid friendly pre-cancel (0 tokens). Thesession_before_switchreason"resume"non-garden pre-cancel was previously only backstopped by thesession_starthard guard — the friendly path was never exercised on its own. A new RESUME-INTO-UUID section drives an in-process RPCswitch_sessioninto a SYNTHETIC legacy-uuid session file (a one-line{type:"session", id:<uuid>}header is enough, because runtimeswitchSessioncallsemitBeforeSwitch("resume", path)BEFORESessionManager.open). It asserts the switch is cancelled (cancelled:true), the friendly "resume is blocked … not garden-native" guidance lands on stderr, the hard guard never fires, 0 tokens (noagent_start), the resident stays on its garden id, and no control socket boots for the uuid. The 0-token sweep is now 30/0 (NEGATIVE + REPLACEMENT + RESUME-INTO-UUID + GNEW).
Removed (follow-up hygiene)
- Dropped the stale "semantic-memory
_entwurf-guidance refresh" follow-up fromNEXT.md.agent-configskills/semantic-memory/SKILL.mdwas already migrated to garden-native discovery in 0.9.0 (no_entwurf-filename species; identity in the JSONL header/name;--session-file-containsreframed as a generic path filter), so the carried item no longer described reality.
Verification
- Final release-prep evidence before the 0.10.0 cut:
pnpm checkPASS, then./run.sh release-gate /tmp/psa-release-gate-0.10.0.EysLWpPASS from log/tmp/pi-shell-acp-release-gate-0.10.0-20260606T184217.log—PASS=17 FAIL=0 SKIP=0with Gemini present. Artifacts:smoke-async-resume/tmp/smoke-async-resume-20260606-184348.json, sentinel/tmp/sentinel-20260606-184958.json(log dir/tmp/sentinel-20260606-184958), session messaging/tmp/session-messaging-smoke-20260606-185236.json.