@junghanacs/entwurf 0.23.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/BASELINE.md +1 -0
  2. package/CHANGELOG.md +628 -0
  3. package/DELIVERY.md +24 -1
  4. package/FAQ.md +47 -0
  5. package/README.md +5 -3
  6. package/VERIFY.md +20 -2
  7. package/demo/README.md +5 -2
  8. package/docs/acp-backend-rail.md +49 -2
  9. package/docs/setup-clean-host.md +8 -7
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +20 -6
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
  23. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
  24. package/mcp/entwurf-bridge/dist/protocol.js +0 -7
  25. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
  26. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
  27. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  28. package/mcp/entwurf-bridge/src/index.ts +46 -5
  29. package/mcp/entwurf-bridge/test.sh +2 -2
  30. package/package.json +11 -9
  31. package/pi-extensions/entwurf-control.ts +83 -154
  32. package/pi-extensions/lib/acp/backend.ts +16 -6
  33. package/pi-extensions/lib/acp/context.ts +30 -10
  34. package/pi-extensions/lib/acp/session-store.ts +2 -2
  35. package/pi-extensions/lib/callback-env.ts +78 -0
  36. package/pi-extensions/lib/compaction-send-guard.ts +6 -1
  37. package/pi-extensions/lib/control-send-receipt.ts +108 -0
  38. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  39. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
  40. package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
  41. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
  42. package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
  43. package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
  44. package/pi-extensions/lib/fresh-call-composition.ts +56 -22
  45. package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
  46. package/pi-extensions/lib/herdr-placement.ts +24 -4
  47. package/pi-extensions/lib/mux-fresh-call.ts +23 -8
  48. package/pi-extensions/meta-bridge-omp.ts +35 -48
  49. package/protocol.js +0 -8
  50. package/run.sh +306 -53
  51. package/scripts/check-acp-backend-preflight.ts +117 -9
  52. package/scripts/check-acp-carrier-augment.ts +6 -4
  53. package/scripts/check-acp-event-mapper.ts +17 -6
  54. package/scripts/check-acp-prompt-builder.ts +17 -9
  55. package/scripts/check-acp-prompt-lifecycle.ts +23 -6
  56. package/scripts/check-acp-session-reuse.ts +64 -51
  57. package/scripts/check-acp-session-store.ts +31 -8
  58. package/scripts/check-acp-stop-reason.ts +20 -2
  59. package/scripts/check-acp-stream-hooks.ts +22 -5
  60. package/scripts/check-acp-usage-accounting.ts +33 -9
  61. package/scripts/check-entwurf-bridge-boot.ts +16 -1
  62. package/scripts/check-entwurf-v2-surface.ts +289 -10
  63. package/scripts/check-fresh-call-dispatch.ts +4 -5
  64. package/scripts/check-gate-qualification.ts +51 -9
  65. package/scripts/check-herdr-activation.ts +73 -0
  66. package/scripts/check-herdr-fresh-call.ts +30 -6
  67. package/scripts/check-herdr-placement.ts +51 -0
  68. package/scripts/check-herdr-plugin-build.ts +11 -4
  69. package/scripts/check-herdr-plugin.ts +745 -38
  70. package/scripts/check-herdr-sandbox.ts +14 -2
  71. package/scripts/check-install-container.sh +1 -1
  72. package/scripts/check-mux-launcher-fence.ts +37 -0
  73. package/scripts/check-pi-launch.ts +8 -2
  74. package/scripts/check-release-gate-outcomes.ts +165 -0
  75. package/scripts/doctor-pi-provider.ts +44 -4
  76. package/scripts/herdr-rails.mjs +75 -0
  77. package/scripts/lib/mutation-qualify.ts +1 -1
  78. package/scripts/meta-bridge-state.py +18 -1
  79. package/scripts/mutants/acp-backend-preflight.json +20 -0
  80. package/scripts/mutants/bridge-boot-resume.json +26 -0
  81. package/scripts/mutants/bridge-command-boot.json +38 -0
  82. package/scripts/mutants/control-send-receipt.json +235 -0
  83. package/scripts/mutants/herdr-activation.json +16 -0
  84. package/scripts/mutants/herdr-fresh-call.json +15 -4
  85. package/scripts/mutants/herdr-plugin-build.json +7 -11
  86. package/scripts/mutants/herdr-plugin.json +177 -9
  87. package/scripts/mutants/meta-retire.json +22 -0
  88. package/scripts/mutants/mux-fresh-call.json +21 -8
  89. package/scripts/mutants/omp-fresh.json +5 -9
  90. package/scripts/mutants/pack-install.json +4 -4
  91. package/scripts/mutants/pi-floor.json +17 -0
  92. package/scripts/mutants/release-gate.json +48 -0
  93. package/scripts/mutants/retired-sent-surface.json +32 -0
  94. package/scripts/mutants/v2-surface.json +7 -7
  95. package/scripts/probe-bridge-command.ts +1 -0
  96. package/scripts/smoke-acp-session-reuse-live.ts +23 -5
  97. package/scripts/smoke-agy-install-state.sh +2 -2
  98. package/scripts/smoke-codex-fresh-live.ts +94 -13
  99. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  100. package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
  101. package/scripts/smoke-herdr-raw-install-live.sh +90 -5
  102. package/scripts/smoke-meta-install-state.sh +107 -1
  103. package/scripts/smoke-mux-lifecycle-live.ts +9 -2
  104. package/scripts/smoke-omp-fresh-live.ts +4 -2
  105. package/scripts/smoke-omp-mcp-state.sh +1 -1
  106. package/scripts/smoke-pi-provider-state.sh +108 -2
  107. package/scripts/smoke-setup-verdict.sh +18 -2
package/run.sh CHANGED
@@ -122,7 +122,7 @@ Usage:
122
122
  ./run.sh setup [project-dir] # ONE presence-driven composition (#86): per-component PASS/SKIP/FAIL for pi/claude/agy/copilot/omp/codex + stable dev bins + v2 install smoke. Codex birth/MCP/status-line atoms are composed at user scope; setup stays non-green until the operator trusts the birth declaration once in visible Codex. Absent harness = zero-state SKIP, detected-incomplete = named FAIL + nonzero exit. Never installs a harness, starts a daemon, touches credentials, or writes the vendor trust receipt
123
123
  ./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live) + the Codex Pi -> visible Codex -> visible Pi receipt chain (smoke-codex-fresh-live; initial Pi and the Codex it opens in one session S, explicit operator-owned app-server PID in a DIFFERENT session — that separation is the lane B claim). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
124
124
  ./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
125
- ./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b/G1e/G1f, IN pnpm run check:full): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2 and entwurf_resume_call registered/schema + the tools/list surface is EXACTLY the seven shipped garden verbs; tools/list only, no auth/side-effect
125
+ ./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b/G1e/G1f/G1h, IN pnpm run check:full): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2, entwurf_resume_call and entwurf_callback registered/schema + the tools/list surface is EXACTLY the eight shipped garden verbs; tools/list only, no auth/side-effect
126
126
  ./run.sh check-entwurf-bridge-pi-free # deterministic gate (0.12.1 A, IN pnpm check): static — bridge index eager value-import closure must carry no @earendil-works/pi-* (type-only + dynamic import excluded); proves the meta-bridge boots pi-free
