@junghanacs/entwurf 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/AGENTS.md +3 -2
  2. package/BASELINE.md +3 -1
  3. package/CHANGELOG.md +658 -0
  4. package/DELIVERY.md +167 -29
  5. package/README.md +104 -14
  6. package/VERIFY.md +73 -11
  7. package/docs/external-mcp-host.md +16 -6
  8. package/docs/setup-clean-host.md +73 -22
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +54 -31
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +289 -57
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +171 -263
  26. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +65 -31
  29. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  30. package/package.json +4 -4
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  33. package/pi-extensions/lib/codex-declaration.js +612 -0
  34. package/pi-extensions/lib/codex-fresh-preflight.ts +325 -52
  35. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  36. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  37. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  38. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  39. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  40. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  41. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  42. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  43. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  44. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  45. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  46. package/pi-extensions/lib/herdr-placement.ts +276 -0
  47. package/pi-extensions/lib/mux-fresh-call.ts +257 -304
  48. package/run.sh +522 -10
  49. package/scripts/check-codex-app-server-launch.ts +445 -0
  50. package/scripts/check-codex-birth-hook.ts +187 -0
  51. package/scripts/check-entwurf-control-rpc.ts +138 -0
  52. package/scripts/check-entwurf-fact-provider.ts +4 -2
  53. package/scripts/check-entwurf-facts.ts +12 -6
  54. package/scripts/check-entwurf-peers-surface.ts +3 -1
  55. package/scripts/check-entwurf-self-address.ts +68 -0
  56. package/scripts/check-entwurf-v2-production.ts +42 -1
  57. package/scripts/check-entwurf-v2-send.ts +26 -7
  58. package/scripts/check-fresh-call-dispatch.ts +301 -0
  59. package/scripts/check-gate-qualification.ts +19 -5
  60. package/scripts/check-herdr-activation.ts +1293 -0
  61. package/scripts/check-herdr-fresh-call.ts +1250 -0
  62. package/scripts/check-herdr-placement.ts +383 -0
  63. package/scripts/check-herdr-plugin-build.ts +842 -0
  64. package/scripts/check-herdr-plugin-profile.ts +371 -0
  65. package/scripts/check-herdr-plugin.ts +523 -0
  66. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  67. package/scripts/check-herdr-sandbox.ts +456 -0
  68. package/scripts/check-herdr-supply.ts +184 -0
  69. package/scripts/check-mux-launch-tmux.ts +331 -35
  70. package/scripts/check-mux-launcher-fence.ts +138 -0
  71. package/scripts/check-peer-facts.ts +331 -0
  72. package/scripts/check-release-gate-outcomes.ts +12 -0
  73. package/scripts/check-typing-call-fence.ts +296 -0
  74. package/scripts/codex-app-server-launch.sh +275 -0
  75. package/scripts/codex-birth-doctor.sh +159 -16
  76. package/scripts/codex-birth-install.sh +178 -72
  77. package/scripts/codex-birth-uninstall.sh +162 -7
  78. package/scripts/codex-socket-path.ts +33 -0
  79. package/scripts/codex-terminal-title-config.py +500 -0
  80. package/scripts/codex_toml_io.py +121 -0
  81. package/scripts/fixtures/herdr-supply.json +29 -0
  82. package/scripts/herdr-activation.mjs +536 -0
  83. package/scripts/herdr-plugin-activate.mjs +270 -0
  84. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  85. package/scripts/herdr-runtime.mjs +1203 -0
  86. package/scripts/install-herdr-ci.sh +96 -0
  87. package/scripts/lib/codex-fresh-live-protocol.ts +13 -5
  88. package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
  89. package/scripts/lib/tree-digest.ts +97 -0
  90. package/scripts/meta-bridge-install.sh +19 -2
  91. package/scripts/meta-bridge-state.py +167 -11
  92. package/scripts/mutants/codex-app-server-launch.json +157 -0
  93. package/scripts/mutants/codex-caller-seat.json +334 -0
  94. package/scripts/mutants/codex-native.json +195 -29
  95. package/scripts/mutants/control-socket-disconnect.json +57 -0
  96. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  97. package/scripts/mutants/herdr-activation.json +305 -0
  98. package/scripts/mutants/herdr-fresh-call.json +448 -0
  99. package/scripts/mutants/herdr-placement.json +161 -0
  100. package/scripts/mutants/herdr-plugin-build.json +160 -0
  101. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  102. package/scripts/mutants/herdr-plugin.json +133 -0
  103. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  104. package/scripts/mutants/herdr-supply.json +133 -0
  105. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  106. package/scripts/mutants/mux-fresh-call.json +184 -25
  107. package/scripts/mutants/mux-launcher-fence.json +13 -0
  108. package/scripts/mutants/omp-fresh.json +4 -4
  109. package/scripts/mutants/peer-facts.json +98 -0
  110. package/scripts/mutants/self-address.json +33 -0
  111. package/scripts/mutants/typing-call-fence.json +17 -0
  112. package/scripts/mutants/v2-surface.json +22 -0
  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-codex-birth.sh +281 -12
  117. package/scripts/smoke-codex-config-state.sh +192 -3
  118. package/scripts/smoke-codex-fresh-live.ts +277 -37
  119. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  120. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  121. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  122. package/scripts/smoke-setup-verdict.sh +13 -11
package/run.sh CHANGED
@@ -120,7 +120,7 @@ usage() {
120
120
  cat <<'EOF'
121
121
  Usage:
122
122
  ./run.sh setup [project-dir] # ONE presence-driven composition (#86): per-component PASS/SKIP/FAIL for pi/claude/agy/copilot/omp/codex + stable dev bins + v2 install smoke. Codex birth/MCP/status-line atoms are composed at user scope; setup stays non-green until the operator trusts the birth declaration once in visible Codex. Absent harness = zero-state SKIP, detected-incomplete = named FAIL + nonzero exit. Never installs a harness, starts a daemon, touches credentials, or writes the vendor trust receipt
123
- ./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live) + the Codex first-admission Pi -> visible Codex -> visible Pi receipt chain (smoke-codex-fresh-live; initial Pi outside the existing exact `codex` home, explicit operator-owned app-server PID inside it). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
123
+ ./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live) + the Codex Pi -> visible Codex -> visible Pi receipt chain (smoke-codex-fresh-live; initial Pi and the Codex it opens in one session S, explicit operator-owned app-server PID in a DIFFERENT session — that separation is the lane B claim). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
124
124
  ./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
125
125
  ./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b/G1e/G1f, IN pnpm run check:full): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2 and entwurf_resume_call registered/schema + the tools/list surface is EXACTLY the seven shipped garden verbs; tools/list only, no auth/side-effect
126
126
  ./run.sh check-entwurf-bridge-pi-free # deterministic gate (0.12.1 A, IN pnpm check): static — bridge index eager value-import closure must carry no @earendil-works/pi-* (type-only + dynamic import excluded); proves the meta-bridge boots pi-free
@@ -134,6 +134,7 @@ Usage:
134
134
  ./run.sh check-omp-birth-hook # #87 gate: drives the real OMP assembler into a temp dir, imports the ASSEMBLED index.ts into a MOCK omp host and fires session_start/session_switch. tui mints one backend:"omp" record + a sender marker keyed to the host's OWN pid (the one-process join) + the garden id on the status line; print/rpc/json mint NOTHING (hasUI is true on the rpc rows, as in the vendor); switch attaches on the same native id and mints the replacement on a new one; the CERTIFIED receiver reader still finds no marker. Also the four-root joint binding: extension and omp-labeled bridge child resolve the same sessions/mailbox/senders/receivers bundle against test-built literals (never the production resolver as its own oracle), under a poisoned PI_CODING_AGENT_DIR and under four distinct ENTWURF_META_* overrides; the override grammar is absolute-or-~ and both halves plus the doctor refuse anything else by name; and a drifted provenance label selects no root policy at all. Hermetic; no omp, no model turn
