@junghanacs/entwurf 0.12.8 → 0.12.9

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 (52) hide show
  1. package/AGENTS.md +4 -4
  2. package/CHANGELOG.md +20 -1
  3. package/DELIVERY.md +1 -1
  4. package/README.md +122 -10
  5. package/demo/README.md +2 -2
  6. package/docs/setup-clean-host.md +14 -3
  7. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +9 -15
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +12 -12
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +1 -1
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +8 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +15 -4
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +483 -34
  14. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +8 -3
  15. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +14 -2
  16. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +155 -29
  17. package/mcp/entwurf-bridge/src/index.ts +10 -12
  18. package/package.json +7 -7
  19. package/pi-extensions/entwurf-control.ts +12 -12
  20. package/pi-extensions/lib/acp/models.ts +12 -12
  21. package/pi-extensions/lib/entwurf-fact-provider.ts +9 -2
  22. package/pi-extensions/lib/entwurf-v2-production.ts +8 -2
  23. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +8 -2
  24. package/pi-extensions/lib/meta-sender-identity.ts +15 -5
  25. package/pi-extensions/lib/meta-session.ts +526 -38
  26. package/pi-extensions/meta-bridge-hook.ts +8 -2
  27. package/run.sh +30 -26
  28. package/scripts/agy-imprint.ts +15 -1
  29. package/scripts/check-acp-session-reuse.ts +1 -1
  30. package/scripts/check-acp-session-store.ts +3 -3
  31. package/scripts/check-agy-sender-identity.ts +83 -0
  32. package/scripts/check-entwurf-control-rpc.ts +2 -2
  33. package/scripts/check-entwurf-fact-provider.ts +9 -1
  34. package/scripts/check-entwurf-facts.ts +1 -1
  35. package/scripts/check-entwurf-mailbox-guard.ts +6 -2
  36. package/scripts/check-entwurf-resume-args.ts +6 -6
  37. package/scripts/check-entwurf-session-identity.ts +7 -6
  38. package/scripts/check-entwurf-v2-production.ts +4 -2
  39. package/scripts/check-entwurf-v2-spawn-production.ts +2 -2
  40. package/scripts/check-fresh-cut-gate.sh +305 -4
  41. package/scripts/check-meta-identity-consumers.ts +501 -1
  42. package/scripts/check-meta-listing.ts +91 -9
  43. package/scripts/check-meta-receiver-marker.ts +54 -0
  44. package/scripts/check-model-lock.ts +1 -1
  45. package/scripts/meta-bridge-fresh-cut.ts +164 -28
  46. package/scripts/pi_settings_io.py +65 -0
  47. package/scripts/register-pi-package.py +183 -37
  48. package/scripts/register-pi-provider.py +68 -10
  49. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  50. package/scripts/smoke-meta-install-state.sh +1 -1
  51. package/scripts/smoke-pi-attach.ts +7 -2
  52. package/scripts/smoke-user-scope-citizen.sh +177 -0
