@junghanacs/entwurf 0.22.0 → 0.23.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 (135) hide show
  1. package/AGENTS.md +4 -1
  2. package/BASELINE.md +1 -0
  3. package/CHANGELOG.md +514 -0
  4. package/DELIVERY.md +10 -2
  5. package/README.md +43 -4
  6. package/VERIFY.md +47 -0
  7. package/docs/acp-backend-rail.md +96 -17
  8. package/docs/setup-clean-host.md +11 -1
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +41 -41
  29. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  30. package/package.json +9 -8
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  33. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  34. package/pi-extensions/lib/acp/backend.ts +4 -4
  35. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  36. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  37. package/pi-extensions/lib/codex-declaration.js +612 -0
  38. package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
  39. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  40. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  41. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  42. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  43. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  44. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  45. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  46. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  47. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  48. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  49. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  50. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  51. package/pi-extensions/lib/herdr-placement.ts +276 -0
  52. package/pi-extensions/lib/mux-fresh-call.ts +95 -301
  53. package/run.sh +588 -204
  54. package/scripts/check-acp-usage-accounting.ts +9 -9
  55. package/scripts/check-codex-birth-hook.ts +187 -0
  56. package/scripts/check-entwurf-control-rpc.ts +138 -0
  57. package/scripts/check-entwurf-fact-provider.ts +4 -2
  58. package/scripts/check-entwurf-peers-surface.ts +3 -1
  59. package/scripts/check-entwurf-self-address.ts +68 -0
  60. package/scripts/check-fresh-call-dispatch.ts +301 -0
  61. package/scripts/check-gate-qualification.ts +17 -4
  62. package/scripts/check-herdr-activation.ts +1293 -0
  63. package/scripts/check-herdr-fresh-call.ts +1250 -0
  64. package/scripts/check-herdr-placement.ts +383 -0
  65. package/scripts/check-herdr-plugin-build.ts +842 -0
  66. package/scripts/check-herdr-plugin-profile.ts +371 -0
  67. package/scripts/check-herdr-plugin.ts +523 -0
  68. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  69. package/scripts/check-herdr-sandbox.ts +456 -0
  70. package/scripts/check-herdr-supply.ts +184 -0
  71. package/scripts/check-mux-launcher-fence.ts +138 -0
  72. package/scripts/check-peer-facts.ts +331 -0
  73. package/scripts/check-pi-launch.ts +358 -0
  74. package/scripts/check-release-gate-outcomes.ts +19 -0
  75. package/scripts/check-typing-call-fence.ts +296 -0
  76. package/scripts/codex-birth-doctor.sh +159 -16
  77. package/scripts/codex-birth-install.sh +178 -72
  78. package/scripts/codex-birth-uninstall.sh +162 -7
  79. package/scripts/fixtures/herdr-supply.json +29 -0
  80. package/scripts/herdr-activation.mjs +536 -0
  81. package/scripts/herdr-plugin-activate.mjs +270 -0
  82. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  83. package/scripts/herdr-runtime.mjs +1203 -0
  84. package/scripts/install-herdr-ci.sh +96 -0
  85. package/scripts/inventory-verification-surface.ts +86 -10
  86. package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
  87. package/scripts/lib/tree-digest.ts +97 -0
  88. package/scripts/meta-bridge-install.sh +19 -2
  89. package/scripts/meta-bridge-state.py +167 -11
  90. package/scripts/mutants/codex-caller-seat.json +6 -8
  91. package/scripts/mutants/codex-native.json +195 -29
  92. package/scripts/mutants/control-socket-disconnect.json +57 -0
  93. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  94. package/scripts/mutants/herdr-activation.json +305 -0
  95. package/scripts/mutants/herdr-fresh-call.json +448 -0
  96. package/scripts/mutants/herdr-placement.json +161 -0
  97. package/scripts/mutants/herdr-plugin-build.json +160 -0
  98. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  99. package/scripts/mutants/herdr-plugin.json +133 -0
  100. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  101. package/scripts/mutants/herdr-supply.json +133 -0
  102. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  103. package/scripts/mutants/mux-fresh-call.json +104 -17
  104. package/scripts/mutants/mux-launcher-fence.json +13 -0
  105. package/scripts/mutants/omp-fresh.json +4 -4
  106. package/scripts/mutants/peer-facts.json +98 -0
  107. package/scripts/mutants/pi-launch.json +113 -0
  108. package/scripts/mutants/resume-args.json +6 -6
  109. package/scripts/mutants/self-address.json +33 -0
  110. package/scripts/mutants/typing-call-fence.json +17 -0
  111. package/scripts/mutants/v2-surface.json +2 -2
  112. package/scripts/mutants/v2-visible-resume.json +17 -17
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  117. package/scripts/smoke-codex-birth.sh +281 -12
  118. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  119. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  120. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  121. package/scripts/smoke-herdr-raw-install-live.sh +849 -0
  122. package/scripts/tsconfig.json +5 -0
  123. package/scripts/check-entwurf-facts.ts +0 -385
  124. package/scripts/check-entwurf-resume-args.ts +0 -122
  125. package/scripts/check-entwurf-v2-decider.ts +0 -657
  126. package/scripts/check-entwurf-v2-lock.ts +0 -371
  127. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  128. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  129. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  130. package/scripts/check-entwurf-v2-release.ts +0 -151
  131. package/scripts/check-entwurf-v2-runner.ts +0 -448
  132. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  133. package/scripts/check-entwurf-v2-send.ts +0 -458
  134. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  135. package/scripts/check-socket-discovery.ts +0 -401
package/run.sh CHANGED
@@ -144,7 +144,7 @@ Usage:
144
144
  ./run.sh check-codex-birth-hook # sandboxed payload + declaration: first-turn V3 birth, one record per thread, top-level event predicate, no markers
145
145
  ./run.sh check-codex-fresh-preflight # pre-mutation birth/MCP/thread-title/default-app-server readiness
146
146
  ./run.sh smoke-codex-config-state # sandboxed MCP + status-line ownership/install/doctor/inverse lifecycle
147
- ./run.sh smoke-codex-birth # sandboxed birth unit: exact trust identity, the vendor trust receipt read (never written), config.toml untouched, foreign/edited/symlink refusals, drift-refusing inverse
147
+ ./run.sh smoke-codex-birth # sandboxed birth unit: exact trust identity, the vendor trust receipt read at our MEASURED index (never written), config.toml untouched, coexistence with a foreign SessionStart group in either ordering (joined not adopted, reported not certified, byte-preserved across install and inverse), own-declaration drift/duplication refusals, drift-refusing inverse
148
148
  ./run.sh check-harness-admission-parity # #87 C: the EDGE the two closed parity loops never had. Every citizen backend is fresh-openable or a declared pre-#82 legacy admission whose exception a reader finds in DELIVERY.md — so a post-contract harness that mints records but cannot be opened by entwurf_fresh_call blocks the release package instead of only carrying an `unsupported` note (docs/adding-a-harness.md step 9)
149
149
  ./run.sh check-capability-bundle-reach # deterministic gate (IN pnpm check): re-ask EVERY shipped copy of meta-session (source + bridge bundle emit) whether metaCapabilitiesFilePath() reaches the registry — the artifact-depth check the source-path gates cannot make; needs a built dist, missing dist FAILS
150
150
  ./run.sh smoke-pi-attach # deterministic gate (#50 C2 checkpoint + C3 ACP tail): a pi session attaches as a V3 meta-record citizen (backend:"pi"), the gardenId is the RECORD's not pi's session id, the control socket is keyed on it, a re-open ATTACHES to the same address (never a second mint), the BUILT DIST ENTRY driven over MCP stdio lists the citizen + delivers entwurf_v2 to that socket with an RPC ack, and the ACP identity chain lands a send AS the host record (enrichMcpServersWithEnvelope env → bridge sender = host gardenId). mkdtemp-isolated; the live store is never read
@@ -158,31 +158,27 @@ Usage:
158
158
  ./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
159
159
  ./run.sh check-project-trust-handler # deterministic gate (0.11 Stage 0, Trust 2층): project_trust handler — decideProjectTrust matrix (escape=inherited-false+interactive+trust-here→{yes,remember:true}; non-interactive→undecided; never undefined) + adapter single-writer, fake prompt, no UI
160
160
  ./run.sh check-entwurf-v2-contract # FROZEN entwurf_v2 contract — control-socket liveness domain is currently pi; self-fetch/native-push citizens are out of that domain, not globally unsupported; pure, no IO
161
- ./run.sh check-entwurf-v2-lock # deterministic gate (0.11 Stage 0 step 5a, 버킷 B F2): per-gid dispatch LOCK primitive — openSync wx atomic acquire, second-acquire=target-locked conflict (holder JSON for human cleanup), nonce-owned release (successor survives late release), stale reclaim same-host+ESRCH-only (EPERM/remote/alive/unknown fail-closed), empty/corrupt=conflict not auto-deleted, F2-P1 malformed gid throws; real temp dir, deps injected
162
- ./run.sh check-entwurf-v2-decider # deterministic gate (0.11 Stage 0 step 5b): PURE dispatch decider decideDispatch — frozen 7-step order over injected fakes, lock acquire+release tracked so reject⇒no-plan-no-lock proven; pre-probe rejects observedLiveness=null, send/resume execute keep lock + mailbox no-lock (?7), resume plan no mode/provider/model, invalid gid throws (F2-P1); pure, no IO
163
- ./run.sh check-entwurf-v2-matrix # deterministic gate (0.11 Stage 0 step 5d-5 a): REACHABILITY + LOCK SSOT table — drives REAL decideDispatch over fakes, fixes every (target kind → transport → lock class) cell as one table (control-socket/meta-mailbox/native-push + bad-target/conflict/locked/undeliverable/dormant/indeterminate rejects), coverage pass fails on a dropped cell; thin coverage not a decider re-impl; pure, no IO
164
- ./run.sh check-entwurf-v2-release # deterministic gate (0.11 Stage 0 step 5c-1): PURE release-policy reducer (decideReleasePolicy + reduceRelease) — the spawn-observation policy and its four release events went with the transport in the visible-first cut, so the shipped machine is meta-mailbox=never release (no lock) + control-socket=release once on send-final; decideReleasePolicy still enforces the lock-nullness invariant; pure, no IO
165
- ./run.sh check-entwurf-v2-send # deterministic gate (0.11 Stage 0 step 5c-2a): control-socket SEND hand (executeControlSocketSend) wiring transport IO onto the 5c-1 reducer — ack→sent, in-band reject→rejected (no fallback), dead→same-lock one-shot re-resolve (control retry / mailbox enqueue), indeterminate→failed+rethrow with NO fallback (no double-delivery); release exactly once, releaseLock throw never masks the send error; IO-via-dep
166
161
  ./run.sh check-compaction-send-guard # deterministic gate (#111): control-socket send during Pi compaction — event-armed refuse compacting, quiet unknown non-idle refuse busy (ctx.signal is not isStreaming); no pi.sendMessage, no delivered:true; idle/live-run steer/followUp preserved; pure, no IO
167
- ./run.sh check-entwurf-v2-send-fallback # deterministic gate (0.11 Stage 0 step 5c-2b): same-lock re-resolve RESOLVER (resolveDeadControlSendFallback) — fire-and-forget re-resolve: alive→control retry, dead→reject (nothing is ever launched), indeterminate→reject, unsupported+deliverable→mailbox plan, undeliverable/bad-target/conflict→reject; resolver never releases, mis-wire fails loud, inspect/probe throws propagate; no IO (fakes)
168
- ./run.sh check-entwurf-v2-runner # deterministic gate (0.11 Stage 0 step 5d-1): execute-router (executeDispatch) routing an already-decided DispatchDecision to its 5c transport hand → one outcome-rich EntwurfV2RunResult. reject→rejected (no hand) / control/mailbox→matching hand with decision.lock verbatim / N3 rejectReason carried / N1 SendDeliveredReleaseFailedError→execution-failed{finalizedOutcome,releaseFailed,retrySafe:false}; fake hands, no IO
169
- ./run.sh check-entwurf-v2-mailbox # deterministic gate (0.11 Stage 0 step 5c-4, LAST 5c transport slice): ENQUEUE-ONLY meta-mailbox SEND body (executeMetaMailboxSend) + production sendViaMailbox adapter — sender→formatMetaMailboxBody with plan.wantsReply threaded (divergence from legacy hard false), sender absent→raw plan.message, enqueue opts EXACTLY {gardenId,body,sessionsDir,mailboxDir}, enqueue throw PROPAGATES (no success:false fold — mailbox has no in-band refuse); adapter NEVER touches lock (release is the hand's job); source guard: no release/routing seam
170
- ./run.sh check-entwurf-resume-args # deterministic gate: resume-argv SSOT (buildResumePiArgs) for the S1 VISIBLE resume. The headless shape was measured wrong for a window before the consumer was written (`-p` is pi's own non-interactive mode), so the one shipped posture is `--entwurf-control` FIRST + ext args exactly once + `--session <abs file>` + optional `--provider` + `--model <m>` — and the gate pins the ABSENCES as hard as the presences: no --mode, no -p, no positional prompt (a resume runs no turn), no --no-extensions, never --session-id (which MINTS a session instead of resuming one)
171
162
  ./run.sh check-mux-resume-call # deterministic gate: S1 resume PLACEMENT composition (mux-resume-call.ts). No fake tmux. cwd rules are MEASURED tmux 3.6a behaviours, each a way a resume looks successful while being wrong: a nonexistent `-c` exits 0 and lands the child in $HOME (so it is refused HERE), `-c` is FORMAT-EXPANDED so `#{…}` silently rewrites the path and `#(…)` was observed running a command (so `#` is refused), and whitespace measured SAFE (so no escaping layer is owed). Also pins `-c` reaching tmux, runtime after `--`, carrier-free argv, zero identity in this module, and the surface seam that keeps the v2 composition from importing mux