135
135
  ./run.sh check-copilot-receive-arm # #82 RAIL 5 gate: the REAL receiver installer + the REAL extension.mjs forked with a stubbed SDK. Arms only after birth, marker owned by the WATCHER pid, self-fetch dispatch answer, doorbell carries the garden id and NOT the body, id-drift/foreign-parent refusals. Hermetic; no Copilot, no model turn
136
136
  ./run.sh check-copilot-launch # #82 RAIL 7 gate: the MANAGED launch `entwurf copilot`, driven through its public address against a FAKE VENDOR on a sandbox PATH. Receiver precondition refusals, EXTENSIONS token + operator token preservation, injected defaults before the `--` terminator, byte-identical argv, the 11 explicit permission/surface policy overrides that suppress `--yolo`, exec (not fork) pid identity, exit passthrough, recursion refusal. Hermetic; no Copilot, no model turn
137
+ ./run.sh check-codex-app-server-launch # #95 gate: the MANAGED app-server launch `entwurf codex-app-server`, driven through its public address against a FAKE VENDOR on a sandbox PATH. The injected `app-server --listen unix://<socket>` required to equal the product's own resolveCodexDefaultSocketPath over an env matrix that includes the inputs a bash transcription was MEASURED to diverge on (BOM-only CODEX_HOME, trailing slash, `..`), non-absolute and control-character addresses refused before any write, byte-identical operator argv, exec (not fork) parent identity, exit passthrough, live-socket and indeterminate-socket and second---listen and recursion refusals, dead-socket launch-over, the tmux fact line, and both identity carriers stripped. Hermetic; no Codex, no app-server, no model turn
137
138
  ./run.sh check-copilot-statusline # #82 Copilot custom-footer renderer. session_id → ready/?/gid + rail `cop`; exit 0. IN pnpm check. No Copilot, no model turn
138
139
  ./run.sh check-entwurf-capabilities # deterministic gate (0.11 Stage 0 step 3C): backend capability registry (pi/entwurf-capabilities.json) — coverage==META_CITIZEN_BACKENDS + agrees with live META_BACKEND_DESCRIPTORS + strict keyset, no API
139
140
  ./run.sh check-omp-fresh-preflight # #87 C: the OMP fresh preflight reproduces omp_agent_dir and the tools.xdev read in TS (it runs from two emit depths and cannot call a sibling script). This drives the SHIPPED shell/python leaves over the same inputs — refusals included — and requires the TS half to agree, so the reproduction cannot silently drift from the installer's own oracle
@@ -143,7 +144,7 @@ Usage:
143
144
  ./run.sh check-codex-birth-hook # sandboxed payload + declaration: first-turn V3 birth, one record per thread, top-level event predicate, no markers
144
145
  ./run.sh check-codex-fresh-preflight # pre-mutation birth/MCP/thread-title/default-app-server readiness
145
146
  ./run.sh smoke-codex-config-state # sandboxed MCP + status-line ownership/install/doctor/inverse lifecycle
146
- ./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
147
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)
148
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
149
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
@@ -170,18 +171,26 @@ Usage:
170
171
  ./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
171
172
  ./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
172
173
  ./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-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
173
175
  ./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
174
176
  ./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
175
177
  ./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
176
178
  ./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
177
179
  ./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
178
- ./run.sh smoke-codex-fresh-live # #95 first-admission RELEASE MUST: record-backed receipt fixture -> PUBLIC initial Pi fresh/callback OUTSIDE the Codex home -> Pi PUBLIC omitted-placement Codex fresh/exact callback INSIDE exact existing `codex` -> Pi-to-Codex addressed native-push v2 -> Codex PUBLIC omitted-placement fresh Pi/exact callback IN that home -> Codex final v2 evidence. Needs LIVE=1 + ENTWURF_CODEX_APP_SERVER_PID + ENTWURF_CODEX_FRESH_MODEL + ENTWURF_CODEX_FRESH_PI_MODEL. The PID names the operator-owned app-server inside `codex`; 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
+ ./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
179
181
  ./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
180
182
  ./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
181
183
  ./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
182
184
  ./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
185
+ ./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
186
+ ./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
187
+ ./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
188
+ ./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
189
+ ./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
190
+ ./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
183
191
  ./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
184
192
  ./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
193
+ ./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
185
194
  ./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
186
195
  ./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)
187
196
  ./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
@@ -250,15 +259,20 @@ Usage:
250
259
  ./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)
251
260
  ./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.
252
261
  ./run.sh doctor-agy-bridge # fail-loud doctor: MCP config + exact permission rule + state + live probe label
253
- ./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
254
- ./run.sh uninstall-codex-birth # exact inverse from the install-state digest inventory; refuses drifted bytes instead of deleting them
255
- ./run.sh doctor-codex-birth # RUNTIME + UNIT bytes/ownership + the vendor TRUST receipt for this declaration (--unit-only skips the receipt; sandboxes only)
262
+ ./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
263
+ ./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
264
+ ./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)
256
265
  ./run.sh install-codex-mcp # own only [mcp_servers.entwurf-bridge] in the user's Codex config
257
266
  ./run.sh uninstall-codex-mcp # remove/restore only the recorded MCP atom
258
267
  ./run.sh doctor-codex-mcp # effective config + ownership + actual bridge boot
259
268
  ./run.sh install-codex-statusline # add thread-title to tui.status_line for visible garden ids
260
269
  ./run.sh uninstall-codex-statusline # remove only the recorded status-line atom
261
270
  ./run.sh doctor-codex-statusline # effective visible-identity config + ownership verdict
271
+ ./run.sh install-codex-terminal-title # add thread-id to tui.terminal_title so the multiplexer reports the caller's thread in #{pane_title} (#95 placement input, never an address)
272
+ ./run.sh uninstall-codex-terminal-title # remove only the recorded terminal-title atom
273
+ ./run.sh doctor-codex-terminal-title # effective caller-seat config + ownership verdict
274
+ ./run.sh codex-socket-path # #95 INTERNAL (the launcher asks this; you do not need it): print the Codex app-server default control-socket path for this environment, from the SAME resolver delivery and preflight read
275
+ ./run.sh codex-app-server [args...] # #95: the ONE COMMAND that starts the operator's Codex app-server. exec()s `codex app-server --listen "unix://$CODEX_HOME/app-server-control/app-server-control.sock"` in THIS terminal (cwd/pid/exit are the vendor's, Ctrl-C is yours) — entwurf owns the SPELLING of that address, never the process's lifecycle: no supervisor, no restart, no daemon, no pid file. It does no path arithmetic: the address comes from `codex-socket-path`, the one leaf every other Codex surface reads, and a resolved address that is not absolute or carries a control character is refused before anything is created. Your arguments are forwarded byte-identical after it; a second --listen is refused by name. Refuses on a live socket (names the /proc owner it can read) or an indeterminate one, and reports which tmux server this seat gives caller-seat lookups. Run it in a DETACHED tmux session other than the one you work in
262
276
  ./run.sh install-agy-statusline # own the agy statusLine subtree with bare entwurf-agy-statusline; preserve unrelated settings
263
277
  ./run.sh uninstall-agy-statusline # honest inverse from statusline install-state