@@ -49,6 +49,7 @@ import * as path from "node:path";
49
49
  import {
50
50
  defaultMetaMailboxDir,
51
51
  defaultMetaSessionsDir,
52
+ isPlausibleOwnerPid,
52
53
  type MetaReceiverArmProvenance,
53
54
  upsertMetaSession,
54
55
  writeMetaReceiverMarker,
@@ -140,7 +141,12 @@ const META_HOOK_LAUNCH_TOKEN = "hook-launch/v1";
140
141
  * no token means we do not know what our parent is, so we claim nothing.
141
142
  * 2. A PLAUSIBLE LIVE PARENT. A reparented orphan (ppid 0/1) is not an owner, and
142
143
  * minting a marker for init would be exactly the "blind ancestor" false-positive
143
- * the old ancestry walk existed to prevent.
144
+ * the old ancestry walk existed to prevent. The rule is `isPlausibleOwnerPid`,
145
+ * shared with the other writer and with every reader: this side already refused
146
+ * what all three readers accepted, and one host's leftover `ownerPid: 1` marker
147
+ * then blocked its fresh-cut until the file was deleted by hand (#53 A). A
148
+ * predicate only one layer knows is how that drift happened, so it is no longer
149
+ * written out by hand here.
144
150
  *
145
151
  * Failing closed costs only reply-addressability, and the doctor sees the ERROR. The
146
152
  * opposite — a marker keyed to a transient wrapper — is a lie a sender acts on.
@@ -148,7 +154,7 @@ const META_HOOK_LAUNCH_TOKEN = "hook-launch/v1";
148
154
  function resolveMetaHookOwnerPid(): number | null {
149
155
  if (process.env[META_HOOK_LAUNCH_ENV] !== META_HOOK_LAUNCH_TOKEN) return null;
150
156
  const ownerPid = process.ppid;
151
- if (!Number.isSafeInteger(ownerPid) || ownerPid <= 1) return null;
157
+ if (!isPlausibleOwnerPid(ownerPid)) return null;
152
158
  return ownerPid;
153
159
  }
154
160
 
package/run.sh CHANGED
@@ -90,7 +90,7 @@ Usage:
90
90
  ./run.sh check-meta-mailbox-state-write # deterministic gate (0.11 Stage 0 step 3D-4 commit2): post-cut receipt is state-only — meta-record file byte-identical across enqueue/read, state carries lastEnqueuedAt/lastReadAt (field isolation), empty inbox no-op on record+state, drift surfaces; no API
91
91
  ./run.sh check-meta-receiver-marker # deterministic gate (SE-2): receiver marker round-trip/start-key/provenance, UserPromptSubmit cannot mint presence, reader does not gate on record existence — marker SEMANTICS only; launch topology moved to check-hook-launch-topology
92
92
  ./run.sh check-hook-launch-topology # #51 gate 1: shipped hooks.json is exec form through hook-launch.sh, launcher is loud on an empty argv (older Claude's silent args drop), exec preserves the pid so the hook's parent is Claude, and a space/$/backtick plugin path survives as one argv element
93
- ./run.sh check-meta-identity-consumers # deterministic gate: V3-only consumer seam — read/scan by body, foreign-generation reads name fresh-cut, the strict upsert refuses an unreadable store before any write, G1 duplicate ambiguity throw, no API
93
+ ./run.sh check-meta-identity-consumers # deterministic gate: V3-only consumer seam — per-entry targeted read + addressable read snapshot uniqueness, non-regular rivals never read, drift/unparseable rivals unreachable, unreadable regular rivals fail loud; strict upsert refuses an unreadable store before any write, no API
94
94
  ./run.sh check-meta-capability-source # deterministic gate (0.11 Stage 0 step 3D-3): capability-source cut-over — mint/parse read wakeMode/deliveryLevel from the registry (metaCapabilityFor, registry-driven via injection), not META_BACKEND_DESCRIPTORS; behaviour-preserving (registry ≡ const); the record.delivery slot 3D-3 preserved was deleted by 3D-4, no API
95
95
  ./run.sh check-socket-probe # deterministic gate (0.11 Stage 0, F3): three-valued control-socket liveness (alive|dead|indeterminate) — GC reclaims dead only, indeterminate survives; pure classify + 2-socket integration, no API
96
96
  ./run.sh check-project-trust-handler # deterministic gate (0.11 Stage 0, Trust 2층): project_trust handler — decideProjectTrust matrix (escape=inherited-false+interactive+trust-here→{yes,remember:true}; non-interactive→undecided; never undefined) + adapter single-writer, fake prompt, no UI
@@ -112,7 +112,7 @@ Usage:
112
112
  ./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), owned-outcome→native-push-no-resume-authority, bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
113
113
  ./run.sh check-entwurf-facts # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 1+2): PURE PeerFact core + resolveFactList union — R1 out-of-domain→unsupported, R3b pi 4-value, facts-only keyset; union: PeerFact + RecordLessSocketFact by gardenId (#50 C4: record-less socket = diagnostic subject, gid+liveness only), dormant→dead, F3 indeterminate preserved, non-pi+socket fail-loud; pure, no IO
114
114
  ./run.sh check-socket-discovery # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 3): SOCKET-axis scanSocketProbes — probes (dir sockets) ∪ (in-domain citizen canonical paths) 3-valued; dormant citizen no-file → dead (resumable, not unprobed), stall → indeterminate (F3), dir hygiene/dedup/missing-dir + e2e → resolveFactList; readdir/probe injected, no IO
115
- ./run.sh check-meta-listing # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4a): META-STORE axis listAllMetaIdentities explicit-partial: parse failure / body-filename drift explicit {filename,message} error (verbatim, no synthetic fields), valid records still listed (corrupt doesn't blind); mode strict throws / collect partial; entries/readRecord injected, no IO
115
+ ./run.sh check-meta-listing # deterministic gate: META-STORE facts axis — kind-carrying entries; non-regular records are never read, parse/drift become diagnostics, duplicate nativeSessionId quarantines every rival but not unrelated citizens; strict throws / collect partial; pure injected IO
116
116
  ./run.sh check-entwurf-fact-provider # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4b): ASSEMBLY listEntwurfFacts — listAllMetaIdentities→scanSocketProbes→pre-quarantine non-pi/socket conflicts→resolveFactList(clean)→{facts,diagnostics}; C-원칙: expected corruption (parse/collision)→diagnostics (listing survives), impossible invariant (dup/unprobed)→throw; collision quarantines BOTH PeerFact+socket; deps injected, no IO
117
117
  ./run.sh check-entwurf-peers-surface # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4c): MCP entwurf_peers RENDER renderEntwurfPeers (#50 C4) — payload keyset exactly {peers, diagnostics}; FORBIDDEN keys sessions/socketOnly/controlDir/socketPath/count + no .sock in text (socket is transport, never identity); record-less socket = aggregated record-less-socket diagnostic (F8, liveness-keyed message, alive names fresh-cut); NO verb-routing key (JSON deep scan) NOR word (text), diagnostics both surfaces, empty→(none), unsupported shown; WIRING guard: both surfaces call provider+render, getLiveSessions + /entwurf-sessions gone; facts fabricated, no IO
118
118
  ./run.sh check-entwurf-self-address # deterministic gate (SE-1/SE-2 slice 1): self-addressability honesty predicate computeSelfAddressability — pi replyable ⟺ live socket; meta ⟺ recordBacked ∧ ownerAlive ∧ watchArmed (regression-proof record-present rows); SOURCE GUARD buildStrictPiSenderEnvelope drops hardcoded replyable:true + existsSync-probes socket, entwurf_self renders alive vs expected. meta watchArmed wired in slice 2 (same release block)
@@ -150,13 +150,13 @@ Usage:
150
150
  ./run.sh uninstall-agy-hooks # honest inverse of install-agy-hooks from install-state
151
151
  ./run.sh doctor-agy-hooks # fail-loud doctor for agy hooks.json imprint wiring
152
152
  ./run.sh meta-bridge-prune # 1.0.0 meta-bridge Phase 4: LISTING-ONLY store hygiene — classify orphan/stale/ambiguous/keep, print manual rm commands, delete NOTHING ([dir] [--ttl-days N])
153
- ./run.sh meta-bridge-fresh-cut # the ONE generation verb (the verb every v3-only rejection names): quiesce-check live sockets/markers/native-push conversations (refusing any surface it cannot inspect), archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<ts>`, clear dead transport residue, open an empty v3 generation. No migration, no restore — the archive is forensic only
153
+ ./run.sh meta-bridge-fresh-cut # the ONE generation verb (the verb every v3-only rejection names): quiesce-check live sockets/markers/native-push conversations (refusing any surface it cannot inspect), archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<ts>`, clear dead transport residue, open an empty v3 generation. No migration, no restore — the archive is forensic only. EXIT CONTRACT (#54, `--help` prints it): 0 complete / 1 NOTHING MOVED (re-run, do not setup) / 2 usage / 3 cut transition incomplete (inspect) / 4 cut complete but residue cleanup failed (`setup` may run; re-run only before new citizen birth, otherwise remove residue manually)
154
154
  ./run.sh meta-bridge-managed-keys # 0.10.0 meta-bridge: print the SSOT of settings keys entwurf OWNS (consumers read this to stay disjoint — keyset-owner invariant)
155
155
  ./run.sh check-keyset-overlap <fragment.json...> # 0.10.0 meta-bridge: PREVENTIVE keyset guard — fail if a consumer fragment collides with any pi-owned key (cross-repo; not in pnpm check)
156
156
  ./run.sh check-dep-versions # local deterministic check that the pi pin agrees across package.json (devDeps + peer range), run.sh (peer-install pins), and the baseline docs (AGENTS/README/ROADMAP/setup-clean-host/demo)
157
157
  ./run.sh check-node-floor-coherence # binds the Node floor (24+, single axis) across engines.node, run.sh setup preflight, meta-bridge install/doctor judgment logic, clean-host docs, the bridge launcher header, and the CI runner node-version — engines.node is the SSOT, everything else is derived; sweeps tracked contract text for an unregistered declaration
158
158
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
159
- ./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm check): seeds host states inline (clean/empty/v3-only/previous-generation/malformed), then drives the real `run.sh install` / `setup` at them — a store the live schema cannot read REFUSES before activation writes (persistent regular-file manifest unchanged), the refusal names fresh-cut in both invocation forms, fresh-cut archives the generation (original bytes intact in the archive) and opens an empty one, the retry PASSES, and a live marker/socket/native-push conversation or any surface the cut cannot inspect — makes fresh-cut refuse (quiesce gate). No model/network/cost
159
+ ./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm check): drives real install/setup/fresh-cut in a sandbox; certification refusal is pre-write, quiescence is fail-closed, archives preserve bytes, and the #54 exit matrix distinguishes complete / no-move / usage / incomplete transition / complete-with-cleanup-residue. No model/network/cost
160
160
  ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with the pinned pi peers (0.82.x) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the INSTALLED generation lifecycle on a seeded previous-generation host (REFUSE before activation writes / zero Claude invocations → installed fresh-cut archives + opens empty → install-meta-bridge PASSES)
