@junghanacs/entwurf 0.17.2 → 0.18.1

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 (65) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +299 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +9 -5
  5. package/VERIFY.md +3 -3
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +46 -12
  8. package/docs/setup-clean-host.md +35 -3
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +6 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +38 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
  21. package/mcp/entwurf-bridge/src/index.ts +16 -8
  22. package/package.json +10 -9
  23. package/pi-extensions/entwurf-control.ts +12 -2
  24. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  25. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  26. package/pi-extensions/lib/acp/backend.ts +3 -3
  27. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  28. package/pi-extensions/lib/entwurf-deliverability.ts +104 -0
  29. package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
  30. package/pi-extensions/lib/entwurf-facts.ts +52 -3
  31. package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
  32. package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
  33. package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
  34. package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
  35. package/pi-extensions/lib/entwurf-v2-surface.ts +6 -0
  36. package/pi-extensions/lib/meta-session.ts +44 -0
  37. package/pi-extensions/meta-bridge-hook.ts +72 -3
  38. package/run.sh +146 -56
  39. package/scripts/check-acp-usage-accounting.ts +9 -9
  40. package/scripts/check-bridge-delivery.ts +42 -1
  41. package/scripts/check-entwurf-fact-provider.ts +22 -1
  42. package/scripts/check-entwurf-facts.ts +17 -2
  43. package/scripts/check-entwurf-peers-surface.ts +20 -2
  44. package/scripts/check-entwurf-self-address.ts +20 -2
  45. package/scripts/check-entwurf-v2-decider.ts +6 -3
  46. package/scripts/check-entwurf-v2-matrix.ts +10 -5
  47. package/scripts/check-entwurf-v2-production.ts +72 -1
  48. package/scripts/check-gate-qualification.ts +63 -12
  49. package/scripts/check-meta-hook-session-switch.ts +560 -0
  50. package/scripts/check-release-gate-outcomes.ts +160 -3
  51. package/scripts/meta-bridge-doctor.sh +29 -4
  52. package/scripts/mutants/bridge-command-boot.json +3 -3
  53. package/scripts/mutants/gate-qualification.json +28 -0
  54. package/scripts/mutants/meta-hook-session-switch.json +223 -0
  55. package/scripts/mutants/pack-install.json +14 -3
  56. package/scripts/mutants/release-gate.json +22 -0
  57. package/scripts/raw-acp-compaction-measure/README.md +77 -0
  58. package/scripts/raw-acp-compaction-measure/probe.ts +196 -0
  59. package/scripts/raw-claude-session-switch/README.md +170 -0
  60. package/scripts/smoke-acp-cortex-live.ts +22 -2
  61. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  62. package/scripts/smoke-acp-v2-send-live.ts +23 -2
  63. package/scripts/smoke-entwurf-chain-live.ts +85 -13
  64. package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
  65. package/scripts/smoke-mux-fresh-call-live.ts +13 -3
package/run.sh CHANGED
@@ -144,6 +144,7 @@ Usage:
144
144
  ./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
145
145
  ./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
146
146
  ./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
147
+ ./run.sh check-meta-hook-session-switch # #101 gate: ONE Claude pid, TWO SessionStarts (resume picker placeholder -> resumed id). The hook retires the marker of the garden it stopped serving (records untouched, same-garden re-registration retires nothing), the reader stays fail-closed on a re-planted stale marker (watchArmed measured against the owner's sender marker, never copied), production dispatch refuses the retired garden and NAMES the failing axis, and exactly one of the two gardens is deliverable. Sandboxed roots, no API
147
148
  ./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
148
149
  ./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
149
150
  ./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
@@ -191,6 +192,7 @@ Usage:
191
192
  ./run.sh smoke-omp-receive-live # #87 bundle B acceptance: another harness wakes an ALREADY-OPEN omp citizen, which reads its inbox and answers in the same session. Decides its own outcome from the capability registry — omp without a drainable mailbox is a protocol SKIP (no receiver unit exists to arm), which an unattended release-gate reports and `--cut` reads as RED; a registry that claims a receive rail with no acceptance body here is a FAIL
192
193
  ./run.sh smoke-entwurf-chain-live # LIVE cross-harness delivery chain: native Claude Code -> pi GPT -> pi ACP Sonnet -> mailbox terminus, proving sender identity/replyable at every hop and a real read receipt at the end. Prerequisites (claude on PATH, pi credentials per backend) report protocol SKIP, never a pass
193
194
  ./run.sh check-release-gate-outcomes # release-gate STEP OUTCOME protocol (P1): one skip exit code shared by the shell + TS halves, classifier never rounds a skip up to a pass, `--cut` refuses a MUST SKIP while a bare diagnostic stays exit 0, no LIVE smoke keeps the old exit-0 skip shape, and both real skip surfaces are INVOKED and observed to propagate the code
195
+ ./run.sh check-gate-manifests # the HEAD of that qualification, on its own: runner self-test + the committed manifests validated against the origin index + the declared lane inventory. Executes ZERO mutants and never snapshots this repo (~8s), which is why it lives in check:hermetic while the body stays on CI/release-gate — three of the five reds qualification ever produced in CI died right here, before a mutant gate ran (#99 B-3)
194
196
  ./run.sh check-gate-qualification # kill-proof qualification (the gate-of-gates): runner self-test (classifier truth table + synthetic negatives incl. wrong-reason/hang/control-red/impurity) + committed mutant manifests (scripts/mutants/*.json) run in an isolated snapshot repo under control→mutant→restore→control; the real checkout is never written. Evidence = claim IDs + killed mutant IDs, never assertion counts
195
197
  ./run.sh check-probe-ordering # §11-7 ordering-probe deterministic gate: raw-client SAMENESS pinned to backend.ts (sequence/args/timeouts/permission policy — the probe may never measure a lookalike), phase attribution incl. set-model, probe-mode fixture wire markers (delay honored, probeRunId REQUIRED, smoke-acp-mcp-live legacy compat), event-log door integrity (reserved keys refused at write; unknown marker name, broken sort axis, or a payload the classifier cannot judge on is MALFORMED, never a quiet event), and the §11-7 paired-verdict truth table (P0/I0 outside the space, phase-qualified D, B promotion ladder, C, A two-delay rule). Offline/deterministic; one product claim (no production prompt cutoff) is replant-qualified via scripts/mutants/probe-ordering.json, while the other assertions are direct [CHECK:*] contracts
