@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/AGENTS.md CHANGED
@@ -82,6 +82,8 @@ Keep at most ten open issues, including at most five implementation issues. Clas
82
82
 
83
83
  Two independent axes are required: deterministic/package gates and opt-in LIVE evidence. Exact commands, evidence levels, and release acceptance are owned by [VERIFY.md](./VERIFY.md); recorded host evidence by [BASELINE.md](./BASELINE.md). Run LIVE gates from scratch with ambient identity carriers stripped so artifacts and callbacks cannot bleed from the operator session.
84
84
 
85
+ A new contract's test goes beside the behavior it certifies — `pi-extensions/**/*.test.ts` or `plugins/herdr/**/*.test.mjs` — and `check-tests-beside-behavior` discovers it by glob, so landing one edits no filename list. `scripts/check-*` stays the home of LIVE, install, spawning and mixed gates; a new hand-built gate there needs a reason the vitest lane could not carry it.
86
+
85
87
  ### Scheduling
86
88
 
87
89
  ```text
@@ -97,7 +99,7 @@ implement → affected focused gates → independent review → one amendment bu
97
99
 
98
100
  ## Type and Working Boundaries
99
101
 
100
- - Each `.ts` file belongs to one declared typecheck fence; never hide a file with `exclude`. Root pi extensions use TypeBox through `@earendil-works/pi-ai`; MCP/scripts use explicit `.ts` imports required by Node strip-types. Installed surfaces route to compiled JS.
102
+ - Each `.ts` file belongs to one declared typecheck fence; `exclude` may move a file between fences but never out of all of them, so an exclude states the receiving `include` in the same change. Root pi extensions use TypeBox through `@earendil-works/pi-ai`; MCP/scripts use explicit `.ts` imports required by Node strip-types. Installed surfaces route to compiled JS. Behavior-adjacent `.test.ts` takes the fence `test/` takes — `scripts/tsconfig.json`, never the emit-capable root program — and is kept out of the tarball by a files-array negation whose tripwire is `check-pack`.
101
103
  - Make surgical, one-contract changes. Removal repairs source and its proof together; a green gate never proves retired behavior.
102
104
  - Before commit, sweep repository-wide for retired authority vocabulary and landed-plan future tense. Historical tombstones may remain; live docs, source, gates, and usage must agree.
103
105
  - Prefer capability-domain language over identity rank. Use tabs unless the project formatter requires otherwise. GLG decides commit, push, and release gates.
@@ -115,6 +117,7 @@ implement → affected focused gates → independent review → one amendment bu
115
117
  - [ROADMAP.md](./ROADMAP.md) — direction and deferred work.
116
118
  - [docs/adding-a-harness.md](./docs/adding-a-harness.md) — entry route for a new harness.
117
119
  - [docs/mux-launch-rail.md](./docs/mux-launch-rail.md) — mux ownership and launch contracts.
120
+ - [docs/herdr-launch-rail.md](./docs/herdr-launch-rail.md) — herdr launch rail (#116): two-step placement, the one-line birth argv, and within-generation reclaim. `entwurf_fresh_call` selects it from process context (`HERDR_ENV=1`, pilot `pi | claude-code`), never from a caller parameter and never as a fallback either direction.
118
121
  - [docs/acp-backend-rail.md](./docs/acp-backend-rail.md) — ACP adapter contract.
119
122
  - [DELIVERY.md](./DELIVERY.md), [VERIFY.md](./VERIFY.md), [BASELINE.md](./BASELINE.md) — delivery coordinates, verification protocol, and evidence.
120
123
  - [README.md](./README.md) — operator-facing package contract.
package/BASELINE.md CHANGED
@@ -34,6 +34,7 @@ not **UNSUPPORTED** — the deliberate never, which today is native Windows only
34
34
  | Maintainer NixOS installed package | **certified** for `0.12.8-repair.1` | 2026-07-25 registry install → doctor exit 0 (HISTORY) |
35
35
  | Secondary Ubuntu installed package | **certified** for `0.12.8-repair.1` | 2026-07-25 same artifact, isolated agent dir → doctor exit 0 (HISTORY) |
36
36
  | macOS Claude meta-bridge | NOT CERTIFIED — pending physical host | No physical-Mac doctor yet; a CI runner has no Claude login. Physical-host rental remains unscheduled; no cut has carried it. |
37
+ | 0.23.1 release acceptance (observer-amended) | accepted by GLG | `pnpm run check:full` P4 exit 0 (577 s); second aggregate `release-gate --cut` at `/tmp/entwurf-release-gate-0.23.1-retry.UIFctR` reached MUST 23/1/0 and BEHAVIOR 1/0, with only `smoke-codex-fresh-live` red because luna dropped `$`/`@` sigils in its relay (`release-gate.log:6156-6157`). The observer-amended standalone cell reran that exact leg with Codex `gpt-5.6-terra`, Pi `openai-codex/gpt-5.6-luna`: 66 assertions, exit 0; A app-server PID `3969303` `$227` ≠ fixture S `$231`; artifact `.probe-artifacts/codex-fresh-live-eZmf6p/`; pipe-pane stdout sha256 `56ee73e046e16fa95ef093fccf037670bf28b53a054bb5eec2a3b3b3ff08d93a`. Luna sigil handling is a post-cut issue, not a production-byte change. |
37
38
  | Native Codex Linux caller-DIRECTORY axis (current; #95 lane C) | **certified** | 2026-09-16, Codex 0.153.4: 65-assertion LIVE, exit 0, at `b3e07dc` with a clean worktree. Same host and topology as the lane B row below (app-server `737636` in A `$30/@41/%45`; initial Pi `20260916T154902-556eb5` `$2/@58`, Codex `20260916T155037-10ddaa` `$2/@59`, Codex-opened Pi `20260916T155102-9add8a` `$2/@61`, `seat-source=codex-title-anchor`). **hop 1 (cwd requested):** pane `#{pane_current_path}` = rollout `session_meta.cwd` = record cwd = requested scratch = `/tmp/entwurf-codex-fresh-live-db65N2`, against app-server cwd `/home/junghan/repos/gh/entwurf` read from `/proc/737636/cwd`. **hop 2 (cwd NOT requested):** the outbound Pi's pane, its own birth-written record and the Codex caller's record all read that same scratch, and its receipt names the caller-record rule. Artifact `.probe-artifacts/codex-fresh-live-w4yJBw/`; stdout `.probe-artifacts/lane-c-live-20260916T154900.log` sha256 `712050e7a8cf03ece98e7f34029ae98a92e9b2a7fd96aff87d0c1e6ba2ee9af2`. Cleanup reclaimed `@58`/`@59`/`@61` with no failure; records and transcripts preserved; app-server and `$30` untouched. 65 and 56 are different contracts — lane C added the cwd axis on top of lane B. Long floors (qualification body, frozen `check:full`) NOT run at this SHA; GLG decides host vs CI. |