172
163
  ./run.sh check-mux-parent-artifact # deterministic gate for the tracked scrubbed parent-transcript fixture — a version-pinned sample of the PARENT-SIDE shape (fresh_call toolResult + the later callback custom_message) so downstream never opens a private transcript. Pins event order, the toolCallId join on the RESULT (pi writes no separate toolCall row), the launch nonce reappearing verbatim in the callback body, the <sender_info> envelope field names, and the absence of operator paths / real garden ids / real uuids. NOT placement evidence
173
164
  ./run.sh check-mux-launcher-fence # deterministic gate for the shared operator-launcher fence (issue #67): scripts/lib/claude-launcher-fence.ts + its wiring into BOTH mux LIVE smokes. Replants the observed install-destruction shape (real HOME + fixture XDG_DATA_HOME → self-update retargets the real `claude` launcher into the fixture tree, teardown deletes it) wholly inside disposable mkdtemp roots — the real launcher is never inspected. Pins fail-closed preflight, retarget/content-change detection before cleanup, removal BLOCKED on fixture reference / unproven safety / surviving tracked panes, exact operator-parity XDG restore (absent = DELETED, not canonical defaults), the lifecycle cell-branch topology, and one shared helper consumed by both smokes
174
- ./run.sh check-entwurf-v2-visible-resume # deterministic gate: S1 visible-resume COMPOSITION (entwurf-v2-visible-resume.ts) with every seam injected the whole state machine incl. the timeout branch runs with no tmux/lock/socket/clock. Pins lock BEFORE liveness, identity under the lock and before any window (no-transcript citizen fails loud, opens nothing), live/indeterminate/address-conflict refused unlaunched, observation as a BOUNDED WAIT (measured: socket answers ~2–4s after launch, so one immediate probe would call a successful resume unobserved), exactly ONE launch on every path, timeout lock released + window left open + nothing retried/killed, failed release throws, and the two receipts staying separate in type and text
165
+ ./run.sh check-typing-call-fence # deterministic gate for the boundary nobody has broken: this product composes ARGUMENTS and LETTERS, never keystrokes. Scans every tracked production source under pi-extensions/ and mcp/ comments blanked, string literals kept for tmux send-keys/paste-buffer/load-buffer and herdr's PTY verbs (agent.prompt, agent.send_keys, pane.send_text, pane.input.set). Prose is exempt on purpose: the sentence stating the rule names the very calls it forbids, and a scanner that read prose could be made green by deleting the law. Carries its own oracle the detector is proven on planted code, proven blind to comments, and the scanned inventory is measured
175
166
  ./run.sh check-resume-launch-identity # deterministic gate for resume-launch-identity.ts, the record-authoritative launch-identity leaf preserved through the visible-first cut (spawn-bg and all its callers are gone; this leaf answers "which being is this, and which conversation is theirs"). Temp meta-store fixture: gardenId→record.transcriptPath happy path with header cwd/provider/model; C3 integrity (header id ≠ record.nativeSessionId → refused, never resumed); #52 ADDRESSABLE read (a gid that no longer holds its nativeSessionId alone is refused from EITHER side — the plain targeted read would resume one transcript twice under two locks); cause fidelity per impossible resume incl. the F7 pin (recorded-but-deleted transcript → MISSING, not "no recorded model"); header↔gate SSOT. No spawn/socket/timer
176
167
  ./run.sh smoke-entwurf-v2-matrix-live # LIVE sentinel (0.11 Stage 0 step 5d-5, D4-b) — OUT of pnpm check, needs LIVE=1. Drives REAL production runEntwurfV2 deps over REAL OS objects, 4 cells: C1 control-socket (real pi --entwurf-control resident → RPC send → lock acquire→release ×1), C1b record-less socket (#50 C4: live record-less pi → EVERY intent rejected pre-probe record-less-socket, no lock, rendered hint names record authority + fresh-cut), C2 meta-mailbox deliverable (armed self-fetch citizen → real .msg enqueue, lock-free), C3 meta-mailbox guard (no armed receiver → reject, no garbage). Model-in-loop OUT (transport/lock/enqueue gate, GPT Q2); negative/timeout stay deterministic. Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.6-luna). LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
177
168
  ./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). COST FENCE: open that agy conversation on gemini-3.6-flash (free account) — never a Pro tier; entwurf never selects the agy model and no assertion reads it. LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
178
169
  ./run.sh smoke-codex-native-push-live # on-demand LIVE acceptance for Codex native-push — needs LIVE=1 + CODEX_LIVE_THREAD_ID loaded by a visible TUI attached to the default app-server; probes the real UDS, routes through production runEntwurfV2, queues one exact-token turn without retry, re-probes, and leaves the visible TUI to show the model result
179
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
180
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
181
- ./run.sh check-entwurf-facts # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 1+2): PURE PeerFact core + resolveFactList union — R1 out-of-domain→unsupported, R3b socket-domain 4-value, facts-only keyset; union: PeerFact + RecordLessSocketFact by gardenId (#50 C4: record-less socket = diagnostic subject, gid+liveness only), dormant→dead, F3 indeterminate preserved, out-of-socket-domain+socket fail-loud; pure, no IO
182
- ./run.sh check-socket-discovery # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 3): SOCKET-axis scanSocketProbes — probes (dir sockets) ∪ (in-domain citizen canonical paths) 3-valued; dormant citizen no-file → dead (resumable, not unprobed), stall → indeterminate (F3), dir hygiene/dedup/missing-dir + e2e → resolveFactList; readdir/probe injected, no IO
183
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
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
+ ./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
+ ./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
177
+ ./run.sh check-herdr-plugin-build # deterministic gate for the #116 M3-b3 build runner (`plugins/herdr/lib/build.mjs`): the ONE command Herdr's [[build]] calls, composing integration status -> pure profile -> runtime bootstrap -> activation THROUGH THE INSTALLED PACKAGE (never this checkout, which herdr deletes on uninstall with no cleanup hook). Pins one [[build]] with argv `node lib/build.mjs` and a runner npm does not ship, missing-herdr vs unanswerable-status as separate named refusals, a selected outdated/needs-repair/malformed/duplicate atom failing BEFORE any runtime work, A-empty as exit 0 with ZERO writes, absent/incapable activation entries as distinct refusals, the two-stage {pi} -> {pi, claude-code} composition with OpenCode never named, and the post-build herdr commit gap named rather than called atomic. Listing via the runner's own spawn seam (no fake herdr); one cell drives the REAL binary, named SKIP without it
178
+ ./run.sh check-peer-facts # deterministic gate for the peer-facts projection (#116 M2-a): drives the REAL CLI with every ambient root sandboxed — placement crosses STRUCTURED (never the human `herdr <pane>` string), the peer keyset is exactly the provider's facts (no herdr agent_status may enter an entwurf payload), no socket coordinate is published (#50 C4), diagnostics in-band, the probed socket world is the one ENTWURF_DIR names (proved by a record-less socket surfacing — there is no field to echo), no observation budget, exit contract 0/2/3, dispatch+emit reachability. No herdr binary
184
179
  ./run.sh check-meta-listing # deterministic gate: META-STORE facts axis — kind-carrying entries; non-regular records are never read, parse/drift become diagnostics, duplicate nativeSessionId quarantines every rival but not unrelated citizens; strict throws / collect partial; pure injected IO
185
180
  ./run.sh check-entwurf-fact-provider # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4b): ASSEMBLY listEntwurfFacts — full-store parse/probe/quarantine/resolve stays intact; #112 bounds expensive receiver/transcript observation to the newest 32 rendered rows while older machine rows say unobserved; Q112 pins full payload + exact observer budget; C-원칙 keeps corruption diagnostic and impossible wiring loud; deps injected, no IO
181
+ ./run.sh check-herdr-placement # deterministic gate (#116 S1): the PURE placement-evidence axis — herdr `pane list` parse (unreadable payload → null → `unobserved`, never `none`), the two measured join rules (claude kind=id byte equality / pi kind=path uuid tail of a .jsonl basename, both strict), unmeasured kind → NO key, two panes on one key → `ambiguous` (never last-write-wins), provider reads the placement owner EXACTLY ONCE per listing (anti-watcher, docs/mux-launch-rail.md §7), and no entwurf_v2 dispatch module names or imports the axis (Hard Rule 16 — a pane is a view, not a rail). No fake herdr, no binary, no IO
186
182
  ./run.sh check-entwurf-peers-surface # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4c): MCP entwurf_peers RENDER renderEntwurfPeers (#50 C4) — payload keyset exactly {peers, diagnostics}; FORBIDDEN keys sessions/socketOnly/controlDir/socketPath/count + no .sock in text (socket is transport, never identity); record-less socket = aggregated record-less-socket diagnostic (F8, liveness-keyed message, alive names fresh-cut); NO verb-routing key (JSON deep scan) NOR word (text), diagnostics both surfaces, empty→(none), unsupported shown; WIRING guard: both surfaces call provider+render, getLiveSessions + /entwurf-sessions gone; facts fabricated, no IO
187
183
  ./run.sh check-entwurf-self-address # deterministic gate (SE-1/SE-2 slice 1): self-addressability honesty predicate computeSelfAddressability — pi replyable ⟺ live socket; meta splits by RAIL: self-fetch ⟺ recordBacked ∧ ownerAlive ∧ watchArmed (regression-proof record-present rows), native-push ⟺ recordBacked ∧ probeAlive (separate axis — no mailbox fact may rescue or sink it), unsupplied rail fail-closed; SOURCE GUARD buildStrictPiSenderEnvelope drops hardcoded replyable:true + existsSync-probes socket, entwurf_self renders alive vs expected AND renders the meta rail per-rail (mailbox only inside the self-fetch branch; native-push denies an inbox and gates injection on the probe)
188
184
  ./run.sh check-entwurf-deliverability # deterministic gate (SE-1/SE-2 slice 2c): conversational-mailbox deliverability predicate — computeMetaReceiverActive (recordBacked ∧ ownerAlive ∧ watchArmed) + mailboxConversationalDeliverable (self-fetch AND active); direct-inject pi refused (SE-1), self-fetch dead/unarmed refused (SE-2); self-address shares the same atom
@@ -251,9 +247,9 @@ Usage:
251
247
  ./run.sh uninstall-agy-bridge # 봉인 7: honest inverse of install-agy-bridge from install-state (restore preimage / remove key; refuse if config became a symlink)
252
248
  ./run.sh probe-bridge-command <cmd> [args...] # #81: BOOT the given bridge invocation and require the entwurf MCP tool surface back. `--invocation-json '{"command":"…","args":[],"env":{}}'` preserves a harness config exactly. It waits for a valid initialize response, then sends initialized + tools/list only (no tools/call, lock, record, or delivery). exit 0 = it serves the bridge; 1 = it does not. The pi/agy doctors use this leaf.
253
249
  ./run.sh doctor-agy-bridge # fail-loud doctor: MCP config + exact permission rule + state + live probe label
