v0.9.0
0.9.0 is the garden-native identity release. This is not just an Entwurf handle rename: the garden's own denote-style naming scheme is imported into the session layer so Entwurf sessions stop being treated as a separate species of worker artifact. Resident sessions, Entwurf children, and the later 1.0.0 meta-bridge direction all converge on the same garden session ontology — one durable sessionId, one human-readable and machine-parseable name surface, one rule that the session comes first and the transcript file is only its trace.
Changed (breaking — Entwurf public handle)
- Entwurf public handle is now
sessionId, nottaskId(atomic migration, Phase 3b of #28 / 0.9.0). The garden-native session idYYYYMMDDTHHMMSS-[0-9a-f]{6}(= JSONL headerid) replaces the old 8-hextaskIdacross the entire local Entwurf public surface in one slice — there is no compatibility shim and a saved-session handle from a pre-migration spawn will not resolve:- Spawn (
runEntwurfSync+ native asyncrunEntwurfAsync): the parent generates the sessionId (generateSessionId), pre-checks for collision (assertSessionIdAvailableForSpawn), builds the denote-style session name (buildSessionName, taggedentwurf), and spawns withpi --session-id <id> --name <name>. The*_entwurf-<taskId>.jsonlfilename species is gone — Pi names the file<created-at>_<sessionId>.jsonl. - Resume (
runEntwurfResumeSync+ asyncspawnEntwurfResumeAsync): looked up by header scan (findSessionFileById), child cwd forced to the saved header cwd, and continued withpi --session-id <id>(appends to the same session). Resume keeps the same durablesessionId; a per-process internal/diagnosticrunIddistinguishes resume runs (never a public handle). - Resume identity authority = first
model_change. NewreadSessionIdentityreads the session's FIRSTmodel_change(provider + modelId) as the locked model identity — not the last assistant message'smodelfield. A later differingmodel_change(drift) or a corrupt session-name mirror (name sessionId/provider/model disagreeing with the header / first model_change) isSessionIdentityErrorfail-fast. - Entwurf-resume is gated on the
entwurfname tag. Since the*_entwurf-<taskId>.jsonlspecies is gone, "is this an Entwurf session?" is now answered by the session name'sentwurftag (requireEntwurf): a general pi session — nosession_infoname, a non-canonical name, or a canonical name without theentwurftag — is refused at resume. No compatibility path. - Surfaces migrated together:
EntwurfResult.sessionId,formatSyncSummary(Session ID:), nativeentwurf/entwurf_resume/entwurf_statustool schemas + result text,entwurf-asyncactive map (keyed by sessionId) / ack / completion payloads, thespawn_async_resumecontrol RPC, the MCP bridgeentwurf/entwurf_resumeZod schema + descriptions, and the cross-cwd / compaction / async-resume / sentinel / MCP-test smokes.
- Spawn (
- Remote/SSH entwurf is out of scope and fails fast (#11). The garden-native sessionId collision pre-check and header-scan resume are local-filesystem only, so spawn/resume/status with a non-
localhost throwsSessionIdentityErrorup front. Remote identity is a later phase.
Changed (breaking — resident --entwurf-control session must be garden-native)
- Every
--entwurf-controlsession is now garden-native or it hard-exits (#28 / 0.9.0 — operator session, not just Entwurf children). Garden identity closes over the operator's own session too: when--entwurf-controlis enabled, the session headeridMUST be a garden sessionId (YYYYMMDDTHHMMSS-[0-9a-f]{6}). pi mints auuidv7when the launcher did not pass--session-id, so a non-garden id means the session was not born through the garden launcher —entwurf-controlrefuses it atsession_startandprocess.exit(1)s before any model turn. No uuid / back-compat path. (A barethroworctx.shutdown()in asession_starthandler is swallowed by pi's extension runner — verified live that the model turn still ran and leaked 26k tokens — so the guard hard-exits.)- Garden launcher. Launch resident sessions through the launcher so the id is injected up front:
pi --session-id "$(<repo>/run.sh new-session-id)" --entwurf-control ….run.sh new-session-idprints one fresh garden sessionId from thegenerateSessionIdSSOT (no shell-side format duplication). See README §Garden launcher. /gnew(/garden-new) starts a fresh garden session in the same terminal. Builtin/newremains blocked because pi'sctx.newSession()mints a uuid before any extension can re-stamp it./gnewuses the safe path instead: a fail-closed writer pre-creates a valid garden session JSONL header, then the command callsctx.switchSession(file), whoseSessionManager.open()reads that garden id beforesession_start. Header, control socket, backend streamsessionId, and MCP-childPI_SESSION_IDtherefore all bind to the new garden id with no torn uuid moment. If the operator quits before the first turn, the empty session remains visible with message count 0; it is a legitimate resident session, not an orphan.- Status label is the screwdriver 🪛, not the word "entwurf". The resident status reads
🪛 readybefore the first assistant turn (session file not yet on disk — model still changeable) and🪛 <gardenId>after (file written = model locked). The id's presence is the model-lock lifecycle signal. The status label is decoupled from the session-name tag (the word "entwurf" no longer appears in the status bar, so it can't be misread as "talking to an entwurf'd session"). - Resident session name is lazy and tagged
control, neverentwurf. On the first turn (model now locked)entwurf-controlsets a garden name viapi.setSessionName(buildGardenSessionName(...))with thecontroltag and the cwd basename as title.buildGardenSessionNameis registry-FREE (a native model likedeepseek/deepseek-v4-prothat is not an Entwurf spawn target passes, where the childbuildSessionNamewould throw) and FORBIDS theentwurftag — so a resident session is never resumable as an Entwurf child viaentwurf_resume(theentwurftag is that resume marker). - Coverage: deterministic
check-entwurf-session-identity(now 158 assertions) coversassertGardenNativeSessionId(uuid→throw / garden→pass),buildGardenSessionName(registry-free native model,entwurftag forbidden, round-trip),computeResidentStatusLabel(🪛 ready / 🪛 id), the regression that acontrolsession is NOTentwurf_resume-able, and the/gnewwriter's fail-closed guarantees (wx, collision refusal, full read-back, guarded orphan cleanup). Livesmoke-resident-garden-guardproves the negative (raw uuid → nonzero exit, no turn, no socket, 0 tokens), replacement safety (builtin/new//clonecancelled, not hard-exit),/gnew0-token E2E (new garden id, socket rebound, no uuid leak), and, opt-in, backend identity after/gnew(entwurf_selfreports the new garden id).
- Garden launcher. Launch resident sessions through the launcher so the id is injected up front:
Changed (release-gate + test harness)
release-gatenow runs the two garden-native identity gates first.smoke-session-id-name(Phase 3a — Pi--session-id/--namesubstrate through the bridge) andsmoke-resident-garden-guard(Phase 3c — the resident--entwurf-controlguard, NEGATIVE 0-token path) run before the Entwurf live gates so an identity-foundation break fails fast instead of surfacing as confusing downstream failures. Both take no project arg and are exempt from the scratch-isolation concern by construction: the substrate smoke runs every pi turn under its ownos.tmpdir()agent dir + cwds (mkdtemp, cleaned up), and the guard's negative path writes no session file at all.smoke-async-resumecompletion detection hardened against a lazy-persist false-negative. pi persists a parent session file only at the first assistant turn-end, and a slow orchestrator can still be mid-turn long after the resume child finished — so the previous singlefind_parent_session_filelookup at completion-check time could miss a parent JSONL that was about to appear, recording FAIL even thoughentwurf-asynchad already delivered+persisted theentwurf-complete(🏁) CustomMessage. The completion phase now re-resolves the parent file every tick and polls its persistedentwurf-completecount (tmux pane is the secondary fast-path channel); fail-closed is preserved (no detected completion → FAIL). Removed the now-unusedwait_jsonl_count_gthelper. Test-harness only; no runtime behavior change. Product was already correct — verified by RESUME_OK in every resume child plus the persisted 🏁 in every parent across all three backends.check-native-asyncexercises a LOCAL async spawn instead of a bogus remote host. The native async spawn smoke usedhost="__native_async_smoke_bogus__"to enterrunEntwurfAsynccheaply, but the 0.9.0 remote-out-of-scope fail-fast (#11) now rejects any non-localhost beforerunEntwurfAsyncruns — so the bogus-host call no longer exercised the async path at all (and failed the gate). The smoke now spawns a local async entwurf, which both matches the 0.9.0 scope and actually drivesrunEntwurfAsyncfor the stale-explicitExtensionsReferenceError guard it exists to catch.
Verification
- The authoritative
/gnew-inclusive 0.9.0 release-gate is green and recorded inBASELINE.md: a cut-time pi-session./run.sh release-gaterun, 17 PASS / 0 FAIL / 0 SKIP (Gemini present, no--allow-skip-gemini), with the resident garden guard at 31/0 (negative + replacement +/gnew0-token E2E + positive/T3) andcheck-entwurf-session-identityat 158 assertions. It supersedes the earlier pre-/gnewClaude Code sweep that/gnewhad invalidated. - The async-resume repair was confirmed in isolation (6 PASS / 0 FAIL across Claude/Codex/Gemini + direct-stdio + external negative paths) before the full gate cycle;
/gnewadds its own deterministic writer coverage plus live resident-guard smoke coverage. - Backend-axis note (Hard Rule #7):
/gnewT3 backend identity was live-measured on the release-gate default Claude lane (claude-sonnet-4-6) only. Codex/Gemini/gnewT3 runs are carried forward inNEXT.md; the general runtime matrix still remains covered bysmoke-allacross all three backends.