127
127
  ./run.sh check-model-lock # deterministic unit test for pi-extensions/model-lock.ts (4-quadrant + edge cases, no API)
128
128
  ./run.sh check-shell-quote # POSIX-safety gate for shellQuote (remote SSH arg quoting in entwurf paths) — source parity + behavior matrix, no SSH
@@ -170,7 +170,7 @@ Usage:
170
170
  ./run.sh smoke-codex-fresh-live # #95 RELEASE MUST: record-backed receipt fixture -> PUBLIC initial Pi fresh/callback in a session S that is NOT the app-server's -> Pi PUBLIC omitted-placement Codex fresh/exact callback, also in S -> Pi-to-Codex addressed native-push v2 -> Codex PUBLIC omitted-placement AND omitted-cwd fresh Pi/exact callback IN S beside its own TUI (lane B claim: the app-server env names another session, so S can only come from the caller's pane title; lane C claim: no cwd was asked for, so the directory can only come from the Codex caller's own record — and the Codex thread's own rollout session_meta.cwd, its pane and its record must all be the requested scratch, not the app-server's repo) -> Codex final v2 evidence. Needs LIVE=1 + ENTWURF_CODEX_APP_SERVER_PID + ENTWURF_CODEX_FRESH_MODEL + ENTWURF_CODEX_FRESH_PI_MODEL, and its ONE stable launch directory answered 'Trust' once in a plain codex -C there: codex records its direct consent per exact directory, so a directory with no answer at all opens a consent screen instead of a first turn. Only THIS gate treats that as a precondition and fails up front as codex-launch-cwd-undecided rather than timing out on a callback — an ordinary entwurf_fresh_call only prints the note and opens the window, because a human can answer the screen. The smoke PRINTS that directory as 'launch-cwd <dir>' and repeats it in the failure as a ready-to-run repair command — do not retype it from memory, and do not write it as \$TMPDIR/... (os.tmpdir() falls back to /tmp when TMPDIR is unset, so that spelling names a different directory in the filesystem root). The PID names the operator-owned app-server, which must sit in a DIFFERENT session from S; the smoke checks/prints all four coordinates, never starts/stops/guesses the app-server/session, never reads screen text, interrupts only its exact still-running Codex turn, and cleans only receipt-named window ids
171
171
  ./run.sh smoke-mux-lifecycle-live # RELEASE MUST integrated LIVE lifecycle acceptance for mux, through the REAL MCP surface — OUT of pnpm check, needs LIVE=1 and spends model turns (two pi siblings: native + recorded-ACP provider, each resumed once; one Claude Code sibling). tools/call fresh_call -> nonce callback sender envelope -> v2 control send landing in the sibling's own transcript -> resume_call REFUSED while live (window count unchanged) -> stable-handle close (pane gone, socket dead, record kept) -> dormant delivery refused honestly -> public entwurf_resume_call with LAUNCH and OBSERVATION receipts kept apart, same-gid socket alive, zero new citizens, zero lock residue, resumed pane_start_path == RECORD cwd (separate tmux query), transcript byte-identical across the resume -> v2 recall of the pre-close fact. claude-code resume refused target-not-pi, no window opened and no lock residue. LIVE=1 ./run.sh smoke-mux-lifecycle-live
172
172
  ./run.sh check-meta-facts # deterministic gate for the meta-facts projection (#65): drives the REAL CLI — full-record join, parse-before-uniqueness, no-winner duplicates, drift/symlink/invalid-UTF-8 defects in-band, deterministic bytes, exit contract 0/2/3, dispatch+emit reachability
173
- ./run.sh check-herdr-plugin # deterministic gate for the #116 M2-b herdr plugin (`plugins/herdr/`): STATIC manifest shape for herdr 0.9.0 + the forbidden sections ([[startup]]/[[events]]/[[actions]]/[[link_handlers]]) asserted absent ([[build]] is check-herdr-plugin-build's subject since #116 M3-b3); BEHAVIOURAL drive of the REAL pane entry against a stub `entwurf` and stub HERDR_BIN_PATH that LOG every call — exactly one peer-facts + one agent list per open (counted, not claimed), skip-by-name when Entwurf is absent, four distinct named refusals instead of an empty table, ambiguous never first-wins, diagnostics shown, activity in its own column, and zero writes to the plugin state/config dirs. No herdr binary, no Entwurf install
173
+ ./run.sh check-herdr-plugin # deterministic gate for the #116 M2-b herdr plugin (`plugins/herdr/`): STATIC manifest shape for herdr 0.9.0 + the forbidden sections ([[startup]]/[[events]]/[[actions]]/[[link_handlers]]) asserted absent ([[build]] is check-herdr-plugin-build's subject since #116 M3-b3); BEHAVIOURAL drive of the REAL pane entry against a stub `entwurf` and stub HERDR_BIN_PATH that LOG every call — exactly one `herdr integration status` + one `entwurf peer-facts` + one `herdr agent list` per open, in that order (counted, not claimed), through ONE spawn call site; the four blocks each naming their own owner/observation time with no aggregate verdict; the D3=C fallback reading the activation ledger from the path its WRITER owns (XDG state, never the runtime's data root) and only when the phase is `active`, with ENTWURF_BIN and PATH still winning outright; the citizen axis alone skipping by name when no binary is reachable; every broken-evidence state a named red rather than a crash or an empty table, and a selected herdr atom that is `outdated`/`needs repair` as block [1]'s OWN red (`not installed` stays a reported skip); ambiguous never first-wins, diagnostics shown, activity in its own column, and zero writes to the plugin state/config dirs. No herdr binary, no Entwurf install
174
174
  ./run.sh check-herdr-runtime-bootstrap # deterministic gate for the #116 M3-b1 runtime leaf (`plugins/herdr/lib/runtime-bootstrap.mjs`): the Entwurf-owned stable active root as a REAL directory (nothing is put on PATH — the scoped wiring above names ABSOLUTE commands under it), ownership decided BEFORE the first mkdir, disk facts from lstat (a dangling symlink is not 'absent' and a link into another tree is not a directory), exact name@version + compiled entry + all three required bins present AND executable + a real `check-bridge`, the plugin-owned artifact lock coherent with the checkout, the owned npm cache and --ignore-scripts, a CERTIFIED journal (non-object/array/scalar/blank-identity/phase-contradicting-digest all refused) as the only ownership proof, all EIGHT active/staging/previous combinations named, the last good runtime never lost (torn swap AND corrupt-active beside a good backup), prior provenance carried across an install that may not finish, a failed candidate leaving the running runtime byte-identical, idempotent same-spec reinstall, a journal believed only while the disk backs it, a preflight-then-mutate inverse taking runtime last, and the refusal to record Herdr's commit as ours. NETWORK ZERO, npm ZERO — it drives a FIXTURE package, so the ACTUAL package proof lives in check-pack-install