264
278
  ./run.sh doctor-agy-statusline # fail-loud statusLine config/bin/state doctor + honest live SKIP
@@ -268,6 +282,7 @@ Usage:
268
282
  ./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])
269
283
  ./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)
270
284
  ./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])
285
+ ./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])
271
286
  ./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)
272
287
  ./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)
273
288
  ./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)
@@ -275,10 +290,13 @@ Usage:
275
290
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
276
291
  ./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
277
292
  ./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
278
- ./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)
293
+ ./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)
279
294
  ./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)
280
295
  ./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
281
296
  ./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json (project scope only; global user-scope citizen left intact)
297
+ ./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
298
+ ./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
299
+ ./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
282
300
  ./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)
283
301
  ./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
284
302
  ./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
@@ -604,6 +622,7 @@ _codex_home() { echo "${CODEX_HOME:-$HOME/.codex}"; }
604
622
  _codex_config() { echo "$(_codex_home)/config.toml"; }
605
623
  _codex_mcp_state() { echo "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/codex-mcp/install-state.json"; }
606
624
  _codex_statusline_state() { echo "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/codex-statusline/install-state.json"; }
625
+ _codex_terminal_title_state() { echo "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/codex-terminal-title/install-state.json"; }
607
626
 
608
627
  codex_mcp() {
609
628
  local verb="$1" config state
@@ -632,8 +651,32 @@ codex_statusline() {
632
651
  esac
633
652
  }
634
653
 
654
+ # #95 lane B: the SECOND visible-identity atom, and a different axis from the
655
+ # status line. `status_line`'s thread-title is what a HUMAN reads in the TUI;
656
+ # `terminal_title`'s thread-id is what the MULTIPLEXER reports back as
657
+ # `#{pane_title}`, which is the only value a Codex caller's `_meta.threadId`
658
+ # can be matched against to find the pane it is sitting in. Placement input
659
+ # only — never an address, delivery or liveness fact.
660
+ codex_terminal_title() {
661
+ local verb="$1" config state
662
+ config="$(_codex_config)"
663
+ state="$(_codex_terminal_title_state)"
664
+ case "$verb" in
665
+ install) python3 "$REPO_DIR/scripts/codex-terminal-title-config.py" install "$config" "$state" ;;
666
+ uninstall) python3 "$REPO_DIR/scripts/codex-terminal-title-config.py" uninstall "$state" ;;
667
+ doctor) python3 "$REPO_DIR/scripts/codex-terminal-title-config.py" doctor-static "$config" "$state" ;;
668
+ esac
669
+ }
670
+
671
+ # $1 (optional): a certified stable plugin runtime root. Given, the provider half is written in
672
+ # PLUGIN MODE — its managed command becomes the absolute bridge under that root instead of the bare
673
+ # bin. That is the whole difference, and it exists because a Herdr plugin activates on a host where
674
+ # nothing entwurf is on PATH, so a bare command cannot resolve. Absent, every byte is what it has
675
+ # always been (#116 M3-b2).
635
676
  register_user_scope_citizen() {
636
677
  local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
678
+ local plugin_mode=()
679
+ [ -n "${1:-}" ] && plugin_mode=(--plugin-runtime "$1")
637
680
  # Shared idempotent implementation (also driven by smoke-user-scope-citizen).
638
681
  # #86 C2: user scope carries a recorded OWNER (packageRoot in the pi-package
639
682
  # install-state). Another owner — live or missing — makes this a zero-write
@@ -646,7 +689,7 @@ register_user_scope_citizen() {
646
689
  python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
647
690
  --scope user --state "$(_pi_package_state)" --preflight >/dev/null
648
691
  python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" \
649
- --scope user --state "$(_pi_provider_state)" --preflight >/dev/null
692
+ --scope user --state "$(_pi_provider_state)" ${plugin_mode[@]+"${plugin_mode[@]}"} --preflight >/dev/null
650
693
  python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" \
651
694
  --scope user --state "$(_pi_package_state)"
652
695
  # #46 Task 2: own entwurfProvider.mcpServers.entwurf-bridge as the bare stable bin at USER scope
@@ -655,7 +698,33 @@ register_user_scope_citizen() {
655
698
  # local and covered by `run.sh remove` (no state) — deliberate, reasoned asymmetry.
656
699
  # #86 C2: the provider state records installerRoot, so the same foreign-owner refusal
657
700
  # holds here — and the package refusal above fires FIRST, before any provider write.
658
- python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$(_pi_provider_state)"
701
+ 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[@]}"}
702
+ }
703
+
704
+ # install-user-scope — the PLUGIN-ONLY forward seam (#116 M3-b2). Generic `install` writes both a
705
+ # project `.pi/settings.json` and the global user-scope citizen; a Herdr plugin activation has no
706
+ # project and must not invent one, so this verb composes the user-scope half and nothing else. Its
707
+ # inverse is the one that already existed, `remove-user-scope` — the asymmetry this closes is that
708
+ # the inverse was public and the forward was not.
709
+ install_user_scope() {
710
+ command -v python3 >/dev/null 2>&1 || { echo "[install-user-scope] requires python3 on PATH; nothing was written." >&2; exit 1; }
711
+ local plugin_runtime=""
712
+ while [ $# -gt 0 ]; do
713
+ case "$1" in
714
+ --plugin-runtime)
715
+ shift
716
+ # A flag with no value must not fall through to bare mode: the caller asked for the plugin
717
+ # runtime and would otherwise be told, silently, that it got it.
718
+ [ -n "${1:-}" ] || { echo "run.sh install-user-scope: --plugin-runtime requires a value" >&2; exit 2; }
719
+ plugin_runtime="$1" ;;
720
+ *) echo "usage: run.sh install-user-scope [--plugin-runtime <stable active root>]" >&2; exit 2 ;;
721
+ esac
722
+ shift
723
+ done
724
+ preflight_v3_store install-user-scope
725
+ section "install-user-scope: the GLOBAL pi citizen only (no project wiring)"
726
+ register_user_scope_citizen "$plugin_runtime"
727
+ ok "install-user-scope: user-scope packages[] + provider registered${plugin_runtime:+ (plugin runtime: $plugin_runtime)}"
659
728
  }
660
729
 
661
730
  # takeover-user-scope — the operator-explicit ownership move (#86 C2). The ONLY
@@ -1026,6 +1095,14 @@ check_copilot_launch() {
1026
1095
  run_ts scripts/check-copilot-launch.ts
1027
1096
  }
1028
1097
 