196
198
  ./run.sh check-probe-cli-shim # §11-7-c B-name-snapshot PRODUCER gate: the CLI shim driven as a REAL process against fake CLIs (no API, no cost). Proves what a defect would buy — FABRICATED evidence (a malformed init is never reported as an empty name set; the boot report carries the true target path+sha256 the classifier verifies against the roster), a DESTROYED turn (byte transparency across mid-UTF8/CRLF/oversized/unterminated framing, exit-code fidelity, signal re-raise, inbound signal forwarding, stderr passthrough, stdout backpressure), and LEAKED operator state (exact-allowlist env scrub, no argv/env/prompt body in the log). Offline/deterministic; 20 direct [CHECK:*] contracts, deliberately no longer replant-qualified after #70 subtraction
@@ -252,7 +254,7 @@ Usage:
252
254
  ./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)
253
255
  ./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
254
256
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
255
- ./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes (version boundary: @0.84.40 must leak, @0.84.4 bare/peer-hash must pass); snapshot-safe qualification oracle, also run first inside check-pack-install
257
+ ./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes, one cell per property: version boundary (@0.85.10 must leak, @0.85.1 bare or with either measured peer-hash must pass) and closure prefix (an off-pin @earendil-works/chord must leak — it carries no `pi-` prefix); snapshot-safe qualification oracle, also run first inside check-pack-install
256
258
  ./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm run check:full): 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
257
259
  ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with the pinned pi peers (pins derived from the package.json devDep; check-dep-versions binds them) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the installed all-absent and copilot-present (four-unit fake-vendor) `entwurf setup` rows + 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) + the INSTALLED-PACKAGE branch of the Copilot and OMP birth installers actually RUN (compiled entry selected, no raw .ts, and a real birth edge mints a citizen — the half a required-artifact list can never stand in for)
258
260
  ./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)
@@ -894,6 +896,23 @@ check_meta_receiver_marker() {
894
896
  run_ts scripts/check-meta-receiver-marker.ts
895
897
  }
896
898
 