254
- ./run.sh install-codex-birth # publish $CODEX_HOME/hooks.json + the helper closure; no root, and the operator trusts the declaration once in their own Codex
255
- ./run.sh uninstall-codex-birth # exact inverse from the install-state digest inventory; refuses drifted bytes instead of deleting them
256
- ./run.sh doctor-codex-birth # RUNTIME + UNIT bytes/ownership + the vendor TRUST receipt for this declaration (--unit-only skips the receipt; sandboxes only)
250
+ ./run.sh install-codex-birth # publish ONE SessionStart declaration inside $CODEX_HOME/hooks.json (a file it shares) + the helper closure; no root, and the operator trusts the declaration once in their own Codex
251
+ ./run.sh uninstall-codex-birth # exact inverse from the install-state digest inventory; removes only entwurf's own declaration (foreign groups byte-for-byte) and refuses drifted bytes instead of deleting them
252
+ ./run.sh doctor-codex-birth # RUNTIME + UNIT bytes/ownership + FOREIGN neighbours (reported, never certified) + the vendor TRUST receipt at this declaration's measured index (--unit-only skips the receipt; sandboxes only)
257
253
  ./run.sh install-codex-mcp # own only [mcp_servers.entwurf-bridge] in the user's Codex config
258
254
  ./run.sh uninstall-codex-mcp # remove/restore only the recorded MCP atom
259
255
  ./run.sh doctor-codex-mcp # effective config + ownership + actual bridge boot
@@ -274,6 +270,7 @@ Usage:
274
270
  ./run.sh meta-bridge-prune # 1.0.0 meta-bridge Phase 4: LISTING-ONLY store hygiene — classify orphan/stale/ambiguous/keep, print manual rm commands, delete NOTHING ([dir] [--ttl-days N])
275
271
  ./run.sh meta-bridge-fresh-cut # the ONE generation verb (the verb every v3-only rejection names): quiesce-check live sockets/markers/native-push conversations (refusing any surface it cannot inspect), archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<ts>`, clear dead transport residue, open an empty v3 generation. No migration, no restore — the archive is forensic only. EXIT CONTRACT (#54, `--help` prints it): 0 complete / 1 NOTHING MOVED (re-run, do not setup) / 2 usage / 3 cut transition incomplete (inspect) / 4 cut complete but residue cleanup failed (`setup` may run; re-run only before new citizen birth, otherwise remove residue manually)
276
272
  ./run.sh meta-facts # #65 owner-normalized READ-ONLY store projection: deterministic JSON {schemaVersion:1, storeDir, citizens: full v3 records sorted by gardenId, defects: {filename,message}} — THE listing contract emitted by the owner so consumers stop copying the certification. No liveness/sockets/transcript contents. EXIT: 0 readable (defects in-band; missing store = empty), 2 usage, 3 unreadable ([dir])
273
+ ./run.sh peer-facts # #116 M2-a owner-normalized READ-ONLY peer projection: the same listEntwurfFacts provider through the same renderer entwurf_peers uses, emitted as deterministic JSON {schemaVersion:1, storeDir, peers, diagnostics} so a consumer never re-implements the placement join (whose pi half is a measured vendor floor). NOT the same bytes as an entwurf_peers call and deliberately so: that surface is a human listing (observationLimit, text over the wire), this is a machine projection with UNBOUNDED observation, because a rationed row says `unobserved` and a machine cannot tell that from "nobody could look". `placement` stays a STRUCTURED tagged union ({kind:"herdr-pane",paneId} / unobserved / none / ambiguous) — never the human `herdr <pane>` string. Publishes NO socket coordinate (ENTWURF_DIR selects what is PROBED, #50 C4) and NO herdr agent_status / interactive_ready / screen text. One placement read per listing, no watcher. EXIT: 0 readable (diagnostics in-band; missing store = empty), 2 usage, 3 unreadable ([dir])
277
274
  ./run.sh meta-bridge-managed-keys # 0.10.0 meta-bridge: print the SSOT of settings keys entwurf OWNS (consumers read this to stay disjoint — keyset-owner invariant)
278
275
  ./run.sh check-keyset-overlap <fragment.json...> # 0.10.0 meta-bridge: PREVENTIVE keyset guard — fail if a consumer fragment collides with any pi-owned key (cross-repo; not in pnpm check)
279
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)
@@ -281,10 +278,13 @@ Usage:
281
278
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
282
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
283
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
284
- ./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)
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)
285
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)
286
283
  ./run.sh install [project-dir] # INTERNAL part of `setup` (project .pi/settings.json wiring) + npm-consumer entry — prefer `setup`, don't call directly for dev
287
284
  ./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json (project scope only; global user-scope citizen left intact)
285
+ ./run.sh install-user-scope [--plugin-runtime <root>] # #116 M3-b2 PLUGIN-ONLY forward seam: compose ONLY the global pi citizen (user-scope packages[] + provider), never a project `.pi/settings.json` — a Herdr plugin activation has no project and must not invent one. With --plugin-runtime the provider's managed command becomes the absolute bridge under that certified stable runtime (a clean host has nothing entwurf on PATH); without it every byte is the historical bare-bin install. Inverse: remove-user-scope
286
+ ./run.sh herdr-plugin-activate <backend...> # #116 M3-b2 public forward activation. Takes a subset of {pi, claude-code} and NOTHING else — the stable runtime root is DERIVED from XDG here and handed down, and both harness writers accept a named root only when it IS that derived one. Certifies the runtime-ready journal AND the installed tree, certifies the existing ledger and its recorded harness roots, plans ADD-ONLY (a backend absent from this request is retained, never removed), preflights every selected component before the first byte, then writes each component `pending` BEFORE mutating and checkpoints `active` the moment its writer returns — so a run that dies halfway leaves a record of how far it got. Which backends is M3-b3's question: this verb is handed the answer, it does not read Herdr's integration status
287
+ ./run.sh herdr-plugin-deactivate # #116 M3-b2 public teardown of a Herdr-plugin activation. Herdr's `plugin uninstall` deletes its checkout and calls NO cleanup hook, so the harness wiring and the stable runtime are retained, not cleaned — this is the shipped verb that takes them back. Certifies the activation ledger, the resolved Pi/Claude roots and BOTH component inverses read-only first; mutates only when every preflight is green; then Claude -> Pi user scope -> runtime -> ledger LAST. A component failure stops with the runtime and ledger intact as retry authority. Static imports only: the process deletes the runtime it is running from, and a lazy import after that fails
288
288
  ./run.sh remove-user-scope # explicit GLOBAL inverse of install's user-scope citizen: drop entwurf from ~/.pi/agent/settings.json packages[] (affects ALL cwds — shared entry, not per-project). #86 C2: same-owner-only — a LIVE foreign owner refuses; a MISSING owner is removed only when package entry + package state + provider installerRoot all align (reported orphan cleanup)
289
289
  ./run.sh takeover-user-scope # #86 C2: operator-EXPLICIT ownership move of the shared user-scope registration to THIS root (old→new reported, packages[] entry + provider installerRoot together). The only writer over another owner — normal install/setup/remove refuse instead. No --force exists
290
290
  ./run.sh doctor-pi-package # #86 C2: package-side user-scope ownership verdict — unregistered / owned / owned-by-other(live) / legacy-no-state / mismatch / missing-owner (nonzero on defect verdicts). Provider runtime verdicts stay with doctor-pi-provider
@@ -656,8 +656,15 @@ codex_terminal_title() {
656
656
  esac
657
657
  }
658
658
 