1098
+ check_codex_app_server_launch() {
1099
+ # #95: the managed app-server SPELLING, proved against a fake vendor. There is no second
1100
+ # spelling of the address to compare any more — the launcher asks `codex-socket-path` — so
1101
+ # these cells are a WIRING oracle, and they carry the hostile inputs a transcription was
1102
+ # measured to diverge on so that re-deriving the path here goes red rather than passing.
1103
+ run_ts scripts/check-codex-app-server-launch.ts
1104
+ }
1105
+
1029
1106
  check_copilot_receive_arm() {
1030
1107
  # #82 RAIL 5 gate: drives the REAL receiver installer into a temp extensions dir, then
1031
1108
  # forks the REAL extension.mjs with the SDK specifier resolved by a loader hook (the
@@ -1317,6 +1394,17 @@ check_mux_launcher_fence() {
1317
1394
  run_ts scripts/check-mux-launcher-fence.ts
1318
1395
  }
1319
1396
 
1397
+ check_typing_call_fence() {
1398
+ # The rule kept since day one, now kept by NAME: entwurf composes arguments and letters, never
1399
+ # keystrokes. An outside reader measured the tree on 2026-09-17 and found the law perfectly kept
1400
+ # and nothing keeping it — zero typing calls in pi-extensions/, zero checks and zero mutants that
1401
+ # would notice one arriving. Scans tracked production sources under pi-extensions/ and mcp/ with
1402
+ # comments blanked and string literals kept, because a typing call spelled as a string is the
1403
+ # call. Comment exemption is load-bearing: the sentence that states the rule contains the names
1404
+ # the rule forbids. Offline, no binaries, no fixtures on disk.
1405
+ run_ts scripts/check-typing-call-fence.ts
1406
+ }
1407
+
1320
1408
  check_mux_parent_artifact() {
1321
1409
  # Deterministic gate for the tracked scrubbed parent-transcript fixture
1322
1410
  # (scripts/fixtures/mux-parent-transcript.scrubbed.jsonl). The fixture is a version-pinned
@@ -1414,7 +1502,10 @@ check_mux_fresh_call() {
1414
1502
  # tmux-coordinate-row rides here because it is the parse every tmux COORDINATE in this lane's
1415
1503
  # LIVE siblings depends on, and because it is the only half of a measured LIVE failure that a
1416
1504
  # deterministic gate can own: the row is the input, so no tmux and no model turn is needed.
1417
- 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
1505
+ # codex-caller-seat rides here for the same reason as the two preflights: the leaf exists
1506
+ # only as freshCall's Codex placement input (#95 lane B), so certifying it apart from the
1507
+ # composition it feeds would let the two halves of one seat decision drift.
1508
+ 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
1418
1509
  }
1419
1510
 
1420
1511
  smoke_mux_fresh_call_live() {
@@ -1771,6 +1862,117 @@ check_meta_facts() {
1771
1862
  run_ts scripts/check-meta-facts.ts
1772
1863
  }
1773
1864
 
1865
+ check_herdr_plugin() {
1866
+ # Deterministic gate for the #116 M2-b herdr plugin (`plugins/herdr/`). Two halves.
1867
+ # STATIC: the manifest is parsed with the package's own TOML reader and held to the
1868
+ # shape herdr 0.9.0 accepts (min_herdr_version required + semver, dot-free pane id,
1869
+ # popup-only sizing, argv arrays), and the ABSENT sections are asserted as hard as the
1870
+ # present ones — [[startup]]/[[events]] are where a watcher would grow, [[actions]]
1871
+ # would send a report nobody can read into a 64 KiB log. ([[build]] LEFT that list in
1872
+ # #116 M3-b3; the one runner it may call is check-herdr-plugin-build's subject.)
1873
+ # BEHAVIOURAL: the REAL pane entry is
1874
+ # spawned with a stub `entwurf` and a stub HERDR_BIN_PATH that log every call, so
1875
+ # "exactly one read each per open" is COUNTED; success, skip-by-name, four distinct
1876
+ # named refusals, the ambiguous pane, diagnostics and the no-write claim are each
1877
+ # driven for real. The stub stands for the CONSUMER's input, never for herdr's
1878
+ # behaviour — no claim about herdr is made here, which is why this is not the fake
1879
+ # fixture check-herdr-sandbox forbids. No herdr binary, no Entwurf install, no writes
1880
+ # outside mkdtemp.
1881
+ run_ts scripts/check-herdr-plugin.ts
1882
+ }
1883
+
1884
+ check_herdr_activation() {
1885
+ # Deterministic gate for the #116 M3-b2 scoped activation. The pi cells drive the REAL writers
1886
+ # (run.sh install-user-scope, register-pi-provider.py, remove-user-scope) against a sandboxed
1887
+ # HOME/XDG/PI_CODING_AGENT_DIR, so the bytes asserted are bytes those writers produced. The Claude
1888
+ # cells drive the REAL state owner's pure surfaces; the vendor `claude` CLI is NOT invoked and its
1889
+ # acceptance of an absolute executable stays a named LIVE boundary. The oracle for a roundtrip is
1890
+ # JSON equality plus unrelated-key survival, never byte equality: the pi settings writer preserves
1891
+ # semantics but may reindent (measured 2026-09-16). No network, no operator HOME.
1892
+ run_ts scripts/check-herdr-activation.ts
1893
+ }
1894
+
1895
+ check_herdr_runtime_bootstrap() {
1896
+ # Deterministic gate for the #116 M3-b1 runtime transaction. Every root comes from XDG, so the
1897
+ # gate hands the module a mkdtemp HOME and asserts nothing appeared outside the two XDG roots it
1898
+ # was given. `acquire` is a FUNCTION seam: the fixture stands in for the REGISTRY, so this gate
1899
+ # proves the transaction (ownership certification, torn-swap recovery, leftover states, rollback,
1900
+ # idempotence, the inverse) but NOT that the real Entwurf package boots — that actual-package
1901
+ # proof is a named cell inside check-pack-install, which runs this module's own
1902
+ # verifyInstalledRuntime against the tarball it packed and installed. npm is pinned here the only
1903
+ # way a hermetic gate honestly can: the production pack/install argv and the npm environment are
1904
+ # pure functions and are asserted directly. No network, no npm, no operator HOME, no writes
1905
+ # outside mkdtemp.
1906
+ run_ts scripts/check-herdr-runtime-bootstrap.ts
1907
+ }
1908
+
1909
+ check_herdr_plugin_profile() {
1910
+ # Deterministic gate for the #116 M3-a pure leaf: the function that turns ONE
1911
+ # `herdr integration status` listing into this plugin's activation plan. Every listing
1912
+ # is an INPUT STRING (verbatim herdr 0.9.0 rows measured 2026-09-16 in a sandboxed
1913
+ # HOME/XDG, plus the two forms cross-checked in herdr source at c77af189), so nothing
1914
+ # here claims a fact about herdr. Pins: the closed atom->backend table, exactly one row
1915
+ # per selected atom or a NAMED refusal (never a first/last-match guess), the state read
1916
+ # FRONT-ANCHORED so an operator's directory name cannot spell a verdict, the display
1917
+ # path leaving no trace in the plan, outdated/needs-repair as named FAILs (measured:
1918
+ # empty, unreadable and marker-less files all collapse into `outdated`), not-installed
1919
+ # as a zero-write SKIP, `current` as herdr's admission with no second floor invented
1920
+ # here, OpenCode observed but never planned, and the leaf's purity. No herdr binary, no
1921
+ # Entwurf install, no environment read, no writes.
1922
+ run_ts scripts/check-herdr-plugin-profile.ts
1923
+ }
1924
+
1925
+ check_herdr_plugin_build() {
1926
+ # Deterministic gate for the #116 M3-b3 build runner (`plugins/herdr/lib/build.mjs`) — the ONE
1927
+ # command Herdr's `[[build]]` calls, and the order it composes: integration status -> pure
1928
+ # profile -> runtime bootstrap -> activation THROUGH THE INSTALLED PACKAGE. There is no fake
1929
+ # herdr binary here (the repo forbids one); the listing arrives through the runner's own spawn
1930
+ # seam as the prose shape 0.9.0 emits, and one cell drives the REAL binary with a named SKIP
1931
+ # when the host has none (ENTWURF_REQUIRE_HERDR=1 makes absence red). Pins: exactly one
1932
+ # [[build]] whose argv is `node lib/build.mjs` and a runner the npm package does NOT ship;
1933
+ # missing herdr vs unanswerable status as separate named refusals with zero writes; a selected
1934
+ # atom that is outdated/needs-repair/malformed/duplicated failing BEFORE any runtime work;
1935
+ # A-empty as a clean exit 0 that writes nothing (no runtime, no wiring, no removal); the
1936
+ # activation entry resolved under the stable runtime with absent/incapable as distinct named
1937
+ # refusals and no checkout fallback; the two-stage {pi} -> {pi, claude-code} composition where a
1938
+ # new commit reinstalls and OpenCode is never named; and the post-build Herdr commit gap being a
1939
+ # NAMED gap, not an atomic transaction. The real `herdr plugin install` + real `npm pack
1940
+ # git+https://...#<sha>` journey needs the network and lives in smoke-herdr-plugin-build-live.
1941
+ run_ts scripts/check-herdr-plugin-build.ts
1942
+ }
1943
+
1944
+ smoke_herdr_plugin_build_live() {
1945
+ # #116 M3-b3 LIVE: a REAL `herdr plugin install` drives this plugin's REAL `[[build]]`, which
1946
+ # packs the FIXED product git spec for real. Needs LIVE=1, herdr/git/npm, and the NETWORK (a
1947
+ # git-spec pack builds the bridge through `prepare`, which installs devDependencies) — which is
1948
+ # the axis check-herdr-plugin-build cannot have and does not claim. Hermetic by two substitutions
1949
+ # only: sandbox HOME/XDG, and git `insteadOf` redirecting the unchanged product remote to a local
1950
+ # bare clone of this worktree, so an unpushed candidate is testable without weakening the argv.
1951
+ # Cells 1/2/4 are the real-Herdr journey: remote-commit available, reinstall widening to
1952
+ # {pi, claude-code} with OpenCode zero-write and ONE COUNTED vendor MCP owner entry, and the
1953
+ # post-build gap where herdr's own commit fails after our runner already rebound the activation.
1954
+ # Cell 3 is deliberately NOT a journey: it calls the shipped runtime leaf directly with the fixed
1955
+ # product argv at a commit the remote lacks, so it is acquisition-leaf evidence — named refusal,
1956
+ # active runtime/ledger/harness bytes preserved, journal left as the certified retry authority.
1957
+ run_ts scripts/smoke-herdr-plugin-build-live.ts
1958
+ }
1959
+
1960
+ check_peer_facts() {
1961
+ # Deterministic gate for the #116 M2-a observed-peer projection. Drives the REAL
1962
+ # scripts/peer-facts.ts as a subprocess with every ambient root replaced (store,
1963
+ # ENTWURF_DIR, HOME, XDG_DATA_HOME, pi agent dir), so the live store, sockets and
1964
+ # markers are never read. Owns only what this verb ADDS on top of the provider:
1965
+ # placement crosses as the structured tagged union rather than the human string,
1966
+ # the peer keyset is exactly the provider's facts (no herdr agent_status may enter
1967
+ # an entwurf payload — that is the door observed activity would use to become
1968
+ # delivery liveness) and no socket coordinate is published at all, diagnostics ride
1969
+ # in-band, the probed socket world is the one ENTWURF_DIR names (proved by a
1970
+ # record-less socket surfacing — there is no field to echo), no observation budget
1971
+ # rations a machine projection, exit contract 0/2/3,
1972
+ # plus dispatch + compiled-twin reachability. No herdr binary, no model turn.
1973
+ run_ts scripts/check-peer-facts.ts
1974
+ }
1975
+
1774
1976
  check_entwurf_fact_provider() {
1775
1977
  # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4b): the
1776
1978
  # ASSEMBLY layer listEntwurfFacts. listAllMetaIdentities → scanSocketProbes →
@@ -1785,6 +1987,94 @@ check_entwurf_fact_provider() {
1785
1987
  run_ts scripts/check-entwurf-fact-provider.ts
1786
1988
  }
1787
1989
 
1990
+ check_herdr_placement() {
1991
+ # Deterministic gate for #116 S1: the placement evidence axis
1992
+ # (pi-extensions/lib/herdr-placement.ts) plus the ONE read the fact provider is
1993
+ # allowed. Pins only what is decidable WITHOUT a herdr binary — payload parse, the
1994
+ # two measured join rules, the ambiguity rule, the four-word vocabulary, the render,
1995
+ # the once-per-listing read, and the STRUCTURAL absence of placement from every
1996
+ # entwurf_v2 dispatch module. There is no fake herdr here for the same reason
1997
+ # check-mux-placement refuses a fake tmux: a stand-in authors the contract before the
1998
+ # real thing is measured. Every payload literal is verbatim herdr 0.9.0 output
1999
+ # recorded 2026-09-14. The real-binary half is an isolated private herdr server
2000
+ # (sandbox HOME/XDG, no client attach); the join/callback round trip stays LIVE.
2001
+ run_ts scripts/check-herdr-placement.ts
2002
+ }
2003
+
2004
+ check_herdr_fresh_call() {
2005
+ # Deterministic gate for #116 S2-c1: the herdr LAUNCH rail core
2006
+ # (pi-extensions/lib/herdr-fresh-call.ts). Pins only what is decidable WITHOUT a herdr
2007
+ # binary — the import fence that keeps the two rails separately deletable, the closed
2008
+ # pi|claude-code pilot, the option-G one-line JSON encoding (exact round trip, zero
2009
+ # Unicode Cc including the C1 block JSON.stringify leaves literal, max public task),
2010
+ # the pre-mutation refusals with the CLI never invoked, the ONE placement policy (a new
2011
+ # tab in the caller's own workspace, read from herdr's own answer about the caller's pane
2012
+ # and never from a pane id's shape, focus left alone), tab-create/start/get/close argv
2013
+ # grammar, strict response parsing, the opaque pane id, the receipt's missing address,
2014
+ # and the conditional-close matrix. No fake herdr executable and no fake server, for the
2015
+ # reason check-mux-placement refuses a fake tmux. Payload literals are verbatim herdr
2016
+ # 0.9.0 output (pane/tab replies 2026-09-15, agent_started 2026-09-14); the real-binary
2017
+ # half is an isolated private server (c2).
2018
+ run_ts scripts/check-herdr-fresh-call.ts
2019
+ }
2020
+
2021
+ check_herdr_sandbox() {
2022
+ # REAL herdr acceptance for #116 C2a: a PRIVATE herdr server in a fully sandboxed
2023
+ # HOME/XDG/PI_CODING_AGENT_DIR creates a tab with the production placement argv, installs
2024
+ # herdr's own pi integration, and starts a BLANK pi in that tab's initial pane from ONE
2025
+ # checkout package registration. Asserts the official witness
2026
+ # triple, the strict path->native id join, exactly one sandbox V3 record, a control socket
2027
+ # that answers the production probe, and the conditional-close decision in both directions
2028
+ # (refuses a pane holding an agent, reclaims one we own and with it the tab it was the
2029
+ # whole of). No fake herdr, no model turn, no
2030
+ # credentials, no keystrokes; the fixture's `pi --approve` is a one-run authorisation that
2031
+ # writes no trust state and never enters production argv. herdr is an OPTIONAL rail: an
2032
+ # absent binary prints a named SKIP and exits 0, a PRESENT-but-incomplete rail FAILS, and
2033
+ # ENTWURF_REQUIRE_HERDR=1 turns absence itself into a failure (CI admission is C2b).
2034
+ run_ts scripts/check-herdr-sandbox.ts
2035
+ }
2036
+
2037
+ check_herdr_supply() {
2038
+ # Deterministic supply-authority gate for #116 C2b: WHICH herdr bytes CI runs, and who
2039
+ # decides. No herdr binary and no network — it reads scripts/fixtures/herdr-supply.json,
2040
+ # scripts/install-herdr-ci.sh and .github/workflows/ci.yml as text. Pins the manifest as the
2041
+ # only copy of version/tag/digest, the digest shape, the fail-closed architecture map, the
2042
+ # verify-before-chmod order, the absence of curl-pipe/latest/package-manager/global install,
2043
+ # the absence of a `gh attestation verify` dependency (measured: gh 2.97.0 cannot close
2044
+ # herdr's in-toto release predicate), the CI install-before-floor order with
2045
+ # ENTWURF_REQUIRE_HERDR=1, Linux-job-only wiring, that no product script or bin reaches the
2046
+ # installer (Hard Rule 17), and that the x86_64 asset stays recorded as never executed.
2047
+ run_ts scripts/check-herdr-supply.ts
2048
+ }
2049
+
2050
+ check_fresh_call_dispatch() {
2051
+ # Deterministic gate for #116 C3: the seam ABOVE the two rails. Pins that the rail is a
2052
+ # capability fact (exactly HERDR_ENV=1) and never a caller parameter, that neither direction
2053
+ # falls back, that a non-pilot backend refuses inside herdr before any preflight or mutation,
2054
+ # that the Codex preflight keeps its pre-existing ordering on the tmux path only, that one
2055
+ # nonce per call reaches the selected rail, that BOTH public surfaces reach one composition
2056
+ # root (no second copy of rail choice, preflight ordering or rendering), that pi still uses
2057
+ # its lazy dynamic import, and that herdr rendering shows view-not-address plus which recovery
2058
+ # happened. No tmux and no herdr binary: the seam runs with an injected spawn.
2059
+ run_ts scripts/check-fresh-call-dispatch.ts
2060
+ }
2061
+
2062
+ smoke_herdr_fresh_call_live() {
2063
+ # LIVE acceptance for the #116 herdr rail: a REAL caller that is itself inside a herdr pane
2064
+ # invokes the REAL PUBLIC entwurf_fresh_call, and a REAL child's first model action is the
2065
+ # nonce callback. Opt-in (LIVE=1), four model turns, one private herdr server PER CELL —
2066
+ # herdr's socket follows XDG_CONFIG_HOME and each cell is fenced into its own.
2067
+ # `[측정 2026-09-14]` BOTH runtimes keep the operator's REAL HOME and real auth roots: a pi
2068
+ # given a fixture HOME dies before readiness (an installed extension resolves its npm
2069
+ # dependency through $HOME) and a claude given one enters first-run onboarding. Only
2070
+ # Entwurf-OWNED writes are fenced into the fixture (XDG roots, the meta roots, the v2 lock
2071
+ # dir). Evidence is source receipts only, in two named grades: pi from its own vendor
2072
+ # transcript, claude from the runtime's own entwurf-bridge MCP activity log joined by exact
2073
+ # sessionId plus entwurf's mailbox artifact and hook journal — never screen text, never a
2074
+ # keystroke. The operator's herdr panes are snapshotted before and after.
2075
+ run_ts scripts/smoke-herdr-fresh-call-live.ts
2076
+ }
2077
+
1788
2078
  check_entwurf_peers_surface() {
1789
2079
  # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4c; #50 C4
1790
2080
  # re-author): the MCP entwurf_peers RENDER/PAYLOAD layer renderEntwurfPeers.
@@ -3100,6 +3390,10 @@ check_pack() {
3100
3390
  # #65 — the owner-normalized store projection consumers call INSTEAD of
3101
3391
  # parsing the store; installed hosts are exactly where those consumers live.
3102
3392
  "mcp/entwurf-bridge/dist/scripts/meta-facts.js"
3393
+ # #116 M2-a — the owner-normalized PEER projection, for the same reason one step
3394
+ # out: the placement join it emits is the copy a herdr-side consumer would
3395
+ # otherwise fork, and such a consumer runs against an installed host.
3396
+ "mcp/entwurf-bridge/dist/scripts/peer-facts.js"
3103
3397
  # #87 Bundle B — omp-receive-doctor drives this operator projection through
3104
3398
  # run.sh, so installed node_modules must have the compiled twin too.
3105
3399
  "mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js"
@@ -3395,6 +3689,8 @@ _check_pack_install_impl() {
3395
3689
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js"
3396
3690
  # #65 — the owner-normalized store projection (see check-pack).
3397
3691
  "mcp/entwurf-bridge/dist/scripts/meta-facts.js"
3692
+ # #116 M2-a — the owner-normalized peer projection (see check-pack).
3693
+ "mcp/entwurf-bridge/dist/scripts/peer-facts.js"
3398
3694
  # #87 Bundle B — omp-receive-doctor reaches this through the installed dispatcher.
3399
3695
  "mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js"
3400
3696
  # 0.12.5 — node_modules-safe plugin hook + lib (see check-pack). The installed
@@ -3582,6 +3878,60 @@ _check_pack_install_impl() {
3582
3878
  }
3583
3879
  echo "[check-pack-install] installed: $probe"
3584
3880
 
3881
+ # #116 M3-b1/b2 — THE ACTUAL-PACKAGE RUNTIME PROOF, THROUGH THE SHIPPED OWNER.
3882
+ # The Herdr plugin's runtime verifier decides whether a tree npm placed is a runtime the scoped
3883
+ # wiring may name: exact name@version, the compiled entry, all three required bins present AND
3884
+ # executable, and a real `entwurf check-bridge`. Its own focused gate drives that verifier against
3885
+ # a FIXTURE package, which proves the transaction but not this package — so the real artifact is
3886
+ # verified HERE, against the tarball this gate already packed and installed. No second pack, no
3887
+ # network.
3888
+ #
3889
+ # It imports from the INSTALLED tree's own scripts/herdr-runtime.mjs, not from the checkout. That
3890
+ # is the point: Herdr's `plugin uninstall` deletes the plugin checkout and calls no cleanup hook,
3891
+ # so the code that retires a runtime has to be IN the package. Reaching into $REPO_DIR here would
3892
+ # prove the checkout works and say nothing about what a clean host receives.
3893
+ local runtime_proof
3894
+ runtime_proof=$(cd "$tmp" && node --input-type=module -e "
3895
+ const pkg = process.cwd() + '/node_modules/@junghanacs/entwurf';
3896
+ const { verifyInstalledRuntime, readCheckoutPackageSpec, REQUIRED_BINS } =
3897
+ await import(pkg + '/scripts/herdr-runtime.mjs');
3898
+ const spec = readCheckoutPackageSpec(pkg);
3899
+ const seen = verifyInstalledRuntime(process.cwd(), spec);
3900
+ console.log(seen.name + '@' + seen.version + ' verified through the SHIPPED owner (' + REQUIRED_BINS.join(', ') + ')');
3901
+ " 2>&1) || {
3902
+ fail "[check-pack-install] #116 runtime verifier REFUSED the actual installed package (or does not ship):"
3903
+ echo "$runtime_proof" | sed 's/^/ /' >&2
3904
+ return 1
3905
+ }
3906
+ echo "[check-pack-install] herdr-plugin runtime verifier: $runtime_proof"
3907
+
3908
+ # #116 M3-b2 — THE DEACTIVATE VERB MUST ACTUALLY TEAR DOWN, WITH NO CHECKOUT. Herdr's `plugin
3909
+ # uninstall` deletes the plugin checkout and calls no cleanup hook, so the teardown entry has to
3910
+ # be IN the package AND has to survive deleting the runtime it is executing from. So this does not
3911
+ # settle for the "nothing to undo" verdict: it stages the real shape — the installed package
3912
+ # sitting AT the stable active root, a certified runtime journal, and a certified activation
3913
+ # ledger whose components are already removed — then runs THAT copy's own entry with a throwaway
3914
+ # HOME/XDG and no reference to this repo. What it proves is the self-delete: the process removes
3915
+ # the tree it is executing from and still finishes retiring the ledger.
3916
+ local deact_home deact_active deact_pkg deact_proof deact_rc runtime_left ledger_left
3917
+ deact_home=$(mktemp -d -t entwurf-deact.XXXXXX)
3918
+ deact_active="$deact_home/.data/entwurf/herdr-plugin/runtime/active"
3919
+ mkdir -p "$deact_active" "$deact_home/.state/entwurf/herdr-plugin"
3920
+ cp -R "$tmp/node_modules" "$deact_active/node_modules"
3921
+ deact_pkg="$deact_active/node_modules/@junghanacs/entwurf"
3922
+ 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"
3923
+ 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)
3924
+ deact_rc=$?
3925
+ runtime_left=$([ -e "$deact_home/.data/entwurf/herdr-plugin/runtime" ] && echo yes || echo no)
3926
+ ledger_left=$([ -e "$deact_home/.state/entwurf/herdr-plugin/activation.json" ] && echo yes || echo no)
3927
+ rm -rf "$deact_home"
3928
+ if [ "$deact_rc" -ne 0 ] || [ "$runtime_left" != "no" ] || [ "$ledger_left" != "no" ]; then
3929
+ 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):"
3930
+ echo "$deact_proof" | sed 's/^/ /' >&2
3931
+ return 1
3932
+ fi
3933
+ echo "[check-pack-install] installed herdr-plugin-deactivate removed its OWN runtime and retired the ledger with no checkout: $deact_proof"
3934
+
3585
3935
  # Pi package loader smoke — actual `pi` reads the manifest and
3586
3936
  # registers the provider. rc=0 + the curated model list in the
3587
3937
  # output means pi accepted the package as a real extension, not
@@ -4498,6 +4848,57 @@ JS
4498
4848
  fi
4499
4849
  echo "[check-pack-install] installed 'entwurf copilot' reached the vendor with the scan flag and the managed argv"
4500
4850
 
4851
+ # #95 — the managed app-server SPELLING, from the INSTALLED package. Same packaging risk as
4852
+ # the copilot cell above and the same reason a file-list assertion cannot stand in for it:
4853
+ # `scripts/codex-app-server-launch.sh` has to be in the tarball AND resolve its siblings
4854
+ # correctly once it lives under node_modules. What makes this cell worth its seconds is the
4855
+ # ADDRESS: this verb exists so a consumer never types that socket path, so the one thing an
4856
+ # installed run must prove is that the path it hands the vendor is the one the product's own
4857
+ # resolver computes — read here from the INSTALLED tree, never from this checkout.
4858
+ local cx_launch_home="$npm_tmp/codex-appserver-home" cx_launch_bin="$npm_tmp/codex-appserver-bin"
4859
+ mkdir -p "$cx_launch_home" "$cx_launch_bin"
4860
+ {
4861
+ printf '#!/usr/bin/env bash\n'
4862
+ printf 'for a in "$@"; do printf "ARG<%%s>\\n" "$a"; done\n'
4863
+ } > "$cx_launch_bin/codex"
4864
+ chmod +x "$cx_launch_bin/codex"
4865
+ # The expectation is read from the INSTALLED COMPILED resolver, not from this checkout and
4866
+ # not transcribed here: compiled JS is what an installed consumer surface reaches (Hard Rule
4867
+ # 11), so this asks the packed tree itself what address it believes in.
4868
+ local cx_resolver="$npm_pkg/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js" cx_want
4869
+ if [ ! -f "$cx_resolver" ]; then
4870
+ fail "[check-pack-install] the tarball carries no compiled Codex socket resolver at $cx_resolver"
4871
+ return 1
4872
+ fi
4873
+ if ! cx_want=$(CODEX_HOME="$cx_launch_home/.codex" node --input-type=module -e "
4874
+ import { resolveCodexDefaultSocketPath } from '$cx_resolver';
4875
+ process.stdout.write(resolveCodexDefaultSocketPath(process.env));
4876
+ " 2>&1); then
4877
+ fail "[check-pack-install] the INSTALLED compiled socket resolver would not load:"
4878
+ echo "$cx_want" | tail -4 | sed 's/^/ /' >&2
4879
+ return 1
4880
+ fi
4881
+ if ! op_out=$(env -u ENTWURF_CODEX_APP_SERVER_ACTIVE -u TMUX \
4882
+ HOME="$cx_launch_home" CODEX_HOME="$cx_launch_home/.codex" PATH="$cx_launch_bin:$PATH" \
4883
+ "$installed_entwurf" codex-app-server --config probe=1 2>&1); then
4884
+ fail "[check-pack-install] installed 'entwurf codex-app-server' FAILED to reach the vendor:"
4885
+ echo "$op_out" | tail -8 | sed 's/^/ /' >&2
4886
+ return 1
4887
+ fi
4888
+ for want in "ARG<app-server>" "ARG<--listen>" "ARG<unix://$cx_want>" "ARG<--config>" "ARG<probe=1>"; do
4889
+ if ! printf '%s' "$op_out" | grep -qF "$want"; then
4890
+ fail "[check-pack-install] installed 'entwurf codex-app-server' did not hand the vendor the resolved address and argv (missing $want):"
4891
+ echo "$op_out" | tail -8 | sed 's/^/ /' >&2
4892
+ return 1
4893
+ fi
4894
+ done
4895
+ if printf '%s' "$op_out" | grep -qF 'ARG<codex-app-server>'; then
4896
+ fail "[check-pack-install] installed 'entwurf codex-app-server' leaked the dispatcher verb into the vendor's argv:"
4897
+ echo "$op_out" | tail -8 | sed 's/^/ /' >&2
4898
+ return 1
4899
+ fi
4900
+ echo "[check-pack-install] installed 'entwurf codex-app-server' reached the vendor at the INSTALLED resolver's address ($cx_want)"
4901
+
4501
4902
  # The generation verb must reach its verdict from under node_modules. On the
4502
4903
  # sandbox 0-record agent dir it archives nothing and opens a fresh generation.
4503
4904
  # The exit code alone can't tell a verdict from a fence crash, so read the line.
@@ -5313,6 +5714,12 @@ setup_all() {
5313
5714
  else
5314
5715
  setup_result codex-statusline FAIL "detected codex, but the visible-identity setting did not complete (see above)"
5315
5716
  fi
5717
+ codex_rc=0; codex_terminal_title install || codex_rc=$?
5718
+ if [ "$codex_rc" -eq 0 ]; then
5719
+ setup_harness_result codex-terminal-title "thread-id terminal title enabled (caller-seat placement input)" "./run.sh doctor-codex-terminal-title"
5720
+ else
5721
+ setup_result codex-terminal-title FAIL "detected codex, but the terminal-title setting did not complete (see above)"
5722
+ fi
5316
5723
  fi
5317
5724
  # ── core bridge boundary ── deterministic preflight lives in `pnpm run
5318
5725
  # check:full`; live substrate acceptance lives in `LIVE=1 ./run.sh
@@ -5992,6 +6399,9 @@ case "$cmd" in
5992
6399
  check-copilot-statusline)
5993
6400
  check_copilot_statusline
5994
6401
  ;;
6402
+ check-codex-app-server-launch)
6403
+ check_codex_app_server_launch
6404
+ ;;
5995
6405
  check-copilot-launch)
5996
6406
  check_copilot_launch
5997
6407
  ;;
@@ -6082,6 +6492,9 @@ case "$cmd" in
6082
6492
  check-mux-launcher-fence)
6083
6493
  check_mux_launcher_fence
6084
6494
  ;;
6495
+ check-typing-call-fence)
6496
+ check_typing_call_fence
6497
+ ;;
6085
6498
  check-entwurf-v2-visible-resume)
6086
6499
  check_entwurf_v2_visible_resume
6087
6500
  ;;
@@ -6169,9 +6582,48 @@ case "$cmd" in
6169
6582
  check-meta-facts)
