@junghanacs/entwurf 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/AGENTS.md +4 -1
  2. package/BASELINE.md +1 -0
  3. package/CHANGELOG.md +514 -0
  4. package/DELIVERY.md +10 -2
  5. package/README.md +43 -4
  6. package/VERIFY.md +47 -0
  7. package/docs/acp-backend-rail.md +96 -17
  8. package/docs/setup-clean-host.md +11 -1
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +41 -41
  29. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  30. package/package.json +9 -8
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  33. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  34. package/pi-extensions/lib/acp/backend.ts +4 -4
  35. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  36. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  37. package/pi-extensions/lib/codex-declaration.js +612 -0
  38. package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
  39. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  40. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  41. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  42. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  43. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  44. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  45. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  46. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  47. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  48. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  49. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  50. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  51. package/pi-extensions/lib/herdr-placement.ts +276 -0
  52. package/pi-extensions/lib/mux-fresh-call.ts +95 -301
  53. package/run.sh +588 -204
  54. package/scripts/check-acp-usage-accounting.ts +9 -9
  55. package/scripts/check-codex-birth-hook.ts +187 -0
  56. package/scripts/check-entwurf-control-rpc.ts +138 -0
  57. package/scripts/check-entwurf-fact-provider.ts +4 -2
  58. package/scripts/check-entwurf-peers-surface.ts +3 -1
  59. package/scripts/check-entwurf-self-address.ts +68 -0
  60. package/scripts/check-fresh-call-dispatch.ts +301 -0
  61. package/scripts/check-gate-qualification.ts +17 -4
  62. package/scripts/check-herdr-activation.ts +1293 -0
  63. package/scripts/check-herdr-fresh-call.ts +1250 -0
  64. package/scripts/check-herdr-placement.ts +383 -0
  65. package/scripts/check-herdr-plugin-build.ts +842 -0
  66. package/scripts/check-herdr-plugin-profile.ts +371 -0
  67. package/scripts/check-herdr-plugin.ts +523 -0
  68. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  69. package/scripts/check-herdr-sandbox.ts +456 -0
  70. package/scripts/check-herdr-supply.ts +184 -0
  71. package/scripts/check-mux-launcher-fence.ts +138 -0
  72. package/scripts/check-peer-facts.ts +331 -0
  73. package/scripts/check-pi-launch.ts +358 -0
  74. package/scripts/check-release-gate-outcomes.ts +19 -0
  75. package/scripts/check-typing-call-fence.ts +296 -0
  76. package/scripts/codex-birth-doctor.sh +159 -16
  77. package/scripts/codex-birth-install.sh +178 -72
  78. package/scripts/codex-birth-uninstall.sh +162 -7
  79. package/scripts/fixtures/herdr-supply.json +29 -0
  80. package/scripts/herdr-activation.mjs +536 -0
  81. package/scripts/herdr-plugin-activate.mjs +270 -0
  82. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  83. package/scripts/herdr-runtime.mjs +1203 -0
  84. package/scripts/install-herdr-ci.sh +96 -0
  85. package/scripts/inventory-verification-surface.ts +86 -10
  86. package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
  87. package/scripts/lib/tree-digest.ts +97 -0
  88. package/scripts/meta-bridge-install.sh +19 -2
  89. package/scripts/meta-bridge-state.py +167 -11
  90. package/scripts/mutants/codex-caller-seat.json +6 -8
  91. package/scripts/mutants/codex-native.json +195 -29
  92. package/scripts/mutants/control-socket-disconnect.json +57 -0
  93. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  94. package/scripts/mutants/herdr-activation.json +305 -0
  95. package/scripts/mutants/herdr-fresh-call.json +448 -0
  96. package/scripts/mutants/herdr-placement.json +161 -0
  97. package/scripts/mutants/herdr-plugin-build.json +160 -0
  98. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  99. package/scripts/mutants/herdr-plugin.json +133 -0
  100. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  101. package/scripts/mutants/herdr-supply.json +133 -0
  102. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  103. package/scripts/mutants/mux-fresh-call.json +104 -17
  104. package/scripts/mutants/mux-launcher-fence.json +13 -0
  105. package/scripts/mutants/omp-fresh.json +4 -4
  106. package/scripts/mutants/peer-facts.json +98 -0
  107. package/scripts/mutants/pi-launch.json +113 -0
  108. package/scripts/mutants/resume-args.json +6 -6
  109. package/scripts/mutants/self-address.json +33 -0
  110. package/scripts/mutants/typing-call-fence.json +17 -0
  111. package/scripts/mutants/v2-surface.json +2 -2
  112. package/scripts/mutants/v2-visible-resume.json +17 -17
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  117. package/scripts/smoke-codex-birth.sh +281 -12
  118. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  119. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  120. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  121. package/scripts/smoke-herdr-raw-install-live.sh +849 -0
  122. package/scripts/tsconfig.json +5 -0
  123. package/scripts/check-entwurf-facts.ts +0 -385
  124. package/scripts/check-entwurf-resume-args.ts +0 -122
  125. package/scripts/check-entwurf-v2-decider.ts +0 -657
  126. package/scripts/check-entwurf-v2-lock.ts +0 -371
  127. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  128. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  129. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  130. package/scripts/check-entwurf-v2-release.ts +0 -151
  131. package/scripts/check-entwurf-v2-runner.ts +0 -448
  132. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  133. package/scripts/check-entwurf-v2-send.ts +0 -458
  134. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  135. package/scripts/check-socket-discovery.ts +0 -401
package/README.md CHANGED
@@ -78,6 +78,19 @@ A few words that look unusual for a coding tool.
78
78
 
79
79
  ## Install
80
80
 
81
+ **Two installation routes, and the one you want depends on where you work.**
82
+
83
+ - **Direct** — `npm install -g @junghanacs/entwurf`, then `entwurf setup <project>`. This is the
84
+ general route: it composes every harness it FINDS on the host — pi, Claude Code, Codex, Copilot,
85
+ Antigravity, the ACP rail — and reports each one PASS / SKIP / FAIL. Use it in an ordinary shell,
86
+ in tmux, anywhere.
87
+ - **Herdr workbench** — `herdr plugin install` from inside a Herdr session. It activates Entwurf for
88
+ the pi and Claude Code sessions Herdr has integrated, and only those. Contract, evidence grades
89
+ and its refusals: [plugins/herdr/README.md](plugins/herdr/README.md).
90
+
91
+ Neither route installs a harness, a subscription, or a login (Hard Rule 17). Everything below this
92
+ paragraph describes the direct route.
93
+
81
94
  **Platform evidence, in one breath.** The Entwurf-only install surface on
82
95
  macOS is CERTIFIED (CI) (`macos-install-surface`). On macOS every
83
96
  garden-native harness rail, marker/request join, ACP turn, and mux is NOT CERTIFIED —
@@ -308,7 +321,7 @@ repair surface: each unit has its own installer, doctor, and inverse.
308
321
  - **Antigravity / agy** (Linux CERTIFIED; macOS NOT CERTIFIED — pending physical host) — `install-agy-bridge`, `install-agy-statusline`, `install-agy-hooks`, each with a matching `doctor-agy-*`.
309
322
  - **GitHub Copilot CLI** (Linux CERTIFIED; macOS NOT CERTIFIED — pending physical host) — four independent units, four independent failure modes: `install-copilot-bridge` (birth: garden id + who-sent, on the first prompt), `install-copilot-mcp` (the entwurf tool hand, where `entwurf_inbox_read` lives), `install-copilot-receive` (the receiver extension: doorbell + receiver marker), `install-copilot-statusline` (optional for a manual citizen, required for supported fresh) — each with a matching `doctor-copilot-*` and `uninstall-copilot-*`.
310
323
  - **OMP (`omp`)** (Linux CERTIFIED; macOS NOT CERTIFIED — pending physical host) — four units, in-process extensions rather than launchers: `install-omp-bridge` (birth: the `mode === "tui"` visible host, its garden id on the status line, and who-sent), `install-omp-mcp` (the omp-native `entwurf-bridge` entry), `install-omp-config` (the one operator setting `tools: xdev: false`, without which the vendor mounts MCP tools as `xd://` devices the model cannot call), `install-omp-receive` (the receiver extension: mailbox watch + announce-only doorbell) — each with a matching `uninstall-omp-*`, and a `doctor-omp-*` for all but the setting, whose runtime axis `doctor-omp-mcp` owns. The setting writer owns exactly the lines it adds and refuses an explicit operator `tools: xdev: true` by name rather than overwriting it.