659
+ # $1 (optional): a certified stable plugin runtime root. Given, the provider half is written in
660
+ # PLUGIN MODE — its managed command becomes the absolute bridge under that root instead of the bare
661
+ # bin. That is the whole difference, and it exists because a Herdr plugin activates on a host where
662
+ # nothing entwurf is on PATH, so a bare command cannot resolve. Absent, every byte is what it has
663
+ # always been (#116 M3-b2).
659
664
  register_user_scope_citizen() {
660
665
  local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
666
+ local plugin_mode=()
667
+ [ -n "${1:-}" ] && plugin_mode=(--plugin-runtime "$1")
661
668
  # Shared idempotent implementation (also driven by smoke-user-scope-citizen).
662
669
  # #86 C2: user scope carries a recorded OWNER (packageRoot in the pi-package
663
670
  # install-state). Another owner — live or missing — makes this a zero-write
@@ -670,7 +677,7 @@ register_user_scope_citizen() {
670
677
  python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
671
678
  --scope user --state "$(_pi_package_state)" --preflight >/dev/null
672
679
  python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" \
673
- --scope user --state "$(_pi_provider_state)" --preflight >/dev/null
680
+ --scope user --state "$(_pi_provider_state)" ${plugin_mode[@]+"${plugin_mode[@]}"} --preflight >/dev/null
674
681
  python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
675
682
  --scope user --state "$(_pi_package_state)"
676
683
  # #46 Task 2: own entwurfProvider.mcpServers.entwurf-bridge as the bare stable bin at USER scope
@@ -679,7 +686,33 @@ register_user_scope_citizen() {
679
686
  # local and covered by `run.sh remove` (no state) — deliberate, reasoned asymmetry.
680
687
  # #86 C2: the provider state records installerRoot, so the same foreign-owner refusal
681
688
  # holds here — and the package refusal above fires FIRST, before any provider write.
682
- python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$(_pi_provider_state)"
689
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$(_pi_provider_state)" ${plugin_mode[@]+"${plugin_mode[@]}"}
690
+ }
691
+
692
+ # install-user-scope — the PLUGIN-ONLY forward seam (#116 M3-b2). Generic `install` writes both a
693
+ # project `.pi/settings.json` and the global user-scope citizen; a Herdr plugin activation has no
694
+ # project and must not invent one, so this verb composes the user-scope half and nothing else. Its
695
+ # inverse is the one that already existed, `remove-user-scope` — the asymmetry this closes is that
696
+ # the inverse was public and the forward was not.
697
+ install_user_scope() {
698
+ command -v python3 >/dev/null 2>&1 || { echo "[install-user-scope] requires python3 on PATH; nothing was written." >&2; exit 1; }
699
+ local plugin_runtime=""
700
+ while [ $# -gt 0 ]; do
701
+ case "$1" in
702
+ --plugin-runtime)
703
+ shift
704
+ # A flag with no value must not fall through to bare mode: the caller asked for the plugin
705
+ # runtime and would otherwise be told, silently, that it got it.
706
+ [ -n "${1:-}" ] || { echo "run.sh install-user-scope: --plugin-runtime requires a value" >&2; exit 2; }
707
+ plugin_runtime="$1" ;;
708
+ *) echo "usage: run.sh install-user-scope [--plugin-runtime <stable active root>]" >&2; exit 2 ;;
709
+ esac
710
+ shift
711
+ done
712
+ preflight_v3_store install-user-scope
713
+ section "install-user-scope: the GLOBAL pi citizen only (no project wiring)"
714
+ register_user_scope_citizen "$plugin_runtime"
715
+ ok "install-user-scope: user-scope packages[] + provider registered${plugin_runtime:+ (plugin runtime: $plugin_runtime)}"
683
716
  }
684
717
 
685
718
  # takeover-user-scope — the operator-explicit ownership move (#86 C2). The ONLY
@@ -1050,6 +1083,14 @@ check_copilot_launch() {
1050
1083
  run_ts scripts/check-copilot-launch.ts
1051
1084
  }
1052
1085
 
1086
+ check_pi_launch() {
1087
+ # #118 홉 2. Same oracle as check-copilot-launch and for the same reason: the subject is
1088
+ # a process replacement, so a fake vendor named `pi` on a sandbox PATH reports the argv,
1089
+ # env and pid it was handed — never a reading of the launcher's source. Also carries the
1090
+ # --entwurf-control literal cross-check, because the shell cannot import the TS constant.
1091
+ run_ts scripts/check-pi-launch.ts
1092
+ }
1093
+
1053
1094
  check_codex_app_server_launch() {
1054
1095
  # #95: the managed app-server SPELLING, proved against a fake vendor. There is no second
1055
1096
  # spelling of the address to compare any more — the launcher asks `codex-socket-path` — so
@@ -1140,123 +1181,28 @@ check_entwurf_v2_contract() {
1140
1181
  run_ts scripts/check-entwurf-v2-contract.ts
1141
1182
  }
1142
1183
 
1143
- check_entwurf_v2_lock() {
1144
- # Deterministic gate for 0.11 Stage 0 step 5a (버킷 B F2): the per-gid dispatch
1145
- # LOCK primitive — what serializes concurrent in-domain dispatch at one
1146
- # target (pi self-guards CREATE but not RESUME, 검증원장 F2). acquire =
1147
- # openSync(lockPath,"wx") atomic; a second acquire without release =
1148
- # target-locked conflict carrying the holder JSON (F2-P2 human cleanup). release
1149
- # = unlink ONLY when the on-disk nonce is still ours (a successor's re-acquire
1150
- # survives a late release). Stale reclaim ONLY for same host + ESRCH; EPERM
1151
- # (other user's live pid) / different host / alive pid / unknown error all
1152
- # fail-closed to conflict. Empty/corrupt lockfile = conflict, never
1153
- # auto-deleted. F2-P1: a malformed gid throws before any path is built. Real
1154
- # temp dir (wx atomicity under test); clock/nonce/pid/host/kill injected.
1155
- run_ts scripts/check-entwurf-v2-lock.ts
1156
- }
1157
-
1158
- check_entwurf_v2_decider() {
1159
- # Deterministic gate for 0.11 Stage 0 step 5b: the PURE dispatch decider
1160
- # decideDispatch. Drives the frozen 7-step order over INJECTED fakes (target
1161
- # lookup / lock / socket inspect+probe / preflight / capability), tracking lock
1162
- # acquire+release so "reject ⇒ no plan AND no lock retained" is PROVEN. Covers:
1163
- # bad-target/target-locked/target-address-conflict carry observedLiveness=null
1164
- # (pre-probe), every other reject carries a measured value; a control-socket send
1165
- # executes KEEPING the lock, meta-mailbox send takes NO lock (?7); an invalid gid throws before
1166
- # any lookup (F2-P1). Pure, no IO, no API.
1167
- run_ts scripts/check-entwurf-v2-decider.ts
1168
- }
1169
-
1170
- check_entwurf_v2_matrix() {
1171
- # Deterministic gate for 0.11 Stage 0 step 5d-5 (a): the REACHABILITY + LOCK SSOT
1172
- # TABLE. Drives the REAL decideDispatch over minimal injected fakes and fixes, as
1173
- # one readable table, every (target kind → transport → lock class) cell the 5d-5
1174
- # claim covers: bad-target/address-conflict/target-locked rejects, unsupported
1175
- # meta-mailbox (deliverable) vs mailbox-undeliverable (inactive) reject,
1176
- # in-domain control-socket (live) / released rejects
1177
- # (ff-dormant, indeterminate, under-lock conflict). A coverage pass
1178
- # FAILS if any transport / lock class / pre-probe reject is missing — a dropped
1179
- # decider cell cannot pass silently. Thin coverage, NOT a decider re-impl; surface
1180
- # parity stays in check-entwurf-v2-surface. Pure, no IO, no API.
1181
- run_ts scripts/check-entwurf-v2-matrix.ts
1182
- }
1183
-
1184
- check_entwurf_v2_release() {
1185
- # Deterministic gate for 0.11 Stage 0 step 5c-1: the PURE release-policy reducer
1186
- # (decideReleasePolicy + reduceRelease) for the 5c transport hand. Proves the
1187
- # Fable-3 "release-after-observation" timing as a pure state machine BEFORE any
1188
- # send IO: meta-mailbox=never release (no lock), control-socket=release once
1189
- # on send-final. The spawn-observation policy and its four release events went with
1190
- # the transport in the visible-first cut; decideReleasePolicy still enforces the
1191
- # lock-nullness invariant (?7). Pure, no IO.
1192
- run_ts scripts/check-entwurf-v2-release.ts
1193
- }
1194
-
1195
1184
  check_entwurf_v2_send() {
1196
- # Deterministic gate for 0.11 Stage 0 step 5c-2a: the control-socket SEND hand
1197
- # (executeControlSocketSend) that WIRES real transport IO onto the 5c-1 release
1198
- # reducer. Proves the send->outcome->release ordering over injected fakes (no socket):
1199
- # ack->sent / in-band reject->rejected (no fallback) / dead->same-lock one-shot
1200
- # re-resolve (control retry or mailbox enqueue)->fallback-sent|rejected|failed /
1201
- # indeterminate->failed+rethrow with deadFallback+mailbox NEVER called (no
1202
- # double-delivery on an alive-but-stalled socket). Release fires exactly once per
1203
- # send-final; a releaseLock throw never masks the send failure (5b). IO-via-dep.
1204
- run_ts scripts/check-entwurf-v2-send.ts
1205
- }
1206
-
1207
- check_entwurf_v2_send_fallback() {
1208
- # Deterministic gate for 0.11 Stage 0 step 5c-2b: the same-lock re-resolve RESOLVER
1209
- # (resolveDeadControlSendFallback) the 5c-2a hand calls on a dead connect. Proves the
1210
- # fire-and-forget re-resolve routing over injected fakes (no filesystem): alive->
1211
- # control-socket retry (inspected socketPath) / dead(absent)->reject (dormant-fire-
1212
- # forget-unsupported, and nothing is launched) / indeterminate->reject / unsupported+deliverable
1213
- # ->meta-mailbox plan (mini-table, no inspect/probe) / unsupported+undeliverable->reject
1214
- # / bad-target + address-conflict->reject pre-probe. Mis-wire (plan/lock gid) fails loud
1215
- # before IO; inspect/probe throws PROPAGATE (the hand owns failed+release); the resolver
1216
- # has NO release seam; every execute plan keeps the held gid.
1217
- run_ts scripts/check-entwurf-v2-send-fallback.ts
1218
- }
1219
-
1220
- check_entwurf_v2_mailbox() {
1221
- # Deterministic gate for 0.11 Stage 0 step 5c-4 (the LAST 5c transport slice): the
1222
- # ENQUEUE-ONLY meta-mailbox SEND body (executeMetaMailboxSend) + its production
1223
- # sendViaMailbox adapter (makeProductionSendViaMailbox). Proves the wiring over an
1224
- # injected fake enqueue (no filesystem): sender present -> formatMetaMailboxBody with
1225
- # plan.wantsReply threaded (yes/no in body, the deliberate divergence from legacy's
1226
- # hard-coded false) / sender absent -> raw plan.message / enqueue opts EXACTLY
1227
- # {gardenId: plan.targetGardenId, body, sessionsDir, mailboxDir} (no re-derivation) /
1228
- # enqueue throw PROPAGATES (never folded into success:false — a mailbox has no in-band
1229
- # refuse) / success -> {success:true}. Production adapter resolves {success:true},
1230
- # consults senderProvider once, and NEVER touches the lock (a poison LockClaim whose
1231
- # every access throws still resolves). Source guard: the lib code has NO release seam
1232
- # and NO routing seam (no releaseLock / inspect / probe / resolve) — a lock leak or
1233
- # re-route is structurally impossible.
1234
- run_ts scripts/check-entwurf-v2-mailbox.ts
1185
+ # MUTANT EXECUTION COORDINATE, not a discovery path the test is found by
1186
+ # check-tests-beside-behavior like every other beside-behaviour lane. This case exists
1187
+ # because the V2SEND-INBAND-REJECT-REASON mutant names it as gate argv, and attribution is
1188
+ # read from the JSON test titles `run_vitest` emits (run.sh:104-113). Narrow file filter: a
1189
+ # mutant pointed at the glob shim would re-run every beside-behaviour test once per mutant.
1190
+ # It stays inside check:contracts even though the discovery door also runs this file — see
1191
+ # check-release-gate-outcomes: a gate a mutant names must be reachable outside qualification.
1192
+ section "control-socket send hand (mutant execution coordinate)"
1193
+ run_vitest pi-extensions/lib/entwurf-v2-send.test.ts
1235
1194
  }
1236
1195
 
1237
1196
  check_entwurf_v2_native_push() {
1238
- # Deterministic gate for 봉인 3/4: the native-push SEND hand (deliverViaNativePush +
1239
- # makeNativePushSend), the executor half of the native-push rail where the 1-shot retry
1240
- # lives (moved out of the adapter leaf). Proves over a fake adapter (no agy/socket): success
1241
- # first try -> {retried:false}, ONE send over the planted route, ZERO re-probe; fail ->
1242
- # re-probe alive -> re-send success -> {retried:true}, TWO sends, the 2nd over the RE-
1243
- # DISCOVERED route; re-send FAIL -> throws (no 3rd attempt); re-probe dead/indeterminate ->
1244
- # throws (not retried), NO second send. makeNativePushSend resolves the adapter from
1245
- # plan.backend and IGNORES the lock (lock-free rail).
1246
- run_ts scripts/check-entwurf-v2-native-push.ts
1247
- }
1248
-
1249
- check_entwurf_v2_runner() {
1250
- # Deterministic gate for 0.11 Stage 0 step 5d-1: the execute-router (executeDispatch) that
1251
- # routes an already-decided DispatchDecision to its 5c transport hand and maps the outcome
1252
- # to one outcome-rich EntwurfV2RunResult. Proves over injected fake hands (no socket/spawn/
1253
- # timer): reject -> rejected (receipt+diagnostic carried, NO hand called) / control-socket ->
1254
- # sendControl(plan, lock) / meta-mailbox -> sendMailbox(plan, NULL
1255
- # lock, ?7). Carry-overs: N3 control `rejected` carries rejectReason verbatim; N1
1256
- # SendDeliveredReleaseFailedError -> execution-failed{finalizedOutcome, releaseFailed,
1257
- # retrySafe:false}; a plain hand throw -> execution-failed{retrySafe:false} with no
1258
- # finalizedOutcome. Exactly one hand runs per execute.
1259
- run_ts scripts/check-entwurf-v2-runner.ts
1197
+ # MUTANT EXECUTION COORDINATE, not a discovery path — the test is found by
1198
+ # check-tests-beside-behavior like every other beside-behaviour lane. This case exists
1199
+ # because the NATIVE-PUSH-RETRY-BYTE-IDENTITY mutant names it as gate argv, and attribution
1200
+ # is read from the JSON test titles `run_vitest` emits (run.sh:104-113). Narrow file filter:
1201
+ # a mutant pointed at the glob shim would re-run every beside-behaviour test once per mutant.
1202
+ # It stays inside check:contracts even though the discovery door also runs this file — see
1203
+ # check-release-gate-outcomes: a gate a mutant names must be reachable outside qualification.
1204
+ section "native-push send hand (mutant execution coordinate)"
1205
+ run_vitest pi-extensions/lib/entwurf-v2-native-push.test.ts
1260
1206
  }
1261
1207
 
1262
1208
  check_entwurf_v2_surface() {
@@ -1323,15 +1269,22 @@ check_compaction_send_guard() {
1323
1269
  }
1324
1270
 
1325
1271
  check_entwurf_resume_args() {
1326
- # Deterministic gate for the resume-argv SSOT (buildResumePiArgs). S1 replaced the headless
1327
- # shape with the VISIBLE one, measured against the runtime before the consumer was written:
1328
- # `-p` is pi's own non-interactive mode, so the old `--mode json -p … <prompt>` prefix would
1329
- # put a JSON stream in the operator's window instead of a session they can use. Pins the one
1330
- # shipped posture — `--entwurf-control` FIRST (the resumed session stands its socket up), no
1331
- # --mode/-p/positional prompt (a resume runs no turn), explicitExtensionArgs exactly once
1332
- # between the control flag and --session (#29 provider-resolution footgun), --session <abs
1333
- # file> never --session-id (which MINTS), and a null provider emitting no --provider.
1334
- run_ts scripts/check-entwurf-resume-args.ts
1272
+ # NOT a discovery path — the test is found by check-tests-beside-behavior like every other
1273
+ # beside-behaviour lane. This case exists as the MUTANT EXECUTION COORDINATE: the six
1274
+ # resume-args mutants name it as their gate argv, the qualification runner re-invokes that
1275
+ # argv once per mutant, and attribution is read from the JSON test titles `run_vitest` emits
1276
+ # under ENTWURF_MUTATION_VITEST_REPORT (run.sh:104-113). A gate argv that called vitest
1277
+ # directly would skip that reporter and every kill would come back unattributed.
1278
+ #
1279
+ # The file filter is NARROW on purpose. Pointing a mutant at the glob shim would make the
1280
+ # runner re-run every beside-behaviour test once per mutant.
1281
+ #
1282
+ # It stays inside check:contracts even though the discovery door also runs this file, and the
1283
+ # duplicate is deliberate: check-release-gate-outcomes requires every gate a mutant names to
1284
+ # run inside check:full, because a gate reachable ONLY through qualification makes the
1285
+ # 28-minute mutant body the only thing that can notice it going red on a clean tree.
1286
+ section "resume-argv SSOT (mutant execution coordinate)"
1287
+ run_vitest pi-extensions/lib/entwurf-resume-args.test.ts
1335
1288
  }
1336
1289
 
1337
1290
  check_mux_launcher_fence() {
@@ -1349,6 +1302,17 @@ check_mux_launcher_fence() {
1349
1302
  run_ts scripts/check-mux-launcher-fence.ts
1350
1303
  }
1351
1304
 
1305
+ check_typing_call_fence() {
1306
+ # The rule kept since day one, now kept by NAME: entwurf composes arguments and letters, never
1307
+ # keystrokes. An outside reader measured the tree on 2026-09-17 and found the law perfectly kept
1308
+ # and nothing keeping it — zero typing calls in pi-extensions/, zero checks and zero mutants that
1309
+ # would notice one arriving. Scans tracked production sources under pi-extensions/ and mcp/ with
1310
+ # comments blanked and string literals kept, because a typing call spelled as a string is the
1311
+ # call. Comment exemption is load-bearing: the sentence that states the rule contains the names
1312
+ # the rule forbids. Offline, no binaries, no fixtures on disk.
1313
+ run_ts scripts/check-typing-call-fence.ts
1314
+ }
1315
+
1352
1316
  check_mux_parent_artifact() {
1353
1317
  # Deterministic gate for the tracked scrubbed parent-transcript fixture
1354
1318
  # (scripts/fixtures/mux-parent-transcript.scrubbed.jsonl). The fixture is a version-pinned
@@ -1376,16 +1340,16 @@ check_mux_resume_call() {
1376
1340
  }
1377
1341
 
1378
1342
  check_entwurf_v2_visible_resume() {
1379
- # Deterministic gate for the S1 visible-resume composition (entwurf-v2-visible-resume.ts).
1380
- # Every seam is injected, so the whole state machine — including the timeout branch — runs
1381
- # without tmux, a lock file, a socket or a clock. Pins: lock BEFORE liveness, identity under
1382
- # the lock and before any window (a citizen with no transcript fails loud and opens nothing),
1383
- # live/indeterminate/address-conflict refused unlaunched, the observation as a BOUNDED WAIT
1384
- # (measured: the socket answers ~2–4s after launch, so one immediate probe would report a
1385
- # successful resume as unobserved), exactly one launch on every path, timeout releasing the
1386
- # lock and leaving the window open, a failed release throwing, and the two receipts staying
1387
- # separate in both the type and the rendered text.
1388
- run_ts scripts/check-entwurf-v2-visible-resume.ts
1343
+ # MUTANT EXECUTION COORDINATE, not a discovery path — the test is found by
1344
+ # check-tests-beside-behavior like every other beside-behaviour lane. This case exists
1345
+ # because SEVENTEEN mutants name it as gate argv, and attribution is read from the JSON test
1346
+ # titles `run_vitest` emits (run.sh:104-113), which is why each of those claims is its own
1347
+ # test title. Narrow file filter: pointing them at the glob shim would re-run every
1348
+ # beside-behaviour test seventeen times. It stays inside check:contracts even though the
1349
+ # discovery door also runs this file see check-release-gate-outcomes: a gate a mutant names
1350
+ # must be reachable outside qualification.
1351
+ section "visible same-id resume (mutant execution coordinate)"
1352
+ run_vitest pi-extensions/lib/entwurf-v2-visible-resume.test.ts
1389
1353
  }
1390
1354
 
1391
1355
  check_resume_launch_identity() {
@@ -1449,7 +1413,7 @@ check_mux_fresh_call() {
1449
1413
  # codex-caller-seat rides here for the same reason as the two preflights: the leaf exists
1450
1414
  # only as freshCall's Codex placement input (#95 lane B), so certifying it apart from the
1451
1415
  # composition it feeds would let the two halves of one seat decision drift.
1452
- run_vitest test/mux-fresh-call.test.ts test/copilot-fresh-preflight.test.ts test/codex-fresh-preflight.test.ts test/fresh-call-surfaces.contract.test.ts test/fresh-call-provider.contract.test.ts test/omp-fresh-bootstrap.contract.test.ts test/tmux-coordinate-row.test.ts test/codex-fresh-live-protocol.test.ts test/codex-caller-seat.test.ts
1416
+ run_vitest test/mux-fresh-call.test.ts test/fresh-call-composition.contract.test.ts test/copilot-fresh-preflight.test.ts test/codex-fresh-preflight.test.ts test/fresh-call-surfaces.contract.test.ts test/fresh-call-provider.contract.test.ts test/omp-fresh-bootstrap.contract.test.ts test/tmux-coordinate-row.test.ts test/codex-fresh-live-protocol.test.ts test/codex-caller-seat.test.ts
1453
1417
  }
1454
1418
 
1455
1419
  smoke_mux_fresh_call_live() {
@@ -1732,7 +1696,7 @@ smoke_acp_cortex_live() {
1732
1696
 
1733
1697
  smoke_entwurf_v2_matrix_live() {
1734
1698
  # LIVE sentinel for 0.11 Stage 0 step 5d-5 (D4-b) — kept OUT of `pnpm check`. The deterministic
1735
- # sibling (check-entwurf-v2-matrix) fixes every (target kind → transport → lock) cell over fakes
1699
+ # sibling (pi-extensions/lib/entwurf-v2-decider.matrix.test.ts) fixes every (target kind → transport → lock) cell over fakes
1736
1700
  # with ZERO IO; this drives the REAL production runEntwurfV2 deps against REAL OS objects on the
1737
1701
  # substrate happy path across 3 cells: C1 control-socket (a real `pi --entwurf-control` resident
1738
1702
  # → control-socket RPC send → lock acquire→release ×1), C2 meta-mailbox deliverable (armed
@@ -1750,18 +1714,6 @@ smoke_entwurf_v2_matrix_live() {
1750
1714
  run_ts scripts/smoke-entwurf-v2-matrix-live.ts
1751
1715
  }
1752
1716
 
1753
- check_entwurf_facts() {
1754
- # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 1): the PURE
1755
- # fact core. Locks the PeerFact shape + R1/R3b liveness invariant before any IO
1756
- # wiring (gate-first). R1: out-of-domain backend (claude-code/codex/antigravity)
1757
- # → unsupported for EVERY socket input (never coerced to the socket value or
1758
- # dead). R3b: in-domain pi → alive/dead/indeterminate, null → indeterminate
1759
- # (no proof ≠ dead). facts-only keyset: identity facts + liveness, NO
1760
- # verb-routing (resumable/sendable/transport/dispatch/action) and NO
1761
- # transcriptPath (동결결정 10). Pure, no IO, no API.
1762
- run_ts scripts/check-entwurf-facts.ts
1763
- }
1764
-
1765
1717
  check_control_socket_path() {
1766
1718
  # Deterministic gate: the control-socket path grammar `<dir>/<gid>.sock` has ONE
1767
1719
  # definition (pi-extensions/lib/control-socket-path.js) instead of one per
@@ -1772,18 +1724,6 @@ check_control_socket_path() {
1772
1724
  run_ts scripts/check-control-socket-path.ts
1773
1725
  }
1774
1726
 
1775
- check_socket_discovery() {
1776
- # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 3): the
1777
- # SOCKET-axis wiring scanSocketProbes. Probes the union of (dir sockets) ∪
1778
- # (every in-domain pi citizen's canonical path) so a dormant citizen with no
1779
- # socket file reads dead (ENOENT) → resumable, never an unprobed gap (slice 2
1780
- # throws on that). Three-valued throughout — a stalled socket stays
1781
- # indeterminate (F3), never folded to dead by an alive-only listing. Dir
1782
- # hygiene (non-.sock / malformed names ignored), dedup, missing-dir, sort, and
1783
- # an end-to-end scanSocketProbes→resolveFactList. readdir/probe injected, no IO.
1784
- run_ts scripts/check-socket-discovery.ts
1785
- }
1786
-
1787
1727
  check_meta_listing() {
1788
1728
  # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4a): the
1789
1729
  # meta-store axis listAllMetaIdentities. Explicit-partial: a parse failure or
@@ -1806,6 +1746,117 @@ check_meta_facts() {
1806
1746
  run_ts scripts/check-meta-facts.ts
1807
1747
  }
1808
1748
 
1749
+ check_herdr_plugin() {
1750
+ # Deterministic gate for the #116 M2-b herdr plugin (`plugins/herdr/`). Two halves.
1751
+ # STATIC: the manifest is parsed with the package's own TOML reader and held to the
1752
+ # shape herdr 0.9.0 accepts (min_herdr_version required + semver, dot-free pane id,
1753
+ # popup-only sizing, argv arrays), and the ABSENT sections are asserted as hard as the
1754
+ # present ones — [[startup]]/[[events]] are where a watcher would grow, [[actions]]
1755
+ # would send a report nobody can read into a 64 KiB log. ([[build]] LEFT that list in
1756
+ # #116 M3-b3; the one runner it may call is check-herdr-plugin-build's subject.)
1757
+ # BEHAVIOURAL: the REAL pane entry is
1758
+ # spawned with a stub `entwurf` and a stub HERDR_BIN_PATH that log every call, so
1759
+ # "exactly one read each per open" is COUNTED; success, skip-by-name, four distinct
1760
+ # named refusals, the ambiguous pane, diagnostics and the no-write claim are each
1761
+ # driven for real. The stub stands for the CONSUMER's input, never for herdr's
1762
+ # behaviour — no claim about herdr is made here, which is why this is not the fake
1763
+ # fixture check-herdr-sandbox forbids. No herdr binary, no Entwurf install, no writes
1764
+ # outside mkdtemp.
1765
+ run_ts scripts/check-herdr-plugin.ts
1766
+ }
1767
+
1768
+ check_herdr_activation() {
1769
+ # Deterministic gate for the #116 M3-b2 scoped activation. The pi cells drive the REAL writers
1770
+ # (run.sh install-user-scope, register-pi-provider.py, remove-user-scope) against a sandboxed
1771
+ # HOME/XDG/PI_CODING_AGENT_DIR, so the bytes asserted are bytes those writers produced. The Claude
1772
+ # cells drive the REAL state owner's pure surfaces; the vendor `claude` CLI is NOT invoked and its
1773
+ # acceptance of an absolute executable stays a named LIVE boundary. The oracle for a roundtrip is
1774
+ # JSON equality plus unrelated-key survival, never byte equality: the pi settings writer preserves
1775
+ # semantics but may reindent (measured 2026-09-16). No network, no operator HOME.
1776
+ run_ts scripts/check-herdr-activation.ts
1777
+ }
1778
+
1779
+ check_herdr_runtime_bootstrap() {
1780
+ # Deterministic gate for the #116 M3-b1 runtime transaction. Every root comes from XDG, so the
1781
+ # gate hands the module a mkdtemp HOME and asserts nothing appeared outside the two XDG roots it
1782
+ # was given. `acquire` is a FUNCTION seam: the fixture stands in for the REGISTRY, so this gate
1783
+ # proves the transaction (ownership certification, torn-swap recovery, leftover states, rollback,
1784
+ # idempotence, the inverse) but NOT that the real Entwurf package boots — that actual-package
1785
+ # proof is a named cell inside check-pack-install, which runs this module's own
1786
+ # verifyInstalledRuntime against the tarball it packed and installed. npm is pinned here the only
1787
+ # way a hermetic gate honestly can: the production pack/install argv and the npm environment are
1788
+ # pure functions and are asserted directly. No network, no npm, no operator HOME, no writes
1789
+ # outside mkdtemp.
1790
+ run_ts scripts/check-herdr-runtime-bootstrap.ts
1791
+ }
1792
+
1793
+ check_herdr_plugin_profile() {
1794
+ # Deterministic gate for the #116 M3-a pure leaf: the function that turns ONE
1795
+ # `herdr integration status` listing into this plugin's activation plan. Every listing
1796
+ # is an INPUT STRING (verbatim herdr 0.9.0 rows measured 2026-09-16 in a sandboxed
1797
+ # HOME/XDG, plus the two forms cross-checked in herdr source at c77af189), so nothing
1798
+ # here claims a fact about herdr. Pins: the closed atom->backend table, exactly one row
1799
+ # per selected atom or a NAMED refusal (never a first/last-match guess), the state read
1800
+ # FRONT-ANCHORED so an operator's directory name cannot spell a verdict, the display
1801
+ # path leaving no trace in the plan, outdated/needs-repair as named FAILs (measured:
1802
+ # empty, unreadable and marker-less files all collapse into `outdated`), not-installed
1803
+ # as a zero-write SKIP, `current` as herdr's admission with no second floor invented
1804
+ # here, OpenCode observed but never planned, and the leaf's purity. No herdr binary, no
1805
+ # Entwurf install, no environment read, no writes.
1806
+ run_ts scripts/check-herdr-plugin-profile.ts
1807
+ }
1808
+
1809
+ check_herdr_plugin_build() {
1810
+ # Deterministic gate for the #116 M3-b3 build runner (`plugins/herdr/lib/build.mjs`) — the ONE
1811
+ # command Herdr's `[[build]]` calls, and the order it composes: integration status -> pure
1812
+ # profile -> runtime bootstrap -> activation THROUGH THE INSTALLED PACKAGE. There is no fake
1813
+ # herdr binary here (the repo forbids one); the listing arrives through the runner's own spawn
1814
+ # seam as the prose shape 0.9.0 emits, and one cell drives the REAL binary with a named SKIP
1815
+ # when the host has none (ENTWURF_REQUIRE_HERDR=1 makes absence red). Pins: exactly one
1816
+ # [[build]] whose argv is `node lib/build.mjs` and a runner the npm package does NOT ship;
1817
+ # missing herdr vs unanswerable status as separate named refusals with zero writes; a selected
1818
+ # atom that is outdated/needs-repair/malformed/duplicated failing BEFORE any runtime work;
1819
+ # A-empty as a clean exit 0 that writes nothing (no runtime, no wiring, no removal); the
1820
+ # activation entry resolved under the stable runtime with absent/incapable as distinct named
1821
+ # refusals and no checkout fallback; the two-stage {pi} -> {pi, claude-code} composition where a
1822
+ # new commit reinstalls and OpenCode is never named; and the post-build Herdr commit gap being a
1823
+ # NAMED gap, not an atomic transaction. The real `herdr plugin install` + real `npm pack
1824
+ # git+https://...#<sha>` journey needs the network and lives in smoke-herdr-plugin-build-live.
1825
+ run_ts scripts/check-herdr-plugin-build.ts
1826
+ }
1827
+
1828
+ smoke_herdr_plugin_build_live() {
1829
+ # #116 M3-b3 LIVE: a REAL `herdr plugin install` drives this plugin's REAL `[[build]]`, which
1830
+ # packs the FIXED product git spec for real. Needs LIVE=1, herdr/git/npm, and the NETWORK (a
1831
+ # git-spec pack builds the bridge through `prepare`, which installs devDependencies) — which is
1832
+ # the axis check-herdr-plugin-build cannot have and does not claim. Hermetic by two substitutions
1833
+ # only: sandbox HOME/XDG, and git `insteadOf` redirecting the unchanged product remote to a local
1834
+ # bare clone of this worktree, so an unpushed candidate is testable without weakening the argv.
1835
+ # Cells 1/2/4 are the real-Herdr journey: remote-commit available, reinstall widening to
1836
+ # {pi, claude-code} with OpenCode zero-write and ONE COUNTED vendor MCP owner entry, and the
1837
+ # post-build gap where herdr's own commit fails after our runner already rebound the activation.
1838
+ # Cell 3 is deliberately NOT a journey: it calls the shipped runtime leaf directly with the fixed
1839
+ # product argv at a commit the remote lacks, so it is acquisition-leaf evidence — named refusal,
1840
+ # active runtime/ledger/harness bytes preserved, journal left as the certified retry authority.
1841
+ run_ts scripts/smoke-herdr-plugin-build-live.ts
1842
+ }
1843
+
1844
+ check_peer_facts() {
1845
+ # Deterministic gate for the #116 M2-a observed-peer projection. Drives the REAL
1846
+ # scripts/peer-facts.ts as a subprocess with every ambient root replaced (store,
1847
+ # ENTWURF_DIR, HOME, XDG_DATA_HOME, pi agent dir), so the live store, sockets and
1848
+ # markers are never read. Owns only what this verb ADDS on top of the provider:
1849
+ # placement crosses as the structured tagged union rather than the human string,
1850
+ # the peer keyset is exactly the provider's facts (no herdr agent_status may enter
1851
+ # an entwurf payload — that is the door observed activity would use to become
1852
+ # delivery liveness) and no socket coordinate is published at all, diagnostics ride
1853
+ # in-band, the probed socket world is the one ENTWURF_DIR names (proved by a
1854
+ # record-less socket surfacing — there is no field to echo), no observation budget
1855
+ # rations a machine projection, exit contract 0/2/3,
1856
+ # plus dispatch + compiled-twin reachability. No herdr binary, no model turn.
1857
+ run_ts scripts/check-peer-facts.ts
1858
+ }
1859
+
1809
1860
  check_entwurf_fact_provider() {
1810
1861
  # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4b): the
1811
1862
  # ASSEMBLY layer listEntwurfFacts. listAllMetaIdentities → scanSocketProbes →
@@ -1820,6 +1871,94 @@ check_entwurf_fact_provider() {
1820
1871
  run_ts scripts/check-entwurf-fact-provider.ts
1821
1872
  }
1822
1873
 
1874
+ check_herdr_placement() {
1875
+ # Deterministic gate for #116 S1: the placement evidence axis
1876
+ # (pi-extensions/lib/herdr-placement.ts) plus the ONE read the fact provider is
1877
+ # allowed. Pins only what is decidable WITHOUT a herdr binary — payload parse, the
1878
+ # two measured join rules, the ambiguity rule, the four-word vocabulary, the render,
1879
+ # the once-per-listing read, and the STRUCTURAL absence of placement from every
1880
+ # entwurf_v2 dispatch module. There is no fake herdr here for the same reason
1881
+ # check-mux-placement refuses a fake tmux: a stand-in authors the contract before the
1882
+ # real thing is measured. Every payload literal is verbatim herdr 0.9.0 output
1883
+ # recorded 2026-09-14. The real-binary half is an isolated private herdr server
1884
+ # (sandbox HOME/XDG, no client attach); the join/callback round trip stays LIVE.
1885
+ run_ts scripts/check-herdr-placement.ts
1886
+ }
1887
+
1888
+ check_herdr_fresh_call() {
1889
+ # Deterministic gate for #116 S2-c1: the herdr LAUNCH rail core
1890
+ # (pi-extensions/lib/herdr-fresh-call.ts). Pins only what is decidable WITHOUT a herdr
1891
+ # binary — the import fence that keeps the two rails separately deletable, the closed
1892
+ # pi|claude-code pilot, the option-G one-line JSON encoding (exact round trip, zero
1893
+ # Unicode Cc including the C1 block JSON.stringify leaves literal, max public task),
1894
+ # the pre-mutation refusals with the CLI never invoked, the ONE placement policy (a new
1895
+ # tab in the caller's own workspace, read from herdr's own answer about the caller's pane
1896
+ # and never from a pane id's shape, focus left alone), tab-create/start/get/close argv
1897
+ # grammar, strict response parsing, the opaque pane id, the receipt's missing address,
1898
+ # and the conditional-close matrix. No fake herdr executable and no fake server, for the
1899
+ # reason check-mux-placement refuses a fake tmux. Payload literals are verbatim herdr
1900
+ # 0.9.0 output (pane/tab replies 2026-09-15, agent_started 2026-09-14); the real-binary
1901
+ # half is an isolated private server (c2).
1902
+ run_ts scripts/check-herdr-fresh-call.ts
1903
+ }
1904
+
1905
+ check_herdr_sandbox() {
1906
+ # REAL herdr acceptance for #116 C2a: a PRIVATE herdr server in a fully sandboxed
1907
+ # HOME/XDG/PI_CODING_AGENT_DIR creates a tab with the production placement argv, installs
1908
+ # herdr's own pi integration, and starts a BLANK pi in that tab's initial pane from ONE
1909
+ # checkout package registration. Asserts the official witness
1910
+ # triple, the strict path->native id join, exactly one sandbox V3 record, a control socket
1911
+ # that answers the production probe, and the conditional-close decision in both directions
1912
+ # (refuses a pane holding an agent, reclaims one we own and with it the tab it was the
1913
+ # whole of). No fake herdr, no model turn, no
1914
+ # credentials, no keystrokes; the fixture's `pi --approve` is a one-run authorisation that
1915
+ # writes no trust state and never enters production argv. herdr is an OPTIONAL rail: an
1916
+ # absent binary prints a named SKIP and exits 0, a PRESENT-but-incomplete rail FAILS, and
1917
+ # ENTWURF_REQUIRE_HERDR=1 turns absence itself into a failure (CI admission is C2b).
1918
+ run_ts scripts/check-herdr-sandbox.ts
1919
+ }
1920
+
1921
+ check_herdr_supply() {
1922
+ # Deterministic supply-authority gate for #116 C2b: WHICH herdr bytes CI runs, and who
1923
+ # decides. No herdr binary and no network — it reads scripts/fixtures/herdr-supply.json,
1924
+ # scripts/install-herdr-ci.sh and .github/workflows/ci.yml as text. Pins the manifest as the
1925
+ # only copy of version/tag/digest, the digest shape, the fail-closed architecture map, the
1926
+ # verify-before-chmod order, the absence of curl-pipe/latest/package-manager/global install,
1927
+ # the absence of a `gh attestation verify` dependency (measured: gh 2.97.0 cannot close
1928
+ # herdr's in-toto release predicate), the CI install-before-floor order with
1929
+ # ENTWURF_REQUIRE_HERDR=1, Linux-job-only wiring, that no product script or bin reaches the
1930
+ # installer (Hard Rule 17), and that the x86_64 asset stays recorded as never executed.
1931
+ run_ts scripts/check-herdr-supply.ts
1932
+ }
1933
+
1934
+ check_fresh_call_dispatch() {
1935
+ # Deterministic gate for #116 C3: the seam ABOVE the two rails. Pins that the rail is a
1936
+ # capability fact (exactly HERDR_ENV=1) and never a caller parameter, that neither direction
1937
+ # falls back, that a non-pilot backend refuses inside herdr before any preflight or mutation,
1938
+ # that the Codex preflight keeps its pre-existing ordering on the tmux path only, that one
1939
+ # nonce per call reaches the selected rail, that BOTH public surfaces reach one composition
1940
+ # root (no second copy of rail choice, preflight ordering or rendering), that pi still uses
1941
+ # its lazy dynamic import, and that herdr rendering shows view-not-address plus which recovery
1942
+ # happened. No tmux and no herdr binary: the seam runs with an injected spawn.
1943
+ run_ts scripts/check-fresh-call-dispatch.ts
1944
+ }
1945
+
1946
+ smoke_herdr_fresh_call_live() {
1947
+ # LIVE acceptance for the #116 herdr rail: a REAL caller that is itself inside a herdr pane
1948
+ # invokes the REAL PUBLIC entwurf_fresh_call, and a REAL child's first model action is the
1949
+ # nonce callback. Opt-in (LIVE=1), four model turns, one private herdr server PER CELL —
1950
+ # herdr's socket follows XDG_CONFIG_HOME and each cell is fenced into its own.
1951
+ # `[측정 2026-09-14]` BOTH runtimes keep the operator's REAL HOME and real auth roots: a pi
1952
+ # given a fixture HOME dies before readiness (an installed extension resolves its npm
1953
+ # dependency through $HOME) and a claude given one enters first-run onboarding. Only
1954
+ # Entwurf-OWNED writes are fenced into the fixture (XDG roots, the meta roots, the v2 lock
1955
+ # dir). Evidence is source receipts only, in two named grades: pi from its own vendor
1956
+ # transcript, claude from the runtime's own entwurf-bridge MCP activity log joined by exact
1957
+ # sessionId plus entwurf's mailbox artifact and hook journal — never screen text, never a
1958
+ # keystroke. The operator's herdr panes are snapshotted before and after.
1959
+ run_ts scripts/smoke-herdr-fresh-call-live.ts
1960
+ }
1961
+
1823
1962
  check_entwurf_peers_surface() {
1824
1963
  # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4c; #50 C4
1825
1964
  # re-author): the MCP entwurf_peers RENDER/PAYLOAD layer renderEntwurfPeers.
@@ -2820,6 +2959,70 @@ check_acp_sdk_surface() {
2820
2959
  run_vitest test/acp-sdk-surface.contract.test.ts
2821
2960
  }
2822
2961
 
2962
+ check_tests_beside_behavior() {
2963
+ # The DISCOVERY door (issue #119 V2). A `.test.ts` beside a pi extension or a
2964
+ # `.test.mjs` beside the herdr plugin lib is run by public `pnpm check` without
2965
+ # anyone adding its filename anywhere. That is the whole contract: the other two
2966
+ # vitest shims list their files, and a door that also listed files would shut
2967
+ # again the first time someone forgot.
2968
+ #
2969
+ # The globs are EXPANDED here, not handed to vitest as positional filters. The first
2970
+ # cut passed `pi-extensions plugins/herdr` as positionals, and vitest reads a positional
2971
+ # as a path SUBSTRING — so `test/pi-extensions-probe.test.ts` was selected too, which is
2972
+ # the opposite of the two-locations contract this gate exists to hold (measured: that file
2973
+ # ran as "Test Files 1 passed"). Expanding first keeps the selected set exactly the two
2974
+ # globs while the shim still contains no filename: it computes them every run.
2975
+ #
2976
+ # Keeping these globs out of the other two shims stays deliberate — those are mutant gate
2977
+ # argv (mux-fresh-call carries 56), and widening their include would rerun unrelated tests
2978
+ # once per mutant.
2979
+ #
2980
+ # An empty expansion RETURNS rather than calling vitest with no arguments, because a
2981
+ # vitest run with no filter runs the whole include — that is, all of test/**. The honest
2982
+ # steady state is "the door is open and unused", and it must not be spelled as a run.
2983
+ section "tests beside behavior (discovery, not a filename list)"
2984
+ local cfg="$REPO_DIR/vitest.config.ts" glob
2985
+ for glob in 'pi-extensions/**/*.test.ts' 'plugins/herdr/**/*.test.mjs'; do
2986
+ if ! grep -qF "\"$glob\"" "$cfg"; then
2987
+ fail "[check-tests-beside-behavior] vitest.config.ts no longer includes $glob — the discovery door is shut, and an expansion that found nothing would still pass"
2988
+ return 1
2989
+ fi
2990
+ done
2991
+
2992
+ local -a selected=()
2993
+ local f
2994
+ while IFS= read -r f; do
2995
+ [ -n "$f" ] && selected+=("$f")
2996
+ done < <(cd "$REPO_DIR" && {
2997
+ find pi-extensions -name node_modules -prune -o -type f -name '*.test.ts' -print
2998
+ find plugins/herdr -name node_modules -prune -o -type f -name '*.test.mjs' -print
2999
+ } 2>/dev/null | LC_ALL=C sort)
3000
+
3001
+ # Independent of HOW the expansion was produced: every selected path must be under one of
3002
+ # the two declared locations, and none may come from test/. The second clause is the one
3003
+ # that names the defect above, so it is asserted rather than left implied by the first.
3004
+ for f in "${selected[@]}"; do
3005
+ case "$f" in
3006
+ test/*)
3007
+ fail "[check-tests-beside-behavior] selected $f — this gate covers the two behavior-adjacent locations, never test/**"
3008
+ return 1
3009
+ ;;
3010
+ pi-extensions/*.test.ts | plugins/herdr/*.test.mjs) ;;
3011
+ *)
3012
+ fail "[check-tests-beside-behavior] selected $f, which matches neither declared glob"
3013
+ return 1
3014
+ ;;
3015
+ esac
3016
+ done
3017
+
3018
+ if [ "${#selected[@]}" -eq 0 ]; then
3019
+ echo " the door is open and unused — 0 tests beside behavior at this HEAD"
3020
+ return 0
3021
+ fi
3022
+ echo " selected ${#selected[@]}: ${selected[*]}"
3023
+ run_vitest "${selected[@]}"
3024
+ }
3025
+
2823
3026
  check_acp_overlay() {
2824
3027
  # Deterministic gate for the S2b Claude config overlay materializer. Drives
2825
3028
  # ensureClaudeConfigOverlay against injected temp realDir/overlayDir (no
@@ -3135,6 +3338,10 @@ check_pack() {
3135
3338
  # #65 — the owner-normalized store projection consumers call INSTEAD of
3136
3339
  # parsing the store; installed hosts are exactly where those consumers live.
3137
3340
  "mcp/entwurf-bridge/dist/scripts/meta-facts.js"
3341
+ # #116 M2-a — the owner-normalized PEER projection, for the same reason one step
3342
+ # out: the placement join it emits is the copy a herdr-side consumer would
3343
+ # otherwise fork, and such a consumer runs against an installed host.
3344
+ "mcp/entwurf-bridge/dist/scripts/peer-facts.js"
3138
3345
  # #87 Bundle B — omp-receive-doctor drives this operator projection through
3139
3346
  # run.sh, so installed node_modules must have the compiled twin too.
3140
3347
  "mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js"
@@ -3219,6 +3426,13 @@ check_pack() {
3219
3426
  # tripwire that fails loud if that negation is ever dropped (결합 규칙).
3220
3427
  '__pycache__'
3221
3428
  '\.pyc$'
3429
+ # Tests written beside behavior (#119 V2) are a dev surface, and `pi-extensions/`
3430
+ # ships WHOLE through the files allowlist, so the `!pi-extensions/**/*.test.ts`
3431
+ # negation is the only thing keeping them out — same 결합 규칙 as __pycache__ above,
3432
+ # this pattern is its tripwire. Written loose rather than anchored under
3433
+ # pi-extensions/ so it also covers any future allowlist entry; `plugins/` needs no
3434
+ # companion negation because `^plugins/` above already refuses that whole tree.
3435
+ '\.test\.[cm]?[jt]s$'
3222
3436
  )
3223
3437
 
3224
3438
  local pass=1 f pat hit
@@ -3430,6 +3644,8 @@ _check_pack_install_impl() {
3430
3644
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js"
3431
3645
  # #65 — the owner-normalized store projection (see check-pack).
3432
3646
  "mcp/entwurf-bridge/dist/scripts/meta-facts.js"
3647
+ # #116 M2-a — the owner-normalized peer projection (see check-pack).
3648
+ "mcp/entwurf-bridge/dist/scripts/peer-facts.js"
3433
3649
  # #87 Bundle B — omp-receive-doctor reaches this through the installed dispatcher.
3434
3650
  "mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js"
3435
3651
  # 0.12.5 — node_modules-safe plugin hook + lib (see check-pack). The installed
@@ -3617,6 +3833,60 @@ _check_pack_install_impl() {
3617
3833
  }
3618
3834
  echo "[check-pack-install] installed: $probe"
3619
3835
 
3836
+ # #116 M3-b1/b2 — THE ACTUAL-PACKAGE RUNTIME PROOF, THROUGH THE SHIPPED OWNER.
3837
+ # The Herdr plugin's runtime verifier decides whether a tree npm placed is a runtime the scoped
3838
+ # wiring may name: exact name@version, the compiled entry, all three required bins present AND
3839
+ # executable, and a real `entwurf check-bridge`. Its own focused gate drives that verifier against
3840
+ # a FIXTURE package, which proves the transaction but not this package — so the real artifact is
3841
+ # verified HERE, against the tarball this gate already packed and installed. No second pack, no
3842
+ # network.
3843
+ #
3844
+ # It imports from the INSTALLED tree's own scripts/herdr-runtime.mjs, not from the checkout. That
3845
+ # is the point: Herdr's `plugin uninstall` deletes the plugin checkout and calls no cleanup hook,
3846
+ # so the code that retires a runtime has to be IN the package. Reaching into $REPO_DIR here would
3847
+ # prove the checkout works and say nothing about what a clean host receives.
3848
+ local runtime_proof
3849
+ runtime_proof=$(cd "$tmp" && node --input-type=module -e "
3850
+ const pkg = process.cwd() + '/node_modules/@junghanacs/entwurf';
3851
+ const { verifyInstalledRuntime, readCheckoutPackageSpec, REQUIRED_BINS } =
3852
+ await import(pkg + '/scripts/herdr-runtime.mjs');
3853
+ const spec = readCheckoutPackageSpec(pkg);
3854
+ const seen = verifyInstalledRuntime(process.cwd(), spec);
3855
+ console.log(seen.name + '@' + seen.version + ' verified through the SHIPPED owner (' + REQUIRED_BINS.join(', ') + ')');
3856
+ " 2>&1) || {
3857
+ fail "[check-pack-install] #116 runtime verifier REFUSED the actual installed package (or does not ship):"
3858
+ echo "$runtime_proof" | sed 's/^/ /' >&2
3859
+ return 1
3860
+ }
3861
+ echo "[check-pack-install] herdr-plugin runtime verifier: $runtime_proof"
3862
+
3863
+ # #116 M3-b2 — THE DEACTIVATE VERB MUST ACTUALLY TEAR DOWN, WITH NO CHECKOUT. Herdr's `plugin
3864
+ # uninstall` deletes the plugin checkout and calls no cleanup hook, so the teardown entry has to
3865
+ # be IN the package AND has to survive deleting the runtime it is executing from. So this does not
3866
+ # settle for the "nothing to undo" verdict: it stages the real shape — the installed package
3867
+ # sitting AT the stable active root, a certified runtime journal, and a certified activation
3868
+ # ledger whose components are already removed — then runs THAT copy's own entry with a throwaway
3869
+ # HOME/XDG and no reference to this repo. What it proves is the self-delete: the process removes
3870
+ # the tree it is executing from and still finishes retiring the ledger.
3871
+ local deact_home deact_active deact_pkg deact_proof deact_rc runtime_left ledger_left
3872
+ deact_home=$(mktemp -d -t entwurf-deact.XXXXXX)
3873
+ deact_active="$deact_home/.data/entwurf/herdr-plugin/runtime/active"
3874
+ mkdir -p "$deact_active" "$deact_home/.state/entwurf/herdr-plugin"
3875
+ cp -R "$tmp/node_modules" "$deact_active/node_modules"
3876
+ deact_pkg="$deact_active/node_modules/@junghanacs/entwurf"
3877
+ node -e 'const fs=require("fs"),path=require("path");const [home,active]=process.argv.slice(1);const pkg=path.join(active,"node_modules","@junghanacs","entwurf");const version=JSON.parse(fs.readFileSync(path.join(pkg,"package.json"),"utf8")).version;fs.writeFileSync(path.join(home,".data","entwurf","herdr-plugin","journal.json"),JSON.stringify({schemaVersion:2,phase:"runtime-ready",runtimeRoot:active,artifactIdentity:{kind:"herdr-checkout",repository:"junghan0611/entwurf",commit:"c".repeat(40),packageName:"@junghanacs/entwurf",packageVersion:version,observedDigest:"sha256-"+"a".repeat(64)},previousRuntime:null}));fs.writeFileSync(path.join(home,".state","entwurf","herdr-plugin","activation.json"),JSON.stringify({schemaVersion:2,phase:"deactivating",runtimeRoot:active,artifactIdentity:{kind:"herdr-checkout",repository:"junghan0611/entwurf",commit:"c".repeat(40),packageName:"@junghanacs/entwurf",packageVersion:version,observedDigest:"sha256-"+"a".repeat(64)},piAgentDir:{path:path.join(home,".pi","agent"),source:"default"},claudeConfigDir:{path:path.join(home,".claude"),source:"default"},claudeUserConfig:{path:path.join(home,".claude.json"),source:"HOME"},activatedBackends:["pi"],components:[{backend:"pi",state:"removed"}]}));' "$deact_home" "$deact_active"
3878
+ deact_proof=$(env -i PATH="$PATH" HOME="$deact_home" XDG_DATA_HOME="$deact_home/.data" XDG_STATE_HOME="$deact_home/.state" XDG_CACHE_HOME="$deact_home/.cache" node "$deact_pkg/scripts/herdr-plugin-deactivate.mjs" 2>&1)
3879
+ deact_rc=$?
3880
+ runtime_left=$([ -e "$deact_home/.data/entwurf/herdr-plugin/runtime" ] && echo yes || echo no)
3881
+ ledger_left=$([ -e "$deact_home/.state/entwurf/herdr-plugin/activation.json" ] && echo yes || echo no)
3882
+ rm -rf "$deact_home"
3883
+ if [ "$deact_rc" -ne 0 ] || [ "$runtime_left" != "no" ] || [ "$ledger_left" != "no" ]; then
3884
+ fail "[check-pack-install] #116 M3-b2 installed herdr-plugin-deactivate did not complete the self-delete (rc=$deact_rc runtime-left=$runtime_left ledger-left=$ledger_left):"
3885
+ echo "$deact_proof" | sed 's/^/ /' >&2
3886
+ return 1
3887
+ fi
3888
+ echo "[check-pack-install] installed herdr-plugin-deactivate removed its OWN runtime and retired the ledger with no checkout: $deact_proof"
3889
+
3620
3890
  # Pi package loader smoke — actual `pi` reads the manifest and
3621
3891
  # registers the provider. rc=0 + the curated model list in the
3622
3892
  # output means pi accepted the package as a real extension, not
@@ -6090,6 +6360,9 @@ case "$cmd" in
6090
6360
  check-copilot-launch)
6091
6361
  check_copilot_launch
6092
6362
  ;;
6363
+ check-pi-launch)
6364
+ check_pi_launch
6365
+ ;;
6093
6366
  check-copilot-receive-arm)
6094
6367
  check_copilot_receive_arm
6095
6368
  ;;
@@ -6105,33 +6378,12 @@ case "$cmd" in
6105
6378
  check-entwurf-v2-contract)
6106
6379
  check_entwurf_v2_contract
6107
6380
  ;;
6108
- check-entwurf-v2-lock)
6109
- check_entwurf_v2_lock
6110
- ;;
6111
- check-entwurf-v2-decider)
6112
- check_entwurf_v2_decider
6113
- ;;
6114
- check-entwurf-v2-matrix)
6115
- check_entwurf_v2_matrix
6116
- ;;
6117
- check-entwurf-v2-release)
6118
- check_entwurf_v2_release
6119
- ;;
6120
6381
  check-entwurf-v2-send)
6121
6382
  check_entwurf_v2_send
6122
6383
  ;;
6123
- check-entwurf-v2-send-fallback)
6124
- check_entwurf_v2_send_fallback
6125
- ;;
6126
- check-entwurf-v2-mailbox)
6127
- check_entwurf_v2_mailbox
6128
- ;;
6129
6384
  check-entwurf-v2-native-push)
6130
6385
  check_entwurf_v2_native_push
6131
6386
  ;;
6132
- check-entwurf-v2-runner)
6133
- check_entwurf_v2_runner
6134
- ;;
6135
6387
  check-entwurf-control-rpc)