6170
6583
  check_meta_facts
6171
6584
  ;;
6585
+ check-herdr-activation)
6586
+ check_herdr_activation
6587
+ ;;
6588
+ check-herdr-runtime-bootstrap)
6589
+ check_herdr_runtime_bootstrap
6590
+ ;;
6591
+ check-herdr-plugin-profile)
6592
+ check_herdr_plugin_profile
6593
+ ;;
6594
+ check-herdr-plugin-build)
6595
+ check_herdr_plugin_build
6596
+ ;;
6597
+ smoke-herdr-plugin-build-live)
6598
+ smoke_herdr_plugin_build_live
6599
+ ;;
6600
+ check-peer-facts)
6601
+ check_peer_facts
6602
+ ;;
6603
+ check-herdr-plugin)
6604
+ check_herdr_plugin
6605
+ ;;
6172
6606
  check-entwurf-fact-provider)
6173
6607
  check_entwurf_fact_provider
6174
6608
  ;;
6609
+ check-herdr-placement)
6610
+ check_herdr_placement
6611
+ ;;
6612
+ check-herdr-fresh-call)
6613
+ check_herdr_fresh_call
6614
+ ;;
6615
+ check-herdr-sandbox)
6616
+ check_herdr_sandbox
6617
+ ;;
6618
+ check-herdr-supply)
6619
+ check_herdr_supply
6620
+ ;;
6621
+ check-fresh-call-dispatch)
6622
+ check_fresh_call_dispatch
6623
+ ;;
6624
+ smoke-herdr-fresh-call-live)
6625
+ smoke_herdr_fresh_call_live
6626
+ ;;
6175
6627
  check-entwurf-peers-surface)