175
175
  ./run.sh check-herdr-activation # deterministic gate for the #116 M3-b2 scoped activation: the pi user-scope-ONLY forward seam (no project write, OpenCode byte-untouched), the absolute command DERIVED from the runtime root for BOTH harnesses (relative/unnormalised refused), default bare/clone bytes unchanged when no mode is asked, the pi inverse refusing a drifted command instead of deleting an override, a certified activation ledger that can never name an atom outside {pi, claude-code}, add-only reinstall (a shrinking H retains, never removes), a roots-drift refusal before the first byte, the teardown order components->runtime->ledger LAST, one refused preflight leaving every byte untouched, and no deferred module load on the path that deletes its own runtime. Drives the REAL pi writers; the vendor `claude` CLI is NOT invoked and its acceptance of an absolute executable stays a named LIVE boundary
176
176
  ./run.sh check-herdr-plugin-profile # deterministic gate for the #116 M3-a PURE activation leaf (`plugins/herdr/lib/integration-profile.mjs`): the closed {pi→pi, claude→claude-code} table, exactly-one-row-or-named-refusal, the FRONT-ANCHORED state grammar (a `/srv/current (v9)/` directory name may not decide a verdict), no path in the plan, outdated/needs-repair as named FAILs, not-installed as a zero-write SKIP, `current` as herdr's admission with no floor of our own, OpenCode observed but never planned, and the leaf's purity. Listing strings only — no herdr binary, no install, no environment, no writes
@@ -276,7 +276,7 @@ Usage:
276
276
  ./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)
277
277
  ./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
278
278
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
279
- ./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
279
+ ./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 (a prefix-extended @0.86.0-beta.1 and an off-pin @0.85.1 must leak, @0.86.0 bare or with any 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
280
280
  ./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
281
281
  ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke + the #116 M3-b1 herdr-plugin RUNTIME VERIFIER run against that actual installed tarball (the focused check-herdr-runtime-bootstrap drives a fixture; THIS is where the real package is proven) 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)
282
282
  ./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)