6136
6388
  check_entwurf_control_rpc
6137
6389
  ;;
@@ -6177,6 +6429,9 @@ case "$cmd" in
6177
6429
  check-mux-launcher-fence)
6178
6430
  check_mux_launcher_fence
6179
6431
  ;;
6432
+ check-typing-call-fence)
6433
+ check_typing_call_fence
6434
+ ;;
6180
6435
  check-entwurf-v2-visible-resume)
6181
6436
  check_entwurf_v2_visible_resume
6182
6437
  ;;
@@ -6249,24 +6504,71 @@ case "$cmd" in
6249
6504
  smoke-acp-socket-citizen-live)
6250
6505
  smoke_acp_socket_citizen_live
6251
6506
  ;;
6252
- check-entwurf-facts)
6253
- check_entwurf_facts
6254
- ;;
6255
6507
  check-control-socket-path)
6256
6508
  check_control_socket_path
6257
6509
  ;;
6258
- check-socket-discovery)
6259
- check_socket_discovery
6260
- ;;
6261
6510
  check-meta-listing)
6262
6511
  check_meta_listing
6263
6512
  ;;
6264
6513
  check-meta-facts)
6265
6514
  check_meta_facts
6266
6515
  ;;
6516
+ check-herdr-activation)
6517
+ check_herdr_activation
6518
+ ;;
6519
+ check-herdr-runtime-bootstrap)
6520
+ check_herdr_runtime_bootstrap
6521
+ ;;
6522
+ check-herdr-plugin-profile)
6523
+ check_herdr_plugin_profile
6524
+ ;;
6525
+ check-herdr-plugin-build)
6526
+ check_herdr_plugin_build
6527
+ ;;
6528
+ smoke-herdr-plugin-build-live)
6529
+ smoke_herdr_plugin_build_live
6530
+ ;;
6531
+ smoke-herdr-raw-install-live)
6532
+ # #118 홉 1: the FIRST USER PATH. Every other herdr gate proves the plugin against a
6533
+ # source we control — a stub, or the product remote redirected by git insteadOf to a
6534
+ # local bare clone asserting identity.kind === "herdr-checkout". None has watched the
6535
+ # plugin acquire its runtime from npm on a machine that has never seen this repo, which
6536
+ # is the one axis VERIFY.md:61 leaves open while the production lock says npm. Clean
6537
+ # container, public remote, no mounts, no insteadOf. Its first question is whether
6538
+ # `plugin install --yes` completes with no herdr server; a "no" is #118 investigation
6539
+ # output, not a completion. on-demand LIVE, never an aggregate MUST.
6540
+ # shift is load-bearing: this dispatcher keeps the verb in "$@" and each branch that
6541
+ # forwards arguments drops it for itself. Without it `--ref` arrives behind the gate name.
6542
+ shift
6543
+ (cd "$REPO_DIR" && bash scripts/smoke-herdr-raw-install-live.sh "$@")
6544
+ ;;
6545
+ check-peer-facts)
6546
+ check_peer_facts
6547
+ ;;
6548
+ check-herdr-plugin)
6549
+ check_herdr_plugin
6550
+ ;;
6267
6551
  check-entwurf-fact-provider)