161
161
  ./run.sh check-install-container # 0.12.8 (#51 C): Linux artifact-CONSUMER gate — one candidate .tgz handed read-only to a checkout-invisible node:<engines-major>-bookworm cell. Default packs once to temp; ENTWURF_CANDIDATE_TGZ=/absolute/preserved.tgz consumes those exact bytes with no re-pack and prints canonical path+sha256 for release. Non-root global PATH install, frozen package, MCP tools/list, fake-Claude install-meta-bridge, path+sha256 fence, strict doctor, and the GENERATION host-state matrix (clean / v3-only store bytes unchanged / previous-generation REFUSE→fresh-cut→retry PASS) seeded inline. Docker missing = honest SKIP; ENTWURF_REQUIRE_DOCKER=1 makes that RED (required CI)
162
162
  ./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
@@ -501,7 +501,8 @@ register_user_scope_citizen() {
501
501
  # exists to hold). Same explicit-global-lifecycle shape as install/uninstall-meta-bridge.
502
502
  # Uses the same is_entwurf_source SSOT + --remove, so it never over-deletes a look-alike
503
503
  # (entwurf-notes, openclaw-entwurf) and preserves every other package/key. Idempotent:
504
- # no entwurf entry → no-op.
504
+ # no entwurf entry → no-op. A settings-relative entry naming this repo is preserved and
505
+ # reported rather than deleted — install never writes that form (#53 B).
505
506
  remove_user_scope_citizen() {
506
507
  local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
507
508
  python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" --remove
@@ -525,7 +526,10 @@ remove_local_package() {
525
526
  preflight_pi_settings_shapes "$project_dir/.pi/settings.json" "$(mktemp -u)"
526
527
  # packages[] cleanup via the shared SSOT — same is_entwurf_source predicate as
527
528
  # install, so remove never over-deletes a look-alike repo (entwurf-notes, …)
528
- # that install would never have registered.
529
+ # that install would never have registered. Same rule, one shape further (#53 B):
530
+ # register only ever WRITES the absolute path, so a settings-RELATIVE entry naming
531
+ # this repo (the committed `".."` in <repo>/.pi/settings.json) is source, not
532
+ # install state — remove leaves it and says so instead of editing tracked bytes.
529
533
  python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR" --remove
530
534
  # entwurfProvider.mcpServers.entwurf-bridge cleanup (project scope) via the shared SSOT: strip
531
535
  # our-managed shapes (the bare stable bin AND the legacy repo start.sh path — a true user
@@ -988,12 +992,12 @@ smoke_entwurf_v2_spawn_live() {
988
992
  smoke_acp_socket_citizen_live() {
989
993
  # S1 acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
990
994
  # Spawns a REAL `pi --entwurf-control` resident on an ACP model
991
- # (entwurf/claude-opus-4-8) and proves it is a first-class socket-citizen:
995
+ # (entwurf/claude-opus-5) and proves it is a first-class socket-citizen:
992
996
  # the control socket stands up, get_info answers with the ACP model (model-lock
993
997
  # did NOT revert — QM1), idle/cwd are reported, and the fail-loud streamSimple
994
998
  # stub never fires (turn-free launch — QM2). No prompt is sent: S1 proves
995
999
  # citizenship, never a backend turn (that is S2). Honest skip when LIVE!=1.
996
- # Model override: ENTWURF_S1_MODEL (default claude-opus-4-8).
1000
+ # Model override: ENTWURF_S1_MODEL (default claude-opus-5).
997
1001
  # LIVE=1 ./run.sh smoke-acp-socket-citizen-live
998
1002
  run_ts scripts/smoke-acp-socket-citizen-live.ts
999
1003
  }
@@ -1442,12 +1446,12 @@ assert.equal(peerTui, piAi,
1442
1446
  // floor tracks the devDep pin so a consumer can't install against a pi lacking
1443
1447
  // the public trust exports the bridge imports at the pinned minor, AND an upper
1444
1448
  // bound at the next minor stops a fresh install from silently pulling a future
1445
- // pi (past the declared ceiling — 0.83+ at the current 0.82.0 pin) whose
1449
+ // pi (past the declared ceiling — 0.83+ at the current 0.82.1 pin) whose
1446
1450
  // internal export surface has drifted from the one we typecheck against.
1447
1451
  // pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
1448
1452
  // factory churn is exactly this), so an open `>=` floor is exactly how the next
1449
1453
  // installer re-acquires the drift. Expected
1450
- // shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.82.0 <0.83`).
1454
+ // shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.82.1 <0.83`).
1451
1455
  const [piMaj, piMin] = piAi.split('.').map(Number);
1452
1456
  assert.equal(piMaj, 0,
1453
1457
  `pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
@@ -1902,13 +1906,13 @@ check_pi_import_surface() {
1902
1906
  # 0.80 moved the standalone root `getModels()` to the deprecated `/compat`
1903
1907
  # entrypoint. This repo's pi-extensions/** are loaded by pi's EXTENSION loader
1904
1908
  # (pi-coding-agent `core/extensions/loader.ts`), whose jiti alias map resolves
1905
- # ONLY three pi-ai specifiers for extensions — the bare root, `/compat`, and
1906
- # `/oauth` all to `ai/dist/compat.js`. A `providers/*` subpath is NOT in that
1909
+ # FOUR pi-ai specifiers for extensions — the bare root, `/compat`, `/oauth`, and
1910
+ # (since pi 0.81) `/providers/all`. Other `providers/*` subpaths are NOT in that
1907
1911
  # map: jiti prefix-matches the bare `@earendil-works/pi-ai` alias and appends
1908
1912
  # the remainder, producing the unresolvable `…/dist/compat.js/providers/
1909
1913
  # anthropic` (verified live: extension load crash — invisible to static
1910
1914
  # typecheck, which resolves against node_modules `exports`). So `/compat` is the
1911
- # sanctioned extension entrypoint for the old global model-catalog API
1915
+ # sanctioned extension entrypoint this repo uses for the old global model-catalog API
1912
1916
  # (lib/acp/models.ts: `getModels`), and the ONLY allowlisted exception. The
1913
1917
  # allow-pattern is closing-quote-anchored (`@earendil-works/pi-ai/compat["'\`]`)
1914
1918
  # so it permits ONLY that exact specifier: `/compat-foo`, `/oauth`, every
@@ -2715,7 +2719,7 @@ _check_pack_install_impl() {
2715
2719
  printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
2716
2720
 
2717
2721
  # pi-agent-core is pinned even though we never import it: pi-coding-agent depends
2718
- # on it by CARET (`^0.82.0`), so with no lockfile in this fresh temp project it
2722
+ # on it by CARET (`^0.82.1`), so with no lockfile in this fresh temp project it
2719
2723
  # floats to whatever pi published last — and that newer core then drags a NESTED
2720
2724
  # pi-ai of its own. Measured 2026-07-21: pinning only the three we import left
2721
2725
  # pi-agent-core@0.80.10 + pi-ai@0.80.10 in the tree while the gate still announced
@@ -2726,10 +2730,10 @@ _check_pack_install_impl() {
2726
2730
  local install_log
2727
2731
  install_log=$(cd "$tmp" && pnpm add \
2728
2732
  "$tgz_path" \
2729
- "@earendil-works/pi-ai@0.82.0" \
2730
- "@earendil-works/pi-coding-agent@0.82.0" \
2731
- "@earendil-works/pi-tui@0.82.0" \
2732
- "@earendil-works/pi-agent-core@0.82.0" \
2733
+ "@earendil-works/pi-ai@0.82.1" \
2734
+ "@earendil-works/pi-coding-agent@0.82.1" \
2735
+ "@earendil-works/pi-tui@0.82.1" \
2736
+ "@earendil-works/pi-agent-core@0.82.1" \
2733
2737
  "typebox@latest" \
2734
2738
  --ignore-workspace --ignore-scripts 2>&1) || {
2735
2739
  fail "[check-pack-install] pnpm add failed:"
@@ -2739,17 +2743,17 @@ _check_pack_install_impl() {
2739
2743
 
2740
2744
  # A pin is a wish until the resolved tree is read back. Assert it: EVERY
2741
2745
  # @earendil-works pi package present — direct or transitive, top level or nested —
2742
- # must be the pinned 0.82.0. Anything else means an unpinned caret floated and the
2746
+ # must be the pinned 0.82.1. Anything else means an unpinned caret floated and the
2743
2747
  # rest of this gate would be exercising a runtime nobody verified, while still
2744
- # printing "pinned pi 0.82.0". Fail loud instead of proving the wrong floor.
2748
+ # printing "pinned pi 0.82.1". Fail loud instead of proving the wrong floor.
2745
2749
  local leaked_pi
2746
- leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | grep '^@earendil-works+pi-' | grep -v '@0\.82\.0' || true)
2750
+ leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | grep '^@earendil-works+pi-' | grep -v '@0\.82\.1' || true)
2747
2751
  if [ -n "$leaked_pi" ]; then
2748
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.82.0):"
2752
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.82.1):"
2749
2753
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
2750
2754
  return 1
2751
2755
  fi
2752
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.82.0"
2756
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.82.1"
2753
2757
 
2754
2758
  # Resolve the installed package.json and confirm pi.extensions
2755
2759
  # arrived intact. If pi.extensions is empty or missing, the
@@ -2834,14 +2838,14 @@ _check_pack_install_impl() {
2834
2838
  # opus anchor (CURATED_ANCHOR_MODEL_ID in lib/acp/models.ts — the model whose
2835
2839
  # absence is a hard registry regression). Checking only one would let half the
2836
2840
  # surface drop silently.
2837
- for anchor in "claude-sonnet-5" "claude-opus-4-8"; do
2841
+ for anchor in "claude-sonnet-5" "claude-opus-5"; do
2838
2842
  if ! grep -q "$anchor" <<<"$loader_out"; then
2839
2843
  fail "[check-pack-install] pi loader output missing curated Claude model $anchor:"
2840
2844
  echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
2841
2845
  return 1
2842
2846
  fi
2843
2847
  done
2844
- echo "[check-pack-install] pi loader smoke pass (entwurf registered, claude-sonnet-5 + claude-opus-4-8 anchor)"
2848
+ echo "[check-pack-install] pi loader smoke pass (entwurf registered, claude-sonnet-5 + claude-opus-5 anchor)"
2845
2849
 
2846
2850
  # npm-managed neutral install regression — the README's PRIMARY install path is
2847
2851
  # now `npm install @junghanacs/entwurf` (NOT `pi install npm:...`). This layout
@@ -2922,7 +2926,7 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
2922
2926
  echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
2923
2927
  return 1
2924
2928
  }
2925
- if grep -qi "Unknown option" <<<"$foreign_out" || ! grep -q "claude-opus-4-8" <<<"$foreign_out"; then
2929
+ if grep -qi "Unknown option" <<<"$foreign_out" || ! grep -q "claude-opus-5" <<<"$foreign_out"; then
2926
2930
  fail "[check-pack-install] foreign-cwd --entwurf-control did not load the entwurf extension from user scope:"
2927
2931
  echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
2928
2932
  return 1
@@ -17,6 +17,7 @@ import * as fs from "node:fs";
17
17
  import * as os from "node:os";
18
18
  import * as path from "node:path";
19
19
  import {
20
+ isPlausibleOwnerPid,
20
21
  parentPid,
21
22
  processStartKey,
22
23
  upsertMetaSession,
@@ -165,8 +166,16 @@ function imprint(raw: string): void {
165
166
  // Written only after the upsert above: the record store is the identity authority, and a
166
167
  // marker pointing at a garden-id with no record would be a window of un-backed identity.
167
168
  // A failed marker costs reply-addressability, never the session — log and move on.
169
+ //
170
+ // The owner rule is `isPlausibleOwnerPid`, the SAME predicate the Claude hook
171
+ // and every reader hold. This writer used to ask `> 0` on its own, so THIS hook
172
+ // could mint `meta-senders/antigravity/1.json` through the very reparenting that
173
+ // stranded a host's fresh-cut (#53 A): agy exits, the imprint is reparented to
174
+ // init, and `process.ppid` reads 1. Refusing is fail-closed — it costs reply
175
+ // addressability for that turn and is logged; honoring it would cost the operator
176
+ // their generation cut, with no in-band way to get it back.
168
177
  const ownerPid = process.ppid;
169
- if (typeof ownerPid === "number" && ownerPid > 0) {
178
+ if (isPlausibleOwnerPid(ownerPid)) {
170
179
  try {
171
180
  writeMetaSenderMarker({
172
181
  backend: "antigravity",
@@ -181,6 +190,11 @@ function imprint(raw: string): void {
181
190
  `sender-marker-failed pid=${ownerPid} gardenId=${result.record.gardenId} ${err instanceof Error ? err.message : String(err)}`,
182
191
  );
183
192
  }
193
+ } else {
194
+ logLine(
195
+ `sender-marker-refused pid=${ownerPid} gardenId=${result.record.gardenId} implausible owner pid ` +
196
+ "(<= 1: this imprint was reparented, so its parent is not the agy process that owns the turn)",
197
+ );
184
198
  }
185
199
  } catch (err) {
186
200
  logLine(
@@ -33,7 +33,7 @@ import { pathToFileURL } from "node:url";
33
33
  import type { Api, AssistantMessageEvent, Context, Message, Model } from "@earendil-works/pi-ai";
34
34
 
35
35
  const sonnet = { id: "claude-sonnet-5" } as unknown as Model<Api>;
36
- const opus = { id: "claude-opus-4-8" } as unknown as Model<Api>;
36
+ const opus = { id: "claude-opus-5" } as unknown as Model<Api>;
37
37
 
38
38
  type Stream = AsyncIterable<AssistantMessageEvent> & {
39
39
  result: () => Promise<{ stopReason: string; errorMessage?: string }>;
@@ -85,7 +85,7 @@ const baseInput = (): BridgeConfigInput => ({
85
85
  );
86
86
  // Model drift → different signature.
87
87
  assert.notEqual(
88
- bridgeConfigSignature({ ...baseInput(), modelId: "claude-opus-4-8" }),
88
+ bridgeConfigSignature({ ...baseInput(), modelId: "claude-opus-5" }),
89
89
  bridgeConfigSignature(baseInput()),
90
90
  "model drift changes the signature",
91
91
  );
@@ -186,7 +186,7 @@ const baseInput = (): BridgeConfigInput => ({
186
186
  const params = facts({ contextMessageSignatures: ["user:text:a", "assistant:text:b"] });
187
187
  assert.ok(isCompatible(facts(), params), "prefix history + same cfg → compatible");
188
188
  assert.ok(!isCompatible(facts({ cwd: "/other" }), params), "cwd drift → incompatible");
189
- assert.ok(!isCompatible(facts({ modelId: "claude-opus-4-8" }), params), "model drift → incompatible");
189
+ assert.ok(!isCompatible(facts({ modelId: "claude-opus-5" }), params), "model drift → incompatible");
190
190
  assert.ok(!isCompatible(facts({ bridgeConfigSignature: "different" }), params), "signature drift → incompatible");
191
191
  assert.ok(
192
192
  !isCompatible(facts({ contextMessageSignatures: ["user:text:EDITED"] }), params),
@@ -292,7 +292,7 @@ const baseInput = (): BridgeConfigInput => ({
292
292
  assert.throws(
293
293
  () =>
294
294
  decideBootstrap(params("process-scoped"), {
295
- existing: { ...aliveCompat, modelId: "claude-opus-4-8" },
295
+ existing: { ...aliveCompat, modelId: "claude-opus-5" },
296
296
  }),
297
297
  SessionModelLockedError,
298
298
  "live model mismatch throws SessionModelLockedError",
@@ -48,6 +48,9 @@ import {
48
48
  import {
49
49
  FRESH_CUT_COMMAND,
50
50
  type MetaIdentity,
51
+ metaRecordExistsByGardenId,
52
+ processStartKey,
53
+ readMetaSenderMarker,
51
54
  upsertMetaSession,
52
55
  writeMetaSenderMarker,
53
56
  } from "../pi-extensions/lib/meta-session.ts";
@@ -358,6 +361,86 @@ try {
358
361
  trusted?.identity.gardenId === record.record.gardenId,
359
362
  );
360
363
  }
364
+ // ── #53 A: an init-owned marker names NOBODY, however live init is ─────────
365
+ // The shape measured on a second Linux host: `meta-senders/claude-code/1.json`,
366
+ // pid 1, and the REAL start-key for init — so the pid-reuse guard PASSES and
367
+ // classifyMarkerOwner answers `live` for as long as the host is up. The defect is
368
+ // one layer up: init owns no Claude session. THIS gate's own writer is one of the
369
+ // two that could produce the shape before #53 A — the agy imprint asked `> 0`
370
+ // while the Claude hook already refused `<= 1` — so "only the shell-form hook
371
+ // could have written it" was never true. After this cut no writer in the tree can.
372
+ {
373
+ clearMarkers();
374
+ const record = upsertMetaSession({
375
+ input: { backend: "claude-code", nativeSessionId: "sess-init", cwd: REPO_DIR },
376
+ });
377
+ const initKey = processStartKey(1);
378
+ ok("this host CAN read a start key for pid 1 (the residue cells are not vacuous)", initKey !== "");
379
+ const initDir = path.join(SENDERS_DIR, "claude-code");
380
+ fs.mkdirSync(initDir, { recursive: true });
381
+ fs.writeFileSync(
382
+ path.join(initDir, "1.json"),
383
+ `${JSON.stringify({
384
+ backend: "claude-code",
385
+ gardenId: record.record.gardenId,
386
+ nativeSessionId: "sess-init",
387
+ cwd: REPO_DIR,
388
+ ownerPid: 1,
389
+ ownerStartKey: initKey,
390
+ updatedAt: "2026-06-10T16:03:10.000Z",
391
+ })}\n`,
392
+ );
393
+ ok(
394
+ "an init-owned sender marker reads as null — it grants no identity",
395
+ readMetaSenderMarker({ backend: "claude-code", ownerPid: 1, sendersDir: SENDERS_DIR }) === null,
396
+ );
397
+ ok(
398
+ "...while its record is present and readable, so the refusal is about the marker's CLAIM",
399
+ metaRecordExistsByGardenId(record.record.gardenId),
400
+ );
401
+ // End to end, with pid 1 supplied as an explicit candidate: the answer is
402
+ // "nobody" — not an identity, and not a throw either. Stated at the size of the
403
+ // evidence: the DEFAULT candidate set is only the bridge's parent and grandparent,
404
+ // so pid 1 enters it just when the native host itself was reparented (a detached
405
+ // Claude). Reachable, not observed — the affected host's own launch shape
406
+ // (bridge → claude → bash → wrapper) never had 1 among its candidates, and no
407
+ // wrong-identity read was measured there. The cut's deadlock is the proven half;
408
+ // this is the code-path half.
409
+ let identityThrew: unknown = null;
410
+ let identityAnswer: unknown = "unset";
411
+ try {
412
+ identityAnswer = resolveTrustedMetaSenderIdentity({ ownerPids: [1], sendersDir: SENDERS_DIR });
413
+ } catch (err) {
414
+ identityThrew = err;
415
+ }
416
+ ok(
417
+ "resolving with pid 1 as a candidate yields NO identity (and no throw)",
418
+ identityAnswer === null && identityThrew === null,
419
+ );
420
+ let initWriteRejected = false;
421
+ try {
422
+ writeMetaSenderMarker({
423
+ backend: "claude-code",
424
+ gardenId: record.record.gardenId,
425
+ nativeSessionId: "sess-init",
426
+ cwd: REPO_DIR,
427
+ ownerPid: 1,
428
+ sendersDir: SENDERS_DIR,
429
+ });
430
+ } catch {
431
+ initWriteRejected = true;
432
+ }
433
+ ok("minting an init-owned sender marker THROWS at the write boundary", initWriteRejected);
434
+ // WIRING: the agy imprint is the SECOND writer, and the one that still asked
435
+ // `> 0` on its own while the Claude hook already refused `<= 1`. A predicate only
436
+ // one writer knows is how #53 A drifted in; both must reach the shared one.
437
+ ok(
438
+ "the agy imprint routes its owner pid through the shared predicate",
439
+ /isPlausibleOwnerPid\(ownerPid\)/.test(fs.readFileSync(HOOK, "utf8")),
440
+ );
441
+ clearMarkers();
442
+ }
443
+
361
444
  ok(
362
445
  "antigravity is a native-push backend → its replyable comes from the adapter probe",
363
446
  nativePushSupported("antigravity"),
@@ -196,14 +196,14 @@ async function main(): Promise<void> {
196
196
  {
197
197
  const base = {
198
198
  sessionId: "20260613T091000-98363c",
199
- agentId: "pi/claude-opus-4-8",
199
+ agentId: "pi/claude-opus-5",
200
200
  cwd: "/w",
201
201
  timestamp: "2026-06-13T09:10:00.000Z",
202
202
  };
203
203
  ok(
204
204
  "6: minimal envelope → exact block (leading blank line, required fields only)",
205
205
  formatSenderInfoBlock(base) ===
206
- `\n\n<sender_info>{"sessionId":"20260613T091000-98363c","agentId":"pi/claude-opus-4-8","cwd":"/w","timestamp":"2026-06-13T09:10:00.000Z"}</sender_info>`,
206
+ `\n\n<sender_info>{"sessionId":"20260613T091000-98363c","agentId":"pi/claude-opus-5","cwd":"/w","timestamp":"2026-06-13T09:10:00.000Z"}</sender_info>`,
207
207
  );
208
208
  const full = formatSenderInfoBlock({ ...base, origin: "pi-session", replyable: false }, true);
209
209
  ok(
@@ -63,6 +63,8 @@ interface SocketOpts {
63
63
  extraNames?: string[];
64
64
  /** readdir throws with this code (e.g. "EACCES" / "ENOENT") (P2e②). */
65
65
  readdirErrorCode?: string;
66
+ /** `.meta.json` entries that are NOT regular files (symlink/dir/special). */
67
+ irregularMeta?: string[];
66
68
  }
67
69
 
68
70
  function deps(
@@ -72,7 +74,13 @@ function deps(
72
74
  ): EntwurfFactsDeps {
73
75
  const symlinkSet = new Set(opts.symlinks ?? []);
74
76
  return {
75
- metaEntries: Object.keys(meta),
77
+ // Kind-carrying entries, like the real bindings: the listing must be able to refuse a
78
+ // symlinked record without following it. `irregularMeta` names the ones that are not
79
+ // regular files.
80
+ metaEntries: Object.keys(meta).map((filename) => ({
81
+ filename,
82
+ regularFile: !(opts.irregularMeta ?? []).includes(filename),
83
+ })),
76
84
  readRecord: (f: string) => {
77
85
  const v = meta[f];
78
86
  if (v === undefined) throw new Error(`ENOENT: ${f}`);
@@ -65,7 +65,7 @@ function identity(backend: MetaCitizenBackend, over: Partial<MetaIdentity> = {})
65
65
  backend,
66
66
  nativeSessionId: "native-abc",
67
67
  cwd: "/home/junghan/repos/gh/entwurf",
68
- model: "claude-opus-4-8",
68
+ model: "claude-opus-5",
69
69
  transcriptPath: "/home/junghan/.claude/projects/x/native-abc.jsonl",
70
70
  createdAt: "2026-06-11T00:38:58.000Z",
71
71
  recordUpdatedAt: "2026-06-11T02:40:00.000Z",
@@ -59,12 +59,16 @@ const liveIdentity = (): MetaIdentity => ({
59
59
  createdAt: "2026-06-14T01:00:00.000Z",
60
60
  recordUpdatedAt: "2026-06-14T01:00:00.000Z",
61
61
  });
62
+ // The owner pid is a plausible one on purpose: these markers are injected straight
63
+ // into the pure predicate, which never reads the field — but a fixture carrying
64
+ // `ownerPid: 1` would model a marker no writer can mint and no reader honors
65
+ // (isPlausibleOwnerPid, #53 A), and fixtures are read as claims about the world.
62
66
  const liveMarker = () =>
63
67
  ({
64
68
  gardenId: GARDEN,
65
69
  backend: "claude-code",
66
70
  nativeSessionId: "n-a",
67
- ownerPid: 1,
71
+ ownerPid: 4242,
68
72
  ownerStartKey: "x",
69
73
  ownerKind: "claude-code-cli",
70
74
  armProvenance: "session-start",
@@ -77,7 +81,7 @@ const driftMarker = () =>
77
81
  gardenId: GARDEN,
78
82
  backend: "claude-code",
79
83
  nativeSessionId: "n-OTHER",
80
- ownerPid: 1,
84
+ ownerPid: 4242,
81
85
  ownerStartKey: "x",
82
86
  ownerKind: "claude-code-cli",
83
87
  armProvenance: "session-start",
@@ -52,7 +52,7 @@ function main(): void {
52
52
  sessionFile: SESSION_FILE,
53
53
  explicitExtensionArgs: EXT,
54
54
  provider: "entwurf",
55
- model: "claude-opus-4-8",
55
+ model: "claude-opus-5",
56
56
  prompt: "continue the task",
57
57
  });
58
58
  ok("1 legacy has --no-extensions", args.includes("--no-extensions"));
@@ -61,14 +61,14 @@ function main(): void {
61
61
  ok("3 legacy prompt is the final positional", args[args.length - 1] === "continue the task");
62
62
  ok("4 legacy keeps ext args exactly once", args.filter((a) => a === "-e").length === 1);
63
63
  ok("6 legacy provider laid out", valueAfter(args, "--provider") === "entwurf");
64
- ok("6 legacy model laid out", valueAfter(args, "--model") === "claude-opus-4-8");
64
+ ok("6 legacy model laid out", valueAfter(args, "--model") === "claude-opus-5");
65
65
  ok("6 legacy resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
66
66
  ok("6 legacy carries NO --session-id (the id is pi's own now)", !args.includes("--session-id"));
67
67
  // model + prompt are the last three tokens: --model <m> <prompt>
68
68
  ok(
69
69
  "6 legacy --model <m> <prompt> tail",
70
70
  args[args.length - 3] === "--model" &&
71
- args[args.length - 2] === "claude-opus-4-8" &&
71
+ args[args.length - 2] === "claude-opus-5" &&
72
72
  args[args.length - 1] === "continue the task",
73
73
  );
74
74
  }
@@ -80,7 +80,7 @@ function main(): void {
80
80
  sessionFile: SESSION_FILE,
81
81
  explicitExtensionArgs: EXT,
82
82
  provider: "entwurf",
83
- model: "claude-opus-4-8",
83
+ model: "claude-opus-5",
84
84
  prompt: "resume now",
85
85
  launchArgs: ["--approve"],
86
86
  });
@@ -93,7 +93,7 @@ function main(): void {
93
93
  ok("5 v2 includes launchArgs --approve", args.includes("--approve"));
94
94
  ok("5 v2 --approve is before the prompt", args.indexOf("--approve") < args.length - 1);
95
95
  ok("6 v2 provider laid out", valueAfter(args, "--provider") === "entwurf");
96
- ok("6 v2 model laid out", valueAfter(args, "--model") === "claude-opus-4-8");
96
+ ok("6 v2 model laid out", valueAfter(args, "--model") === "claude-opus-5");
97
97
  ok("6 v2 resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
98
98
  ok(
99
99
  "6 v2 carries NO --session-id (a garden id would MINT a session, not resume one)",
@@ -102,7 +102,7 @@ function main(): void {
102
102
  ok(
103
103
  "6 v2 --model <m> <prompt> tail",
104
104
  args[args.length - 3] === "--model" &&
105
- args[args.length - 2] === "claude-opus-4-8" &&
105
+ args[args.length - 2] === "claude-opus-5" &&
106
106
  args[args.length - 1] === "resume now",
107
107
  );
108
108
  }
@@ -103,7 +103,7 @@ try {
103
103
  fileA,
104
104
  sessionLine(idA, "/identity/a") +
105
105
  mc("openai-codex", "gpt-5.5") +
106
- msg({ content: "drifted", model: "claude-opus-4-8", provider: "entwurf" }),
106
+ msg({ content: "drifted", model: "claude-opus-5", provider: "entwurf" }),
107
107
  );
108
108
  const recA = readSessionIdentity(fileA);
109
109
  eq(recA?.provider, "openai-codex", "identity: provider = first model_change (not assistant message)");
@@ -115,7 +115,7 @@ try {
115
115
  const fileB = path.join(idDir, `2026-06-03T22-00-00-000Z_${idB}.jsonl`);
116
116
  fs.writeFileSync(
117
117
  fileB,
118
- sessionLine(idB, "/identity/b") + mc("entwurf", "claude-opus-4-8") + mc("openai-codex", "gpt-5.5"),
118
+ sessionLine(idB, "/identity/b") + mc("entwurf", "claude-opus-5") + mc("openai-codex", "gpt-5.5"),
119
119
  );
120
120
  throws(() => readSessionIdentity(fileB), "identity: later model_change drift → fail-fast");
121
121
 
@@ -124,7 +124,7 @@ try {
124
124
  // judge — identity must resolve from the transcript alone, no throw.
125
125
  const idC = "20260603T220000-3333cc";
126
126
  const fileC = path.join(idDir, `2026-06-03T22-00-00-000Z_${idC}.jsonl`);
127
- const mismatchName = `${idC}==entwurf/claude-opus-4-8--x__entwurf`;
127
+ const mismatchName = `${idC}==entwurf/claude-opus-5--x__entwurf`;
128
128
  fs.writeFileSync(fileC, sessionLine(idC, "/identity/c") + mc("openai-codex", "gpt-5.5") + infoLine(mismatchName));
129
129
  noThrow(() => readSessionIdentity(fileC), "identity: name provider/model disagreement is pi's business — no throw");
130
130
  eq(readSessionIdentity(fileC)?.modelId, "gpt-5.5", "identity: model still resolves from first model_change");
@@ -165,17 +165,18 @@ try {
165
165
  // ---- T-name-blind: the reader takes NO options and never reads the name (#50 C3) ----
166
166
  // The old `requireEntwurf` name-tag authorization (0.9.0 "entwurf 여부 = name tag")
167
167
  // is deleted, not just unused: resume authorization is record existence
168
- // (readMetaIdentityByGardenId) + the header-id record.nativeSessionId integrity
168
+ // (readAddressableMetaIdentity the record must also hold its nativeSessionId
169
+ // alone, #52) + the header-id ↔ record.nativeSessionId integrity
169
170
  // check in resolveResumeLaunchIdentity. A session with no name, a non-canonical
170
171
  // name, or no `entwurf` tag resolves identity exactly like any other pi session.
171
172
  const idG = "20260603T230000-7777aa";
172
173
  const fileG = path.join(idDir, `2026-06-03T23-00-00-000Z_${idG}.jsonl`);
173
174
  fs.writeFileSync(
174
175
  fileG,
175
- sessionLine(idG, "/identity/g") + mc("entwurf", "claude-opus-4-8") + infoLine("not a canonical name"),
176
+ sessionLine(idG, "/identity/g") + mc("entwurf", "claude-opus-5") + infoLine("not a canonical name"),
176
177
  );
177
178
  noThrow(() => readSessionIdentity(fileG), "name-blind: non-canonical name resolves like any pi session");
178
- eq(readSessionIdentity(fileG)?.modelId, "claude-opus-4-8", "name-blind: identity from model_change only");
179
+ eq(readSessionIdentity(fileG)?.modelId, "claude-opus-5", "name-blind: identity from model_change only");
179
180
  eq(readSessionIdentity.length, 1, "name-blind: readSessionIdentity takes exactly one arg (no opts, compiler-pinned)");
180
181
 
181
182
  // ========================================================================
@@ -63,13 +63,15 @@ function identity(backend: MetaIdentity["backend"], gardenId = GID): MetaIdentit
63
63
  }
64
64
 
65
65
  /** A receiver presence marker. Matches the record identity (gardenId/backend/nativeSessionId)
66
- * unless `nativeSessionId` is overridden to simulate an identity-drifted/foreign marker. */
66
+ * unless `nativeSessionId` is overridden to simulate an identity-drifted/foreign marker.
67
+ * The owner pid is a plausible one: the predicate never reads it, but a fixture carrying
68
+ * `ownerPid: 1` would model a marker no writer can mint and no reader honors (#53 A). */
67
69
  function receiverMarker(gid: string, backend: string, nativeSessionId = "n"): MetaReceiverMarker {
68
70
  return {
69
71
  gardenId: gid,
70
72
  backend: backend as MetaReceiverMarker["backend"],
71
73
  nativeSessionId,
72
- ownerPid: 1,
74
+ ownerPid: 4242,
73
75
  ownerStartKey: "x",
74
76
  ownerKind: "claude-code-cli",
75
77
  armProvenance: "session-start",
@@ -59,7 +59,7 @@ const IDENTITY: LaunchIdentity = {
59
59
  cwd: "/home/test/repo",
60
60
  explicitExtensionArgs: ["-e", "/path/to/entwurf/index.ts"],
61
61
  provider: "entwurf",
62
- model: "claude-opus-4-8",
62
+ model: "claude-opus-5",
63
63
  };
64
64
 
65
65
  // True if `p` has NOT settled after a macrotask tick (all pending microtasks drained).
@@ -224,7 +224,7 @@ async function main(): Promise<void> {
224
224
  ok("2 argv carries plan.launchArgs (--approve)", args.includes("--approve"));
225
225
  ok("2 argv carries the ext args", args.includes("-e"));
226
226
  ok("2 argv carries provider", args[args.indexOf("--provider") + 1] === "entwurf");
227
- ok("2 argv carries model", args[args.indexOf("--model") + 1] === "claude-opus-4-8");
227
+ ok("2 argv carries model", args[args.indexOf("--model") + 1] === "claude-opus-5");
228
228
  // #50 C2: argv names the exact transcript FILE, never a garden id. `--session-id`
229
229
  // would CREATE a session at that id when it is missing — post-cut the garden id is
230
230
  // never a pi session id, so the old flag would have minted an empty session and