6176
6628
  check_entwurf_peers_surface
6177
6629
  ;;
@@ -6616,6 +7068,35 @@ case "$cmd" in
6616
7068
  doctor-codex-statusline)
6617
7069
  codex_statusline doctor
6618
7070
  ;;
7071
+ install-codex-terminal-title)
7072
+ codex_terminal_title install
7073
+ ;;
7074
+ uninstall-codex-terminal-title)
7075
+ codex_terminal_title uninstall
7076
+ ;;
7077
+ doctor-codex-terminal-title)
7078
+ codex_terminal_title doctor
7079
+ ;;
7080
+ codex-socket-path)
7081
+ # #95 INTERNAL: print the Codex app-server default control-socket path for this
7082
+ # environment. It exists because `entwurf codex-app-server` is bash and cannot import
7083
+ # `resolveCodexDefaultSocketPath`; re-deriving that path in bash was MEASURED to diverge
7084
+ # (BOM-only CODEX_HOME, path normalization), so the launcher asks instead of transcribing.
7085
+ # `run_ts` is the crossing (compiled twin when installed, strip-types in a clone), which is
7086
+ # the whole reason this sits in the dispatcher rather than inside the leaf.
7087
+ shift || true
7088
+ run_ts scripts/codex-socket-path.ts "$@"
7089
+ ;;
7090
+ codex-app-server)
7091
+ # #95: the managed app-server SPELLING. Same shape and same reasons as the `copilot`
7092
+ # branch below — `exec`, no subshell and no cd, because this operates on the operator's
7093
+ # own session rather than on the repo, and the `shift` keeps the dispatcher verb out of
7094
+ # the vendor's argv. It is NOT the same KIND of thing: `entwurf copilot` becomes a
7095
+ # sibling-bearing CLI, while this becomes the long-lived server those siblings' MCP
7096
+ # children hang off. Neither one supervises what it becomes.
7097
+ shift || true
7098
+ exec bash "$REPO_DIR/scripts/codex-app-server-launch.sh" "$@"
7099
+ ;;
6619
7100
  copilot)