6268
6552
  check_entwurf_fact_provider
6269
6553
  ;;
6554
+ check-herdr-placement)
6555
+ check_herdr_placement
6556
+ ;;
6557
+ check-herdr-fresh-call)
6558
+ check_herdr_fresh_call
6559
+ ;;
6560
+ check-herdr-sandbox)
6561
+ check_herdr_sandbox
6562
+ ;;
6563
+ check-herdr-supply)
6564
+ check_herdr_supply
6565
+ ;;
6566
+ check-fresh-call-dispatch)
6567
+ check_fresh_call_dispatch
6568
+ ;;
6569
+ smoke-herdr-fresh-call-live)
6570
+ smoke_herdr_fresh_call_live
6571
+ ;;
6270
6572
  check-entwurf-peers-surface)
6271
6573
  check_entwurf_peers_surface
6272
6574
  ;;
@@ -6755,6 +7057,54 @@ case "$cmd" in
6755
7057
  shift || true
6756
7058
  exec bash "$REPO_DIR/scripts/copilot-launch.sh" "$@"
6757
7059
  ;;
7060
+ pi)
7061
+ # #118 홉 2: the managed pi launch. Same shape and same reasons as `copilot` above —
7062
+ # `exec`, no subshell and no cd, because this operates on the operator's session; and
7063
+ # `shift`, because this branch forwards straight into the VENDOR argv where a stray
7064
+ # "pi" would arrive as a prompt argument.
7065
+ #
7066
+ # NO separate launcher script, and no precondition check, because the precondition is
7067
+ # ZERO and that was measured rather than assumed: on a host where entwurf's pi extension
7068
+ # is not registered, `pi --entwurf-control` already fails as
7069
+ # `Error: Unknown option: --entwurf-control` with exit 1. A pre-check here would only
7070
+ # duplicate a refusal pi owns and would go stale the day pi renames it.
7071
+ #
7072
+ # ONE recursion fence, not copilot's two, and the difference is measured. The sentinel
7073
+ # closes the only real loop: a PATH executable named `pi` that shells back to
7074
+ # `entwurf pi` re-enters this branch, and finds the flag already exported. Copilot's
7075
+ # second fence — resolving the binary and refusing our own entrypoints — closes a case
7076
+ # that is not a loop here: a PATH `pi` symlinked to run.sh would be exec'd as
7077
+ # `run.sh --entwurf-control …`, which is an unknown verb this dispatcher already
7078
+ # refuses. `type -P` is what keeps the operator's own shell wrappers out of it
7079
+ # entirely: it returns a PATH file only, never a function or alias, so a
7080
+ # `pi() { entwurf pi "$@"; }` in a profile can never be re-entered through it.
7081
+ #
7082
+ # The flag is added, never deduplicated: `pit`/`pius`-style wrappers already pass
7083
+ # `--entwurf-control`, and passing it twice was measured to be byte-identical to
7084
+ # passing it once. An argv scan would be code earning nothing.
7085
+ #
7086
+ # Nothing else is injected. `--emacs-agent-socket` and friends are operator taste, and
7087
+ # a launcher that decides them stops being a spelling (Hard Rule 9).
7088
+ shift || true
7089
+ if [ -n "${ENTWURF_PI_LAUNCH_ACTIVE:-}" ]; then
7090
+ echo "entwurf: recursive managed launch detected (ENTWURF_PI_LAUNCH_ACTIVE is already set)." >&2
7091
+ echo " Something on PATH named 'pi' resolves back to this launcher. Fix PATH so 'pi'" >&2
7092
+ echo " is the pi coding agent, or run the vendor binary by its full path." >&2
7093
+ exit 1
7094
+ fi
7095
+ entwurf_pi_bin="$(type -P pi 2>/dev/null || true)"
7096
+ if [ -z "$entwurf_pi_bin" ]; then
7097
+ echo "entwurf: no 'pi' executable found on PATH." >&2
7098
+ echo " 'entwurf pi' launches the pi coding agent; it does not install one." >&2
7099
+ exit 1
7100
+ fi
7101
+ if [ ! -f "$entwurf_pi_bin" ] || [ ! -x "$entwurf_pi_bin" ]; then
7102
+ echo "entwurf: '$entwurf_pi_bin' is not an executable regular file — refusing to exec it." >&2
7103
+ exit 1
7104
+ fi
7105
+ export ENTWURF_PI_LAUNCH_ACTIVE=1
7106
+ exec "$entwurf_pi_bin" --entwurf-control "$@"
7107
+ ;;
6758
7108
  install-copilot-receive)