@@ -709,6 +709,31 @@ install_user_scope() {
709
709
  esac
710
710
  shift
711
711
  done
712
+ # pi FLOOR — the same closed range `setup` enforces (#119 door symmetry). setup refuses an
713
+ # out-of-range pi by name before it writes any Pi wiring; this verb is the OTHER door onto the
714
+ # same registration (herdr plugin activation reaches it through plugins/herdr/lib/build.mjs →
715
+ # scripts/herdr-plugin-activate.mjs → `run.sh install-user-scope --plugin-runtime …`) and had no
716
+ # version check at all, so a host pi setup would name as FAIL got its citizen written here in
717
+ # silence. Both doors now tell the same truth, off the SAME derived range — `pi_supported_range`
718
+ # reads the package.json devDep pin, so there is one range in the repo and no second parser.
719
+ #
720
+ # ABSENT pi is deliberately NOT changed here. This verb is called by an activation that has
721
+ # already decided pi is one of its backends; inventing a presence verdict at the writer would put
722
+ # a second detection authority next to setup's. Out-of-range is the asymmetry that was real, and
723
+ # it is the only one closed. PI_BIN is the same probe seam setup uses (hermetic gates pin it;
724
+ # production leaves it unset).
725
+ local pi_bin="${PI_BIN:-pi}" pi_range pi_ver
726
+ if command -v "$pi_bin" >/dev/null 2>&1; then
727
+ pi_range=$(pi_supported_range)
728
+ # Bounded capture, same reason as setup's: a resolvable pi whose --version crashes or prints
729
+ # garbage must reach the refusal below, never abort through set -e with nothing said.
730
+ pi_ver="$("$pi_bin" --version 2>/dev/null | head -n 1 | tr -d '[:space:]')" || pi_ver=""
731
+ if ! pi_version_in_range "${pi_ver:-0}" "$pi_range"; then
732
+ echo "[install-user-scope] pi: FAIL — detected pi ${pi_ver:-<unreadable version>} is outside the supported range $pi_range; nothing was written. Align pi, then re-run." >&2
733
+ exit 1
734
+ fi
735
+ fi
736
+
712
737
  preflight_v3_store install-user-scope
713
738
  section "install-user-scope: the GLOBAL pi citizen only (no project wiring)"
714
739
  register_user_scope_citizen "$plugin_runtime"
@@ -1193,6 +1218,30 @@ check_entwurf_v2_send() {
1193
1218
  run_vitest pi-extensions/lib/entwurf-v2-send.test.ts
1194
1219
  }
1195
1220
 
1221
+ check_entwurf_v2_runner() {
1222
+ # MUTANT EXECUTION COORDINATE, not a discovery path — the file is found by
1223
+ # check-tests-beside-behavior like every other beside-behaviour lane. This case exists because
1224
+ # the #120 P2 SEND-DIRTY-LOCK-RUNNER-MOVES-ACCEPTANCE mutant names it as gate argv, and
1225
+ # attribution is read from the JSON test titles `run_vitest` emits. Narrow file filter, same
1226
+ # reason as check-entwurf-v2-send: a mutant pointed at the discovery door would re-run every
1227
+ # beside-behaviour test once per mutant.
1228
+ # It stays inside check:contracts even though the discovery door also runs this file — see
1229
+ # check-release-gate-outcomes: a gate a mutant names must be reachable outside qualification.
1230
+ section "v2 dispatch runner (mutant execution coordinate)"
1231
+ run_vitest pi-extensions/lib/entwurf-v2-runner.test.ts
1232
+ }
1233
+
1234
+ check_control_send_receipt() {
1235
+ # MUTANT EXECUTION COORDINATE, not a discovery path — the test is found by
1236
+ # check-tests-beside-behavior like every other beside-behaviour lane. This case exists because
1237
+ # the #120 P2 SEND-RECEIPT-* mutants name it as gate argv, and attribution is read from the JSON
1238
+ # test titles `run_vitest` emits. Narrow file filter, same reason as check-entwurf-v2-send.
1239
+ # It stays inside check:contracts even though the discovery door also runs this file — see
1240
+ # check-release-gate-outcomes: a gate a mutant names must be reachable outside qualification.
1241
+ section "control-socket acceptance boundary (mutant execution coordinate)"
1242
+ run_vitest pi-extensions/lib/control-send-receipt.test.ts
1243
+ }
1244
+
1196
1245
  check_entwurf_v2_native_push() {
1197
1246
  # MUTANT EXECUTION COORDINATE, not a discovery path — the test is found by
1198
1247
  # check-tests-beside-behavior like every other beside-behaviour lane. This case exists
@@ -2098,7 +2147,14 @@ const runSh = readFileSync('run.sh', 'utf8');
2098
2147
  const piAi = pkg.devDependencies?.['@earendil-works/pi-ai'];
2099
2148
  const piCoding = pkg.devDependencies?.['@earendil-works/pi-coding-agent'];
2100
2149
  const piTui = pkg.devDependencies?.['@earendil-works/pi-tui'];
2150
+ // #120 P2: pi-agent-core is now DIRECTLY imported by test/pi-queue.oracle.test.ts, which proves
2151
+ // what the control-socket receipt says about a receiver's queues. An oracle pinned to a DIFFERENT
2152
+ // pi than the one this repo targets would answer honestly about a pi nobody runs, so its exact
2153
+ // dev pin moves with the floor or the floor is not a floor.
2154
+ const piCore = pkg.devDependencies?.['@earendil-works/pi-agent-core'];
2101
2155
  assert.ok(piAi, 'package.json devDependencies must pin @earendil-works/pi-ai');
2156
+ assert.equal(piCore, piAi,
2157
+ `[QK:PI-CORE-PIN-TRACKS-FLOOR] @earendil-works/pi-agent-core (${piCore}) must match @earendil-works/pi-ai (${piAi}) — the queue oracle imports it directly to prove what the control-socket receipt claims about a receiver's queues, so a pin that drifts off the floor would answer honestly about a pi nobody here runs`);
2102
2158
  assert.equal(piCoding, piAi,
2103
2159
  `@earendil-works/pi-coding-agent (${piCoding}) must match @earendil-works/pi-ai (${piAi})`);
2104
2160
  assert.equal(piTui, piAi,
@@ -2120,13 +2176,16 @@ assert.equal(peerTui, piAi,
2120
2176
  // floor tracks the devDep pin so a consumer can't install against a pi lacking
2121
2177
  // the public trust exports the bridge imports at the pinned minor, AND an upper
2122
2178
  // bound at the next minor stops a fresh install from silently pulling a future
2123
- // pi (past the declared ceiling — 0.86+ at the current 0.85.1 pin) whose
2179
+ // pi (past the declared ceiling — 0.87+ at the current 0.86.0 pin) whose
2124
2180
  // internal export surface has drifted from the one we typecheck against.
2125
2181
  // pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
2126
- // factory churn is exactly this), so an open `>=` floor is exactly how the next
2127
- // installer re-acquires the drift. The floor is also the HARD MINIMUM a consumer
2128
- // install resolves: at `>=0.85.1` an existing 0.85.0 host is upgraded, not kept.
2129
- // Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.85.1 <0.86`).
2182
+ // factory churn is exactly this, and 0.86 is the same shape again: a custom
2183
+ // provider's streamSimple now receives a branded TranscriptContext instead of a
2184
+ // raw Context, with systemPrompt/tools folded into a leading system message), so
2185
+ // an open `>=` floor is exactly how the next installer re-acquires the drift.
2186
+ // The floor is also the HARD MINIMUM a consumer install resolves: at `>=0.86.0`
2187
+ // an existing 0.85.1 host is upgraded, not kept.
2188
+ // Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.86.0 <0.87`).
2130
2189
  const [piMaj, piMin] = piAi.split('.').map(Number);
2131
2190
  assert.equal(piMaj, 0,
2132
2191
  `pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
@@ -2157,10 +2216,48 @@ assert.equal(peerDepTui, expectedPeer,
2157
2216
  // read, and sat two bumps stale (`>=0.84.3 <0.85` at a 0.85.1 pin) precisely
2158
2217
  // because it was outside this list. Its declaration is a plain range, so the
2159
2218
  // range scan below binds it — no new PROSE_DECL is needed for it.
2160
- const BASELINE_DOCS = ['README.md', 'ROADMAP.md', 'docs/setup-clean-host.md', 'demo/README.md', 'VERIFY.md'];
2219
+ // plugins/herdr/README.md joined on 2026-09-20, the same repair one bump later:
2220
+ // its step-1 `npm install -g @earendil-works/pi-coding-agent` was UNPINNED, so
2221
+ // from the hour 0.86.0 published it installed a pi outside the range the plugin
2222
+ // ships against — and nothing read that line. It now carries the range and is
2223
+ // scanned here, which is the only reason writing the number down is allowed.
2224
+ // docs/acp-backend-rail.md joined on 2026-09-20 for the same reason the two before it did:
2225
+ // its support table declared `peer >=0.85.1 <0.86` in prose no gate read, and the 0.86.0 bump
2226
+ // would have carried that into a release. Its declaration is a plain closed range and it is the
2227
+ // ONLY one in that file (the other rows declare exact versions of non-pi packages, which neither
2228
+ // pattern below matches), so the range scan binds it with no new PROSE_DECL. The bare `0.86.0`
2229
+ // devDep mentions in the same cell are prose, not an install pin, and stay unscanned — the range
2230
+ // half is what holds the row honest.
2231
+ const BASELINE_DOCS = ['README.md', 'ROADMAP.md', 'docs/setup-clean-host.md', 'demo/README.md', 'VERIFY.md', 'plugins/herdr/README.md', 'docs/acp-backend-rail.md'];
2232
+
2233
+ // ROADMAP.md is a baseline doc AND the home of the dated bump ledger, and those
2234
+ // are opposite kinds of sentence. A ledger entry is a RECEIPT: `2026-09-06 bump
2235
+ // — pi 0.84.4 → 0.85.1` records the range that landed THAT day, and rewriting it
2236
+ // at the next bump would destroy the evidence trail the entry exists to keep.
2237
+ // The comment above always said history keeps its old versions; until 0.86.0
2238
+ // that was only true because the newest entry happened to name the live pin, so
2239
+ // the FIRST bump after an entry was written is what exposed it. A ledger entry
2240
+ // opens at its dated bullet and runs until the enclosing list resumes at column
2241
+ // 0 (measured: every ` - ` bullet inside the Dep-bump block is a bump entry).
2242
+ function stripBumpLedger(file, text) {
2243
+ if (file !== 'ROADMAP.md') return text;
2244
+ let inLedger = false, dropped = 0;
2245
+ const kept = text.split('\n').filter((line) => {
2246
+ if (/^ - \*\*\d{4}-\d{2}-\d{2} bump — /.test(line)) { inLedger = true; }
2247
+ else if (inLedger && /^(- |#|\S)/.test(line)) { inLedger = false; }
2248
+ if (inLedger) dropped++;
2249
+ return !inLedger;
2250
+ });
2251
+ // Guard the exclusion itself: it must find a real ledger and must not eat the
2252
+ // whole file. Either way the range/exact scans below would pass vacuously.
2253
+ assert.ok(dropped > 0, 'ROADMAP.md: no dated bump-ledger entry found — the ledger moved or its bullet shape changed; re-point stripBumpLedger instead of leaving history inside the pin scan');
2254
+ assert.ok(kept.length > 100, `ROADMAP.md: the bump-ledger exclusion swallowed the document (${kept.length} lines left) — the live pin declarations would no longer be scanned`);
2255
+ return kept.join('\n');
2256
+ }
2257
+
2161
2258
  let rangeDecls = 0, exactDecls = 0;
2162
2259
  for (const file of BASELINE_DOCS) {
2163
- const text = readFileSync(file, 'utf8');
2260
+ const text = stripBumpLedger(file, readFileSync(file, 'utf8'));
2164
2261
  // Closed-range declarations: `>=<floor> <0.<ceiling>` (spaces optional).
2165
2262
  for (const [decl, floor, ceilMinor] of text.matchAll(/>=\s?(\d+\.\d+\.\d+)\s?<\s?0\.(\d+)/g)) {
2166
2263
  rangeDecls++;
@@ -2188,6 +2285,79 @@ for (const [file, re, shape] of PROSE_DECLS) {
2188
2285
  assert.ok(m, `${file}: the baseline sentence "${shape}" is gone — restore it or update check-dep-versions; a doc reword must not silently drop the pin from the gate`);
2189
2286
  assert.equal(m[1], piAi, `${file}: "${shape}" declares ${m[1]}, but the devDep pin is ${piAi}`);
2190
2287
  }
2288
+ // ── #120 P4: ONE FLOOR, FOUR MECHANICAL SURFACES ──────────────────────────
2289
+ // The bump is mostly a number substitution, and that is exactly how a bump goes
2290
+ // wrong: three surfaces move and the fourth is noticed months later by an install
2291
+ // that fails somewhere else. `[측정 2026-09-22]` the workspace closure was already
2292
+ // outside this gate — nothing here read `pnpm-workspace.yaml` at all — so a stale
2293
+ // row there could only surface at `pnpm install` time, far from the change.
2294
+ //
2295
+ // ONE assertion, because the claim token may appear exactly once in a gate source.
2296
+ // Every way the floor can fragment is collected first and named individually in the
2297
+ // failure, so "which surface" is never lost to the single-token rule.
2298
+ const nextFloorRange = `>=${piAi} <0.${piMin + 1}`;
2299
+ const floorGaps = [];
2300
+ const PI_CONSTELLATION = [
2301
+ '@earendil-works/pi-ai', '@earendil-works/pi-coding-agent', '@earendil-works/pi-tui',
2302
+ '@earendil-works/pi-agent-core', '@earendil-works/pi-client', '@earendil-works/pi-protocol',
2303
+ '@earendil-works/pi-telemetry', '@earendil-works/chord',
2304
+ ];
2305
+ const sameSet = (got, want, where) => {
2306
+ const missing = want.filter((n) => !got.includes(n));
2307
+ const extra = got.filter((n) => !want.includes(n));
2308
+ if (missing.length > 0) floorGaps.push(`${where}: missing ${missing.join(', ')}`);
2309
+ if (extra.length > 0) floorGaps.push(`${where}: unexpected ${extra.join(', ')}`);
2310
+ };
2311
+ // run.sh check-pack-install: the packages a consumer's fresh tree must resolve.
2312
+ const installed = [];
2313
+ for (const [, name, ver] of runSh.matchAll(/"(@earendil-works\/[a-z-]+)@(\d+\.\d+\.\d+)"/g)) {
2314
+ installed.push(name);
2315
+ if (ver !== piAi) floorGaps.push(`run.sh install constellation: ${name}@${ver} is not the floor ${piAi}`);
2316
+ }
2317
+ sameSet(installed, PI_CONSTELLATION, 'run.sh install constellation');
2318
+ // pnpm-workspace.yaml minimumReleaseAgeExclude: the same eight, each ADMITTING the
2319
+ // current floor. History stays (`0.85.0 || 0.85.1 || …`) — only the newest term is
2320
+ // the contract, because that is what an install resolves against.
2321
+ const closure = [...readFileSync('pnpm-workspace.yaml', 'utf8').matchAll(/^\s*-\s*'(@earendil-works\/[a-z-]+)@([^']+)'/gm)];
2322
+ sameSet(closure.map(([, name]) => name), PI_CONSTELLATION, 'pnpm-workspace.yaml closure');
2323
+ for (const [, name, spec] of closure) {
2324
+ if (!spec.split('||').map((t) => t.trim()).includes(piAi)) {
2325
+ floorGaps.push(`pnpm-workspace.yaml closure: ${name}@${spec} does not admit the floor ${piAi} — a stranded row fails only at install time`);
2326
+ }
2327
+ }
2328
+ // The PIN-LEAK MATCHER's own version bound. `[측정 2026-09-22]` this one was missed by the
2329
+ // first cut of this very claim and only surfaced when check-pack-install actually ran: the
2330
+ // install resolved 0.87.0 correctly and the gate refused it, because the matcher still said
2331
+ // `@0\.86\.0`. It is a regex literal, not a quoted `@pkg@version` spec, so the scan above
2332
+ // walked straight past it — which is exactly the "surface nothing else reads" this claim is
2333
+ // about, found the hard way. It is bound by name now.
2334
+ const matcherPin = runSh.match(/grep -Ev '@(\d+\\?\.\d+\\?\.\d+)\(_\|\$\)'/);
2335
+ if (!matcherPin) {
2336
+ floorGaps.push("run.sh pack_install_leaked_pi: the version-bounded pin regex is gone — a matcher that stopped bounding the version would pass every leaked runtime");
2337
+ } else if (matcherPin[1].replace(/\\/g, '') !== piAi) {
2338
+ floorGaps.push(`run.sh pack_install_leaked_pi: the pin-leak matcher bounds @${matcherPin[1].replace(/\\/g, '')}, but the floor is ${piAi} — the consumer install proof would refuse the very runtime this repo pins`);
2339
+ }
2340
+
2341
+ // The three CURRENT-contract doc sentences the range/exact scans above cannot see,
2342
+ // each anchored on its own wording. Historical samples (the 0.84.x placement
2343
+ // measurement in docs/mux-launch-rail.md, for one) are deliberately NOT scanned:
2344
+ // they record what was true then, and rewriting them would falsify evidence.
2345
+ const CURRENT_CONTRACT_DECLS = [
2346
+ ['plugins/herdr/README.md', /npm install -g "@earendil-works\/pi-coding-agent@(>=[\d.]+ <0\.\d+)"/, 'the herdr install line'],
2347
+ ['docs/mux-launch-rail.md', /현 supported range 는 `(>=[\d.]+ <0\.\d+)`/, "mux-launch-rail's current supported range"],
2348
+ ['docs/acp-backend-rail.md', /devDep exact `(\d+\.\d+\.\d+)`, peer `(>=[\d.]+ <0\.\d+)`/, "the ACP support row's exact+range pair"],
2349
+ ];
2350
+ for (const [file, re, shape] of CURRENT_CONTRACT_DECLS) {
2351
+ const m = readFileSync(file, 'utf8').match(re);
2352
+ if (!m) { floorGaps.push(`${file}: ${shape} is gone — a reworded current-contract sentence must fail loud, not drop out silently`); continue; }
2353
+ for (const captured of m.slice(1)) {
2354
+ const expected = captured.startsWith('>=') ? nextFloorRange : piAi;
2355
+ if (captured !== expected) floorGaps.push(`${file}: ${shape} declares "${captured}", but the floor is ${piAi} (${nextFloorRange})`);
2356
+ }
2357
+ }
2358
+ assert.deepEqual(floorGaps, [],
2359
+ `[QK:PI-FLOOR-SINGLE-SOURCE] the pi floor is declared on four mechanical surfaces — the package's exact dev pins and peer ceiling, run.sh's install constellation, the pnpm workspace closure, and the current-contract sentences in the docs — and every one of them must name the SAME floor. A bump that moves three of the four is the ordinary way this breaks, and the surface left behind is the one nothing else reads`);
2360
+
2191
2361
  // Guard the guard: if the patterns ever stop matching, the loops above pass
2192
2362
  // vacuously and the docs fall back OUT of the gate without a word.
2193
2363
  assert.ok(rangeDecls >= 5, `expected at least 5 pi range declarations across the baseline docs, found ${rangeDecls} — the doc scan matched (almost) nothing and would pass vacuously`);
@@ -2959,6 +3129,21 @@ check_acp_sdk_surface() {
2959
3129
  run_vitest test/acp-sdk-surface.contract.test.ts
2960
3130
  }
2961
3131
 
3132
+ check_pi_queue_oracle() {
3133
+ # #120 P2. The PUBLIC execution coordinate for `test/pi-queue.oracle.test.ts`, and the reason it
3134
+ # needs one: the discovery door below selects `pi-extensions/**/*.test.ts` and
3135
+ # `plugins/herdr/**/*.test.mjs` and deliberately NOT `test/**`, so a file under `test/` that
3136
+ # nothing names is green only when somebody runs `pnpm test` by hand. This oracle backs every
3137
+ # sentence the receipt and DELIVERY.md make about `queued-steer` / `queued-follow-up`, which is
3138
+ # exactly the kind of claim that must not rest on a manual run.
3139
+ #
3140
+ # It is a Vitest file, not a new hand-built gate: the subject is the INSTALLED
3141
+ # @earendil-works/pi-agent-core (exact dev pin), driven with a fake stream function. Zero model,
3142
+ # zero network, zero child process.
3143
+ section "pi receiver queue oracle (installed pi-agent-core, no model)"
3144
+ run_vitest test/pi-queue.oracle.test.ts
3145
+ }
3146
+
2962
3147
  check_tests_beside_behavior() {
2963
3148
  # The DISCOVERY door (issue #119 V2). A `.test.ts` beside a pi extension or a
2964
3149
  # `.test.mjs` beside the herdr plugin lib is run by public `pnpm check` without
@@ -2989,6 +3174,44 @@ check_tests_beside_behavior() {
2989
3174
  fi
2990
3175
  done
2991
3176
 
3177
+ # #120 P2 — THE OTHER HALF OF THE SAME CONTRACT: what this door deliberately does NOT open.
3178
+ # `test/**` is excluded on purpose (see the positional-substring measurement above), so a vitest
3179
+ # file under `test/` is reached by public `pnpm check` ONLY if some run.sh verb names it AND that
3180
+ # verb is in the `check:vitest` chain. Measured: a queue-oracle file under test/ landed green under
3181
+ # a manual `pnpm test` while the floor never ran it at all. A claim proven only when somebody
3182
+ # remembers to run it by hand is not on the floor, so the reachability is asserted rather than
3183
+ # assumed — every file, every time, with no filename list living here either.
3184
+ #
3185
+ # AND NO PATH IS SPELLED IN THIS FUNCTION. Measured: naming the oracle's path in the comment above
3186
+ # made the scan match its OWN comment — this verb is in the chain, so every file appeared reachable
3187
+ # and the regression mutant survived. A checker that can satisfy itself is not a checker.
3188
+ local chain tf verb
3189
+ chain="$(cd "$REPO_DIR" && node -e 'process.stdout.write(require("./package.json").scripts["check:vitest"] ?? "")')"
3190
+ while IFS= read -r tf; do
3191
+ [ -n "$tf" ] || continue
3192
+ # The enclosing `check_xxx()` is the coordinate; `check:vitest` must call its `check-xxx` form.
3193
+ # ONE failure message, because the claim token may appear exactly once in a gate source — and
3194
+ # the two ways this can break ("nothing names it" / "what names it is never called") are the
3195
+ # same defect seen from two sides: the file is not on the floor.
3196
+ # THE COORDINATE IS AN EXECUTABLE LINE, NOT A MENTION. Measured: when this scan accepted any
3197
+ # line inside an enclosing function that contained the path, deleting the `run_vitest` call
3198
+ # itself left the function's own COMMENT matching — the file stopped being run and the checker
3199
+ # stayed green. So only `run_vitest <path>` counts, and the two seams are separated: does
3200
+ # something execute it, and is that something on the floor.
3201
+ verb="$(awk -v needle="$tf" -v chain="$chain" '
3202
+ /^check_[a-z0-9_]+\(\) \{/ { fn = $1; sub(/\(\).*/, "", fn); gsub(/_/, "-", fn) }
3203
+ $1 == "run_vitest" && fn != "" { for (i = 2; i <= NF; i++) if ($i == needle) { print fn; found = 1; exit } }
3204
+ END { if (!found) print "" }' "$REPO_DIR/run.sh")"
3205
+ if [ -z "$verb" ]; then
3206
+ fail "[QK:VITEST-TEST-DIR-HAS-COORDINATE] $tf is under test/, which the discovery door deliberately excludes, and no run.sh verb EXECUTES it — a mention in a comment is not a coordinate, so nothing runs this file at all"
3207
+ return 1
3208
+ fi
3209
+ if ! printf '%s' "$chain" | grep -qF "$verb"; then
3210
+ fail "[QK:VITEST-TEST-DIR-ON-THE-FLOOR] $tf is executed by run.sh verb '$verb', which check:vitest does not call — the file has a coordinate nobody walks, so it is green only when somebody remembers to run pnpm test by hand"
3211
+ return 1
3212
+ fi
3213
+ done < <(cd "$REPO_DIR" && find test -type f -name '*.test.ts' -print | LC_ALL=C sort)
3214
+
2992
3215
  local -a selected=()
2993
3216
  local f
2994
3217
  while IFS= read -r f; do
@@ -3470,8 +3693,10 @@ check_pack() {
3470
3693
  #
3471
3694
  # (1) The version BOUNDARY. A pnpm .pnpm entry is `<name>@<version>` followed by either
3472
3695
  # `_<peer-hash>` or end-of-name (measured pnpm 11.20.0 on this tree), so an unbounded
3473
- # substring match would bless a lookalike such as `@0.85.10` while announcing the pinned
3474
- # floor — a false-green oracle (found by independent review, 2026-08-25).
3696
+ # substring match would bless a lookalike whose version merely STARTS with the pin — a
3697
+ # prerelease such as `@0.86.0-beta.1`, or the `@0.85.10` shape that first exposed this —
3698
+ # while announcing the pinned floor: a false-green oracle (found by independent review,
3699
+ # 2026-08-25).
3475
3700
  # (2) The name PREFIX covers the whole @earendil-works closure, not the `pi-` families alone.
3476
3701
  # 0.85.0 added `@earendil-works/chord`, declared as a runtime `dependencies` entry by
3477
3702
  # pi-coding-agent, pi-agent-core, pi-client and pi-protocol — inside the closure, with a
@@ -3479,15 +3704,17 @@ check_pack() {
3479
3704
  # `@earendil-works+chord@0.85.1` next to the seven pi entries, and the old
3480
3705
  # `^@earendil-works+pi-` filter did not see it: chord would have floated silently while
3481
3706
  # this gate printed a verified pin. The prefix is the closure's org, not one family in it.
3482
- # (The measured peer-hash suffix moved too — `_ws@8.21.3` at 0.85.1 where 0.84.4 carried
3483
- # `_@modelcontextprotocol+sdk@…` — which is exactly what property (1)'s `(_|$)` absorbs;
3484
- # both shapes are fixtures below so a future suffix change cannot pass vacuously.)
3707
+ # (The measured peer-hash suffix keeps moving — `_@modelcontextprotocol+sdk@…` at 0.84.4,
3708
+ # `_ws@8.21.3` at 0.85.1, and `[측정 2026-09-20]` the combined
3709
+ # `_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6` on a 0.86.0 tree —
3710
+ # which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
3711
+ # below so a future suffix change cannot pass vacuously.)
3485
3712
  pack_install_leaked_pi() {
3486
- grep '^@earendil-works+' | grep -Ev '@0\.85\.1(_|$)' || true
3713
+ grep '^@earendil-works+' | grep -Ev '@0\.87\.1(_|$)' || true
3487
3714
  }
3488
3715
 
3489
3716
  # Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
3490
- # false-green shape (it contains no 0.85.10 and no off-pin chord), so the oracle is proven
3717
+ # false-green shape (it contains no prefix-extended version and no off-pin chord), so the oracle is proven
3491
3718
  # against fixture listings. One cell per property of pack_install_leaked_pi, so a regression
3492
3719
  # in either is named by its own signature rather than folded into a single verdict. Exposed
3493
3720
  # as its own snapshot-safe subcommand because the heavy check-pack-install cannot run inside
@@ -3497,30 +3724,33 @@ pack_install_leaked_pi() {
3497
3724
  check_pack_pin_matcher() {
3498
3725
  local matcher_probe
3499
3726
 
3500
- # Cell 1 — the version boundary. Both measured peer-hash shapes are fixtures: the
3501
- # 0.84.4-era `_@modelcontextprotocol+sdk@…` and the `_ws@8.21.3` observed on a real
3502
- # 0.85.1 tree. Neither may leak; the two lookalikes must.
3727
+ # Cell 1 — the version boundary. Every measured peer-hash shape is a fixture: the
3728
+ # 0.84.4-era `_@modelcontextprotocol+sdk@…`, the `_ws@8.21.3` observed on a real
3729
+ # 0.85.1 tree, and `[측정 2026-09-20]` the combined suffix a 0.86.0 install carries.
3730
+ # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`0.86.0-beta.1`,
3731
+ # the prerelease shape that an unanchored match would bless) and an off-pin version.
3503
3732
  matcher_probe=$(printf '%s\n' \
3504
- '@earendil-works+pi-ai@0.85.1' \
3505
- '@earendil-works+pi-ai@0.85.1_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
3506
- '@earendil-works+pi-ai@0.85.1_ws@8.21.3' \
3507
- '@earendil-works+pi-ai@0.85.10' \
3508
- '@earendil-works+pi-agent-core@0.85.0' | pack_install_leaked_pi)
3509
- if [ "$matcher_probe" != '@earendil-works+pi-ai@0.85.10
3510
- @earendil-works+pi-agent-core@0.85.0' ]; then
3511
- 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>}"
3733
+ '@earendil-works+pi-ai@0.87.1' \
3734
+ '@earendil-works+pi-ai@0.87.1_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
3735
+ '@earendil-works+pi-ai@0.87.1_ws@8.21.3' \
3736
+ '@earendil-works+pi-ai@0.87.1_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
3737
+ '@earendil-works+pi-ai@0.87.1-beta.1' \
3738
+ '@earendil-works+pi-agent-core@0.87.0' | pack_install_leaked_pi)
3739
+ if [ "$matcher_probe" != '@earendil-works+pi-ai@0.87.1-beta.1
3740
+ @earendil-works+pi-agent-core@0.87.0' ]; then
3741
+ fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 0.87.1-beta.1 and the off-pin 0.87.0 lookalikes, and pass 0.87.1 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
3512
3742
  return 1
3513
3743
  fi
3514
3744
 
3515
3745
  # Cell 2 — the closure prefix. chord is a non-`pi-` member of the same runtime closure
3516
- # (0.85.0 onward). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
3746
+ # (0.85.0 onward, still true at 0.87.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
3517
3747
  # back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
3518
3748
  matcher_probe=$(printf '%s\n' \
3519
- '@earendil-works+chord@0.85.1' \
3520
- '@earendil-works+chord@0.85.0' \
3521
- '@earendil-works+pi-ai@0.85.1' | pack_install_leaked_pi)
3522
- if [ "$matcher_probe" != '@earendil-works+chord@0.85.0' ]; then
3523
- 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>}"
3749
+ '@earendil-works+chord@0.87.1' \
3750
+ '@earendil-works+chord@0.87.0' \
3751
+ '@earendil-works+pi-ai@0.87.1' | pack_install_leaked_pi)
3752
+ if [ "$matcher_probe" != '@earendil-works+chord@0.87.0' ]; then
3753
+ 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 and pi-protocol — `[측정 2026-09-20]` at 0.86.0, re-measured `[측정 2026-09-22]` at 0.87.0 and `[측정 2026-09-23]` at 0.87.1: the same four @earendil-works direct deps) — got: ${matcher_probe:-<nothing leaked>}"
3524
3754
  return 1
3525
3755
  fi
3526
3756
 
@@ -3752,8 +3982,10 @@ _check_pack_install_impl() {
3752
3982
 
3753
3983
  printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
3754
3984
 
3755
- # pi-agent-core is pinned even though we never import it: pi-coding-agent depends
3756
- # on it by CARET (`^0.85.x`), so with no lockfile in this fresh temp project it
3985
+ # pi-agent-core is pinned for TWO reasons now. Directly: `test/pi-queue.oracle.test.ts` imports
3986
+ # it to drive a real `Agent` (#120 P2), so its version is part of what the receipt's claims are
3987
+ # true OF. And transitively, which is why it was pinned here first: pi-coding-agent depends
3988
+ # on it by CARET (`^0.86.x`), so with no lockfile in this fresh temp project it
3757
3989
  # floats to whatever pi published last — and that newer core then drags a NESTED
3758
3990
  # pi-ai of its own. Measured 2026-07-21: pinning only the three we import left
3759
3991
  # pi-agent-core@0.80.10 + pi-ai@0.80.10 in the tree while the gate still announced
@@ -3782,19 +4014,31 @@ _check_pack_install_impl() {
3782
4014
  # It is pinned here for the first time. The leak assertion below covers every
3783
4015
  # other @earendil-works package too — but only because the matcher's prefix was
3784
4016
  # widened from `pi-` to the org in the same change; before that it was blind to
3785
- # precisely this class of new member. The verified floor is 0.85.1 as of 2026-09-06.
3786
- echo "[check-pack-install] pnpm add into $tmp (with 0.85.x peers + chord + typebox)"
4017
+ # precisely this class of new member.
4018
+ #
4019
+ # `[측정 2026-09-20]` at 0.86.0 the constellation SHRANK for the first time:
4020
+ # `npm view @earendil-works/pi-coding-agent@0.86.0 dependencies` lists only
4021
+ # {chord, pi-ai, pi-tui, pi-agent-core} — pi-client and pi-protocol are no
4022
+ # longer reachable from the three packages we import (pi-client still carries
4023
+ # {chord, pi-protocol}, pi-protocol still carries {chord}, both published at
4024
+ # 0.86.0). Their explicit pins stay: a pin that is momentarily redundant costs
4025
+ # two frozen rows in a temp tree, while retiring one is how a caret re-enters
4026
+ # the closure unwatched on the next minor — the same shape as the 2026-07-21
4027
+ # pi-agent-core incident. The list moves WITH the floor and is re-measured at
4028
+ # each bump, never pruned on a single quiet minor.
4029
+ # The verified floor is 0.86.0 as of 2026-09-20.
4030
+ echo "[check-pack-install] pnpm add into $tmp (with 0.87.x peers + chord + typebox)"
3787
4031
  local install_log
3788
4032
  install_log=$(cd "$tmp" && pnpm add \
3789
4033
  "$tgz_path" \
3790
- "@earendil-works/pi-ai@0.85.1" \
3791
- "@earendil-works/pi-coding-agent@0.85.1" \
3792
- "@earendil-works/pi-tui@0.85.1" \
3793
- "@earendil-works/pi-agent-core@0.85.1" \
3794
- "@earendil-works/pi-client@0.85.1" \
3795
- "@earendil-works/pi-protocol@0.85.1" \
3796
- "@earendil-works/pi-telemetry@0.85.1" \
3797
- "@earendil-works/chord@0.85.1" \
4034
+ "@earendil-works/pi-ai@0.87.1" \
4035
+ "@earendil-works/pi-coding-agent@0.87.1" \
4036
+ "@earendil-works/pi-tui@0.87.1" \
4037
+ "@earendil-works/pi-agent-core@0.87.1" \
4038
+ "@earendil-works/pi-client@0.87.1" \
4039
+ "@earendil-works/pi-protocol@0.87.1" \
4040
+ "@earendil-works/pi-telemetry@0.87.1" \
4041
+ "@earendil-works/chord@0.87.1" \
3798
4042
  "typebox@latest" \
3799
4043
  --ignore-workspace --ignore-scripts 2>&1) || {
3800
4044
  fail "[check-pack-install] pnpm add failed:"
@@ -3804,17 +4048,17 @@ _check_pack_install_impl() {
3804
4048
 
3805
4049
  # A pin is a wish until the resolved tree is read back. Assert it: EVERY
3806
4050
  # @earendil-works package present — direct or transitive, top level or nested,
3807
- # chord included — must be the pinned 0.85.1. Anything else means an unpinned caret
4051
+ # chord included — must be the pinned 0.86.0. Anything else means an unpinned caret
3808
4052
  # floated and the rest of this gate would be exercising a runtime nobody verified,
3809
- # while still printing "pinned pi 0.85.1". Fail loud instead of proving the wrong floor.
4053
+ # while still printing "pinned pi 0.86.0". Fail loud instead of proving the wrong floor.
3810
4054
  local leaked_pi
3811
4055
  leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
3812
4056
  if [ -n "$leaked_pi" ]; then
3813
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.85.1):"
4057
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.87.1):"
3814
4058
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
3815
4059
  return 1
3816
4060
  fi
3817
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 0.85.1 (chord included)"
4061
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 0.87.1 (chord included)"
3818
4062
 
3819
4063
  # Resolve the installed package.json and confirm pi.extensions
3820
4064
  # arrived intact. If pi.extensions is empty or missing, the
@@ -4620,7 +4864,7 @@ function finish(trimmed) {
4620
4864
  // gate that exists to catch "green clone, dead consumer". Equality also catches the
4621
4865
  // other direction the narrow-surface rule cares about: an extra or duplicated verb
4622
4866
  // reaching hosts without a decision. Written sorted; `names` is sorted above.
4623
- const EXPECT_TOOLS = ['entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
4867
+ const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
4624
4868
  if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
4625
4869
  console.error('installed MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
4626
4870
  process.exit(1);
@@ -5179,12 +5423,12 @@ function finishOk(trimmed) {
5179
5423
  }
5180
5424
  const names = tools.map((t) => t?.name).sort();
5181
5425
  // EXACT set, not a floor. This is the assertion behind the operator-facing claim that
5182
- // `entwurf check-bridge` proves the installed bytes list the seven garden tools — and
5426
+ // `entwurf check-bridge` proves the installed bytes list the eight garden tools — and
5183
5427
  // under an installed package this launcher IS the dist branch of start.sh, so a subset
5184
5428
  // check here let an artifact missing entwurf_fresh_call / entwurf_resume_call read as a
5185
5429
  // green bridge. Equality also refuses an undecided extra verb. Written sorted; `names`
5186
5430
  // is sorted above.
5187
- const expected = ['entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
5431
+ const expected = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
5188
5432
  if (names.length !== expected.length || expected.some((n, i) => names[i] !== n)) {
5189
5433
  console.error(`MCP tool set MISMATCH — want exactly [${expected.join(',')}] got [${names.join(',')}]`);
5190
5434
  process.exit(1);
@@ -6381,6 +6625,12 @@ case "$cmd" in
6381
6625
  check-entwurf-v2-send)
6382
6626
  check_entwurf_v2_send
6383
6627
  ;;
6628
+ check-entwurf-v2-runner)
6629
+ check_entwurf_v2_runner
6630
+ ;;
6631
+ check-control-send-receipt)
6632
+ check_control_send_receipt
6633
+ ;;
6384
6634
  check-entwurf-v2-native-push)
6385
6635
  check_entwurf_v2_native_push
6386
6636
  ;;
@@ -7342,6 +7592,9 @@ case "$cmd" in
7342
7592
  check-acp-sdk-surface)
7343
7593
  check_acp_sdk_surface
7344
7594
  ;;
7595
+ check-pi-queue-oracle)
7596
+ check_pi_queue_oracle
7597
+ ;;
7345
7598
  check-tests-beside-behavior)
7346
7599
  check_tests_beside_behavior
7347
7600
  ;;