6620
7101
  # #82 RAIL 7: the managed launch. `exec` and NO subshell/cd on purpose — the vendor
6621
7102
  # must inherit this terminal exactly: the caller's cwd, this pid, this tty, and its own
@@ -6802,6 +7283,22 @@ case "$cmd" in
6802
7283
  shift || true
6803
7284
  run_ts scripts/meta-facts.ts "$@"
6804
7285
  ;;
7286
+ peer-facts)
7287
+ # #116 M2-a READ-ONLY projection of the OBSERVED peer listing: one provider
7288
+ # (`listEntwurfFacts`) through one renderer (`renderEntwurfPeers().payload`), so
7289
+ # the payload SHAPE is the entwurf_peers payload shape and the join is never
7290
+ # re-implemented — the pi half of that join is a measured vendor floor, and a
7291
+ # forked copy decays silently. It is NOT the same bytes as an entwurf_peers call:
7292
+ # that surface rations observation and returns text, this one is unbounded because
7293
+ # a rationed row says `unobserved` and a machine consumer cannot tell that from
7294
+ # "nobody could look". `placement` stays the structured tagged union. No socket
7295
+ # coordinate is published (#50 C4 retired the legacy listing with the controlDir it
7296
+ # exposed; ENTWURF_DIR only selects what is probed) and herdr's own
7297
+ # agent_status/screen verdicts are absent (docs/herdr-launch-rail.md §9). No
7298
+ # writes, no watcher, no dispatch. Same exit contract as meta-facts.
7299
+ shift || true
7300
+ run_ts scripts/peer-facts.ts "$@"
7301
+ ;;
6805
7302
  meta-bridge-managed-keys)