38
39
  | Native Codex Linux caller-seat topology (current; #95 lane B) | **certified** | 2026-09-16, Codex 0.153.4: 56-assertion LIVE, exit 0. App-server `737636` alone in session A `$30/@41/%45`; initial Pi `20260916T144205-e876ff` `$2/@46`, omitted-placement Codex `20260916T144230-d050d7` `$2/@47`, Codex-opened Pi `$2/@48`. A ≠ S is the decisive half: the app-server env names A, so S can only have come from the caller's pane title — receipt `seat-source=codex-title-anchor`. Artifact `.probe-artifacts/codex-fresh-live-nYcGC1/run-stdout.log` sha256 `ed60c2bd…`; the 43-assertion hop-1 run that measured the D1 retirement is kept at `codex-fresh-live-4aFCDD/run-stdout.log` sha256 `8392a603…`. Cleanup reclaimed `@46`/`@47`/`@48` with no failure; app-server and `$30` untouched. |
39
40
  | Native Codex Linux home topology (PAST CONTRACT — #95 D1 retired the fixed home 2026-09-16) | **certified** for 0.21.0 as shipped | 2026-09-12, Codex 0.153.4: 57-assertion explicit-home LIVE. Initial Pi `$150/@397`; operator app-server `$158/@390/%390`; omitted-placement Codex `$158/@398`; Codex-opened Pi `$158/@399`; exact callbacks and addressed delivery both ways. Receipt and digest: `DELIVERY.md`. Deterministic closure of that axis: qualification 460/460 across 43 lanes with origin/snapshot purity green, then frozen full floor exit 0 in 506s — both pre-amendment. **Observer-amended acceptance, 2026-09-12:** standalone LIVE 48 assertions exit 0, source audit `initial-pi=3/3`/`codex=3/3` completed exact, initial Pi `$150/@431`, Codex `$158/@432`, Codex-opened Pi `$158/@433`, all three reclaimed by their own source receipts; artifact `.probe-artifacts/codex-fresh-live-fZccoK/`. The 48 and 57 counts are different contracts, not a regression. **Release acceptance at that same fingerprint:** `check:full` exit 0 502s, LIVE release gate `--cut` MUST 24/0/0 · BEHAVIOR 1/0/0 · `cut: OK`, `check-gate-qualification` 475/475 across 43 lanes with origin purity green, and the aggregate's Codex leg repeating the 48-assertion acceptance (artifact `codex-fresh-live-2oId4C`). Unrestricted attached-TUI placement remains unsupported and unclaimed. |
package/CHANGELOG.md CHANGED
@@ -4,6 +4,520 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.23.1 - 2026-09-19
8
+
9
+ ### Changed
10
+
11
+ - **The plugin runtime lock names `herdr-checkout` again, for the length of the 0.23.1 candidate
12
+ window.** This is a state, not a direction: an `npm` lock must name a version that is ALREADY
13
+ published and coherent with this checkout's `package.json`
14
+ (`certifyLockCoherence`, asserted on the committed lock by `check-herdr-runtime-bootstrap` 8a,
15
+ which sits inside `check:hermetic`), and the version bump necessarily lands before the publish.
16
+ So a cut cannot ride an npm lock naming the version it is about to create. `dd84ac0` is the
17
+ precedent and the exit: 0.23.0 was tagged on the checkout carrier and pinned to npm afterwards,
18
+ and 0.23.1 follows the same two steps. While this window is open, `smoke-herdr-raw-install-live`
19
+ reports the npm acquisition axis as a NAMED SKIP rather than a failure, and still asserts that
20
+ the runtime came from the source this ref locks.
21
+
22
+ **A pin and its inverse move the lock AND `check-herdr-plugin-build` together**, which is a fact
23
+ worth writing down because it has now been rediscovered twice. That gate reads the COMMITTED lock
24
+ in three places — the two-stage journey expects one acquisition and the locked `kind@version`, the
25
+ activation-authority cell seeds its ledger from the committed kind, and the progress cell pins the
26
+ long-step wording — so `dd84ac0` changed 90 lines of it alongside the eight lines of lock, and
27
+ this commit is the measured inverse of both halves (14/14 assertions, exit 0). No mutant is
28
+ disturbed: every claim in `scripts/mutants/herdr-plugin-build.json` names a production file
29
+ (`herdr-plugin.toml`, `lib/build.mjs`, `lib/build-progress.mjs`), none names the gate, and the
30
+ `HPB-PROGRESS-NAMED-SEQUENCE` needle re-aimed by `37b81e7` still occurs exactly once in
31
+ `lib/build.mjs`, which this commit does not touch. Teaching the gate to read either lock kind,
32
+ so neither direction needs it flipped again, is deferred to its own issue after the cut.
33
+
34
+ ### Added
35
+
36
+ - **A test written beside the behaviour it certifies is run by public `pnpm check`, and nobody
37
+ names the file** (#119 V2). `vitest.config.ts` include grows two globs —
38
+ `pi-extensions/**/*.test.ts` and `plugins/herdr/**/*.test.mjs` — and `check-tests-beside-behavior`
39
+ expands them every run and hands the resulting paths to `run_vitest`. Both halves were measured
40
+ the hard way. The first cut passed `pi-extensions plugins/herdr` as vitest POSITIONALS, which are
41
+ path substrings, so `test/pi-extensions-probe.test.ts` was selected too (measured: it ran);
42
+ expanding first keeps the selected set exactly the two globs while the shim still contains no
43
+ filename. And an empty expansion RETURNS rather than calling vitest with no arguments, because a
44
+ vitest run with no filter runs the whole include. The literal cross-check against
45
+ `vitest.config.ts` stays, because the shell cannot read vitest's resolved config and a deleted
46
+ glob would otherwise pass. Proven by three still photos in one public `pnpm check`: two failing
47
+ beside-behaviour tests turn it red (`selected 2`), and a third file planted under `test/` is not
48
+ selected.
49
+ - **`entwurf pi`** (#118 hop 2) — `exec pi --entwurf-control "$@"`, a dispatcher case with no new
50
+ bin. Two things it deliberately does NOT do, both measured on pi 0.85.1: there is no
51
+ precondition check, because an unregistered host already gets `Error: Unknown option:
52
+ --entwurf-control` and exit 1 from pi itself; and the flag is added, never deduplicated, because
53
+ passing it twice was measured byte-identical to passing it once. One recursion fence, not
54
+ copilot's two — the sentinel closes the real loop, and the case copilot's second fence covers is
55
+ measured here to be a fail-closed unknown verb rather than a loop. `check-pi-launch` reuses
56
+ `check-copilot-launch`'s fake-vendor posture and carries the `--entwurf-control` literal
57
+ cross-check, since the shell cannot import `ENTWURF_CONTROL_FLAG`. Nine mutants, each killed by
58
+ exactly its own claim; exit-status passthrough is asserted without one, because `set -euo
59
+ pipefail` couples it to `exec` and a mutant that kills two claims attributes to neither.
60
+ - **`smoke-herdr-raw-install-live`** (#118 hop 1) — the plugin's first user path, on a clean
61
+ `node:24` container with no host socket/config/cache mounted, no git `insteadOf`, and the public
62
+ remote spelled as a user would type it. It closes the one npm re-proof axis VERIFY.md demanded
63
+ and the production lock had left empty since `dd84ac0`. Receipts: **(a)** `--ref main` →
64
+ `37b81e725cde4d0a548f1c0faab4fcb5c62942b2`, **(b)** `--ref set/119-verify-herdr` →
65
+ `11ec0c38af36d5cd4a59614c3c8e07c5b5b0f9ae`, both `kind=npm @junghanacs/entwurf@0.23.0` with the
66
+ ledger widening `["pi"]` → `["pi","claude-code"]` and the pi wiring byte-identical across the
67
+ reinstall. Three facts it measured rather than assumed: herdr's offline-persist path carries
68
+ 0.9.1, so `plugin install --yes` completes with no server; `herdr integration install` refuses a
69
+ harness that has never run, because it creates `~/.pi/agent/extensions` only when the parent
70
+ exists and refuses Claude without `~/.claude`; and `--ref` takes a REMOTE REF, not an arbitrary
71
+ commit (`--ref <sha>` fails as `couldn't find remote ref`).
72
+
73
+ ### Changed
74
+
75
+ - **`plugins/herdr` 0.3.0 — the Install path, rewritten as the nine steps a raw box needs.** The
76
+ eighth and ninth are the ones nobody guesses and they come from the container measurement above:
77
+ each harness must be started once (`pi --help`, `claude mcp list`) before herdr will integrate
78
+ it. The release says what it does NOT give you, because the honest combination is this plugin
79
+ plus the runtime its lock still names: `entwurf pi` exists in the repository and is not in
80
+ `@junghanacs/entwurf@0.23.0`, so the user command stays `pi --entwurf-control` until the next
81
+ cut carries the launcher. **`runtime-lock.json` is untouched on purpose** — a pin can only move
82
+ after the version it names is published, so the 0.23.1 pin is a follow-up commit after that
83
+ release, the same two-step `dd84ac0` took for 0.23.0.
84
+ - **Thirteen v2-spine gates moved out of `scripts/` and beside the modules they certify** (#119
85
+ V3), assertion for assertion, with the inventory's denominator fixed FIRST so the move reads as
86
+ a move. `inventory-verification-surface.ts` now counts both vitest locations; without that, the
87
+ migration would have subtracted 2,799 lines from the one number that exists to make migration
88
+ honest. What actually left is 133 lines of plumbing — per-file `ok()` scaffolding, `main()`
89
+ wrappers, summary trailers — and the combined file count is unchanged, seven out and seven in
90
+ per slice.
91
+ - The six mutant-carrying lanes needed four rules the mutant-free seven did not, each learned by
92
+ getting it wrong: a QK claim must BE an `it` title (attribution reads the failed test title);
93
+ the token must appear exactly once per file (title keeps the token, assertion keeps the
94
+ sentence); the `run.sh` case survives as a mutant EXECUTION COORDINATE and keeps its
95
+ `check:contracts` entry (branch CI caught the removal — a gate a mutant names must run inside
96
+ `check:full`); and attribution is containment, not exclusivity (vitest runs every test, so one
97
+ mutation surfaces 1–15 failed titles). 25/25 mutants verified killed and attributed by hand;
98
+ 733 mutants across 58 lanes unchanged, `signatureSource` moved, gate argv did not.
99
+ - `entwurf-v2-visible-resume.test.ts` grew 445 → 908 lines and the growth is the design:
100
+ seventeen claims that shared four drives of the subject now each replay their own, because
101
+ nearly every one is an "X happened BEFORE Y" reading a call-order trace, and a trace polluted
102
+ by a neighbouring test answers wrongly while staying green.
103
+ - Three gates stay under `scripts/` with a reason: `check-entwurf-v2-contract` and
104
+ `check-entwurf-v2-production` are cross-lane (`mux-boundary.json` and `codex-native.json` call
105
+ them as gate argv), and `check-entwurf-v2-surface` generates its seven QK labels inside shared
106
+ helpers called twice from `await` blocks, so the established method would either lose
107
+ attribution or require an async `describe`.
108
+
109
+
110
+ - **The bundled Claude ACP adapter is `@agentclientprotocol/claude-agent-acp` 0.79.0** (from
111
+ 0.76.0, crossing 0.77.0/0.78.0/0.79.0; `@anthropic-ai/claude-agent-sdk` 0.3.257 → 0.3.274,
112
+ ACP wire SDK 1.4.0 and `@anthropic-ai/sdk` 0.100.1 unchanged). Three upstream releases, three
113
+ changes with a reachable shape, and **no entwurf code change** — each "inert" is a fresh
114
+ measurement, not the previous bump's argument reused:
115
+ - 0.77.0's BREAKING removal of `claudeCode.options.agent` misses us twice: our
116
+ `buildClaudeSessionMeta` never set that key, and we spawn the adapter's binary rather than
117
+ importing the removed agent-picker exports (repo-wide grep: 0 hits).
118
+ - 0.77.0's `allowDangerouslySkipPermissions` host opt-out is a NEW lever we do not pull. We set
119
+ the option nowhere, `ALLOW_BYPASS` is byte-identical across both versions, and the overlay's
120
+ `permissions.defaultMode: "bypassPermissions"` resolves the same under each, so a sibling's
121
+ effective permission mode is unchanged.
122
+ - 0.78.0's `compaction_update` / `compaction_summary_chunk` are the first new `sessionUpdate`
123
+ kinds since the pin moved, and they are gated off by our `clientCapabilities: {}` — with the
124
+ mapper's forward-compatible `default` arm as an independent second reason. The `stopReason`
125
+ axis, where "unknown is an error" actually applies, is unchanged.
126
+ - 0.79.0's shell-command permission prompts reorder the option array reject-first under the new
127
+ `defaultToNo` hint. Our approve-all policy selects by option KIND, not position, and every
128
+ upstream option builder emits an allow option, so the order-sensitive fallback is unreachable.
129
+ Re-measured and unchanged: the #96 `mcpServerStatus` evidence (2 call sites, now at
130
+ `v0.79.0 src/acp-agent.ts:1773`/`:1866`, surrounding 200-line window byte-identical) and all four
131
+ model-forcing/accounting wire calls (`setSessionConfigOption`, `sessionUsage`, `turnQuotaMeta`,
132
+ `resolveModelPreference` — byte-identical). Full per-change classification with evidence state:
133
+ [ROADMAP.md](./ROADMAP.md) 2026-09-18 bump entry and
134
+ [docs/acp-backend-rail.md](./docs/acp-backend-rail.md) capability posture.
135
+
136
+ - **The Herdr plugin is 0.2.0 and pins its runtime to npm `@junghanacs/entwurf@0.23.0`.** Its
137
+ committed lock carries the published sha512
138
+ `sha512-ZR2VCui7JjK3w56rQSDs3AuAJMMuiXCNWH7HB52SQ3E/7p0oPhcxD+fb6Gdzi0VcBnheqxPzvJHMPQQcdYtNiw==`;
139
+ the 0.1.0 `herdr-checkout` source remains the candidate-verification path.
140
+
141
+ ## 0.23.0 - 2026-09-18
142
+
143
+ ### Added
144
+
145
+ - **`entwurf_fresh_call` opens a sibling on the herdr rail, beside the tmux one (#116).** A session
146
+ running inside [herdr](https://github.com/steipete/herdr) has no tmux server to place a window
147
+ on, so the rail is chosen from PROCESS CONTEXT — `HERDR_ENV=1`, herdr's own marker, read for its
148
+ exact value — and never from a caller parameter, which would let someone request tmux placement
149
+ from inside herdr and fail after a mutation instead of before one. There is **no fallback either
150
+ direction**: an incomplete herdr context is refused by its own herdr reason rather than quietly
151
+ opening a window the operator cannot see from where they are standing. The pilot backends are
152
+ `pi` and `claude-code`; `placement` is refused BY NAME there, because placement inside herdr
153
+ belongs to herdr and a tmux session name would silently relocate the sibling.
154
+ Coordinates, refusals, reclaim and evidence grades: [docs/herdr-launch-rail.md](./docs/herdr-launch-rail.md).
155
+ - One composition root (`fresh-call-dispatch.ts`) now owns rail choice, the Codex preflights and
156
+ the render call for BOTH public surfaces, so a defect planted in one surface can no longer
157
+ survive on the other. The rail-neutral half of the composition (argv dialects, callback tool
158
+ names, first-turn framing, nonce, input contract) moved to `fresh-call-composition.ts`, which
159
+ knows nothing about tmux; each rail supplies only what it alone can state.
160
+ - A sibling is placed in a NEW TAB of the caller's own herdr workspace, without taking focus,
161
+ with the workspace read from herdr's answer about the caller's own pane — never the focused
162
+ workspace and never the `w<N>:` prefix of an id this rail treats as opaque. Orphan reclaim is
163
+ proven within a generation and closes the PANE rather than the tab (`[측정 2026-09-15]`).
164
+ - `entwurf_peers` carries a read-only `placement` column with four words
165
+ (`unobserved` / `none` / `ambiguous` / `herdr <pane>`), joined only on an official herdr report
166
+ for its own backend. A pane coordinate is a VIEW, never an address: the garden id still arrives
167
+ in the callback envelope (Hard Rule 16).
168
+ - **A herdr plugin that installs entwurf onto a host where nothing entwurf is on `PATH`.**
169
+ `plugins/herdr/` bootstraps a stable, scoped plugin runtime from an exact checkout candidate,
170
+ classifies which integrations the host already has, activates only what it owns, and ships a
171
+ read-only citizen status pane built on the core's own `peer-facts` projection. The install
172
+ reports NAMED steps on the operator's terminal and ends with one usage example per activated
173
+ backend, because `[GLG 직접, 2026-09-17]` "정확한 모델명을 모른다" was a real blocker on a clean
174
+ install. Entwurf still installs only Entwurf bytes — never a harness, subscription or login
175
+ (Hard Rule 17) — and a clean NixOS host additionally needs `programs.nix-ld.enable = true` for
176
+ the ACP rail's dynamically linked vendor binary.
177
+
178
+ - **A gate keeps the typing-call boundary by NAME, not by prose alone.** `[외부 관측 2026-09-17
179
+ 19:35]` an outside reader measured this tree and found the law perfectly kept and nothing keeping
180
+ it: zero typing calls across `pi-extensions/`, and zero checks and zero mutants that would notice
181
+ one arriving. Gates here grow on top of incidents, and this boundary was named and REFUSED at
182
+ design time (`docs/herdr-launch-rail.md` §3 threw the keystroke verb away before it shipped), so
183
+ it never produced the failure that would have produced a test — and a rule kept since day one is
184
+ the most defenceless kind, because its green means only that nobody has broken it yet.
185
+ `check-typing-call-fence` scans every TRACKED AND UNTRACKED production source under
186
+ `pi-extensions/`, `mcp/`, `plugins/herdr/` and `scripts/herdr-*.mjs` for the names of typing calls
187
+ — tmux `send-keys` / `paste-buffer` / `load-buffer` and herdr's PTY verbs `agent.prompt` /
188
+ `agent.send_keys` / `pane.send_text` / `pane.input.set` — with comments blanked and string
189
+ literals KEPT, because a typing call spelled as a string is the call.
190
+ - The comment exemption is load-bearing rather than a convenience: the sentence that states the
191
+ rule contains the very names the rule forbids, so a scanner that read prose would be red on the
192
+ law itself and the cheapest way to green would be deleting the law.
193
+ - Three assertions stand in front of the claim for the same reason — the detector is proven to SEE
194
+ each name planted in code, proven BLIND to the same name in a comment, and the scanned inventory
195
+ is measured, because without those "zero hits" and "looked at nothing" print identically.
196
+ - **What it does not catch is named in its own header rather than implied:** it reads SPELLINGS,
197
+ so a verb assembled at runtime passes it. That limit is accepted rather than closed. This is a
198
+ tripwire for the slip — a module reaching for the vendor's typing verb because the argv route
199
+ looked harder that afternoon — not a defence against somebody hiding one, and a gate claiming to
200
+ be the latter would be the more dangerous object. The claim therefore says CARRIES a typing call
201
+ spelled out, which is what it measures.
202
+ - `scripts/` stays out as a whole: this gate lives there, and a gate that scanned itself could
203
+ only be made green by deleting the names it exists to forbid. Wired into `check:hermetic`;
204
+ 8 checks over 85 production sources, one exact-once mutant.
205
+ - **The framing OFFERS its corroboration in a spelling the sibling can actually use.** The callback
206
+ tool name goes through a per-backend dialect map; the peer listing the first turn offers did not,
207
+ and four of the five backends reach that tool under a composed MCP name. The offer was unusable by
208
+ exactly the siblings it was written for — and it was written to replace a prohibition, removed
209
+ because a sibling should be able to check who called it. `FRESH_CALL_PEERS_TOOL` is the same
210
+ dialects applied to the other tool, so the two maps drift together or not at all.
211
+
212
+ ### Fixed
213
+
214
+ - **`smoke-codex-fresh-live` now directs Codex to the callback sender garden id by the envelope's `session:` line.** In the first 0.23.0 retry Luna copied the model label from `from:` instead; the red receipt is `/tmp/entwurf-release-gate-0.23.0-retry.17pfMp`.
215
+
216
+ - **A Claude child that could not see its callback tool now knows how to load it.** The framing said
217
+ "FIRST ACTION … call `entwurf_v2`" to a child for which that tool was not callable yet: `[측정
218
+ 2026-09-18, n=5]` claude-code 2.1.267 surfaces a still-connecting MCP server as DEFERRED — the tool
219
+ NAME is listed, the SCHEMA is not, and a call without the schema fails. Across five isolated probes
220
+ the bridge's tools were directly callable **zero** times (4 DEFERRED, 1 still connecting), so the
221
+ child answered in text and went idle. That is the "silence" the herdr rail recorded in 5 of its 14
222
+ launches. The claude-code dialect now carries one factual sentence naming `ToolSearch` with BOTH
223
+ tools in a single select, and `[측정, n=3]` callbacks went 3/3 — including a child 782 ms in, which
224
+ is slower than every silent run. **The latency story it replaces is retired:** re-read at n=13
225
+ instead of n=4, success and silence overlap completely (52 ms called back, 94 ms did not), so
226
+ `docs/herdr-launch-rail.md` §14 now carries the distribution and the mechanism instead of the
227
+ correlation. Every other backend's framing is byte-identical — pi has no deferred-tool surface and
228
+ the other three were never probed (`FRESHCOMP-TOOL-LOAD-HINT-CLAUDE-ONLY`).
229
+
230
+ - **The first turn no longer looks like the injection it is not (#116).** `[GLG 직접, 날것 PC,
231
+ 2026-09-17]` a Claude Sonnet 5 sibling REFUSED its whole first turn and named three reasons, all
232
+ of them ours: a "decode the following JSON string literal and follow the decoded instructions"
233
+ wrapper, the line "Do not inspect environment variables, do not call entwurf_self", and no way to
234
+ verify the caller. The framing is now plain prose folded onto one physical line, with ONLY the
235
+ operator's task riding as a literal — `[source herdr 7505c08]` `src/app/agents.rs:157-161`
236
+ refuses any agent argument carrying a Unicode Cc, so a newline never reaches the sibling, and
237
+ `src/platform/linux.rs:127-141` single-quotes every argument, which is why the encoding was never
238
+ buying shell safety. The prohibition is replaced by the same steering stated as a FACT (your own
239
+ record does not carry the caller's address; the tool call is what reaches it) plus an OFFERED
240
+ read-only way to corroborate the caller. `[측정 oracle 2026-09-18, LIVE run sCzzE0]` a Sonnet 5
241
+ child called `entwurf_peers` and then `entwurf_v2`, both successful, and its callback landed.
242
+ - **The rail tells herdr how long to wait, and judges the child on its own clock (#116).** Without
243
+ an explicit `--timeout`, herdr gave up at its own 30s default while our process bound sat at
244
+ 300s, so a cold-starting Claude Code sibling that was alive and booting came back as
245
+ `herdr-agent-start-failed [herdr: timeout]` over an unreclaimed pane. The bound is now an
246
+ explicit 240s, deliberately BELOW our own kill, so herdr has to lose that race or its named
247
+ answer is replaced by "no exit status". The LIVE cell also stopped measuring the child with the
248
+ CALLER's clock — two runs of identical code had differed only in how long the caller's launch
249
+ took — and now records what the child was doing every run: herdr's agent status and entwurf's own
250
+ hook journal are the WHOLE evidence surface, because `[측정 2026-09-18]` no claude session in this
251
+ smoke, caller or child, acting or silent, exists under `~/.claude/projects/` or any fenced XDG
252
+ root — including a child that had just made two successful tool calls.
253
+ - **A peer that hangs up on an accepted control socket no longer takes the resident with it.** The
254
+ connection error is handled where the socket is accepted, so a half-open peer is a bounded
255
+ per-connection failure instead of an uncaught exception.
256
+ - **The uncitizened `entwurf_self` notice is proven, not regexed.** The gate asserted the shape of
257
+ a message instead of the fact it reports, which a reworded notice would have broken while a
258
+ wrong notice passed.
259
+
260
+
261
+ - **The Codex birth unit owns its own declaration, not the whole `hooks.json` (#117).** Installing
262
+ Herdr's official Codex integration appends a second `SessionStart` group to
263
+ `$CODEX_HOME/hooks.json`. The vendor kept running BOTH declarations — `[source]`
264
+ `codex-rs/hooks/src/engine/discovery.rs:664-665` hashes a normalized event/matcher/group/handler
265
+ and keys trust by `<path>:<event>:<group_idx>:<handler_idx>`, so trust was declaration-scoped all
266
+ along — but entwurf's certification was file-scoped: `install-state` recorded a whole-file
267
+ sha256 and the fresh preflight additionally required `SessionStart.length === 1`. On a host with
268
+ both installed, `doctor-codex-birth` was RED and every Codex fresh launch was refused
269
+ `codex-birth-unit-missing`. **v0.22.0's release LIVE was measured with Herdr's hooks moved
270
+ aside; this is the debt that names paid.**
271
+ - Ownership is now exactly one `SessionStart` group holding one handler whose `command` is our
272
+ quoted launcher path. It is certified by SHAPE (which keys may exist) and by a NORMALIZED
273
+ digest (recursively sorted keys, no whitespace) — blind to indentation and key order, because
274
+ `[측정 2026-09-17 oracle]` Herdr re-serializes the entire document through serde, which changed
275
+ our own handler's key order from `type,command,timeout` to `command,timeout,type`.
276
+ - The vendor trust receipt is read at the index our declaration was MEASURED at, never at the
277
+ constant `:0:0`. With a neighbour at index 0 that constant read THEIR approval and reported it
278
+ as ours — a green for a hook the vendor had never been asked to run.
279
+ - Neighbours are `present-but-foreign`: reported in the doctor's own FOREIGN section, certified
280
+ by nothing, never overwritten or absorbed, and no input to any verdict of ours. Editing one
281
+ moves nothing.
282
+ - Install into a file somebody else already declares in is an APPEND by text splice (never an
283
+ adoption and never a refusal), and appending rather than inserting leaves a neighbour's index —
284
+ and therefore their existing trust receipt — alone. Uninstall splices out only entwurf's group
285
+ (plus entwurf's own top-level `description`), removing the file only when entwurf was its sole
286
+ declaration. Every splice is re-parsed and deep-compared to the value it intended before it
287
+ reaches disk; a splice that lands anywhere else is a zero-write refusal.
288
+ - Our own declaration missing, edited or DUPLICATED is still a named red, in install, doctor,
289
+ inverse and preflight alike.
290
+ - Entwurf still never creates, computes, pre-seeds or bypasses a vendor trust receipt, and
291
+ deliberately does NOT recompute what that hash should be: the vendor normalizes with its own
292
+ code, and reimplementing it here would make entwurf a second opinion about somebody else's
293
+ security decision that drifts with every vendor release.
294
+ - `install-state` moves to `codex-birth-install-state/v2` (declaration receipt instead of
295
+ `hooksSha256`). The uninstall, doctor and preflight read v2 only and refuse v1 by name; the
296
+ installer reads a v1 receipt once, to supersede it forward — it removes nothing and rewrites no
297
+ foreign byte.
298
+ - New shared leaf `pi-extensions/lib/codex-declaration.js` is the single definition the
299
+ installer, inverse, doctor, fresh preflight and gates all decide with.
300
+ - Gates: `smoke-codex-birth` 102 checks (both orderings green, foreign bytes proven across
301
+ install and inverse, foreign edits neutral, own-declaration drift/duplication/v1 named red,
302
+ trust read at the measured index, the shared file's mode carried over, and an unscanned
303
+ FOREIGN axis reporting NOT READ rather than "none"), `check-codex-birth-hook` 101 checks (the
304
+ leaf's digest, selection, named refusals, span reader and splice post-condition), six new
305
+ preflight cells, and 8 exact-once mutants (inventory 520 → 527). Six existing
306
+ `FRESHCALL-CODEX-*` mutants were re-pointed at the predicates this narrowing moved;
307
+ `FRESHCALL-CODEX-HOOK-KEYS` moved gate as well, because under the preflight's oracle an extra
308
+ handler key is caught by the digest anyway — what its predicate still buys is the NAME.
309
+ Verification receipts at `ac3f1f8`: `pnpm run check:full` exit 0 (403s) and
310
+ `check-gate-qualification` **527/527 killed**, both in the exact-SHA CI run
311
+ [35239086438](https://github.com/junghan0611/entwurf/actions/runs/35239086438).
312
+ - **Not in scope, by decision:** adding Codex to `HERDR_FRESH_CALL_BACKENDS`, Herdr env carriers
313
+ on the Codex MCP child, plugin supervision of the app-server, and the identical ownership
314
+ defect in the statusline/terminal-title atoms (recorded as an Observation, its own atom).
315
+
316
+ - **A sibling herdr has not been introduced to yet is no longer killed for it.** `[측정 oracle
317
+ 2026-09-18, LIVE run XimC19]` a Claude Code child booted, wrote its own birth record at
318
+ 01:53:38.297Z, and 0.585s later this rail closed the pane it was sitting in. The same bytes passed
319
+ five minutes later (run TCatjF) because that child took 1m27s to become ready instead of 4.9s — so
320
+ the FAST path was the dangerous one, and every green run before it was luck rather than proof.
321
+ - The premise was that `agent start` waits for detection before returning. It does not:
322
+ `[file:line @ c77af189]` `src/cli/agent.rs:592-615` returns on `agent_status ∈ {idle,done}` plus
323
+ `interactive_ready`, and `agent_session` is not in that condition at all. On the Claude rail that
324
+ id arrives afterwards from the CHILD's own one-shot hook
325
+ (`src/integration/assets/claude/herdr-agent-state.sh:60-99`, 0.5s socket deadline, failures
326
+ swallowed), so "herdr has not been told the id" is a reachable resting state for a healthy
327
+ sibling.
328
+ - `settleAgentWitness` re-READS instead of judging on the spot: `agent get <the exact name we
329
+ started>`, never a listing, bounded and rebound to the create receipt every time. An unreadable
330
+ or failed read costs one poll, because herdr answers a missing agent and a socket that blinked
331
+ with the same exit status.
332
+ - **Expiry buys nothing.** The window closing is a SUCCESSFUL launch carrying a
333
+ `witness: unavailable` diagnosis that says what it costs (the peers `placement` column has
334
+ nothing to join) and what it does not (the address was never a pane). The old
335
+ `witness-missing → reclaim` path is gone; the one failure this stage can still name is a
336
+ readable reply about a DIFFERENT agent, `herdr-agent-start-vanished`.
337
+ - The reclaim predicate now reads OCCUPANCY, not the session id: a pane herdr says an agent is in
338
+ is never closed, whether or not anybody has reported that agent's session. Collapsing those two
339
+ facts is what made a booted child read as an empty pane, and it would have survived in every
340
+ other post-create failure branch if only the witness path had been patched.
341
+ - The settle bound is a PROPOSAL and says so in its own comment: the red run still had nothing
342
+ 585ms after the child's birth record, the reporter's own deadline is 500ms, and the slow run's
343
+ 85s was a race being hidden rather than a ceiling.
344
+ - **The Codex birth unit decides who owns the shared `hooks.json` ONCE, for all four surfaces.** The
345
+ fresh-call preflight required a plausible owner and refused a group/world-writable mode; the
346
+ installer and the inverse asked only symlink-and-regular; the doctor read the file's CONTENT
347
+ without ever asking who owned it. A host could install clean, read GREEN, and have every Codex
348
+ launch refuse it as `codex-birth-unit-missing` — three surfaces saying yes about bytes the fourth
349
+ said no about. `classifyOwnedPath` now lives in the shared declaration leaf all four already load,
350
+ and it is PURE: it judges a stat record, not a path. A neighbour's MODE stays theirs (the install
351
+ still carries it over untouched); WRITING into a file anyone else can rewrite is the different
352
+ question, and the answer there is a zero-write refusal, because a receipt cannot bind bytes
353
+ somebody else controls. Symlink keeps its own verdict — the repair is not a mode change, it is a
354
+ different file.
355
+ - **The inverse's FILE verdict earns its delete.** It could remove a `hooks.json` this unit never
356
+ created: the arm asked one question — is our group the last declaration — and that is true of a
357
+ file we merely APPENDED to. `{"description":"foreign","hooks":{"SessionStart":[]}}` has no foreign
358
+ GROUP, so after our install it classified FILE and the whole document went, a neighbour's
359
+ top-level bytes included. The receipt had recorded `hooksExistedBefore` since the installer was
360
+ written and nothing read it; it does now, with two more proofs beside it (the top-level keys are
361
+ exactly the two this unit writes, and the description is ours). Anything else SPLICES. An absent
362
+ field reads as "it was already there", so an older receipt licenses a splice and never a delete.
363
+ - **The herdr LIVE oracle joins the callback it claims to have proved.** Its pi cell compared
364
+ CHARACTER OFFSETS across the whole transcript, and both strings are already in the birth prompt in
365
+ that order — so "called back, then worked" and "never called back" read identically. It now wants
366
+ ONE record carrying the delivered nonce and the `sent` outcome together, with the task token in a
367
+ later one. Its claude cell asserted only that the first `entwurf_v2` completed, which a call
368
+ delivering somebody else's nonce also satisfies; the join it can actually make is across two
369
+ independent artifacts — the delivered message carrying the exact nonce and the child as sender,
370
+ enqueued inside that call's own window.
371
+ - **Three Codex branches and one peers read stop being claims nobody tests.** An independent
372
+ coverage audit found the whole production path behind the peers `placement` column — env gate,
373
+ binary execution, timeout, parse failure — with zero assertions and zero mutants, so a typo in
374
+ `["pane","list"]` or a deleted timeout would have left the deterministic floor green. It has two
375
+ claims now, driven through a stub PROCESS (not a fake herdr: every answer-shaped fact still comes
376
+ from the verbatim 0.9.0 recordings, and what the stub supplies is an exit code, a hang, an
377
+ absence). The doctor's ORPHAN verdict, the inverse's SPLICE arm and its FILE arm gained the
378
+ mutants their behaviour cells never had, and the herdr rail's documented cwd asymmetry — `#` is
379
+ DATA here, because herdr has no format expansion for the tmux refusal to protect against — gained
380
+ a cell that drives a real directory through to the argv.
381
+
382
+ - **The herdr rail's two-step `agent prompt` proposal is closed at the VENDOR SOURCE, not with LIVE
383
+ runs — and not implemented.** §14 proposed starting the agent with no prompt and sending the first
384
+ turn with `herdr agent prompt`, which would remove the tool-list race. The question standing in
385
+ front of "does the child call back" is what that verb SENDS, and the vendor answers it:
386
+ `[source herdr 7505c08]` `api/agents.rs:195` hands the text to `encode_api_submission_parts`,
387
+ `api_helpers.rs:25-32` wraps it in bracketed paste and writes it to the child pane's PTY, and
388
+ `:13` / `:208-212` write an encoded Enter 300 ms later. It runs NO control-character check — only
389
+ a non-empty one, unlike the argv path — so whether a newline pastes or submits is decided by the
390
+ child terminal's paste mode at that instant rather than by a contract, and what is acknowledged is
391
+ that input was written (the vendor's own help: "before any input is sent", "It does not track
392
+ turns"). So the two-step is the keystroke channel §3 already measured and refused, and adopting it
393
+ would put screen input on the FIRST turn — exactly what Hard Rule 16 keeps out of delivery
394
+ evidence. The three LIVE runs budgeted for it were NOT spent characterising a path that cannot
395
+ ship. The remaining legitimate direction for the race is the framing that stands before the tool
396
+ list, the same axis as omp's `--entwurf-bootstrap`.
397
+ - **Two mutant claims the #116 merge moved now aim at what they were about.** Replaying every lane
398
+ the merge touched answered 484/486, and both refusals were the merge's own bookkeeping.
399
+ `FRESHCALL-CWD-SURFACE-PARITY` SURVIVED because the regenerated find was extracted from the FIRST
400
+ `cwd: z` block in the bridge — which belongs to `entwurf_register_native`, not
401
+ `entwurf_fresh_call` — so the mutation deleted a required cwd from a DIFFERENT tool and nothing
402
+ failed; both parity finds are now extracted from inside the `fresh_call` registration span.
403
+ `FRESHCALL-CWD-CALLER-ONLY` came back WRONG-REASON because the mutation now lands in the
404
+ composition leaf while the cell carrying its signature read only the tmux module, so three other
405
+ cells failed instead and the kill could not be attributed.
406
+
407
+ - **The first turn says where the result goes.** `[GLG 직접, 2026-09-18]` a fresh sibling finished
408
+ its task, printed the answer in its own window, and the caller never saw it. The investigation
409
+ found no broken injection: NEITHER framing, the retired one or the current one, had ever said
410
+ this. It used to work because a hand-written briefing named the target and the tool, and when the
411
+ briefing stopped doing that the sentence turned out to live nowhere. The framing now ends with two
412
+ lines — where to send the result (the callback tool, in the same backend dialect) and why sending
413
+ is needed at all, because a sibling with a visible window has no way to know its window is not the
414
+ delivery. It is TOPOLOGY, not a command: nothing watches for completion or sends on the sibling's
415
+ behalf, and a supervisor is what this rail refuses to be.
416
+ - **A LIVE smoke stops taking the operator's `claude` launcher** — #67's failure on a rail born
417
+ after its fence. The vendor resolves the two halves of its install from different roots: the
418
+ version store from `XDG_DATA_HOME`, the launcher from `HOME`. This smoke fenced XDG while keeping
419
+ the real HOME, so each Claude child found an empty store beside a real launcher, installed itself
420
+ into the fixture, and repointed `~/.local/bin/claude` at `<fixture>/claude/versions/2.1.267`.
421
+ **That is not a hazard, it happened** — seven preserved fixture roots each hold that install, and
422
+ the operator's launcher pointed into the newest of them, one `/tmp` sweep away from a `claude`
423
+ that does not start. The fixture's `xdg-data/claude` is now a symlink to the operator's vendor
424
+ data dir, which puts store and launcher back in one install while the rest of the tree stays
425
+ fenced, and the smoke consumes the shared fence: fail-closed preflight, integrity oracle in
426
+ teardown, cleanup verdict. The gate side stopped naming files — `LAUNCHFENCE-EXPOSED-SMOKE-WIRED`
427
+ asks the POPULATION (every LIVE smoke that assigns a fixture `XDG_DATA_HOME` either consumes the
428
+ fence or carries an exemption verified against its own source: four today, three wired, one
429
+ exempt), because the two claims that listed smokes by name are exactly what the newest rail walked
430
+ past. `[LIVE 2026-09-18]` the first run in eight to leave zero version copies behind.
431
+ - **The herdr LIVE ordering oracle reads the join pi actually writes.** The claim asked for ONE
432
+ transcript record carrying both the delivered nonce and the `sent` outcome; pi cannot write that
433
+ record, because the nonce rides the toolCall and the outcome rides a separate toolResult, joined
434
+ by `toolCallId`. So the oracle failed a run whose child did exactly what the claim describes
435
+ (callback 04:15:01.381, `sent` .415, task token .03.728) — the most expensive kind of red: correct
436
+ product, red gate, and a cut that stops for nothing. The join was what the claim was always about:
437
+ the `sent` outcome must belong, by that id, to THE call that carried THIS nonce, and the task
438
+ token must appear after it. Proven on the failing transcript — the real run holds, a forged pair
439
+ whose `sent` belongs to a different call does not, and a run whose token appears only before the
440
+ callback does not.
441
+
442
+ ### Changed
443
+
444
+ - **The two tool descriptions say what the code does.** `entwurf_v2` no longer offers `codex` as an
445
+ example of a backend with no adapter: Codex is a native-push backend, and that sentence pointed a
446
+ caller at the wrong dispatch outcome. `entwurf_fresh_call` says it opens a NEW UNFOCUSED TAB in
447
+ the caller's own herdr workspace rather than "a herdr pane" — the contract has been tab-first
448
+ since it shipped. Both surfaces lost the top-level cwd sentence the `cwd` parameter description
449
+ already owns, which is also what kept them inside the 2048-char host cap.
450
+ - **The herdr rail document describes the first turn that actually ships.** §3 still recorded the
451
+ retired shape — the whole framing wrapped in one JSON literal behind "decode the following" —
452
+ which a Sonnet 5 sibling refused outright. The current shape is plain prose with only the
453
+ operator's task as a literal, and the section now says why: the encoding never bought shell
454
+ safety (herdr single-quotes every argument), and what is left is the vendor's control-character
455
+ refusal, so only the thing that must carry newlines is encoded.
456
+ - **`ROADMAP.md` and `docs/setup-clean-host.md` stop calling accepted work pending.** The Codex
457
+ caller-seat and caller-DIRECTORY re-runs were accepted on 2026-09-16 with 56 and 65 assertions;
458
+ both documents said "pending", and the roadmap's current-package line still read 0.20.1.
459
+ - **`plugins/herdr/README.md` separates an admission floor from a measured supply.** Herdr
460
+ `>=0.9.0` is the manifest floor; Entwurf's reproducible and CI rail is pinned to exactly 0.9.1,
461
+ and a different Herdr inside the admission window carries no receipt of ours. Its install-output
462
+ example uses `<version>` / `<full-commit>` placeholders instead of a frozen `0.21.0` and a frozen
463
+ commit that no longer reproduce. Both READMEs now name the two installation routes and link to
464
+ each other.
465
+
466
+ ### Observations — named rather than carried
467
+
468
+ These were raised by the pre-cut review round and deliberately NOT acted on in this release. Each
469
+ one is recorded here so the next lane inherits the finding instead of rediscovering it.
470
+
471
+ - **The vendor trust receipt is bound by KEY, not by VALUE.** `doctor-codex-birth` and the fresh
472
+ preflight read the receipt at the index our declaration was measured at and check that its value
473
+ has the shape `sha256:<64 hex>` — they do not check that the hash is the one the vendor would
474
+ compute for the CURRENT declaration, so a stale or arbitrary well-shaped value reads as ready.
475
+ Closing it would mean reimplementing the vendor's normalization here, which is exactly the second
476
+ opinion about somebody else's security decision that Hard Rule 9 and the doctor's own prose
477
+ refuse. The live failure mode is bounded and visible: a stale receipt makes the vendor show its
478
+ consent screen again to the human sitting there, and an unattended launch reads as a callback
479
+ timeout.
480
+ - **The typing fence does not detect an assembled typing call** — see its header, and the entry
481
+ above.
482
+ - **The declaration span fixtures do not pin CRLF, BOM or deeply nested arrays by name.** The parser
483
+ and its splice post-condition handled all three correctly in a direct memory-only sample; what is
484
+ missing is a labelled cell, and BOM in particular would read better as an explicit refusal than as
485
+ an incidental one.
486
+ - **Four more coverage gaps were enumerated and left for their own lane:** the uninstall's v1-state
487
+ refusal has a behaviour cell but no mutant of its own, the `trustReceiptKey` composition is
488
+ asserted separately by the doctor and the preflight rather than jointly, and two spawn-seam
489
+ sub-claims were deliberately not split into their own QKs.
490
+ - **The Claude child's `--allowedTools` names only the callback tool**, so the corroboration the
491
+ framing offers is not on the auto-approved list. Measured behaviour is unchanged by this release
492
+ (the bare spelling was not on it either), and widening auto-approval is a decision rather than a
493
+ repair.
494
+ - **`doctor-codex-birth` FOREIGN 축은 이웃 선언의 trust 부재를 판단하지 않지만 vendor 동의 화면은
495
+ 파일 단위로 첫 턴을 막는다.** `smoke-codex-fresh-live` 전제조건은 폴더 trust만 이름짓고 hook
496
+ trust는 timeout으로 읽힌다 — 컷 후 atom.
497
+
498
+ ### Verification
499
+
500
+ - **Land exact-SHA CI.** Push run
501
+ [`35312087142`](https://github.com/junghan0611/entwurf/actions/runs/35312087142) at
502
+ `09aea576a80983709a7e2bb7593d6fb546234eee` concluded green for `check`,
503
+ `install-surface`, `artifact-consumer`, and `macos-install-surface`; the `check` job's
504
+ `check-gate-qualification` body also concluded green.
505
+ - **LIVE Codex acceptance is a focused rerun, not a final `release-gate --cut` exit 0.** First cut
506
+ attempt `/tmp/entwurf-release-gate-0.23.0.Ire9Nr` was red because the vendor's Herdr hook trust
507
+ receipt was absent; the operator answered the vendor consent screen once. Retry
508
+ `/tmp/entwurf-release-gate-0.23.0-retry.17pfMp` recorded `MUST: PASS=23 FAIL=1 SKIP=0`; its only
509
+ red cell was `smoke-codex-fresh-live`, where Luna copied the model label from the callback
510
+ envelope's `from:` line. The frozen-candidate delta afterwards is exactly the two instruction
511
+ sentences in `scripts/lib/codex-fresh-live-protocol.ts` plus these CHANGELOG records. After
512
+ `pnpm run build-bridge` and `./run.sh check-bridge-delivery` (19 PASS), the repaired standalone
513
+ `smoke-codex-fresh-live` passed 66 assertions:
514
+ `/tmp/entwurf-codex-smoke-0.23.0-amend.BHrPTU/smoke-codex-fresh-live.log`.
515
+ - **Deterministic floor and explicit variance.** `pnpm run check:full` passed before the amendment
516
+ at `/tmp/entwurf-check-full-0.23.0.Q6GGeI`; it also passed on the amended working bytes in 579 s
517
+ at `/tmp/entwurf-check-full-0.23.0-amend.zBu9ic/check-full.log`. VERIFY §389 문자 조건 미충족 —
518
+ GLG 명시 승인 하의 편차, 사유: 통과 스텝 재실행은 자원낭비. The prepared-HEAD exact-SHA CI owns
519
+ the final deterministic proof.
520
+
7
521
  ## 0.22.0 - 2026-09-17
8
522
 
9
523
  ### Added
package/DELIVERY.md CHANGED
@@ -202,8 +202,16 @@ for the inherited case.
202
202
 
203
203
  Four ownership atoms remain separate:
204
204
 
205
- 1. `entwurf install-codex-birth` owns `$CODEX_HOME/hooks.json` — one whole file, ours or
206
- absent — plus its launcher and import closure under `$XDG_DATA_HOME/entwurf/codex-birth`.
205
+ 1. `entwurf install-codex-birth` owns **one `SessionStart` declaration inside**
206
+ `$CODEX_HOME/hooks.json`not the file — plus its launcher and import closure under
207
+ `$XDG_DATA_HOME/entwurf/codex-birth`. The vendor keys trust per
208
+ `<path>:<event>:<group>:<handler>`, so a neighbouring integration (Herdr's official Codex
209
+ integration appends its own group) coexists: entwurf certifies the NORMALIZED digest and
210
+ shape of its own group, requires it exactly once, reads the vendor receipt at the index that
211
+ group was measured at, and reports every other group as present-but-foreign — certified by
212
+ nothing, rewritten never. Install appends and uninstall splices by text, so a neighbour's
213
+ bytes survive both unchanged. The ownership receipt is `codex-birth-install-state/v2`; a v1
214
+ receipt (whole-file digest) is refused by name and superseded forward by one reinstall.
207
215
  No root: every path belongs to the operator. The vendor gates a user-layer declaration on
208
216
  ONE interactive "Trust all", which entwurf never answers, pre-seeds or computes; the
209
217
  receipt is read as its own doctor axis, and until it exists setup is honestly non-green.