6759
7109
  # #82 RAIL 5: install the RECEIVER extension into the Copilot USER extensions dir.
6760
7110
  # Artifact ownership only — it sets no launch flag (`run.sh copilot` owns one
@@ -6926,6 +7276,22 @@ case "$cmd" in
6926
7276
  shift || true
6927
7277
  run_ts scripts/meta-facts.ts "$@"
6928
7278
  ;;
7279
+ peer-facts)
7280
+ # #116 M2-a READ-ONLY projection of the OBSERVED peer listing: one provider
7281
+ # (`listEntwurfFacts`) through one renderer (`renderEntwurfPeers().payload`), so
7282
+ # the payload SHAPE is the entwurf_peers payload shape and the join is never
7283
+ # re-implemented — the pi half of that join is a measured vendor floor, and a
7284
+ # forked copy decays silently. It is NOT the same bytes as an entwurf_peers call:
7285
+ # that surface rations observation and returns text, this one is unbounded because
7286
+ # a rationed row says `unobserved` and a machine consumer cannot tell that from
7287
+ # "nobody could look". `placement` stays the structured tagged union. No socket
7288
+ # coordinate is published (#50 C4 retired the legacy listing with the controlDir it
7289
+ # exposed; ENTWURF_DIR only selects what is probed) and herdr's own
7290
+ # agent_status/screen verdicts are absent (docs/herdr-launch-rail.md §9). No
7291
+ # writes, no watcher, no dispatch. Same exit contract as meta-facts.
7292
+ shift || true
7293
+ run_ts scripts/peer-facts.ts "$@"
7294
+ ;;
6929
7295
  meta-bridge-managed-keys)