6806
7303
  # 0.10.0 meta-bridge: emit the SSOT of settings.json/~/.claude.json keys that
6807
7304
  # entwurf's install OWNS. Consumers (agent-config fragment, future
@@ -6960,6 +7457,21 @@ case "$cmd" in
6960
7457
  remove)
6961
7458
  remove_local_package "$TARGET_PROJECT_DIR"
6962
7459
  ;;
7460
+ herdr-plugin-activate)
7461
+ # #116 M3-b2 public forward verb. Takes a subset of {pi, claude-code} and NOTHING else — the
7462
+ # stable runtime root is derived, never supplied. Plain `node` in both modes: shipped .mjs.
7463
+ shift
7464
+ (cd "$REPO_DIR" && node "$REPO_DIR/scripts/herdr-plugin-activate.mjs" "$@")
7465
+ ;;
7466
+ herdr-plugin-deactivate)
7467
+ # #116 M3-b2 public verb. Plain `node` in BOTH modes on purpose: this is shipped .mjs, not TS,
7468
+ # because it has to run from an installed package after Herdr deleted the plugin checkout.
7469
+ (cd "$REPO_DIR" && node "$REPO_DIR/scripts/herdr-plugin-deactivate.mjs" "$@")
7470
+ ;;
7471
+ install-user-scope)
7472
+ shift
7473
+ install_user_scope "$@"
7474
+ ;;
6963
7475
  remove-user-scope)
6964
7476
  remove_user_scope_citizen
6965
7477
  ;;