311
- - **OpenAI Codex CLI** (supported in 0.21.0 on Linux; macOS NOT CERTIFIED — pending physical host) — `install-codex-birth` publishes the `SessionStart` declaration into `$CODEX_HOME/hooks.json` with its launcher closure under `$XDG_DATA_HOME/entwurf/codex-birth`, all operator-owned; the vendor trust receipt for that declaration is the operator's single answer, and `doctor-codex-birth` reports it as its own axis (present, or red with the exact instruction — never computed, never written); `install-codex-mcp` owns `[mcp_servers.entwurf-bridge]`, including the `env_vars` boundary for `CODEX_HOME`, Entwurf garden/control roots, and the app-server's `TMUX`/`TMUX_PANE`; `install-codex-statusline` owns `thread-title` in `tui.status_line`; `install-codex-terminal-title` owns `thread-id` in `tui.terminal_title`. Each has a matching doctor and inverse. **One Codex precondition is the vendor's own and no installer supplies it: the directory a sibling starts in must already be answered in this Codex.** A direct consent decision is recorded per exact directory on this rail — not the parent, not the git root — and a directory the vendor has no answer for at all opens a consent screen instead of running a first turn, so `entwurf_fresh_call` prints `codex-launch-cwd-undecided` and opens the window anyway; that screen is self-repairing, and answering it once teaches the vendor the directory for good. **Nothing here refuses a launch** — the note is a diagnostic, and the one caller that treats it as a precondition is `smoke-codex-fresh-live`, where nobody is at the keyboard. Two neighbouring cases read differently: a directory answered `untrusted` is not noted at all (the vendor skips its screen on this rail, so that turn starts), and a directory INSIDE an explicitly untrusted project is noted as `codex-launch-cwd-untrusted-ancestor` because the vendor answers that one with an error naming the repository root rather than a prompt. The check is a narrow local read of the operator's own config, never the vendor's verdict: the vendor also consents through project layers it reads from its app-server, and everything the check cannot see stays silent. The operator owns the app-server and seats it wherever they like; Entwurf never creates or supervises it. Omitted fresh placement follows the CALLER, never the backend being opened: an explicit `placement` always wins, then a Codex CALLER opens beside its own TUI pane — located by the `thread-id` that pane's terminal title carries, with 0 or 2+ matching panes refused and no fallback — then the caller's own session. (#95 first shipped a fixed `codex` home for omitted-placement Codex TARGETS; #95 D1 retired it on 2026-09-16 once the anchor made the room unnecessary.) A pane title is a placement input only, never an address, liveness or delivery fact. Exact 0.153.4 source's missing per-client carrier still bounds placing a sibling beside a TUI whose thread nobody named as unsupported.
324
+ - **OpenAI Codex CLI** (supported in 0.21.0 on Linux; macOS NOT CERTIFIED — pending physical host) — `install-codex-birth` publishes the `SessionStart` declaration into `$CODEX_HOME/hooks.json` with its launcher closure under `$XDG_DATA_HOME/entwurf/codex-birth`, all operator-owned; it owns **that declaration, not the file** — another integration's `SessionStart` group coexists (the vendor keys trust per `<path>:<event>:<group>:<handler>`), so install appends, uninstall splices only ours out, foreign bytes survive both, and `doctor-codex-birth` reports neighbours in a FOREIGN section it certifies nothing in; the vendor trust receipt is read at the index our declaration was measured at, is the operator's single answer, and `doctor-codex-birth` reports it as its own axis (present, or red with the exact instruction — never computed, never written); `install-codex-mcp` owns `[mcp_servers.entwurf-bridge]`, including the `env_vars` boundary for `CODEX_HOME`, Entwurf garden/control roots, and the app-server's `TMUX`/`TMUX_PANE`; `install-codex-statusline` owns `thread-title` in `tui.status_line`; `install-codex-terminal-title` owns `thread-id` in `tui.terminal_title`. Each has a matching doctor and inverse. **One Codex precondition is the vendor's own and no installer supplies it: the directory a sibling starts in must already be answered in this Codex.** A direct consent decision is recorded per exact directory on this rail — not the parent, not the git root — and a directory the vendor has no answer for at all opens a consent screen instead of running a first turn, so `entwurf_fresh_call` prints `codex-launch-cwd-undecided` and opens the window anyway; that screen is self-repairing, and answering it once teaches the vendor the directory for good. **Nothing here refuses a launch** — the note is a diagnostic, and the one caller that treats it as a precondition is `smoke-codex-fresh-live`, where nobody is at the keyboard. Two neighbouring cases read differently: a directory answered `untrusted` is not noted at all (the vendor skips its screen on this rail, so that turn starts), and a directory INSIDE an explicitly untrusted project is noted as `codex-launch-cwd-untrusted-ancestor` because the vendor answers that one with an error naming the repository root rather than a prompt. The check is a narrow local read of the operator's own config, never the vendor's verdict: the vendor also consents through project layers it reads from its app-server, and everything the check cannot see stays silent. The operator owns the app-server and seats it wherever they like; Entwurf never creates or supervises it. Omitted fresh placement follows the CALLER, never the backend being opened: an explicit `placement` always wins, then a Codex CALLER opens beside its own TUI pane — located by the `thread-id` that pane's terminal title carries, with 0 or 2+ matching panes refused and no fallback — then the caller's own session. (#95 first shipped a fixed `codex` home for omitted-placement Codex TARGETS; #95 D1 retired it on 2026-09-16 once the anchor made the room unnecessary.) A pane title is a placement input only, never an address, liveness or delivery fact. Exact 0.153.4 source's missing per-client carrier still bounds placing a sibling beside a TUI whose thread nobody named as unsupported.
312
325
 
313
326
  Run them as `entwurf <command>`. Which unit a doctor's refusal names, and the clean-host
314
327
  walk-through for each harness, live in [docs/setup-clean-host.md](./docs/setup-clean-host.md).
@@ -522,7 +535,7 @@ pnpm run check:full # full deterministic floor (adds the her
522
535
  ./run.sh check-agy-sender-identity # record-backed pid/start-key sender identity
523
536
  ./run.sh check-codex-native-push # deterministic Codex UDS probe + no-replay queue contract
524
537
  ./run.sh smoke-codex-config-state # Codex MCP/status-line install, doctor, and inverse
525
- ./run.sh check-codex-birth-hook # sandboxed payload + declaration: one V3 per top-level thread, the envelope's own axes, no markers
538
+ ./run.sh check-codex-birth-hook # sandboxed payload + declaration: one V3 per top-level thread, the envelope's own axes, no markers, and the declaration leaf (normalized digest, selection by launcher command, byte-preserving splice)
526
539
 
527
540
  # source-maintainer only — qualification snapshots the git work surface, and both
528
541
  # commands are source-contract gates rather than installed operator checks:
@@ -623,7 +636,7 @@ A two-pane recording covers the pre-0.12 v1 surface end-to-end — sibling resum
623
636
 
624
637
  Live peer messaging carries a sender envelope `{ sessionId, agentId, cwd, timestamp, origin?, replyable? }`; `entwurf_self` returns that authoritative envelope for the current pi session or trusted meta-session. Plain external MCP hosts are non-replyable. A garden-native meta-session carries a trusted `meta-session` envelope, but **`replyable` is a fact its own rail decides, not a consequence of being trusted** — a self-fetch citizen needs a live armed receiver, a native-push citizen needs an alive adapter probe, and a pi session needs its control socket. `entwurf_self` also reports which rail a meta-session reply would ride, because a native-push citizen has no mailbox to name. `wants_reply` is an etiquette marker rendered as a `(wants reply)` badge — not a transport contract, no wait, no polling. **v2 never gates on it:** a `wants_reply` from an external/non-replyable caller is passed through and surfaced honestly beside that sender's `replyable: false`, not rejected — the decider routes on target + intent, never on sender replyability. (The retired v1 `entwurf_send` did reject it; that behaviour went with the verb.)
625
638
 
626
- In ACP-backed and external native-harness sessions, `entwurf-bridge` exposes seven tools: `entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_self`, `entwurf_inbox_read`, and the explicit/manual `entwurf_register_native` fallback. Native pi exposes the shared capability directly through the extension surface (`entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call` tools; the socket-scan `/entwurf-sessions` command is gone — #50 C4). **For garden-id delivery/reply use `entwurf_v2`** — the canonical surface that classifies the target and routes to live-pi / self-fetch meta-mailbox (Claude Code, Copilot, OMP) / native-push (Antigravity, Codex), and refuses a dormant target instead of waking it. **To open a sibling that does not exist yet use `entwurf_fresh_call`** — it launches one of five fixed backends (`pi`, `claude-code`, `copilot`, `omp`, `codex`) as a visible window on the operator's own tmux server, requires an explicit model, accepts one optional literal absolute `cwd` (omit it or pass `""` to use the caller's cwd) and one optional `placement.tmuxSession` naming an EXISTING session on that server to open the sibling in instead of the caller's own (the operator's project seat — nothing is ever created: an absent session is `tmux-session-missing` and a name outside the addressed grammar `[A-Za-z0-9][A-Za-z0-9_-]*` is `tmux-session-name-invalid`, with no window and no session left behind. The two inputs never infer each other, and the positive form of that matters more than the prohibition: **with `cwd` omitted, a seated sibling starts in THIS agent's own working directory**, measured — it does not inherit the target session's path or its active pane, so naming the `org` seat does not put the sibling in the `org` project. Pass `cwd` when you want a different directory. entwurf never resolves that path: it hands the literal string to tmux, so with a SYMLINKED directory the receipt echoes what you asked for while the record, the transcript path, the callback envelope and `pane_current_path` all show the path the OS resolved it to — two spellings of one place, and the resolved one is what `entwurf_resume_call` later reopens), passes the model and selected directory through the runtime's visible launch path, and hands it a first task; a `copilot` launch goes through entwurf's own managed invocation and is refused before any window opens if this host lacks the Copilot birth, MCP, receiver or visible-footer units, and an `omp` launch carries its task in the `--entwurf-bootstrap` payload instead of an argv prompt because that vendor connects its MCP tools after the session has already started. Omitted placement follows the CALLER rather than the backend being opened: a Codex CALLER opens its sibling beside its own TUI pane, located by the `thread-id` that pane's terminal title carries (0 or 2+ matching panes reject with no fallback); every other caller opens in its own seat. (0.21.0 shipped a backend-selected default instead — an omitted-placement `codex` TARGET resolved a fixed operator-owned session named `codex` — and #95 D1 retired it on 2026-09-16.) Any of the five can still be sent to an operator-owned existing seat with an explicit `placement.tmuxSession`, and named `claude`/`pi` seats remain available that way, but they are not omitted-placement defaults. Read tmux here as the VISIBLE LAUNCH SEAT, never as an address: the garden id is the address, and it arrives in the callback envelope. Sibling symmetry means each backend keeps its own transcript, auth, native tools and visibility — not that the transports are made identical. The sibling's first action is a nonce callback whose sender envelope carries its garden id. The launch receipt records the requested model/cwd/seat plus tmux coordinates — including the RESOLVED target session id, which is where the window actually is — and never claims that the runtime accepted them or completed delivery. (The v1 verbs `entwurf` / `entwurf_resume` / `entwurf_send` are gone.) Garden-native operator commands require `--entwurf-control`. There is no spawn target allowlist — the target registry is gone (#50 C3): `entwurf_v2` and `entwurf_resume_call` address an existing record-backed citizen, while `entwurf_fresh_call` takes its explicit backend/model/task and optional cwd directly rather than resolving a model tuple from a registry file. **To reopen a DORMANT pi citizen use `entwurf_resume_call {target}`** — the record supplies transcript, model, provider and cwd, so it takes no prompt, no task and no model override; it runs no turn, returns a LAUNCH receipt and a separate OBSERVATION receipt (only the second says the citizen is back), refuses a non-pi target as `target-not-pi`, and on an unobserved socket leaves the visible window open and releases its lock rather than retrying.
639
+ In ACP-backed and external native-harness sessions, `entwurf-bridge` exposes seven tools: `entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_self`, `entwurf_inbox_read`, and the explicit/manual `entwurf_register_native` fallback. Native pi exposes the shared capability directly through the extension surface (`entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call` tools; the socket-scan `/entwurf-sessions` command is gone — #50 C4). **For garden-id delivery/reply use `entwurf_v2`** — the canonical surface that classifies the target and routes to live-pi / self-fetch meta-mailbox (Claude Code, Copilot, OMP) / native-push (Antigravity, Codex), and refuses a dormant target instead of waking it. **To open a sibling that does not exist yet use `entwurf_fresh_call`** — WHERE it opens is a fact about the calling process, not a parameter: inside herdr (`HERDR_ENV=1`) it opens a NEW TAB in the caller's own workspace, without taking focus, and admits only `pi` and `claude-code`, refusing `placement` by name; everywhere else it launches one of five fixed backends (`pi`, `claude-code`, `copilot`, `omp`, `codex`) as a visible window on the operator's own tmux server. There is no fallback between the two — an incomplete herdr context is refused by its herdr reason rather than quietly opening a tmux window nobody inside herdr can see. Everything that follows in this paragraph describes the tmux rail; the herdr rail's coordinates, refusals and reclaim are owned by [docs/herdr-launch-rail.md](./docs/herdr-launch-rail.md). On tmux it requires an explicit model, accepts one optional literal absolute `cwd` (omit it or pass `""` to use the caller's cwd) and one optional `placement.tmuxSession` naming an EXISTING session on that server to open the sibling in instead of the caller's own (the operator's project seat — nothing is ever created: an absent session is `tmux-session-missing` and a name outside the addressed grammar `[A-Za-z0-9][A-Za-z0-9_-]*` is `tmux-session-name-invalid`, with no window and no session left behind. The two inputs never infer each other, and the positive form of that matters more than the prohibition: **with `cwd` omitted, a seated sibling starts in THIS agent's own working directory**, measured — it does not inherit the target session's path or its active pane, so naming the `org` seat does not put the sibling in the `org` project. Pass `cwd` when you want a different directory. entwurf never resolves that path: it hands the literal string to tmux, so with a SYMLINKED directory the receipt echoes what you asked for while the record, the transcript path, the callback envelope and `pane_current_path` all show the path the OS resolved it to — two spellings of one place, and the resolved one is what `entwurf_resume_call` later reopens), passes the model and selected directory through the runtime's visible launch path, and hands it a first task; a `copilot` launch goes through entwurf's own managed invocation and is refused before any window opens if this host lacks the Copilot birth, MCP, receiver or visible-footer units, and an `omp` launch carries its task in the `--entwurf-bootstrap` payload instead of an argv prompt because that vendor connects its MCP tools after the session has already started. Omitted placement follows the CALLER rather than the backend being opened: a Codex CALLER opens its sibling beside its own TUI pane, located by the `thread-id` that pane's terminal title carries (0 or 2+ matching panes reject with no fallback); every other caller opens in its own seat. (0.21.0 shipped a backend-selected default instead — an omitted-placement `codex` TARGET resolved a fixed operator-owned session named `codex` — and #95 D1 retired it on 2026-09-16.) Any of the five can still be sent to an operator-owned existing seat with an explicit `placement.tmuxSession`, and named `claude`/`pi` seats remain available that way, but they are not omitted-placement defaults. Read tmux here as the VISIBLE LAUNCH SEAT, never as an address: the garden id is the address, and it arrives in the callback envelope. Sibling symmetry means each backend keeps its own transcript, auth, native tools and visibility — not that the transports are made identical. The sibling's first action is a nonce callback whose sender envelope carries its garden id. The launch receipt records the requested model/cwd/seat plus tmux coordinates — including the RESOLVED target session id, which is where the window actually is — and never claims that the runtime accepted them or completed delivery. (The v1 verbs `entwurf` / `entwurf_resume` / `entwurf_send` are gone.) Garden-native operator commands require `--entwurf-control`. There is no spawn target allowlist — the target registry is gone (#50 C3): `entwurf_v2` and `entwurf_resume_call` address an existing record-backed citizen, while `entwurf_fresh_call` takes its explicit backend/model/task and optional cwd directly rather than resolving a model tuple from a registry file. **To reopen a DORMANT pi citizen use `entwurf_resume_call {target}`** — the record supplies transcript, model, provider and cwd, so it takes no prompt, no task and no model override; it runs no turn, returns a LAUNCH receipt and a separate OBSERVATION receipt (only the second says the citizen is back), refuses a non-pi target as `target-not-pi`, and on an unobserved socket leaves the visible window open and releases its lock rather than retrying.
627
640
 
628
641
  ### `entwurf_v2` — canonical dispatch verb
629
642
 
@@ -642,7 +655,7 @@ In ACP-backed and external native-harness sessions, `entwurf-bridge` exposes sev
642
655
 
643
656
  **`entwurf_v2` is the canonical surface for garden-id delivery.** When you have a garden id and want to reach whoever it names — message, reply, or hand-off — `entwurf_v2` is the one surface that reads whether the target is live pi, dormant pi, mailbox-backed Claude Code, Copilot or OMP, or native-push Antigravity or Codex, and routes correctly; *when unsure which transport, use `entwurf_v2`*. This prevents callers from guessing a rail from the shape of an id.
644
657
 
645
- What v2 provides is a **deterministic dispatch substrate** that moves the "which transport?" decision out of the fallible caller/model and into the decider, with transport-appropriate locking and an honest reject (no `✓ delivered`, no `.msg` garbage) when a target cannot receive. It still does **not** mint siblings, and it does not relaunch one either: every row above either reaches a citizen that is already running or refuses. Reopening a dormant pi citizen is `entwurf_resume_call`, a separate lifecycle verb that never routes through this decider. Fresh creation is the separate `entwurf_fresh_call` verb. It opens one of five fixed runtimes — Pi, Claude Code, Copilot, OMP or Codex — visibly on the caller's tmux server. Omitting placement follows the CALLER rather than the backend being opened: an explicit seat always wins; then a Codex CALLER opens beside its own TUI pane, found by the `thread-id` in that pane's terminal title (0 or 2+ matching panes reject, with no fallback); then the caller's own session. An explicit `placement.tmuxSession` sends ANY of the five to an existing named session instead (see the `entwurf_fresh_call` paragraph above for the owning detail). Each launch carries a required explicit model passed in that runtime's measured CLI dialect and one optional literal absolute `cwd`; omitted or `""` means the caller's cwd. Copilot opens through `entwurf copilot`, never the bare vendor; OMP is the opposite — the bare `omp` runtime with no positional prompt at all, because that vendor connects its MCP tools after the session has started, so the task rides a two-stage `--entwurf-bootstrap` payload the installed birth extension releases once the callback tool has actually answered. It returns only a synchronous launch receipt and lets the sibling report its new address asynchronously through the sender envelope of a nonce callback. Use this cwd input for a new cross-repository sibling; do not resume a dormant citizen as a cwd substitute. The meta-mailbox row requires an **active** self-fetch receiver; native-push requires a record-backed, probe-alive native conversation and never borrows mailbox state. The [mux launch lane](./docs/mux-launch-rail.md) owns placement, fixed-runtime launch, and the two narrow compositions above it (fresh-call and resume-call placement); delivery does not import launch, and mux is not a delivery transport.
658
+ What v2 provides is a **deterministic dispatch substrate** that moves the "which transport?" decision out of the fallible caller/model and into the decider, with transport-appropriate locking and an honest reject (no `✓ delivered`, no `.msg` garbage) when a target cannot receive. It still does **not** mint siblings, and it does not relaunch one either: every row above either reaches a citizen that is already running or refuses. Reopening a dormant pi citizen is `entwurf_resume_call`, a separate lifecycle verb that never routes through this decider. Fresh creation is the separate `entwurf_fresh_call` verb. Inside herdr it opens a Pi or Claude Code sibling in a new tab in the caller's own workspace; everywhere else it opens one of five fixed runtimes — Pi, Claude Code, Copilot, OMP or Codex — visibly on the caller's tmux server. The rest of this paragraph is the tmux rail. Omitting placement follows the CALLER rather than the backend being opened: an explicit seat always wins; then a Codex CALLER opens beside its own TUI pane, found by the `thread-id` in that pane's terminal title (0 or 2+ matching panes reject, with no fallback); then the caller's own session. An explicit `placement.tmuxSession` sends ANY of the five to an existing named session instead (see the `entwurf_fresh_call` paragraph above for the owning detail). Each launch carries a required explicit model passed in that runtime's measured CLI dialect and one optional literal absolute `cwd`; omitted or `""` means the caller's cwd. Copilot opens through `entwurf copilot`, never the bare vendor; OMP is the opposite — the bare `omp` runtime with no positional prompt at all, because that vendor connects its MCP tools after the session has started, so the task rides a two-stage `--entwurf-bootstrap` payload the installed birth extension releases once the callback tool has actually answered. It returns only a synchronous launch receipt and lets the sibling report its new address asynchronously through the sender envelope of a nonce callback. Use this cwd input for a new cross-repository sibling; do not resume a dormant citizen as a cwd substitute. The meta-mailbox row requires an **active** self-fetch receiver; native-push requires a record-backed, probe-alive native conversation and never borrows mailbox state. The [mux launch lane](./docs/mux-launch-rail.md) owns placement, fixed-runtime launch, and the two narrow compositions above it (fresh-call and resume-call placement); delivery does not import launch, and mux is not a delivery transport.
646
659
 
647
660
  A live pi target is *reached* over its control socket, but the socket is dispatch-internal transport, never identity (#50 C4). A control socket that no meta-record claims — a pre-record-era resident, an unreadable store, or a stale/planted file — is refused for **every** intent as `record-less-socket`, and the reject names the fix (restart the resident so `session_start` births its record, or quiesce and run the fresh-cut). `entwurf_peers` reports the same state as an aggregated `record-less-socket` diagnostic rather than a peer row.
648
661
 
@@ -656,6 +669,32 @@ A `--entwurf-control` session needs **no special launcher** (#50 C2): pi mints i
656
669
  pi --entwurf-control
657
670
  ```
658
671
 
672
+ **Opening pi inside herdr.** herdr starts a supported agent with its canonical executable and no
673
+ extra argv — measured on herdr 0.9.0, `herdr agent start <name> --kind pi --pane <id>` runs plain
674
+ `pi`. Citizenship is argv-gated (`pi-extensions/entwurf-control.ts`), so a pi opened that way is a
675
+ running pi and *not* a garden citizen: no record, no control socket, no garden id. Pass the flag
676
+ through herdr's own argv passthrough:
677
+
678
+ ```bash
679
+ herdr agent start <name> --kind pi --pane <pane-id> -- --entwurf-control
680
+ ```
681
+
682
+ A sibling that entwurf itself opens already carries the flag; this is only for a pi you start from
683
+ herdr by hand. The claude axis needs nothing extra — its record is minted by the meta-bridge plugin's
684
+ own `SessionStart` hook, which entwurf installs and herdr does not touch. Where such a citizen is
685
+ visible is reported as evidence in `entwurf_peers` (`placement=herdr <pane>`), never as an address:
686
+ [`docs/mux-launch-rail.md`](./docs/mux-launch-rail.md) §7-a.
687
+
688
+ The wiring those two axes need does not have to be installed by hand on a Herdr host: the Entwurf
689
+ Herdr plugin's install-time build reads Herdr's own `integration status`, and for every harness in
690
+ `{pi, claude-code}` that Herdr reports as integrated it acquires an Entwurf runtime at a stable
691
+ Entwurf-owned address and registers the user-scope wiring against it — no Entwurf clone, no manual
692
+ `npm install`. `herdr plugin install junghan0611/entwurf/plugins/herdr` is the whole command, every
693
+ reinstall is the refresh trigger, and `entwurf herdr-plugin-deactivate` is the explicit inverse
694
+ (Herdr's own uninstall removes only its checkout and calls no cleanup hook).
695
+ [`plugins/herdr/README.md`](./plugins/herdr/README.md) owns that lane, including which acquisition
696
+ source the committed lock names.
697
+
659
698
  **Resuming an existing garden session.** Use `entwurf_resume_call {target}`. It reopens a DORMANT pi citizen under the SAME garden id in a visible window in the caller's own tmux session, resolving the transcript, model, provider and cwd from the record — so it takes only the target id, and it runs no turn: the window comes back with the conversation and waits, and talking to it is still `entwurf_v2`. Two receipts arrive and mean different things: a LAUNCH receipt (tmux made a window and was asked to start pi) and an OBSERVATION receipt (the control socket answered under the same id, or `resume-unobserved`). Unobserved is a real outcome, not an error to retry — the window is visible, so read it. A citizen that is already live is refused; so is a non-pi target (`target-not-pi`), because only pi stands a control socket up. The predecessor, `entwurf_v2 intent=owned-outcome`, resumed by launching a hidden window-less background child and was withdrawn under the visible-first rule; delivery still starts no process. Identity preconditions live in `resume-launch-identity.ts`, gated by `check-resume-launch-identity`.
660
699
 
661
700
  **Starting a new session in-process — pi's own `/new`.** Since the #50 C2 cut there is nothing to replace it with: `/new`, `/fork`, `/clone` and RPC session replacement are pi's again. The replacement session fires `session_start`, which upserts its own meta-record and rebinds the control socket to that record's garden id; the old socket is dropped. pi's session id (a uuidv7) is recorded as the citizen's `nativeSessionId` and is never an address. Gate: `run.sh smoke-resident-garden-guard` REPLACEMENT section (0-token RPC E2E).
package/VERIFY.md CHANGED
@@ -44,6 +44,45 @@ When you write a new entry, mark its rung. "L1 only" is honest; "L2 reached" is
44
44
 
45
45
  Verification here is not a benchmark. In production we exchange short turns and stop immediately to isolate a cause before resuming when something looks off. This document records **verification intent (what we look at) and pass criteria (how to judge)**; the execution shape is the agent's choice as long as the criteria are met.
46
46
 
47
+ ### Where a verification lives — two locations, and what decides
48
+
49
+ A new contract's test goes BESIDE the behaviour it certifies: `pi-extensions/**/*.test.ts` or
50
+ `plugins/herdr/**/*.test.mjs`. Nothing names those files. `check-tests-beside-behavior` expands
51
+ the two globs every run and hands the resulting paths to `run_vitest`, so landing a test edits no
52
+ list — and a failure is attributed from the JSON test TITLE (`run.sh:104-113`), never from the
53
+ code frame, which quotes an adjacent passing test's own title.
54
+
55
+ `scripts/check-*` is not a legacy pile; it is the home of what a vitest lane cannot hold. What
56
+ stays there, and why: LIVE lanes that need real accounts, models, tmux or a Docker daemon;
57
+ install and pack gates that consume a tarball; gates that spawn a subprocess or stand a server up;
58
+ gates that read product source as text; and the mutant EXECUTION COORDINATES described below. A
59
+ new hand-built gate needs a reason one of those five applies.
60
+
61
+ **Four rules bind a migrated gate that carries mutants**, and each of them was measured rather
62
+ than assumed (#119 V3 slice 2):
63
+
64
+ 1. **Every QK claim is its own `it` title.** Attribution reads the failed test title, so a QK
65
+ that lives only in an assertion message leaves its mutant unattributable.
66
+ 2. **The token appears exactly once in the file.** Putting it in the title AND the assertion
67
+ label is refused by the manifest set-integrity contract — the title keeps the token, the
68
+ assertion keeps the sentence.
69
+ 3. **The `run.sh` case survives as a mutant execution coordinate, and keeps its `check:contracts`
70
+ entry.** The case is not a discovery path: the door already finds the test. It exists because
71
+ the qualification runner re-invokes a gate's argv once per mutant, and a gate argv calling
72
+ vitest directly would skip the reporter attribution depends on. Its file filter is narrow,
73
+ because a mutant pointed at the glob shim would re-run every beside-behaviour test once per
74
+ mutant. It stays in `check:contracts` because `check-release-gate-outcomes` requires a gate a
75
+ mutant names to run inside `check:full` — reachable only through qualification, its going red
76
+ on a clean tree would be noticed by nothing but the mutant body.
77
+ 4. **Attribution is containment, not exclusivity.** `assert.ok` aborted at the first broken claim,
78
+ so exactly one label was ever reported; vitest runs every test, so one mutation surfaces
79
+ several failed titles (measured: 1–15). A kill is attributed when the claim's token is AMONG
80
+ them.
81
+
82
+ Migration moves lines BETWEEN the two axes `inventory-verification-surface.ts` counts, and only a
83
+ drop in the COMBINED number is subtraction. That is why the inventory reads both locations: a gate
84
+ that moved out of `scripts/` and into a file beside its subject would otherwise read as deleted.
85
+
47
86
  ### The canonical floor — two entry points
48
87
 
49
88
  - **Deterministic floor — two tiers (#70):** `pnpm check` is the everyday CORE — toolchain (lint + typecheck), the vitest lanes, and the pure-unit / behavioral-contract / source-topology gates plus the cheap static coherence checks. It prints its own total wall time; acceptance is ≤60s on the reference host `oracle` (an operator measurement, never a hard wall-clock gate on arbitrary hosts). `pnpm run check:full` is the FULL deterministic floor — the core plus the hermetic-integration and package/install tiers — and is what the frozen-candidate protocol, push CI, release-gate, and `prepublishOnly` run. Exact membership is the named `check:*` group scripts in `package.json` (the executable SSOT — this document records meaning and principles, not the command list); a gate changes tier by semantic-class decision, never by getting faster or slower. The FULL tier carries the qualification HEAD (`check-gate-manifests`, through `check:hermetic`); the everyday core does not, and neither tier carries the separately scheduled mutant-executing body (`check-gate-qualification`).
@@ -54,6 +93,14 @@ Verification here is not a benchmark. In production we exchange short turns and
54
93
  - **Every MUST step is invoked and reports its own outcome.** Prerequisite-bearing LIVE MUST steps ride the P1 STEP OUTCOME protocol (`scripts/lib/step-outcome.sh`): exit 0 = PASS, exit 97 = SKIP (a prerequisite the step does not have, printed as an `[entwurf:skip]` line), anything else = FAIL. The two non-skippable static steps (`pnpm run check:full`, `check-gate-qualification`) are inline: 0 = PASS, nonzero = FAIL, no SKIP arm — they have no prerequisite to decline. A skip is never counted as a pass — that hole is what let a cortex-less host read as cortex acceptance. Without `--cut` this is the unattended diagnostic: SKIPs are reported and the run still exits 0. **`--cut` makes it acceptance and any MUST SKIP is red**, which is how "a real cut needs `LIVE=1` with `SKIP=0`" stopped being prose. A green MUST gate is **necessary, not sufficient** — GLG authorizes the cut.
55
94
  - **When cost-bearing MUST gates run (fixed 2026-07-23, the F6/F7 lesson):** a commit that touches a rail a MUST-tier live gate covers runs that gate **before cross-review is requested** — never parked behind "run it at approval time". Deferring a wired gate to a human decision is what let F6/F7 ship reviewed-and-approved; the wiring exists so the verdict never depends on who pressed enter. "배선이 없어 못 한 것은 OK, 배선이 있는데 안 돌린 것은 우리가 남긴 구멍이다." Model-in-loop cost is spent via the subscription-backed `entwurf` provider where the gate allows it, a free-tier native model otherwise; cost is a reason to pick the cheap target, not to skip the gate.
56
95
 
96
+ > **The herdr rail is an on-demand axis until it has run.** `smoke-herdr-fresh-call-live` opens a real caller inside a real herdr pane, has it invoke the PUBLIC `entwurf_fresh_call`, and reads the child's nonce callback — the one axis no deterministic gate reaches, since the rail is selected from the caller's own process environment. **The release aggregate does not yet require the herdr rail**: herdr is optional (a cut on a host without it must not block), and first evidence precedes promotion — a rail whose first acceptance run is days old has not earned a place in the gate that blocks every cut. Run it directly with `LIVE=1 ./run.sh smoke-herdr-fresh-call-live` whenever a cut changes herdr rail code, and promote it to a MUST only after the axis has a run history to point at. Without `LIVE=1`, or without herdr/pi/claude on PATH, it reports protocol SKIP — never a pass.
97
+
98
+ > **The plugin's FIRST USER PATH is its own on-demand axis, and until it runs the npm acquisition has no receipt (#118 홉 1).** Every other herdr gate proves the plugin against a source this repository controls: `check-herdr-plugin-build` drives a stub, and `smoke-herdr-plugin-build-live` redirects the product remote to a local bare clone with git `insteadOf` and asserts `identity.kind === "herdr-checkout"`. The production lock has said `npm` since `dd84ac0`, and the re-proof this same document demands above (§ source switch) still has an empty acquisition row. `LIVE=1 ./run.sh smoke-herdr-raw-install-live [--ref <REF>]` fills that row in a clean `node:24` container with no socket, config, cache or repo mounted, no `insteadOf`, the public remote spelled as a user would type it, and pi / herdr / Claude Code installed in the IMAGE BUILD as scaffolding (Hard Rule 17 — the product face begins at `herdr integration install pi`). herdr itself comes from `scripts/install-herdr-ci.sh` against `scripts/fixtures/herdr-supply.json`, so no version or digest lives here. `--ref` defaults to `main` rather than a pinned SHA because receipt (a) is defined as what a user gets today; the resolved commit is printed, so the receipt stays exact. It takes a REMOTE REF and not an arbitrary commit — measured on herdr 0.9.1, `--ref <sha>` fails as `couldn't find remote ref`, because the checkout fetches by name — so a candidate is addressed by its branch and pinned by the resolved commit the gate prints. **Its first question is a measurement, not an assumption:** herdr registers a plugin through a running server and falls back to `persist_plugin_offline` only on a connection error (`src/cli/plugin.rs:914-981,1016-1021` @ `c77af189`, 0.9.0 source), and a container has no server — whether 0.9.1 carries that fallback is unmeasured, and a "no" is #118 investigation output rather than a completion. `--yes` is always passed so a non-interactive exit 2 (`:189`) can never be misread as a missing server. **At a ref whose lock names `herdr-checkout` the acquisition axis reports a NAMED SKIP rather than a red, and the rest of the journey still runs.** That lock is the candidate carrier a cut legitimately sits on — 0.23.0 was tagged that way and `dd84ac0` pinned npm afterwards — so a red there would report a broken install when what is true is that the ref does not lock npm. What is asserted at BOTH lock kinds is that the runtime came from the source the ref locks, and the package name is read from the checkout manifest rather than the lock, because a `herdr-checkout` lock carries no `name` (`herdr-runtime.mjs:317-328`) and joining `undefined` throws instead of failing an assertion. **Cells [6]–[9] carry it past the install into the USE path (#118 H1-4/H1-5).** An install is not a use, so the same container then runs what those bytes became: the compiled bridge entry, the three `REQUIRED_BINS` as executables, and a real `entwurf check-bridge` printing its exact seven-verb set — the installed-runtime row below, measured rather than inferred from the bootstrap's refusal to install without them. Then the pi that this plugin WIRED is started with no `-e` and no `--no-extensions`, because the subject is the wiring and not this checkout: one `get_state` in `--mode rpc` costs zero tokens and needs no provider argument (a bogus `--provider` exits 1 before session_start; omitting the pair lets pi resolve the default the extension itself registers), and while that session is alive the cell reads its V3 record, its gardenId-keyed control socket, and the installed `entwurf peer-facts` reporting it `alive`. The plugin's own status fan is then driven with NEITHER side stubbed, in both states a user can be in: with no herdr server it must name `herdr-agent-list-failed` and go red rather than draw an empty table, and under a headless `herdr server` — scaffolding for that second read only, stopped again before the teardown — it exits 0 and COUNTS the citizen as `unobserved` instead of dropping it. Last, the shipped `entwurf herdr-plugin-deactivate` runs from the installed bin, deletes the runtime it is executing from, and the plugin reinstalls onto the host it fully left — the deactivation row, on the npm source. **What these cells still do not close:** swap / torn-swap recovery on the npm source, the package-consumer proof, and the fan drawing a citizen as a ROW — that needs `placement.kind === "herdr-pane"`, so the pi session must live in a herdr pane a session reference joins, and a headless container has no panes. **This smoke carries no test beside behavior and no mutant**, and that is a property of its class rather than an omission: it is a real-live container journey whose oracle is the container's own end state, so there is no pure seam to assert against and nothing a source mutation could kill deterministically. Like the other two herdr LIVE lanes it is **on-demand and not an aggregate MUST** — herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion. Without `LIVE=1`, or without a usable Docker daemon, it reports a named SKIP; `ENTWURF_REQUIRE_DOCKER=1` turns the Docker absence red. Neither skip is a pass.
99
+
100
+ > **The Herdr plugin's install-time build has two axes, and one of them needs the network (#116 M3-b3).** `check-herdr-plugin-build` is deterministic and rides `check:hermetic`: the listing reaches the runner through its own spawn seam (there is no fake `herdr` binary in this repo), and the one cell that drives the REAL binary reports a named SKIP on a host without it — `ENTWURF_REQUIRE_HERDR=1` makes that absence red. What it cannot have is the real acquisition: `npm pack git+https://github.com/junghan0611/entwurf.git#<full sha>` builds the bridge through `prepare`, which installs devDependencies from the registry. So `LIVE=1 ./run.sh smoke-herdr-plugin-build-live` owns that axis, and its cells are **two kinds of evidence, labelled as such**: three are the real Herdr journey — a real `herdr plugin install` driving the real `[[build]]`, with the UNCHANGED product remote redirected to a local bare clone by git `insteadOf` (the one substitution, which is what makes an unpushed candidate testable) — covering remote-commit available, the `{pi}` → `{pi, claude-code}` reinstall with OpenCode as a zero-write negative control and exactly ONE counted vendor MCP owner entry, and the post-build Herdr-commit gap (where the ledger has already rebound to the gap commit, so the honest assertion is that transition and not unchanged bytes). The fourth cell — remote-commit **unavailable** — calls the shipped runtime leaf directly with the fixed product argv and is therefore **acquisition-leaf evidence, not a journey**; routing it through Herdr would add a second explanation for one red. Its oracle is the honest one too: active runtime, ledger and harness bytes preserved, while the journal is left as the certified `installing` retry authority the transaction deliberately writes before acquiring, and a normal install afterwards is green. Its Claude cell is where the vendor question the deterministic gates refuse to claim gets answered — whether Claude Code accepts an ABSOLUTE executable as an MCP command — and it reports a named skip on a host without `claude`, never a pass. Like the herdr fresh-call rail, this smoke is **on-demand and not an aggregate MUST**. The release aggregate does not require the plugin build smoke: herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion.
101
+
102
+ > **Switching the plugin's production acquisition source is a re-proof, not a configuration change (#116 M3-b3).** `runtime-lock.json` carries a closed `source` discriminant: `npm` (exact `name@version` plus the published sha512, compared against the tarball's own bytes) is the production authority, and `herdr-checkout` (the exact commit Herdr checked out, from the fixed remote) is a VERIFICATION-ONLY carrier that exists so a candidate needs no npm release. Evidence does not transfer between them. Before a release that moves the production source — to npm, or to a GitHub Release tarball plus its sha512 — the exact acquisition and integrity comparison, the installed runtime (name@version, compiled entry, three executable bins, a real `check-bridge`), the swap and torn-swap recovery, activation and deactivation, and the package-consumer proof must all be re-run against THAT source. A candidate proof green on the checkout carrier says nothing about a registry artifact, and the reverse is equally true. **Records from before this contract are refused, not migrated:** a v1 runtime journal or activation ledger fails certification by name, and the way forward on such a host is an explicit `entwurf herdr-plugin-deactivate` (or a hand-cleared runtime/ledger state) before the next install — there is no reader that accepts both schemas, because the older shape cannot say WHICH artifact it was.
103
+
57
104
  > **Cortex is an on-demand axis, not an aggregate one.** Its rail needs an external Snowflake connection and login that the host owns, not the repo — so wiring it into the aggregate would block every cut taken on a host without that account. **The release aggregate does not re-certify Cortex**; `LIVE=1 ENTWURF_ACP_CORTEX_CONNECTION=<conn> ./run.sh smoke-acp-cortex-live` stays a required direct call whenever a cut changes Cortex rail code or an operator elects to certify that host. Its honest-skip behaviour is unchanged: run it without the connection and it reports protocol SKIP, never a pass. **Operator-host limit (2026-08-20, measured):** the source-owning host has no Snowflake subscription, and Cortex Code offers no free tier that a live turn can reach — a direct attempt confirmed it. So on this host `smoke-acp-cortex-live` can only ever report protocol SKIP, and no LIVE Cortex evidence can be produced here until Snowflake supplies a free-usable route or the account is subscribed. Record that SKIP as an absent axis; never soften it into a pass, and never substitute a different backend as Cortex evidence.
58
105
  >
59
106
  > A cut that touches the prompt-lifecycle contract (no wall clock on a running turn) owes one long-turn acceptance the aggregate floor is too short to carry: `LIVE=1 ./run.sh smoke-acp-long-turn-live` drives a real turn whose tool work outlasts the retired 600s cutoff and requires exactly one cold ACP bootstrap in the transcript. It takes >12 minutes by construction and is on-demand, not part of `release-gate`.
@@ -92,11 +92,11 @@ undifferentiated "supported" column is what let a Claude PASS read as if it also
92
92
 
93
93
  | Surface | Declaration | Class | What a green actually says |
94
94
  |---|---|---|---|
95
- | Entwurf package | `0.20.0` | shipped baseline | the package contract these rows belong to |
95
+ | Entwurf package | `0.23.0` | shipped baseline | the package contract these rows belong to |
96
96
  | pi runtime | devDep exact `0.85.1`, peer `>=0.85.1 <0.86` | **exact** oracle + **closed range** | built and certified against 0.85.1; hosts inside the range are accepted, and the ceiling moves only on measurement |
97
97
  | ACP wire SDK | `@agentclientprotocol/sdk 1.4.0` | **exact** | the shared wire oracle both adapters speak |
98
- | Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.76.0` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
99
- | Claude Agent SDK | `0.3.257` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
98
+ | Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.79.0` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
99
+ | Claude Agent SDK | `0.3.274` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
100
100
  | Anthropic SDK | `0.100.1` | **exact**, peer-resolution only | satisfies the Agent SDK peer floor (0.93.0+); never an API client here (gate L4) |
101
101
  | Claude Code runtime | `>=2.1.217` (`entwurf.claudeCodeFloor`) | **floor** | below it, hook args are silently dropped; entwurf enforces this itself |
102
102
  | Node | `>=24` (`engines.node`) | **floor** | single axis, derived everywhere else |
@@ -168,6 +168,83 @@ different reasons, and collapsing them would hide a real risk**:
168
168
  `resolvedProvider` and up onto the unconditional `session/new` path. entwurf passes `settings`
169
169
  as an OBJECT (`tool-surface.ts:153`), so the branch is never taken. That makes "entwurf never
170
170
  hands `settings` as a path" a contract rather than an accident; it is stated at that call site.
171
+ - **0.77.0's BREAKING change does not reach us, twice over.** `claudeCode.options.agent` is no
172
+ longer forwarded — the adapter now shallow-copies the options object and `delete`s `agent` on
173
+ the unconditional `session/new` path (`[측정 2026-09-18, 0.79.0 dist/acp-agent.js:5934-5941 직독]`).
174
+ entwurf's `buildClaudeSessionMeta` never sets that key (`tool-surface.ts:145-199` 직독: the
175
+ literal carries `model`/`tools`/`settingSources`/`settings` plus optional `plugins`/
176
+ `disallowedTools`/`extraArgs`, and nothing else), and the removed agent-picker exports
177
+ (`DEFAULT_AGENT_ID`, `AGENT_CONFIG_ID`, `BUILTIN_AGENT_NAMES`, `discoverCustomAgents`) are
178
+ imported nowhere: `[측정 2026-09-18]` `git grep -c` over `pi-extensions/`, `test/`, `scripts/`,
179
+ `mcp/` is **0**. We never imported the adapter as a library at all — we spawn its binary.
180
+ - **0.77.0's `allowDangerouslySkipPermissions` opt-out is a new lever we deliberately do not
181
+ pull, and our effective permission mode is unchanged.** 0.76.0 sent
182
+ `allowDangerouslySkipPermissions: ALLOW_BYPASS` unconditionally and computed
183
+ `initialPermissionMode = creationOpts.permissionMode ?? resolvePermissionMode(settings…)`.
184
+ 0.79.0 computes `allowBypass = ALLOW_BYPASS && sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false`
185
+ and routes the whole thing through `resolvePermissionMode(…, logger, allowBypass)`
186
+ (`[측정 2026-09-18, 0.79.0 dist/acp-agent.js:5932-5933 직독]`). entwurf sets that option
187
+ nowhere, so `undefined !== false` holds and `allowBypass === ALLOW_BYPASS`;
188
+ `ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX` is **byte-identical** between the two
189
+ versions (`dist/permissions/modes.js` 직독). Our overlay pins
190
+ `permissions.defaultMode: "bypassPermissions"` (`overlay.ts:122`), which resolves the same
191
+ under both. The lever now EXISTS for a host that wants a non-bypass sibling; declaring it is a
192
+ separate axis, not a one-line flip, and nothing in this bump takes it.
193
+ - **0.77.0's system-reminder strip never touches our first-user-message augment.**
194
+ `INJECTED_CONTEXT_MARKERS = ["system-reminder"]` joins the local-command markers in
195
+ `stripMarkerTags`, and `stripLocalCommandMetadata` has exactly two call sites
196
+ (`[측정 2026-09-18, 0.79.0 dist/acp-agent.js grep -n]`): `:4129`, gated on the message content
197
+ containing `<local-command-stdout>`, and `:5061`, in the `session/load` transcript replay. Both
198
+ run agent→client on text coming BACK from the transcript; our augment rides client→agent on the
199
+ first `session/prompt` and is never re-emitted, because entwurf calls `session/load` nowhere
200
+ (`[측정 2026-09-18]` `git grep loadSession` in `pi-extensions/lib/acp/` hits only the
201
+ `session-store.ts` capability TYPE, never a wire call). The augment also emits no
202
+ `<system-reminder>` tag of its own (`augment.ts` 직독), so no prose of ours is strippable.
203
+ - **0.78.0's compaction update is a NEW `sessionUpdate` kind and is inert for us, twice over.**
204
+ `compaction_update` and `compaction_summary_chunk` are new in `dist/context-compaction.js`
205
+ (`[측정 2026-09-18]` `sessionUpdate: "…"` literal sweep across both dists: 0.76.0 has 15 distinct kinds,
206
+ 0.79.0 has 17, and the two new ones are exactly these). Gate one: the lifecycle's
207
+ `presentation` is `clientSupportsCompactionUpdates(this.clientCapabilities) ? "compaction_update"
208
+ : "tool_call"` (`dist/acp-agent.js:1875-1879`), and that predicate reads
209
+ `capabilities?.session?.compaction` — entwurf sends `clientCapabilities: {}` (`backend.ts:1758`),
210
+ so we keep the 0.75.0 `tool_call` presentation §11-8 already measured. The replay path
211
+ (`:5082`) is guarded by the same predicate at `:4915`. Gate two, independent: our mapper's
212
+ update switch has a `default: break` — "unknown update kinds are ignored (forward-compatible)"
213
+ (`event-mapper.ts:311-313`, `:377-378`). The "map every terminal reason, unknown is an error" rule
214
+ is about ACP **stopReason**, a different axis; `stopReason` literals are unchanged across the
215
+ two dists (`[측정 2026-09-18]` sweep: `"cancelled"` only, both versions). No code needed.
216
+ - **0.78.0's checkpoint file-change report and AIR diff counts are both behind the AIR gate.**
217
+ `supportsAgentFileChangeReport` is `clientSupportsAirCapability(capabilities, "agentFileChangeReport")`
218
+ (`dist/file-change-audit.js:27-29` 직독), and `air-extension.js`'s only delta is one added
219
+ constant `AIR_DIFF_STATS_KEY` (full-file `diff`, one line). Same `clientCapabilities: {}`
220
+ argument as the 0.76.0 `recommendedValue` entry — re-measured, not inherited.
221
+ - **0.79.0's shell-command permission prompts reach our permission handler's INPUT and change no
222
+ decision.** The change reorders options: when the CLI hints `defaultToNo` (new in 0.79.0 —
223
+ `[측정 2026-09-18]` `grep -rn defaultToNo` over the 0.76.0 dist is **0 hits**), the option array
224
+ now sorts reject-first (`dist/permissions/options.js:5-10`). entwurf's approve-all policy
225
+ selects `options.find((o) => o.kind === "allow_once" || o.kind === "allow_always")` and only
226
+ falls back to `options[0]` when that find fails (`backend.ts:824-831`) — a find by KIND, so
227
+ order cannot flip it. The fallback is unreachable besides: every builder in
228
+ `dist/permissions/options/` routes through `withOptionalUpdate`/`withGeneratedUpdate`, both of
229
+ which lead with `allowOnce()`, and the hand-rolled `tools.js` sets each carry an `allow_once` or
230
+ `allow_always` (직독 of `shared.js` + `tools.js`). The title change (Bash/PowerShell titles now
231
+ bypass `humanText` compaction) lands on a field we never read.
232
+ - **0.77.0–0.79.0's remaining fixes are unreachable under our capability posture.** The
233
+ AskUserQuestion multi-select/custom-text fixes (#1031, #1131) require form elicitation:
234
+ the adapter computes `disallowedTools = elicitationSupport.form ? [] : ["AskUserQuestion"]`
235
+ from `clientCapabilities.elicitation.form`, which our `{}` leaves false, so AskUserQuestion is
236
+ disabled on every session we open. The TaskList regex fix (#1006) is internal parsing. #1128's
237
+ tool names land on `presentation.toolCall._meta.claudeCode` on the permission-request path
238
+ (`dist/acp-agent.js:5317-5325`), which our handler ignores; our `titleForTool` already read
239
+ `_meta.claudeCode.toolName` as a fallback behind `update.title`, so it is additive at worst.
240
+ - **Our model-forcing and accounting wire calls are byte-identical across 0.76.0 → 0.79.0.**
241
+ `[측정 2026-09-18, brace-matched extraction from both dists, md5]`: `setSessionConfigOption`
242
+ (4,858 B, identical), `sessionUsage` (373 B, identical), `turnQuotaMeta` and `quotaTokenCount`
243
+ (identical md5), and `resolveModelPreference` lives in `dist/session-model.js`, whose whole file
244
+ is byte-identical (`md5 cfd031d0…` both versions). The `settings`-as-STRING-PATH branch our
245
+ call-site contract names is still on the unconditional `session/new` path
246
+ (`dist/acp-agent.js:6010-6012`).
247
+
171
248
  - **The one 0.73.0 → 0.75.1 change that DOES reach us:** context compaction is now surfaced as a
172
249
  synthetic ACP tool lifecycle (0.75.0, #991) — a `tool_call` with `kind: "think"`, title
173
250
  `Compact conversation`, and `_meta.contextCompaction` schema v1 — where it used to arrive as
@@ -292,23 +369,25 @@ caller-session `_meta`, and cross-machine certification.
292
369
 
293
370
  A backend can return `newSession` before its declared MCP server is callable. This was
294
371
  observed intermittently on the Claude rail and directly on Cortex's private `mcp.json`
295
- path. Neither `claude-agent-acp` 0.76.0 nor the Cortex landing adds a client-side
372
+ path. Neither `claude-agent-acp` 0.79.0 nor the Cortex landing adds a client-side
296
373
  readiness fence over a session's declared MCP servers, and entwurf's common loop
297
374
  calls `mcpServerStatus()` nowhere.
298
- (Re-measured at the 0.75.1 → 0.76.0 bump, not inherited — the previous bump's argument is
299
- not reused, the way the 0.73.0 → 0.75.1 entry did not reuse 0.70.0 → 0.73.0's.
300
- `mcpServerStatus` call sites in `src/acp-agent.ts` are **2 at v0.75.1 and 2 at v0.76.0**
301
- `[측정 2026-09-10, upstream v0.76.0/src/acp-agent.ts read directly, grep -n]`; they first
375
+ (Re-measured at the 0.76.0 → 0.79.0 bump, not inherited — the previous bump's argument is
376
+ not reused, the way the 0.75.1 → 0.76.0 entry did not reuse 0.73.0 → 0.75.1's.
377
+ `mcpServerStatus` call sites in `src/acp-agent.ts` are **2 at v0.76.0 and 2 at v0.79.0**
378
+ `[측정 2026-09-18, upstream v0.79.0/src/acp-agent.ts read directly, grep -n]`; they first
302
379
  appeared in 0.71.0 via `0cbbaf3` (MCP OAuth, LLM-25012), so the ADAPTER calls it where it
303
- once did not. Both were re-read at `v0.76.0 src/acp-agent.ts:1762` and `:1855`
304
- (v0.75.1: `:1736` / `:1829`; v0.73.0: `:1618` / `:1711`): the first sits inside
305
- `authenticateMcpServers` behind `supportsMcpOAuth(query)` and skips every status that is not
306
- `needs-auth`; the second polls a SINGLE named server to `connected` under an OAuth deadline.
307
- Neither waits on every declared server before `newSession` returns. That is an auth
308
- handshake, not a readiness fence, so the boundary below is unchanged. The surrounding 200
309
- lines are byte-identical and the region moved +26; the whole 0.76.0 delta is one refactor
310
- plus one opt-in AIR extension that `clientCapabilities: {}` never enables, so the other
311
- reachable-surface findings stand as re-measured at the previous bump.
380
+ once did not. Both were re-read at `v0.79.0 src/acp-agent.ts:1773` and `:1866`
381
+ (v0.76.0: `:1762` / `:1855`; v0.75.1: `:1736` / `:1829`; v0.73.0: `:1618` / `:1711`): the
382
+ first sits inside `authenticateMcpServers` behind `supportsMcpOAuth(query)` and skips every
383
+ status that is not `needs-auth`; the second polls a SINGLE named server to `connected` under
384
+ an OAuth deadline. Neither waits on every declared server before `newSession` returns. That
385
+ is an auth handshake, not a readiness fence, so the boundary below is unchanged. The
386
+ surrounding 200-line window is byte-identical (`diff v0.76.0:1662-1862 v0.79.0:1673-1873`,
387
+ empty) and the region moved +11 while the file shrank 10,405 10,329 lines. The 0.77.0
388
+ agent-picker removal, the 0.78.0 compaction/checkpoint/AIR work and the 0.79.0 permission
389
+ presentation touch no part of this path, so the other reachable-surface findings stand as
390
+ re-measured in the capability-posture section above.
312
391
  This bump changes no readiness behavior and closes no part of #72.)
313
392
 
314
393
  ### 11-7-a/b. Instrument and first measurement
@@ -449,6 +449,13 @@ The birth atom publishes a `SessionStart` declaration into `$CODEX_HOME/hooks.js
449
449
  launcher closure under `$XDG_DATA_HOME/entwurf/codex-birth`; it mints a V3 record on the
450
450
  thread's first turn and sets the thread title to the garden id.
451
451
 
452
+ It owns that ONE declaration, not the file. Another integration may declare its own
453
+ `SessionStart` group in the same hooks.json — Herdr's official Codex integration does — and the
454
+ vendor runs both, because trust is keyed per declaration. So installing into a file somebody else
455
+ already wrote is an append, not a refusal; uninstalling takes out only entwurf's group and leaves
456
+ every neighbouring byte where it was; and `doctor-codex-birth` lists the neighbours in their own
457
+ FOREIGN section, certifying none of them and letting none of them move its verdict.
458
+
452
459
  **One step is the operator's, and no command can do it for them.** The vendor will not run a
453
460
  user-layer hook until it has been trusted once: open a visible plain Codex, answer
454
461
  `Trust all and continue`, then send one first turn. `doctor-codex-birth` reads the receipt the
@@ -492,7 +499,10 @@ The 2026-09-11 loaded-thread run remains pre-amendment native-push/identity evid
492
499
  first-admission acceptance passed on 2026-09-12 under the fixed-home topology #95 D1 later retired:
493
500
  real initial Pi outside `codex`, then omitted-placement Codex and outbound Pi inside it, with exact
494
501
  callbacks and addressed delivery both ways. `DELIVERY.md` owns that 57-assertion receipt, its four
495
- coordinates, and its digest as history; the caller-seat re-run is pending.
502
+ coordinates, and its digest as history. The caller-seat re-run is **accepted** — 2026-09-16, 56
503
+ assertions for the seat topology and 65 for the caller-DIRECTORY axis, both exit 0 — and it, not the
504
+ fixed home, is the current contract (`DELIVERY.md`, `BASELINE.md:37-38`, `VERIFY.md` release
505
+ acceptance).
496
506
 
497
507
  `entwurf_fresh_call` accepts backend `codex` — supported in 0.21.0 — and requires an explicit model. Its
498
508
  preflight must prove the safe birth closure, exact MCP/env atom, `thread-title`, and app-server
@@ -64,7 +64,6 @@ import * as process from "node:process";
64
64
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
65
65
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
66
66
  import { z } from "zod";
67
- import { codexCallerFreshPreflight, codexFreshPreflight } from "../../../pi-extensions/lib/codex-fresh-preflight.js";
68
67
  import { controlSocketPathIn, defaultControlSocketDir } from "../../../pi-extensions/lib/control-socket-path.js";
69
68
  import { resolveMailboxReceiverFacts } from "../../../pi-extensions/lib/entwurf-deliverability.js";
70
69
  import { listEntwurfFacts } from "../../../pi-extensions/lib/entwurf-fact-provider.js";
@@ -74,9 +73,9 @@ import { nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contr
74
73
  import { resolveMailboxWakeModeCapability } from "../../../pi-extensions/lib/entwurf-v2-decider.js";
75
74
  import { runAndRenderEntwurfV2FromSurface } from "../../../pi-extensions/lib/entwurf-v2-surface.js";
76
75
  import { makeVisibleResumeDeps, renderVisibleResume, visibleResume, } from "../../../pi-extensions/lib/entwurf-v2-visible-resume.js";
76
+ import { dispatchFreshCall, renderDispatchedFreshCall } from "../../../pi-extensions/lib/fresh-call-dispatch.js";
77
77
  import { probeNativeSenderAlive, reconcileSenderIdentityClaims, resolveCodexRequestSenderIdentity, resolveTrustedMetaSenderIdentity, } from "../../../pi-extensions/lib/meta-sender-identity.js";
78
78
  import { applyOmpBridgeChildRootPolicy, defaultMetaMailboxDir, defaultMetaSessionsDir, makeStoreRecordReader, readActiveStoreEntries, readMetaInbox, readMetaReceiverMarker, readMetaSenderMarker, requireBackend, } from "../../../pi-extensions/lib/meta-session.js";
79
- import { freshCall, renderFreshCall } from "../../../pi-extensions/lib/mux-fresh-call.js";
80
79
  import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.js";
81
80
  import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.js";
82
81
  const HOME = os.homedir();
@@ -598,29 +597,29 @@ server.tool("entwurf_register_native", "Register an ALREADY-RUNNING native conve
598
597
  // its own garden id, a fresh cell answered with its uuidv7 `PI_SESSION_ID` value read out of the
599
598
  // environment by an MCP server it had spawned itself — confidently, and wrong. A sibling launched
600
599
  // against that answer would call home to a garden id nobody holds.
601
- server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operator's tmux and hand it a first task. Five fixed " +
602
- "backends only: pi, claude-code, copilot, omp, codex. The sibling's FIRST action is a callback to you carrying a nonce, and the " +
603
- "sender envelope of that callback is its garden id — that is how you learn the address of something that " +
604
- "did not exist a moment ago. This returns a LAUNCH receipt (tmux window/pane plus that nonce) and nothing " +
605
- "else: it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls " +
606
- "for the callback; if it never arrives the window is visible. For EXISTING " +
607
- "citizens use entwurf_v2 this tool only creates, and entwurf_peers only reports. Model is REQUIRED and " +
608
- "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code; a model name " +
609
- "or `auto` for copilot; a fuzzy model pattern for omp or codex). Copilot, omp, and codex are refused BEFORE " +
610
- "any window opens when their required birth, MCP, receive/delivery, or visible-identity units are absent; " +
611
- "Codex also requires the operator-owned default app-server socket, which entwurf never starts. An optional " +
612
- "cwd starts the sibling in ONE literal absolute existing directory (cross-repo fresh) never pick resume " +
613
- "for a dormant record's cwd. Omitted/empty cwd means the caller's own directory. " +
614
- "An optional placement.tmuxSession is an expert override naming ONE EXISTING session on this agent's own " +
615
- "tmux server, and it ALWAYS wins. Omitted, the seat follows the CALLER: a CODEX CALLER opens beside its own " +
616
- "TUI pane (matched by thread-id in that pane's title; 0 or 2+ matches REFUSE, never fall back), and every " +
617
- "other caller opens in its own session. " +
618
- "A missing named session is tmux-session-missing and NOTHING is created. " +
619
- "There are no arbitrary command/env knobs. Do not put secrets in the task — model and task argv are visible to " +
620
- "same-user processes on this host. Requires that this agent itself runs inside tmux.", {
600
+ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and hand it a first task. WHERE it opens is decided by where THIS " +
601
+ "agent runs, never by a parameter: inside herdr (HERDR_ENV=1) it opens a NEW UNFOCUSED TAB in your own " +
602
+ "herdr workspace, pi and claude-code " +
603
+ "only; everywhere else it opens in the operator's tmux with all five backends (pi, " +
604
+ "claude-code, copilot, omp, codex). No fallback: an incomplete herdr context is refused by name, " +
605
+ "never by a tmux window you cannot see from herdr. The sibling's FIRST action " +
606
+ "is a callback to you carrying a nonce, whose sender envelope is its garden id — that is how you learn the " +
607
+ "address of a thing that did not exist a moment ago. This returns a LAUNCH receipt (the owner's " +
608
+ "coordinates plus that nonce): it does NOT mean the runtime started, the first turn ran, or " +
609
+ "the task was delivered. Those coordinates are a VIEW, never an address — a herdr tab/pane id can change under " +
610
+ "a running sibling. Nothing polls. " +
611
+ "For EXISTING citizens use entwurf_v2 this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the chosen " +
612
+ "runtime CLI (`provider/model` for pi, an id/alias for Claude Code, a pattern for the rest). On tmux, copilot/omp/codex are refused BEFORE any window " +
613
+ "opens when their birth, MCP, receive or visible-identity units are absent, and codex also needs the " +
614
+ "operator-owned app-server socket entwurf never starts; in herdr they are refused by name first. " +
615
+ "Optional placement.tmuxSession is a TMUX-ONLY seat naming ONE EXISTING session and " +
616
+ "ALWAYS wins; omitted, the seat follows the CALLER, never the backend opened: a CODEX CALLER opens beside " +
617
+ "its own TUI pane, every other caller in its own session; a missing one is tmux-session-missing and " +
618
+ "NOTHING is created. In herdr the field is refused by name. Do not put secrets in the task — model and task argv are visible to same-user " +
619
+ "processes.", {
621
620
  backend: z
622
621
  .enum(["pi", "claude-code", "copilot", "omp", "codex"])
623
- .describe("Which fixed runtime to open. Only these five; there is no arbitrary command."),
622
+ .describe("Which fixed runtime to open. Only these five, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command."),
624
623
  model: z
625
624
  .string()
626
625
  .min(1)
@@ -643,7 +642,7 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operato
643
642
  cwd: z
644
643
  .string()
645
644
  .optional()
646
- .describe("Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Taken exactly as given — no trim, no realpath, no project-name resolution; '#' is refused (tmux format expansion). Omit or pass \"\" to start where the CALLER is: its own directory, or for a Codex caller its own record directory, because this bridge is the app-server's child and that process's directory is not the caller's. The receipt echoes the directory that was REQUESTED or the caller record it came from, never an observation of where the pane landed."),
645
+ .describe("Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Omit or pass \"\" to start where the CALLER is this agent's own cwd on BOTH rails, or for a Codex caller its own record directory, because this bridge is the app-server's child and that process's directory is not the caller's. Taken exactly as given: no trim, no realpath, no project-name resolution. '#' is refused on the tmux rail only, because tmux format-expands a start directory; inside herdr it is an ordinary path character. The receipt echoes the directory that was REQUESTED or the caller record it came from, never an observation of where the pane landed."),
647
646
  placement: z
648
647
  .object({
649
648
  tmuxSession: z
@@ -651,7 +650,7 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operato
651
650
  .describe("EXACT name of an EXISTING session on this agent's own tmux server. Nothing is created: an absent session is refused as tmux-session-missing, and a name outside [A-Za-z0-9][A-Za-z0-9_-]* as tmux-session-name-invalid."),
652
651
  })
653
652
  .optional()
654
- .describe("Optional expert seat override: open the sibling in ONE EXISTING tmux session of this agent's own server, and it always wins. When omitted the seat follows the CALLER, never the backend being opened: a Codex CALLER opens beside its own TUI pane, matched by thread-id in that pane's terminal title (0 or 2+ matching panes refuse, never fall back); every other caller opens in its own session. A pane title is a placement input only — never an address, liveness or delivery fact. Nothing is ever created. Independent of cwd; neither is inferred from the other. The receipt reports the selected name (absent for the caller-pane rule, which observed a session rather than requesting a name), its source, and resolved target session id."),
653
+ .describe("Optional expert seat override, TMUX ONLY: open the sibling in ONE EXISTING tmux session of this agent's own server, and it always wins. When omitted the seat follows the CALLER, never the backend being opened: a Codex CALLER opens beside its own TUI pane, matched by thread-id in that pane's terminal title (0 or 2+ matching panes refuse, never fall back); every other caller opens in its own session. A pane title is a placement input only — never an address, liveness or delivery fact. Nothing is ever created. Inside herdr this field is refused by name — placement there belongs to herdr, and a tmux session name would silently place the sibling somewhere else. Independent of cwd; neither is inferred from the other. The receipt reports the selected name (absent for the caller-pane rule, which observed a session rather than requesting a name), its source, and resolved target session id."),
655
654
  }, async ({ backend, model, task, cwd, placement }, extra) => {
656
655
  let callerGardenId = null;
657
656
  // Present exactly when the reconciled caller is a record-backed codex citizen. Its ONLY
@@ -685,21 +684,21 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operato
685
684
  callerGardenId = null;
686
685
  }
687
686
  try {
688
- // TWO capability axes, in this order, both pre-mutation and neither standing in for
689
- // the other. The TARGET axis first "entwurf cannot open a Codex sibling here at
690
- // all" is the more fundamental answer than "and it would not know where to put it".
691
- // The CALLER axis second, and only when the seat anchor will actually be consulted:
692
- // a codex caller that named an explicit placement never reads a pane title, so
693
- // refusing it for a missing `thread-id` would refuse an unused capability.
694
- const targetMissing = backend === "codex" ? await codexFreshPreflight(process.env) : null;
695
- const callerMissing = targetMissing === null && callerNativeSessionId !== undefined && placement === undefined
696
- ? codexCallerFreshPreflight(process.env)
697
- : null;
698
- const missing = targetMissing ?? callerMissing;
699
- const result = missing
700
- ? { ok: false, reason: missing }
701
- : freshCall({ backend, model, task, cwd, placement, callerGardenId, callerNativeSessionId, callerCwd });
702
- const rendered = renderFreshCall(result);
687
+ // Rail choice, Codex preflight ordering and rendering all live in the composition root,
688
+ // so this surface and pi's own cannot drift apart on any of them. The two caller-derived
689
+ // codex inputs ride with the request: the root consults them only on the tmux rail,
690
+ // which is where #95 lane B/C measured them.
691
+ const dispatched = await dispatchFreshCall({
692
+ backend,
693
+ model,
694
+ task,
695
+ cwd,
696
+ placement,
697
+ callerGardenId,
698
+ callerNativeSessionId,
699
+ callerCwd,
700
+ });
701
+ const rendered = renderDispatchedFreshCall(dispatched);
703
702
  return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
704
703
  }
705
704
  catch (err) {
@@ -123,8 +123,8 @@ export const claudeAdapter = {
123
123
  },
124
124
  // MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
125
125
  // session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
126
- // and a context-occupancy scalar (`used`; both read at 0.76.0
127
- // `dist/acp-agent.js:3471-3486`). Both are measured, so backend.ts seals them.
126
+ // and a context-occupancy scalar (`used`; both read at 0.79.0
127
+ // `dist/acp-agent.js:3504-3519`). Both are measured, so backend.ts seals them.
128
128
  // The token partition is NOT declared here: ACP's only token carrier is a
129
129
  // per-turn round-trip aggregate, which is not what pi's four fields mean.
130
130
  sealsTurnAccounting: true,
@@ -124,7 +124,8 @@ export function buildClaudeSessionMeta(params, normalizedSystemPrompt) {
124
124
  // claude-agent-acp also accepts a STRING PATH, and at 0.76.0 the readFile +
125
125
  // JSON.parse for that form moved out of `resolvedProvider` up onto the
126
126
  // unconditional `session/new` path — a shape that WOULD reach us, and does not,
127
- // only because we never hand it a path. Keep it inline: a path would put file
127
+ // only because we never hand it a path. Still on that path at 0.79.0
128
+ // (`dist/acp-agent.js:6010-6012`). Keep it inline: a path would put file
128
129
  // IO, and its failure modes, inside every session creation.
129
130
  settings: {
130
131
  permissions: {