6930
7296
  # 0.10.0 meta-bridge: emit the SSOT of settings.json/~/.claude.json keys that
6931
7297
  # entwurf's install OWNS. Consumers (agent-config fragment, future
@@ -6976,6 +7342,9 @@ case "$cmd" in
6976
7342
  check-acp-sdk-surface)
6977
7343
  check_acp_sdk_surface
6978
7344
  ;;
7345
+ check-tests-beside-behavior)
7346
+ check_tests_beside_behavior
7347
+ ;;
6979
7348
  check-acp-overlay)
6980
7349
  check_acp_overlay
6981
7350
  ;;
@@ -7084,6 +7453,21 @@ case "$cmd" in
7084
7453
  remove)
7085
7454
  remove_local_package "$TARGET_PROJECT_DIR"
7086
7455
  ;;
7456
+ herdr-plugin-activate)
7457
+ # #116 M3-b2 public forward verb. Takes a subset of {pi, claude-code} and NOTHING else — the
7458
+ # stable runtime root is derived, never supplied. Plain `node` in both modes: shipped .mjs.
7459
+ shift
7460
+ (cd "$REPO_DIR" && node "$REPO_DIR/scripts/herdr-plugin-activate.mjs" "$@")
7461
+ ;;
7462
+ herdr-plugin-deactivate)
7463
+ # #116 M3-b2 public verb. Plain `node` in BOTH modes on purpose: this is shipped .mjs, not TS,
7464
+ # because it has to run from an installed package after Herdr deleted the plugin checkout.
7465
+ (cd "$REPO_DIR" && node "$REPO_DIR/scripts/herdr-plugin-deactivate.mjs" "$@")
7466
+ ;;
7467
+ install-user-scope)
7468
+ shift
7469
+ install_user_scope "$@"
7470
+ ;;
7087
7471
  remove-user-scope)
7088
7472
  remove_user_scope_citizen
7089
7473
  ;;