899
+ check_meta_hook_session_switch() {
900
+ # #101 gate: a Claude Code SESSION SWITCH — one native process that stops serving one
901
+ # garden and starts serving another. The resume picker (and `/clear`) fire SessionStart
902
+ # twice inside one pid: once for the TUI's placeholder id, once for the id the operator
903
+ # picked. Before this gate nothing asked "one owner pid, two gardens", so a placeholder
904
+ # citizen stayed registered with an armed-looking doorbell and real mail rotted in it.
905
+ # Plays Claude the way check-hook-launch-topology does (spawn the SHIPPED launcher with
906
+ # the manifest's argv, so the hook's parent is this process = one fake owner pid) with
907
+ # every meta root sandboxed. Asserts: both records survive (marker retired, never the
908
+ # record); the retired garden's marker is gone; a same-garden re-registration retires
909
+ # NOTHING; a re-planted stale marker is still undeliverable (the sender-marker join,
910
+ # fail-closed at the reader); production dispatch refuses it and carries the predicate's
911
+ # reason to the rendered surface; exactly one garden is deliverable and it is the served
912
+ # one. Real tmpdir, no API.
913
+ run_ts scripts/check-meta-hook-session-switch.ts
914
+ }
915
+
897
916
  check_copilot_birth_hook() {
898
917
  # #82 gate: the Copilot BIRTH path, proven without Copilot. Drives the REAL
899
918
  # assembler (--assemble-only, into a temp dir), then fires the baked launcher the
@@ -1827,13 +1846,13 @@ assert.equal(peerTui, piAi,
1827
1846
  // floor tracks the devDep pin so a consumer can't install against a pi lacking
1828
1847
  // the public trust exports the bridge imports at the pinned minor, AND an upper
1829
1848
  // bound at the next minor stops a fresh install from silently pulling a future
1830
- // pi (past the declared ceiling — 0.85+ at the current 0.84.4 pin) whose
1849
+ // pi (past the declared ceiling — 0.86+ at the current 0.85.1 pin) whose
1831
1850
  // internal export surface has drifted from the one we typecheck against.
1832
1851
  // pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
1833
1852
  // factory churn is exactly this), so an open `>=` floor is exactly how the next
1834
1853
  // installer re-acquires the drift. The floor is also the HARD MINIMUM a consumer
1835
- // install resolves: at `>=0.84.4` an existing 0.84.3 host is upgraded, not kept.
1836
- // Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.84.4 <0.85`).
1854
+ // install resolves: at `>=0.85.1` an existing 0.85.0 host is upgraded, not kept.
1855
+ // Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.85.1 <0.86`).
1837
1856
  const [piMaj, piMin] = piAi.split('.').map(Number);
1838
1857
  assert.equal(piMaj, 0,
1839
1858
  `pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
@@ -1860,7 +1879,11 @@ assert.equal(peerDepTui, expectedPeer,
1860
1879
  // Scope is deliberately narrow: only sentences that DECLARE the pi pin. History
1861
1880
  // (CHANGELOG/NEXT) keeps its old versions, and a pi mention without a version
1862
1881
  // (an uninstall line, a type import) is not a declaration.
1863
- const BASELINE_DOCS = ['AGENTS.md', 'README.md', 'ROADMAP.md', 'docs/setup-clean-host.md', 'demo/README.md'];
1882
+ // VERIFY.md joined on 2026-09-06: it declared the pi range in prose that no gate
1883
+ // read, and sat two bumps stale (`>=0.84.3 <0.85` at a 0.85.1 pin) precisely
1884
+ // because it was outside this list. Its declaration is a plain range, so the
1885
+ // range scan below binds it — no new PROSE_DECL is needed for it.
1886
+ const BASELINE_DOCS = ['AGENTS.md', 'README.md', 'ROADMAP.md', 'docs/setup-clean-host.md', 'demo/README.md', 'VERIFY.md'];
1864
1887
  let rangeDecls = 0, exactDecls = 0;
1865
1888
  for (const file of BASELINE_DOCS) {
1866
1889
  const text = readFileSync(file, 'utf8');
@@ -3096,35 +3119,65 @@ check_pack() {
3096
3119
  }
3097
3120
 
3098
3121
  # The pin-leak filter for the install tree's .pnpm listing, shared by the real scan and its
3099
- # self-test below. The version BOUNDARY is load-bearing: a pnpm .pnpm entry is
3100
- # `<name>@<version>` followed by either `_<peer-hash>` or end-of-name (measured pnpm 11.20.0
3101
- # on this tree), so an unbounded substring match would bless a lookalike such as `@0.84.40`
3102
- # while announcing the pinned floor a false-green oracle (found by independent review,
3103
- # 2026-08-25).
3122
+ # self-test below. TWO properties are load-bearing and each has its own cell below.
3123
+ #
3124
+ # (1) The version BOUNDARY. A pnpm .pnpm entry is `<name>@<version>` followed by either
3125
+ # `_<peer-hash>` or end-of-name (measured pnpm 11.20.0 on this tree), so an unbounded
3126
+ # substring match would bless a lookalike such as `@0.85.10` while announcing the pinned
3127
+ # floor — a false-green oracle (found by independent review, 2026-08-25).
3128
+ # (2) The name PREFIX covers the whole @earendil-works closure, not the `pi-` families alone.
3129
+ # 0.85.0 added `@earendil-works/chord`, declared as a runtime `dependencies` entry by
3130
+ # pi-coding-agent, pi-agent-core, pi-client and pi-protocol — inside the closure, with a
3131
+ # name that carries no `pi-` prefix. `[측정 2026-09-06]` a real 0.85.1 install tree lists
3132
+ # `@earendil-works+chord@0.85.1` next to the seven pi entries, and the old
3133
+ # `^@earendil-works+pi-` filter did not see it: chord would have floated silently while
3134
+ # this gate printed a verified pin. The prefix is the closure's org, not one family in it.
3135
+ # (The measured peer-hash suffix moved too — `_ws@8.21.3` at 0.85.1 where 0.84.4 carried
3136
+ # `_@modelcontextprotocol+sdk@…` — which is exactly what property (1)'s `(_|$)` absorbs;
3137
+ # both shapes are fixtures below so a future suffix change cannot pass vacuously.)
3104
3138
  pack_install_leaked_pi() {
3105
- grep '^@earendil-works+pi-' | grep -Ev '@0\.84\.4(_|$)' || true
3139
+ grep '^@earendil-works+' | grep -Ev '@0\.85\.1(_|$)' || true
3106
3140
  }
3107
3141
 
3108
- # Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise the
3109
- # false-green shape (it contains no 0.84.40), so the oracle is proven against a fixture
3110
- # listing. Expected: the two lookalikes leak, the pinned version passes bare and with a
3111
- # peer-hash suffix. Exposed as its own snapshot-safe subcommand because the heavy
3112
- # check-pack-install cannot run inside the qualification snapshot (no install environment),
3113
- # and a kill-proof needs a control-green gate there; check-pack-install still runs this
3114
- # first so the heavy gate cannot proceed on a broken oracle.
3142
+ # Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
3143
+ # false-green shape (it contains no 0.85.10 and no off-pin chord), so the oracle is proven
3144
+ # against fixture listings. One cell per property of pack_install_leaked_pi, so a regression
3145
+ # in either is named by its own signature rather than folded into a single verdict. Exposed
3146
+ # as its own snapshot-safe subcommand because the heavy check-pack-install cannot run inside
3147
+ # the qualification snapshot (no install environment), and a kill-proof needs a control-green
3148
+ # gate there; check-pack-install still runs this first so the heavy gate cannot proceed on a
3149
+ # broken oracle.
3115
3150
  check_pack_pin_matcher() {
3116
3151
  local matcher_probe
3152
+
3153
+ # Cell 1 — the version boundary. Both measured peer-hash shapes are fixtures: the
3154
+ # 0.84.4-era `_@modelcontextprotocol+sdk@…` and the `_ws@8.21.3` observed on a real
3155
+ # 0.85.1 tree. Neither may leak; the two lookalikes must.
3117
3156
  matcher_probe=$(printf '%s\n' \
3118
- '@earendil-works+pi-ai@0.84.4' \
3119
- '@earendil-works+pi-ai@0.84.4_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
3120
- '@earendil-works+pi-ai@0.84.40' \
3121
- '@earendil-works+pi-agent-core@0.84.3' | pack_install_leaked_pi)
3122
- if [ "$matcher_probe" != '@earendil-works+pi-ai@0.84.40
3123
- @earendil-works+pi-agent-core@0.84.3' ]; then
3124
- fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the 0.84.40/0.84.3 lookalikes and pass 0.84.4 bare or with a peer-hash — got: ${matcher_probe:-<nothing leaked>}"
3157
+ '@earendil-works+pi-ai@0.85.1' \
3158
+ '@earendil-works+pi-ai@0.85.1_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
3159
+ '@earendil-works+pi-ai@0.85.1_ws@8.21.3' \
3160
+ '@earendil-works+pi-ai@0.85.10' \
3161
+ '@earendil-works+pi-agent-core@0.85.0' | pack_install_leaked_pi)
3162
+ if [ "$matcher_probe" != '@earendil-works+pi-ai@0.85.10
3163
+ @earendil-works+pi-agent-core@0.85.0' ]; then
3164
+ fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the 0.85.10/0.85.0 lookalikes and pass 0.85.1 bare or with either measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
3125
3165
  return 1
3126
3166
  fi
3127
- echo "[check-pack-pin-matcher] ok — the pin-leak matcher is version-bounded (lookalikes leak, pinned version passes bare and with a peer-hash)"
3167
+
3168
+ # Cell 2 — the closure prefix. chord is a non-`pi-` member of the same runtime closure
3169
+ # (0.85.0 onward). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
3170
+ # back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
3171
+ matcher_probe=$(printf '%s\n' \
3172
+ '@earendil-works+chord@0.85.1' \
3173
+ '@earendil-works+chord@0.85.0' \
3174
+ '@earendil-works+pi-ai@0.85.1' | pack_install_leaked_pi)
3175
+ if [ "$matcher_probe" != '@earendil-works+chord@0.85.0' ]; then
3176
+ fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it is a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol at 0.85.x) — got: ${matcher_probe:-<nothing leaked>}"
3177
+ return 1
3178
+ fi
3179
+
3180
+ echo "[check-pack-pin-matcher] ok — the pin-leak matcher is version-bounded (lookalikes leak, pinned version passes bare and with either measured peer-hash) and covers the whole @earendil-works closure (chord included)"
3128
3181
  }
3129
3182
 
3130
3183
  _check_pack_install_impl() {
@@ -3351,7 +3404,7 @@ _check_pack_install_impl() {
3351
3404
  printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
3352
3405
 
3353
3406
  # pi-agent-core is pinned even though we never import it: pi-coding-agent depends
3354
- # on it by CARET (`^0.84.x`), so with no lockfile in this fresh temp project it
3407
+ # on it by CARET (`^0.85.x`), so with no lockfile in this fresh temp project it
3355
3408
  # floats to whatever pi published last — and that newer core then drags a NESTED
3356
3409
  # pi-ai of its own. Measured 2026-07-21: pinning only the three we import left
3357
3410
  # pi-agent-core@0.80.10 + pi-ai@0.80.10 in the tree while the gate still announced
@@ -3369,21 +3422,30 @@ _check_pack_install_impl() {
3369
3422
  # `^0.84.x` carets on it), and upstream's 0.84.4 patch publish (2026-08-28
3370
3423
  # 22:04Z) floated that caret in this lockfile-less temp install, turning CI
3371
3424
  # red through the leak assertion below. That explicit pin is what held the
3372
- # line until the bump lane ran; the verified floor is 0.84.4 as of
3373
- # 2026-09-01, and the pin moved WITH it rather than being retired. The leak
3374
- # assertion below still covers every other pi package, including any package
3375
- # a future pi bump adds to the closure.
3376
- echo "[check-pack-install] pnpm add into $tmp (with 0.84.x peers + typebox)"
3425
+ # line until the bump lane ran; the pin moved WITH each verified floor rather
3426
+ # than being retired.
3427
+ #
3428
+ # At 0.85.1 the constellation GREW AGAIN, and this time outside the `pi-`
3429
+ # naming: `@earendil-works/chord` is new in 0.85.0 and is a runtime
3430
+ # `dependencies` entry of pi-coding-agent, pi-agent-core, pi-client and
3431
+ # pi-protocol (`[측정 2026-09-06]` `npm view <pkg>@0.85.1 dependencies`), so an
3432
+ # unpinned `^0.85.x` caret on it floats exactly like pi-agent-core did in 2026-07-21.
3433
+ # It is pinned here for the first time. The leak assertion below covers every
3434
+ # other @earendil-works package too — but only because the matcher's prefix was
3435
+ # widened from `pi-` to the org in the same change; before that it was blind to
3436
+ # precisely this class of new member. The verified floor is 0.85.1 as of 2026-09-06.
3437
+ echo "[check-pack-install] pnpm add into $tmp (with 0.85.x peers + chord + typebox)"
3377
3438
  local install_log
3378
3439
  install_log=$(cd "$tmp" && pnpm add \
3379
3440
  "$tgz_path" \
3380
- "@earendil-works/pi-ai@0.84.4" \
3381
- "@earendil-works/pi-coding-agent@0.84.4" \
3382
- "@earendil-works/pi-tui@0.84.4" \
3383
- "@earendil-works/pi-agent-core@0.84.4" \
3384
- "@earendil-works/pi-client@0.84.4" \
3385
- "@earendil-works/pi-protocol@0.84.4" \
3386
- "@earendil-works/pi-telemetry@0.84.4" \
3441
+ "@earendil-works/pi-ai@0.85.1" \
3442
+ "@earendil-works/pi-coding-agent@0.85.1" \
3443
+ "@earendil-works/pi-tui@0.85.1" \
3444
+ "@earendil-works/pi-agent-core@0.85.1" \
3445
+ "@earendil-works/pi-client@0.85.1" \
3446
+ "@earendil-works/pi-protocol@0.85.1" \
3447
+ "@earendil-works/pi-telemetry@0.85.1" \
3448
+ "@earendil-works/chord@0.85.1" \
3387
3449
  "typebox@latest" \
3388
3450
  --ignore-workspace --ignore-scripts 2>&1) || {
3389
3451
  fail "[check-pack-install] pnpm add failed:"
@@ -3392,18 +3454,18 @@ _check_pack_install_impl() {
3392
3454
  }
3393
3455
 
3394
3456
  # A pin is a wish until the resolved tree is read back. Assert it: EVERY
3395
- # @earendil-works pi package present — direct or transitive, top level or nested
3396
- # must be the pinned 0.84.4. Anything else means an unpinned caret floated and the
3397
- # rest of this gate would be exercising a runtime nobody verified, while still
3398
- # printing "pinned pi 0.84.4". Fail loud instead of proving the wrong floor.
3457
+ # @earendil-works package present — direct or transitive, top level or nested,
3458
+ # chord included — must be the pinned 0.85.1. Anything else means an unpinned caret
3459
+ # floated and the rest of this gate would be exercising a runtime nobody verified,
3460
+ # while still printing "pinned pi 0.85.1". Fail loud instead of proving the wrong floor.
3399
3461
  local leaked_pi
3400
3462
  leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
3401
3463
  if [ -n "$leaked_pi" ]; then
3402
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.84.4):"
3464
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.85.1):"
3403
3465
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
3404
3466
  return 1
3405
3467
  fi
3406
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.84.4"
3468
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 0.85.1 (chord included)"
3407
3469
 
3408
3470
  # Resolve the installed package.json and confirm pi.extensions
3409
3471
  # arrived intact. If pi.extensions is empty or missing, the
@@ -3701,10 +3763,23 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
3701
3763
  # SAME installed candidate, on a FRESH sandbox home with every harness absent.
3702
3764
  # This is the first living consumer of package-installed `entwurf setup`:
3703
3765
  # mode=installed decided by name before anything else, NO pnpm bootstrap inside
3704
- # node_modules, pi/claude/agy explicit zero-state SKIPs, stable bins PASS as
3705
- # npm-provided, core bridge boundary PASS, computed green, zero harness/auth
3706
- # writes. Harness probes are pinned absent via the PI_BIN/CLAUDE_BIN/AGY_BIN/
3707
- # COPILOT_BIN seams so the gate host's real harnesses never leak in.
3766
+ # node_modules, pi/claude/agy/copilot/omp explicit zero-state SKIPs, stable bins
3767
+ # PASS as npm-provided, core bridge boundary PASS, computed green, zero
3768
+ # harness/auth writes. Harness probes are pinned absent via the FIVE
3769
+ # PI_BIN/CLAUDE_BIN/AGY_BIN/COPILOT_BIN/OMP_BIN seams so the gate host's real
3770
+ # harnesses never leak in.
3771
+ #
3772
+ # OMP_BIN was MISSING here from the 0.16.0 OMP admission until 2026-09-06, while
3773
+ # the two sibling fixtures (`smoke-setup-verdict.sh:70`,
3774
+ # `check-setup-qualification.sh:76`) both carried it. On a host that HAS omp on
3775
+ # PATH the row's own premise was false: setup detected omp, and the omp config /
3776
+ # receiver installers then refused — correctly — because this row also exports
3777
+ # `PI_CODING_AGENT_DIR`, which omp reads too, so the target directory is
3778
+ # ambiguous. Nothing was wrong with the product; the fixture was. It stayed
3779
+ # invisible because CI runners carry no omp AND `release_gate()` does not run
3780
+ # this gate (it is prepublishOnly + the CI install-surface job). The skip probe
3781
+ # below now requires FIVE zero-states, so a sixth harness cannot be admitted
3782
+ # while this seam is quietly left behind again.
3708
3783
  local setup_home="$npm_tmp/setuphome" setup_proj="$npm_tmp/setupproj" setup_out setup_rc setup_auth
3709
3784
  mkdir -p "$setup_home/.pi/agent" "$setup_proj"
3710
3785
  setup_auth="$setup_home/.pi/agent/auth.json"
@@ -3715,7 +3790,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
3715
3790
  setup_out=$(HOME="$setup_home" XDG_DATA_HOME="$setup_home/.local/share" XDG_STATE_HOME="$setup_home/.local/state" \
3716
3791
  XDG_CACHE_HOME="$setup_home/.cache" XDG_CONFIG_HOME="$setup_home/.config" PI_CODING_AGENT_DIR="$setup_home/.pi/agent" \
3717
3792
  PI_BIN="$npm_tmp/definitely-absent" CLAUDE_BIN="$npm_tmp/definitely-absent" AGY_BIN="$npm_tmp/definitely-absent" \
3718
- COPILOT_BIN="$npm_tmp/definitely-absent" \
3793
+ COPILOT_BIN="$npm_tmp/definitely-absent" OMP_BIN="$npm_tmp/definitely-absent" \
3719
3794
  "$npmroot/node_modules/.bin/entwurf" setup "$setup_proj" 2>&1)
3720
3795
  setup_rc=$?
3721
3796
  set -e
@@ -3735,7 +3810,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
3735
3810
  return 1
3736
3811
  fi
3737
3812
  local skip_probe
3738
- for skip_probe in "pi: SKIP" "claude: SKIP" "agy: SKIP" "copilot: SKIP"; do
3813
+ for skip_probe in "pi: SKIP" "claude: SKIP" "agy: SKIP" "copilot: SKIP" "omp: SKIP"; do
3739
3814
  if ! grep -q "$skip_probe" <<<"$setup_out"; then
3740
3815
  fail "[check-pack-install] installed all-absent setup missing explicit zero-state '$skip_probe':"
3741
3816
  echo "$setup_out" | tail -25 | sed 's/^/ /' >&2
@@ -3760,7 +3835,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
3760
3835
  fail "[check-pack-install] installed setup touched the credential store (byte drift or .bak)"
3761
3836
  return 1
3762
3837
  fi
3763
- echo "[check-pack-install] installed all-absent setup pass (mode-first, no bootstrap, 4x SKIP, bins npm-provided, core PASS, computed green, zero writes)"
3838
+ echo "[check-pack-install] installed all-absent setup pass (mode-first, no bootstrap, 5x SKIP, bins npm-provided, core PASS, computed green, zero writes)"
3764
3839
 
3765
3840
  # Installed copilot-present aggregate `setup` row (#86 C3b): the SAME consumer
3766
3841
  # bin on a fresh sandbox home, with the shared fake vendor
@@ -3786,6 +3861,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
3786
3861
  setup_out=$(HOME="$cop_setup_home" XDG_DATA_HOME="$cop_setup_home/.local/share" XDG_STATE_HOME="$cop_setup_home/.local/state" \
3787
3862
  XDG_CACHE_HOME="$cop_setup_home/.cache" XDG_CONFIG_HOME="$cop_setup_home/.config" PI_CODING_AGENT_DIR="$cop_setup_home/.pi/agent" \
3788
3863
  PI_BIN="$npm_tmp/definitely-absent" CLAUDE_BIN="$npm_tmp/definitely-absent" AGY_BIN="$npm_tmp/definitely-absent" \
3864
+ OMP_BIN="$npm_tmp/definitely-absent" \
3789
3865
  COPILOT_BIN="$cop_fake/copilot" PATH="$cop_fake:$npmroot/node_modules/.bin:$PATH" \
3790
3866
  "$npmroot/node_modules/.bin/entwurf" setup "$cop_setup_proj" 2>&1)
3791
3867
  setup_rc=$?
@@ -5440,10 +5516,11 @@ release_gate() {
5440
5516
  results+=("FAIL static (pnpm run check:full)"); failc=$((failc + 1))
5441
5517
  fi
5442
5518
 
5443
- # 1b. Discriminating power of that floor. check-gate-qualification left the
5444
- # default check chains (operator inner-loop cost), so release acceptance
5445
- # carries it explicitly as its own MUST step: a cut must re-prove the gates
5446
- # still kill what they claim to kill.
5519
+ # 1b. Discriminating power of that floor. The mutant-EXECUTING body left the
5520
+ # default check chains (operator inner-loop cost) only its head rides along,
5521
+ # as check-gate-manifests inside check:hermetic so release acceptance carries
5522
+ # the body explicitly as its own MUST step: a cut must re-prove the gates still
5523
+ # kill what they claim to kill.
5447
5524
  section "release-gate step: check-gate-qualification"
5448
5525
  if (cd "$REPO_DIR" && bash "$self" check-gate-qualification); then
5449
5526
  ok "check-gate-qualification: PASS"
@@ -5700,6 +5777,9 @@ case "$cmd" in
5700
5777
  check-meta-receiver-marker)
5701
5778
  check_meta_receiver_marker
5702
5779
  ;;
5780
+ check-meta-hook-session-switch)
5781
+ check_meta_hook_session_switch
5782
+ ;;
5703
5783
  check-meta-identity-consumers)
5704
5784
  check_meta_identity_consumers
5705
5785
  ;;
@@ -6015,6 +6095,16 @@ case "$cmd" in
6015
6095
  # per cell; offline + deterministic (deps: python3).
6016
6096
  (cd "$REPO_DIR" && python3 scripts/check-agy-permission-matrix.py)
6017
6097
  ;;
6098
+ check-gate-manifests)
6099
+ # The HEAD of check-gate-qualification, exposed on its own so the deterministic
6100
+ # floor can pay for it: the runner self-test (classifier truth table + synthetic
6101
+ # negatives) plus the REAL manifests — schema, global claim uniqueness, subjects
6102
+ # tracked and lstat-regular in the origin index, claim tokens exactly once in their
6103
+ # gate sources — plus the declared lane inventory. It executes ZERO mutants and
6104
+ # makes NO snapshot of this repo; the body (check-gate-qualification) owns that and
6105
+ # is unchanged. Cheap enough for check:hermetic (~8s).
6106
+ run_ts scripts/check-gate-qualification.ts --manifests-only
6107
+ ;;
6018
6108
  check-gate-qualification)
6019
6109
  # Kill-proof qualification — the gate-of-gates. Proves the committed defect mutants
6020
6110
  # (scripts/mutants/*.json) make their gates fail BOUNDED and FOR THE CLAIMED REASON
@@ -72,7 +72,7 @@ interface WireUsage {
72
72
  * One `_meta.quota.token_count` row. The field NAMES deliberately differ from
73
73
  * `PromptResponse.usage`: cache reads are `cachedInputTokens` here because the
74
74
  * shape is shared with codex-acp, and `cachedWriteTokens` is Claude's extra
75
- * sibling (read at claude-agent-acp 0.73.0 `dist/acp-agent.js:5750-5765`).
75
+ * sibling (read at claude-agent-acp 0.75.1 `dist/acp-agent.js:6493-6502`).
76
76
  * Reading a quota row with the `usage` field names silently yields zeros, so the
77
77
  * fixture below spells the vendor's names out rather than reusing `WireUsage`.
78
78
  */
@@ -137,7 +137,7 @@ function makeFakeChild() {
137
137
  *
138
138
  * Each `prompt` call consumes the next TurnScript: it first pushes that turn's
139
139
  * `usage_update` notification (the wire the running cost total actually arrives
140
- * on — read at claude-agent-acp 0.73.0 `dist/acp-agent.js:2918-2933`), then answers the
140
+ * on — read at claude-agent-acp 0.75.1 `dist/acp-agent.js:3467-3482`), then answers the
141
141
  * prompt with that turn's `PromptResponse.usage` (the wire the turn aggregate
142
142
  * arrives on). Both orderings are the real one: the notification precedes the
143
143
  * response, because the SDK emits it from the `result` message that ENDS the turn.
@@ -393,8 +393,8 @@ try {
393
393
  // AGENT LOOP only. `_meta.quota.model_usage` comes from `result.modelUsage` and
394
394
  // also counts Task subagents, sidechains and INTERNAL CALLS SUCH AS COMPACTION;
395
395
  // the vendor states its rows "can total more than `token_count`" and are "the
396
- // fuller picture, not a decomposition of it" (read at claude-agent-acp 0.73.0
397
- // `dist/acp-agent.js:5732-5738`).
396
+ // fuller picture, not a decomposition of it" (read at claude-agent-acp 0.75.1
397
+ // `dist/acp-agent.js:6468-6474`).
398
398
  //
399
399
  // The wide one is required, not merely nicer, because the DENOMINATOR already
400
400
  // has that scope: turn cost is the adjacent diff of the backend's running total,
@@ -608,8 +608,8 @@ try {
608
608
  // identity that produces the bound is a property of the MAIN AGENT LOOP's
609
609
  // cache breakpoints. `_meta.quota.model_usage` is a WIDER scope: the vendor
610
610
  // states those rows also count Task subagents, sidechains, and INTERNAL
611
- // CALLS SUCH AS COMPACTION (read at claude-agent-acp 0.73.0
612
- // `dist/acp-agent.js:5728-5748`). Mixing the two scopes inflates the bound
611
+ // CALLS SUCH AS COMPACTION (read at claude-agent-acp 0.75.1
612
+ // `dist/acp-agent.js:6465-6485`). Mixing the two scopes inflates the bound
613
613
  // through both remaining terms that mention cacheWrite:
614
614
  // max(0, occupancy − cacheWrite) shrinks as wide cacheWrite grows, so
615
615
  // less is subtracted; min(rawBound, cacheWrite) rises with it.
@@ -749,9 +749,9 @@ try {
749
749
  // CELL 3 — a turn with NO cost notification holds the baseline.
750
750
  //
751
751
  // Measured upstream: the result-path `usage_update` carries cost (read at
752
- // claude-agent-acp 0.73.0 `dist/acp-agent.js:2913-2924`), while other
752
+ // claude-agent-acp 0.75.1 `dist/acp-agent.js:3467-3482`), while other
753
753
  // `usage_update` paths can carry `used` without cost (for example the
754
- // rate-limit path at `:3661-3671`). A live thinkpad ledger shows such turns
754
+ // rate-limit path at `:4269-4277`). A live thinkpad ledger shows such turns
755
755
  // really occur. The honest handling is to HOLD the baseline so the amount lands
756
756
  // in the NEXT diff: misattributed by turn, exact by session. Rebaselining to 0
757
757
  // there would double-count the whole prefix.
@@ -831,7 +831,7 @@ try {
831
831
  // CELL 4 — a DECREASING session total is never silently absorbed.
832
832
  //
833
833
  // `conversation_reset` switches the session to a fresh transcript (read at
834
- // claude-agent-acp 0.73.0 `dist/acp-agent.js:3675-3682`), but whether that
834
+ // claude-agent-acp 0.75.1 `dist/acp-agent.js:4282-4289`), but whether that
835
835
  // changes `total_cost_usd` is an SDK-internal value we cannot observe here.
836
836
  // A diff can therefore go negative in a session we are still holding.
837
837
  // Absorbing it quietly would both misreport the turn and destroy the only
@@ -181,16 +181,40 @@ writeMetaReceiverMarker({
181
181
 
182
182
  // RECEIVER — a different armed self-fetch citizen. A dead/unarmed target must remain
183
183
  // mailbox-undeliverable, so this marker isolates artifact delivery from target liveness.
184
+ //
185
+ // It is owned by a DIFFERENT live pid than the sender, and that is not decoration (#101):
186
+ // a claude-code watch owner serves exactly one garden at a time, and deliverability now
187
+ // requires that owner's sender marker to still name the garden being addressed. Two
188
+ // citizens armed under ONE pid is a state the real world does not produce — it is what a
189
+ // session switch leaves behind — so a fixture that modelled it would be asking the
190
+ // artifact to deliver into a retired watch.
191
+ //
192
+ // The stand-in second native CLI is a spawned idle process, NOT this gate's parent: the
193
+ // subject bridge resolves its own sender identity through its ancestry, and an ancestor
194
+ // holding a second garden's marker is the "ambiguous sender identity" refusal the bridge
195
+ // already ships. A live pid outside that ancestry is what a real second CLI looks like.
196
+ // It exits on its own timer, and is killed in the outer finally.
197
+ const receiverOwner = spawn(process.execPath, ["-e", "setTimeout(() => {}, 300000)"], { stdio: "ignore" });
198
+ const receiverOwnerPid = receiverOwner.pid;
199
+ if (typeof receiverOwnerPid !== "number") throw new Error("could not spawn the stand-in receiver owner process");
184
200
  const receiver = upsertMetaSession({
185
201
  input: { backend: "claude-code", nativeSessionId: `bridge-delivery-receiver-${process.pid}`, cwd: tmp },
186
202
  dir: sessionsDir,
187
203
  });
188
204
  const gid = receiver.record.gardenId;
205
+ writeMetaSenderMarker({
206
+ backend: "claude-code",
207
+ gardenId: gid,
208
+ nativeSessionId: receiver.record.nativeSessionId,
209
+ cwd: tmp,
210
+ ownerPid: receiverOwnerPid,
211
+ sendersDir,
212
+ });
189
213
  writeMetaReceiverMarker({
190
214
  gardenId: gid,
191
215
  backend: "claude-code",
192
216
  nativeSessionId: receiver.record.nativeSessionId,
193
- ownerPid: process.pid,
217
+ ownerPid: receiverOwnerPid,
194
218
  armProvenance: "session-start",
195
219
  receiversDir,
196
220
  });
@@ -471,6 +495,20 @@ try {
471
495
  await fsp.writeFile(receiverRecordFile, receiverRecordV3Bytes);
472
496
  const anonSendersDir = path.join(tmp, "meta-senders-anon");
473
497
  await fsp.mkdir(anonSendersDir, { recursive: true });
498
+ // The TARGET's owner marker still belongs in this dir (#101). Anonymity is about the
499
+ // CALLER — no marker under the bridge child's own owner pid — while deliverability asks
500
+ // a different question of a different pid: is the target's watch owner still serving the
501
+ // target's garden? Leaving it out would make every cell below fail as
502
+ // mailbox-undeliverable and prove nothing about the anonymous hatch. In production no
503
+ // one swaps this dir; the split exists only because this fixture does.
504
+ writeMetaSenderMarker({
505
+ backend: "claude-code",
506
+ gardenId: gid,
507
+ nativeSessionId: receiver.record.nativeSessionId,
508
+ cwd: tmp,
509
+ ownerPid: receiverOwnerPid,
510
+ sendersDir: anonSendersDir,
511
+ });
474
512
  const oneShotV2 = async (
475
513
  cellEnv: NodeJS.ProcessEnv,
476
514
  message: string,
@@ -582,6 +620,9 @@ try {
582
620
  try {
583
621
  child?.kill("SIGTERM");
584
622
  } catch {}
623
+ try {
624
+ receiverOwner.kill("SIGTERM");
625
+ } catch {}
585
626
  await fsp.rm(tmp, { recursive: true, force: true });
586
627
  }
587
628
 
@@ -19,7 +19,12 @@
19
19
  * socket-dir-read-error diagnostic (P2e②),
20
20
  * - diagnostics are kind-tagged and sorted.
21
21
  *
22
- * No IO — meta entries/reader and socket dir/readdir/probe are injected fakes.
22
+ * No IO — meta entries/reader and socket dir/readdir/probe are injected fakes, and so is
23
+ * the #101 `observe` seam. That seam is the one dep whose PRODUCTION default is real IO
24
+ * (one default is safer than two wiring sites each passing their own observer), so a gate
25
+ * that left it out would quietly stat the operator's real garden roots for fabricated
26
+ * garden ids. It is injected here for that reason, and the cell below proves the injected
27
+ * observation actually reaches the fact rather than being dropped on the way.
23
28
  */
24
29
 
25
30
  import assert from "node:assert/strict";
@@ -74,6 +79,11 @@ function deps(
74
79
  ): EntwurfFactsDeps {
75
80
  const symlinkSet = new Set(opts.symlinks ?? []);
76
81
  return {
82
+ // Injected observation (see the header): a per-identity fake, never the real IO.
83
+ observe: (identity) => ({
84
+ receiver: identity.backend === "claude-code" ? "inactive" : "n/a",
85
+ transcript: identity.gardenId === GID_CLAUDE ? "absent" : "exists",
86
+ }),
77
87
  // Kind-carrying entries, like the real bindings: the listing must be able to refuse a
78
88
  // symlinked record without following it. `irregularMeta` names the ones that are not
79
89
  // regular files.
@@ -144,6 +154,17 @@ async function main(): Promise<void> {
144
154
  );
145
155
  ok("basic: record-less-socket diagnostic keyset exact", true);
146
156
  }
157
+ // #101: the injected observation rides onto each peer. A provider that dropped the
158
+ // seam would return `unobserved` rows — visible, but useless to the caller the
159
+ // columns exist for.
160
+ {
161
+ const claude = r.facts.peers.find((p) => p.gardenId === GID_CLAUDE);
162
+ const pi = r.facts.peers.find((p) => p.gardenId === GID_PI);
163
+ ok(
164
+ "basic: the injected observation reaches every peer fact",
165
+ claude?.receiver === "inactive" && claude?.transcript === "absent" && pi?.receiver === "n/a",
166
+ );
167
+ }
147
168
  // A DEAD record-less socket groups under a different (stale) message — no fresh-cut
148
169
  // pointer for a leftover file, so same-state sockets aggregate per liveness.
149
170
  const r2 = await listEntwurfFacts(deps({}, { [GID_SOCKET_ONLY]: "dead" }));
@@ -138,7 +138,8 @@ for (const backend of META_CITIZEN_BACKENDS) {
138
138
  ok("passthrough recordUpdatedAt", fact.recordUpdatedAt === id.recordUpdatedAt);
139
139
  }
140
140
 
141
- // ── facts-only keyset: identity facts + liveness, NO verb-routing/transcript
141
+ // ── facts-only keyset: identity facts + liveness + the two observed axes, and
142
+ // still NO verb-routing / no transcript PATH ────────────────────────────────
142
143
  {
143
144
  const fact = resolvePeerFact(identity("pi"), "alive");
144
145
  const keys = Object.keys(fact).sort();
@@ -151,9 +152,23 @@ for (const backend of META_CITIZEN_BACKENDS) {
151
152
  "model",
152
153
  "nativeSessionId",
153
154
  "recordUpdatedAt",
155
+ "receiver",
156
+ "transcript",
154
157
  ].sort();
155
158
  assert.deepStrictEqual(keys, expected, `PeerFact keyset drift: got ${keys.join(",")}`);
156
- ok("facts-only keyset exact (identity facts + liveness)", true);
159
+ ok("facts-only keyset exact (identity facts + liveness + observed receiver/transcript)", true);
160
+
161
+ // #101: an unmeasured row says so. `unobserved` is the default for a caller that
162
+ // injected no observer — never `none`/`absent`, which would be a fabricated fact.
163
+ ok(
164
+ "an observer-less composition reports both axes as unobserved (never a default fact)",
165
+ fact.receiver === "unobserved" && fact.transcript === "unobserved",
166
+ );
167
+ const observed = resolvePeerFact(identity("claude-code"), null, { receiver: "inactive", transcript: "absent" });
168
+ ok(
169
+ "an injected observation rides onto the fact verbatim",
170
+ observed.receiver === "inactive" && observed.transcript === "absent",
171
+ );
157
172
 
158
173
  const FORBIDDEN = [
159
174
  "resumable",
@@ -47,7 +47,12 @@ const FORBIDDEN = ["sendable", "resumable", "dispatch", "action", "transport", "
47
47
  // #50 C4: socket-shaped identity must not reappear on this surface.
48
48
  const FORBIDDEN_C4_KEYS = ["sessions", "socketOnly", "controlDir", "socketPath", "count"];
49
49
 
50
- function peer(gardenId: string, backend: MetaCitizenBackend, liveness: FactLiveness): PeerFact {
50
+ function peer(
51
+ gardenId: string,
52
+ backend: MetaCitizenBackend,
53
+ liveness: FactLiveness,
54
+ observed: Partial<Pick<PeerFact, "receiver" | "transcript">> = {},
55
+ ): PeerFact {
51
56
  return {
52
57
  gardenId,
53
58
  backend,
@@ -57,6 +62,8 @@ function peer(gardenId: string, backend: MetaCitizenBackend, liveness: FactLiven
57
62
  createdAt: "2026-06-11T00:00:00.000Z",
58
63
  recordUpdatedAt: "2026-06-11T00:00:00.000Z",
59
64
  liveness,
65
+ receiver: observed.receiver ?? "n/a",
66
+ transcript: observed.transcript ?? "exists",
60
67
  };
61
68
  }
62
69
 
@@ -91,7 +98,7 @@ function main(): void {
91
98
  peer(GID_PI_ALIVE, "pi", "alive"),
92
99
  peer(GID_PI_DEAD, "pi", "dead"),
93
100
  peer(GID_PI_INDET, "pi", "indeterminate"),
94
- peer(GID_CLAUDE, "claude-code", "unsupported"),
101
+ peer(GID_CLAUDE, "claude-code", "unsupported", { receiver: "inactive", transcript: "absent" }),
95
102
  ],
96
103
  // The provider has already folded these into diagnostics; the render layer
97
104
  // never re-renders them as a section (facts carry them for the union math).
@@ -116,6 +123,17 @@ function main(): void {
116
123
  ok("C4: payload is exactly { peers, diagnostics }", true);
117
124
  }
118
125
  ok("peers carry all 4 citizens (unsupported NOT dropped)", payload.peers.length === 4);
126
+ // #101: the two observed axes are the only thing separating two same-cwd claude rows,
127
+ // since `liveness` reads `unsupported` for every one of them. They are FACTS on the
128
+ // row — the forbidden-word scans below still apply to them unchanged.
129
+ ok(
130
+ "observed receiver + transcript ride the row in text",
131
+ new RegExp(`${GID_CLAUDE}.*receiver=inactive.*transcript=absent`).test(text),
132
+ );
133
+ ok(
134
+ "observed axes ride the JSON payload too",
135
+ payload.peers.some((p) => p.gardenId === GID_CLAUDE && p.receiver === "inactive" && p.transcript === "absent"),
136
+ );
119
137
  ok(
120
138
  "unsupported citizen present in peers",
121
139
  payload.peers.some((p) => p.gardenId === GID_CLAUDE && p.liveness === "unsupported"),