@junghanacs/entwurf 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/AGENTS.md +3 -2
  2. package/BASELINE.md +3 -1
  3. package/CHANGELOG.md +658 -0
  4. package/DELIVERY.md +167 -29
  5. package/README.md +104 -14
  6. package/VERIFY.md +73 -11
  7. package/docs/external-mcp-host.md +16 -6
  8. package/docs/setup-clean-host.md +73 -22
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +54 -31
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +289 -57
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +171 -263
  26. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +65 -31
  29. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  30. package/package.json +4 -4
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  33. package/pi-extensions/lib/codex-declaration.js +612 -0
  34. package/pi-extensions/lib/codex-fresh-preflight.ts +325 -52
  35. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  36. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  37. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  38. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  39. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  40. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  41. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  42. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  43. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  44. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  45. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  46. package/pi-extensions/lib/herdr-placement.ts +276 -0
  47. package/pi-extensions/lib/mux-fresh-call.ts +257 -304
  48. package/run.sh +522 -10
  49. package/scripts/check-codex-app-server-launch.ts +445 -0
  50. package/scripts/check-codex-birth-hook.ts +187 -0
  51. package/scripts/check-entwurf-control-rpc.ts +138 -0
  52. package/scripts/check-entwurf-fact-provider.ts +4 -2
  53. package/scripts/check-entwurf-facts.ts +12 -6
  54. package/scripts/check-entwurf-peers-surface.ts +3 -1
  55. package/scripts/check-entwurf-self-address.ts +68 -0
  56. package/scripts/check-entwurf-v2-production.ts +42 -1
  57. package/scripts/check-entwurf-v2-send.ts +26 -7
  58. package/scripts/check-fresh-call-dispatch.ts +301 -0
  59. package/scripts/check-gate-qualification.ts +19 -5
  60. package/scripts/check-herdr-activation.ts +1293 -0
  61. package/scripts/check-herdr-fresh-call.ts +1250 -0
  62. package/scripts/check-herdr-placement.ts +383 -0
  63. package/scripts/check-herdr-plugin-build.ts +842 -0
  64. package/scripts/check-herdr-plugin-profile.ts +371 -0
  65. package/scripts/check-herdr-plugin.ts +523 -0
  66. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  67. package/scripts/check-herdr-sandbox.ts +456 -0
  68. package/scripts/check-herdr-supply.ts +184 -0
  69. package/scripts/check-mux-launch-tmux.ts +331 -35
  70. package/scripts/check-mux-launcher-fence.ts +138 -0
  71. package/scripts/check-peer-facts.ts +331 -0
  72. package/scripts/check-release-gate-outcomes.ts +12 -0
  73. package/scripts/check-typing-call-fence.ts +296 -0
  74. package/scripts/codex-app-server-launch.sh +275 -0
  75. package/scripts/codex-birth-doctor.sh +159 -16
  76. package/scripts/codex-birth-install.sh +178 -72
  77. package/scripts/codex-birth-uninstall.sh +162 -7
  78. package/scripts/codex-socket-path.ts +33 -0
  79. package/scripts/codex-terminal-title-config.py +500 -0
  80. package/scripts/codex_toml_io.py +121 -0
  81. package/scripts/fixtures/herdr-supply.json +29 -0
  82. package/scripts/herdr-activation.mjs +536 -0
  83. package/scripts/herdr-plugin-activate.mjs +270 -0
  84. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  85. package/scripts/herdr-runtime.mjs +1203 -0
  86. package/scripts/install-herdr-ci.sh +96 -0
  87. package/scripts/lib/codex-fresh-live-protocol.ts +13 -5
  88. package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
  89. package/scripts/lib/tree-digest.ts +97 -0
  90. package/scripts/meta-bridge-install.sh +19 -2
  91. package/scripts/meta-bridge-state.py +167 -11
  92. package/scripts/mutants/codex-app-server-launch.json +157 -0
  93. package/scripts/mutants/codex-caller-seat.json +334 -0
  94. package/scripts/mutants/codex-native.json +195 -29
  95. package/scripts/mutants/control-socket-disconnect.json +57 -0
  96. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  97. package/scripts/mutants/herdr-activation.json +305 -0
  98. package/scripts/mutants/herdr-fresh-call.json +448 -0
  99. package/scripts/mutants/herdr-placement.json +161 -0
  100. package/scripts/mutants/herdr-plugin-build.json +160 -0
  101. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  102. package/scripts/mutants/herdr-plugin.json +133 -0
  103. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  104. package/scripts/mutants/herdr-supply.json +133 -0
  105. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  106. package/scripts/mutants/mux-fresh-call.json +184 -25
  107. package/scripts/mutants/mux-launcher-fence.json +13 -0
  108. package/scripts/mutants/omp-fresh.json +4 -4
  109. package/scripts/mutants/peer-facts.json +98 -0
  110. package/scripts/mutants/self-address.json +33 -0
  111. package/scripts/mutants/typing-call-fence.json +17 -0
  112. package/scripts/mutants/v2-surface.json +22 -0
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-codex-birth.sh +281 -12
  117. package/scripts/smoke-codex-config-state.sh +192 -3
  118. package/scripts/smoke-codex-fresh-live.ts +277 -37
  119. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  120. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  121. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  122. package/scripts/smoke-setup-verdict.sh +13 -11
package/CHANGELOG.md CHANGED
@@ -4,6 +4,664 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.23.0 - 2026-09-18
8
+
9
+ ### Added
10
+
11
+ - **`entwurf_fresh_call` opens a sibling on the herdr rail, beside the tmux one (#116).** A session
12
+ running inside [herdr](https://github.com/steipete/herdr) has no tmux server to place a window
13
+ on, so the rail is chosen from PROCESS CONTEXT — `HERDR_ENV=1`, herdr's own marker, read for its
14
+ exact value — and never from a caller parameter, which would let someone request tmux placement
15
+ from inside herdr and fail after a mutation instead of before one. There is **no fallback either
16
+ direction**: an incomplete herdr context is refused by its own herdr reason rather than quietly
17
+ opening a window the operator cannot see from where they are standing. The pilot backends are
18
+ `pi` and `claude-code`; `placement` is refused BY NAME there, because placement inside herdr
19
+ belongs to herdr and a tmux session name would silently relocate the sibling.
20
+ Coordinates, refusals, reclaim and evidence grades: [docs/herdr-launch-rail.md](./docs/herdr-launch-rail.md).
21
+ - One composition root (`fresh-call-dispatch.ts`) now owns rail choice, the Codex preflights and
22
+ the render call for BOTH public surfaces, so a defect planted in one surface can no longer
23
+ survive on the other. The rail-neutral half of the composition (argv dialects, callback tool
24
+ names, first-turn framing, nonce, input contract) moved to `fresh-call-composition.ts`, which
25
+ knows nothing about tmux; each rail supplies only what it alone can state.
26
+ - A sibling is placed in a NEW TAB of the caller's own herdr workspace, without taking focus,
27
+ with the workspace read from herdr's answer about the caller's own pane — never the focused
28
+ workspace and never the `w<N>:` prefix of an id this rail treats as opaque. Orphan reclaim is
29
+ proven within a generation and closes the PANE rather than the tab (`[측정 2026-09-15]`).
30
+ - `entwurf_peers` carries a read-only `placement` column with four words
31
+ (`unobserved` / `none` / `ambiguous` / `herdr <pane>`), joined only on an official herdr report
32
+ for its own backend. A pane coordinate is a VIEW, never an address: the garden id still arrives
33
+ in the callback envelope (Hard Rule 16).
34
+ - **A herdr plugin that installs entwurf onto a host where nothing entwurf is on `PATH`.**
35
+ `plugins/herdr/` bootstraps a stable, scoped plugin runtime from an exact checkout candidate,
36
+ classifies which integrations the host already has, activates only what it owns, and ships a
37
+ read-only citizen status pane built on the core's own `peer-facts` projection. The install
38
+ reports NAMED steps on the operator's terminal and ends with one usage example per activated
39
+ backend, because `[GLG 직접, 2026-09-17]` "정확한 모델명을 모른다" was a real blocker on a clean
40
+ install. Entwurf still installs only Entwurf bytes — never a harness, subscription or login
41
+ (Hard Rule 17) — and a clean NixOS host additionally needs `programs.nix-ld.enable = true` for
42
+ the ACP rail's dynamically linked vendor binary.
43
+
44
+ - **A gate keeps the typing-call boundary by NAME, not by prose alone.** `[외부 관측 2026-09-17
45
+ 19:35]` an outside reader measured this tree and found the law perfectly kept and nothing keeping
46
+ it: zero typing calls across `pi-extensions/`, and zero checks and zero mutants that would notice
47
+ one arriving. Gates here grow on top of incidents, and this boundary was named and REFUSED at
48
+ design time (`docs/herdr-launch-rail.md` §3 threw the keystroke verb away before it shipped), so
49
+ it never produced the failure that would have produced a test — and a rule kept since day one is
50
+ the most defenceless kind, because its green means only that nobody has broken it yet.
51
+ `check-typing-call-fence` scans every TRACKED AND UNTRACKED production source under
52
+ `pi-extensions/`, `mcp/`, `plugins/herdr/` and `scripts/herdr-*.mjs` for the names of typing calls
53
+ — tmux `send-keys` / `paste-buffer` / `load-buffer` and herdr's PTY verbs `agent.prompt` /
54
+ `agent.send_keys` / `pane.send_text` / `pane.input.set` — with comments blanked and string
55
+ literals KEPT, because a typing call spelled as a string is the call.
56
+ - The comment exemption is load-bearing rather than a convenience: the sentence that states the
57
+ rule contains the very names the rule forbids, so a scanner that read prose would be red on the
58
+ law itself and the cheapest way to green would be deleting the law.
59
+ - Three assertions stand in front of the claim for the same reason — the detector is proven to SEE
60
+ each name planted in code, proven BLIND to the same name in a comment, and the scanned inventory
61
+ is measured, because without those "zero hits" and "looked at nothing" print identically.
62
+ - **What it does not catch is named in its own header rather than implied:** it reads SPELLINGS,
63
+ so a verb assembled at runtime passes it. That limit is accepted rather than closed. This is a
64
+ tripwire for the slip — a module reaching for the vendor's typing verb because the argv route
65
+ looked harder that afternoon — not a defence against somebody hiding one, and a gate claiming to
66
+ be the latter would be the more dangerous object. The claim therefore says CARRIES a typing call
67
+ spelled out, which is what it measures.
68
+ - `scripts/` stays out as a whole: this gate lives there, and a gate that scanned itself could
69
+ only be made green by deleting the names it exists to forbid. Wired into `check:hermetic`;
70
+ 8 checks over 85 production sources, one exact-once mutant.
71
+ - **The framing OFFERS its corroboration in a spelling the sibling can actually use.** The callback
72
+ tool name goes through a per-backend dialect map; the peer listing the first turn offers did not,
73
+ and four of the five backends reach that tool under a composed MCP name. The offer was unusable by
74
+ exactly the siblings it was written for — and it was written to replace a prohibition, removed
75
+ because a sibling should be able to check who called it. `FRESH_CALL_PEERS_TOOL` is the same
76
+ dialects applied to the other tool, so the two maps drift together or not at all.
77
+
78
+ ### Fixed
79
+
80
+ - **`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`.
81
+
82
+ - **A Claude child that could not see its callback tool now knows how to load it.** The framing said
83
+ "FIRST ACTION … call `entwurf_v2`" to a child for which that tool was not callable yet: `[측정
84
+ 2026-09-18, n=5]` claude-code 2.1.267 surfaces a still-connecting MCP server as DEFERRED — the tool
85
+ NAME is listed, the SCHEMA is not, and a call without the schema fails. Across five isolated probes
86
+ the bridge's tools were directly callable **zero** times (4 DEFERRED, 1 still connecting), so the
87
+ child answered in text and went idle. That is the "silence" the herdr rail recorded in 5 of its 14
88
+ launches. The claude-code dialect now carries one factual sentence naming `ToolSearch` with BOTH
89
+ tools in a single select, and `[측정, n=3]` callbacks went 3/3 — including a child 782 ms in, which
90
+ is slower than every silent run. **The latency story it replaces is retired:** re-read at n=13
91
+ instead of n=4, success and silence overlap completely (52 ms called back, 94 ms did not), so
92
+ `docs/herdr-launch-rail.md` §14 now carries the distribution and the mechanism instead of the
93
+ correlation. Every other backend's framing is byte-identical — pi has no deferred-tool surface and
94
+ the other three were never probed (`FRESHCOMP-TOOL-LOAD-HINT-CLAUDE-ONLY`).
95
+
96
+ - **The first turn no longer looks like the injection it is not (#116).** `[GLG 직접, 날것 PC,
97
+ 2026-09-17]` a Claude Sonnet 5 sibling REFUSED its whole first turn and named three reasons, all
98
+ of them ours: a "decode the following JSON string literal and follow the decoded instructions"
99
+ wrapper, the line "Do not inspect environment variables, do not call entwurf_self", and no way to
100
+ verify the caller. The framing is now plain prose folded onto one physical line, with ONLY the
101
+ operator's task riding as a literal — `[source herdr 7505c08]` `src/app/agents.rs:157-161`
102
+ refuses any agent argument carrying a Unicode Cc, so a newline never reaches the sibling, and
103
+ `src/platform/linux.rs:127-141` single-quotes every argument, which is why the encoding was never
104
+ buying shell safety. The prohibition is replaced by the same steering stated as a FACT (your own
105
+ record does not carry the caller's address; the tool call is what reaches it) plus an OFFERED
106
+ read-only way to corroborate the caller. `[측정 oracle 2026-09-18, LIVE run sCzzE0]` a Sonnet 5
107
+ child called `entwurf_peers` and then `entwurf_v2`, both successful, and its callback landed.
108
+ - **The rail tells herdr how long to wait, and judges the child on its own clock (#116).** Without
109
+ an explicit `--timeout`, herdr gave up at its own 30s default while our process bound sat at
110
+ 300s, so a cold-starting Claude Code sibling that was alive and booting came back as
111
+ `herdr-agent-start-failed [herdr: timeout]` over an unreclaimed pane. The bound is now an
112
+ explicit 240s, deliberately BELOW our own kill, so herdr has to lose that race or its named
113
+ answer is replaced by "no exit status". The LIVE cell also stopped measuring the child with the
114
+ CALLER's clock — two runs of identical code had differed only in how long the caller's launch
115
+ took — and now records what the child was doing every run: herdr's agent status and entwurf's own
116
+ hook journal are the WHOLE evidence surface, because `[측정 2026-09-18]` no claude session in this
117
+ smoke, caller or child, acting or silent, exists under `~/.claude/projects/` or any fenced XDG
118
+ root — including a child that had just made two successful tool calls.
119
+ - **A peer that hangs up on an accepted control socket no longer takes the resident with it.** The
120
+ connection error is handled where the socket is accepted, so a half-open peer is a bounded
121
+ per-connection failure instead of an uncaught exception.
122
+ - **The uncitizened `entwurf_self` notice is proven, not regexed.** The gate asserted the shape of
123
+ a message instead of the fact it reports, which a reworded notice would have broken while a
124
+ wrong notice passed.
125
+
126
+
127
+ - **The Codex birth unit owns its own declaration, not the whole `hooks.json` (#117).** Installing
128
+ Herdr's official Codex integration appends a second `SessionStart` group to
129
+ `$CODEX_HOME/hooks.json`. The vendor kept running BOTH declarations — `[source]`
130
+ `codex-rs/hooks/src/engine/discovery.rs:664-665` hashes a normalized event/matcher/group/handler
131
+ and keys trust by `<path>:<event>:<group_idx>:<handler_idx>`, so trust was declaration-scoped all
132
+ along — but entwurf's certification was file-scoped: `install-state` recorded a whole-file
133
+ sha256 and the fresh preflight additionally required `SessionStart.length === 1`. On a host with
134
+ both installed, `doctor-codex-birth` was RED and every Codex fresh launch was refused
135
+ `codex-birth-unit-missing`. **v0.22.0's release LIVE was measured with Herdr's hooks moved
136
+ aside; this is the debt that names paid.**
137
+ - Ownership is now exactly one `SessionStart` group holding one handler whose `command` is our
138
+ quoted launcher path. It is certified by SHAPE (which keys may exist) and by a NORMALIZED
139
+ digest (recursively sorted keys, no whitespace) — blind to indentation and key order, because
140
+ `[측정 2026-09-17 oracle]` Herdr re-serializes the entire document through serde, which changed
141
+ our own handler's key order from `type,command,timeout` to `command,timeout,type`.
142
+ - The vendor trust receipt is read at the index our declaration was MEASURED at, never at the
143
+ constant `:0:0`. With a neighbour at index 0 that constant read THEIR approval and reported it
144
+ as ours — a green for a hook the vendor had never been asked to run.
145
+ - Neighbours are `present-but-foreign`: reported in the doctor's own FOREIGN section, certified
146
+ by nothing, never overwritten or absorbed, and no input to any verdict of ours. Editing one
147
+ moves nothing.
148
+ - Install into a file somebody else already declares in is an APPEND by text splice (never an
149
+ adoption and never a refusal), and appending rather than inserting leaves a neighbour's index —
150
+ and therefore their existing trust receipt — alone. Uninstall splices out only entwurf's group
151
+ (plus entwurf's own top-level `description`), removing the file only when entwurf was its sole
152
+ declaration. Every splice is re-parsed and deep-compared to the value it intended before it
153
+ reaches disk; a splice that lands anywhere else is a zero-write refusal.
154
+ - Our own declaration missing, edited or DUPLICATED is still a named red, in install, doctor,
155
+ inverse and preflight alike.
156
+ - Entwurf still never creates, computes, pre-seeds or bypasses a vendor trust receipt, and
157
+ deliberately does NOT recompute what that hash should be: the vendor normalizes with its own
158
+ code, and reimplementing it here would make entwurf a second opinion about somebody else's
159
+ security decision that drifts with every vendor release.
160
+ - `install-state` moves to `codex-birth-install-state/v2` (declaration receipt instead of
161
+ `hooksSha256`). The uninstall, doctor and preflight read v2 only and refuse v1 by name; the
162
+ installer reads a v1 receipt once, to supersede it forward — it removes nothing and rewrites no
163
+ foreign byte.
164
+ - New shared leaf `pi-extensions/lib/codex-declaration.js` is the single definition the
165
+ installer, inverse, doctor, fresh preflight and gates all decide with.
166
+ - Gates: `smoke-codex-birth` 102 checks (both orderings green, foreign bytes proven across
167
+ install and inverse, foreign edits neutral, own-declaration drift/duplication/v1 named red,
168
+ trust read at the measured index, the shared file's mode carried over, and an unscanned
169
+ FOREIGN axis reporting NOT READ rather than "none"), `check-codex-birth-hook` 101 checks (the
170
+ leaf's digest, selection, named refusals, span reader and splice post-condition), six new
171
+ preflight cells, and 8 exact-once mutants (inventory 520 → 527). Six existing
172
+ `FRESHCALL-CODEX-*` mutants were re-pointed at the predicates this narrowing moved;
173
+ `FRESHCALL-CODEX-HOOK-KEYS` moved gate as well, because under the preflight's oracle an extra
174
+ handler key is caught by the digest anyway — what its predicate still buys is the NAME.
175
+ Verification receipts at `ac3f1f8`: `pnpm run check:full` exit 0 (403s) and
176
+ `check-gate-qualification` **527/527 killed**, both in the exact-SHA CI run
177
+ [35239086438](https://github.com/junghan0611/entwurf/actions/runs/35239086438).
178
+ - **Not in scope, by decision:** adding Codex to `HERDR_FRESH_CALL_BACKENDS`, Herdr env carriers
179
+ on the Codex MCP child, plugin supervision of the app-server, and the identical ownership
180
+ defect in the statusline/terminal-title atoms (recorded as an Observation, its own atom).
181
+
182
+ - **A sibling herdr has not been introduced to yet is no longer killed for it.** `[측정 oracle
183
+ 2026-09-18, LIVE run XimC19]` a Claude Code child booted, wrote its own birth record at
184
+ 01:53:38.297Z, and 0.585s later this rail closed the pane it was sitting in. The same bytes passed
185
+ five minutes later (run TCatjF) because that child took 1m27s to become ready instead of 4.9s — so
186
+ the FAST path was the dangerous one, and every green run before it was luck rather than proof.
187
+ - The premise was that `agent start` waits for detection before returning. It does not:
188
+ `[file:line @ c77af189]` `src/cli/agent.rs:592-615` returns on `agent_status ∈ {idle,done}` plus
189
+ `interactive_ready`, and `agent_session` is not in that condition at all. On the Claude rail that
190
+ id arrives afterwards from the CHILD's own one-shot hook
191
+ (`src/integration/assets/claude/herdr-agent-state.sh:60-99`, 0.5s socket deadline, failures
192
+ swallowed), so "herdr has not been told the id" is a reachable resting state for a healthy
193
+ sibling.
194
+ - `settleAgentWitness` re-READS instead of judging on the spot: `agent get <the exact name we
195
+ started>`, never a listing, bounded and rebound to the create receipt every time. An unreadable
196
+ or failed read costs one poll, because herdr answers a missing agent and a socket that blinked
197
+ with the same exit status.
198
+ - **Expiry buys nothing.** The window closing is a SUCCESSFUL launch carrying a
199
+ `witness: unavailable` diagnosis that says what it costs (the peers `placement` column has
200
+ nothing to join) and what it does not (the address was never a pane). The old
201
+ `witness-missing → reclaim` path is gone; the one failure this stage can still name is a
202
+ readable reply about a DIFFERENT agent, `herdr-agent-start-vanished`.
203
+ - The reclaim predicate now reads OCCUPANCY, not the session id: a pane herdr says an agent is in
204
+ is never closed, whether or not anybody has reported that agent's session. Collapsing those two
205
+ facts is what made a booted child read as an empty pane, and it would have survived in every
206
+ other post-create failure branch if only the witness path had been patched.
207
+ - The settle bound is a PROPOSAL and says so in its own comment: the red run still had nothing
208
+ 585ms after the child's birth record, the reporter's own deadline is 500ms, and the slow run's
209
+ 85s was a race being hidden rather than a ceiling.
210
+ - **The Codex birth unit decides who owns the shared `hooks.json` ONCE, for all four surfaces.** The
211
+ fresh-call preflight required a plausible owner and refused a group/world-writable mode; the
212
+ installer and the inverse asked only symlink-and-regular; the doctor read the file's CONTENT
213
+ without ever asking who owned it. A host could install clean, read GREEN, and have every Codex
214
+ launch refuse it as `codex-birth-unit-missing` — three surfaces saying yes about bytes the fourth
215
+ said no about. `classifyOwnedPath` now lives in the shared declaration leaf all four already load,
216
+ and it is PURE: it judges a stat record, not a path. A neighbour's MODE stays theirs (the install
217
+ still carries it over untouched); WRITING into a file anyone else can rewrite is the different
218
+ question, and the answer there is a zero-write refusal, because a receipt cannot bind bytes
219
+ somebody else controls. Symlink keeps its own verdict — the repair is not a mode change, it is a
220
+ different file.
221
+ - **The inverse's FILE verdict earns its delete.** It could remove a `hooks.json` this unit never
222
+ created: the arm asked one question — is our group the last declaration — and that is true of a
223
+ file we merely APPENDED to. `{"description":"foreign","hooks":{"SessionStart":[]}}` has no foreign
224
+ GROUP, so after our install it classified FILE and the whole document went, a neighbour's
225
+ top-level bytes included. The receipt had recorded `hooksExistedBefore` since the installer was
226
+ written and nothing read it; it does now, with two more proofs beside it (the top-level keys are
227
+ exactly the two this unit writes, and the description is ours). Anything else SPLICES. An absent
228
+ field reads as "it was already there", so an older receipt licenses a splice and never a delete.
229
+ - **The herdr LIVE oracle joins the callback it claims to have proved.** Its pi cell compared
230
+ CHARACTER OFFSETS across the whole transcript, and both strings are already in the birth prompt in
231
+ that order — so "called back, then worked" and "never called back" read identically. It now wants
232
+ ONE record carrying the delivered nonce and the `sent` outcome together, with the task token in a
233
+ later one. Its claude cell asserted only that the first `entwurf_v2` completed, which a call
234
+ delivering somebody else's nonce also satisfies; the join it can actually make is across two
235
+ independent artifacts — the delivered message carrying the exact nonce and the child as sender,
236
+ enqueued inside that call's own window.
237
+ - **Three Codex branches and one peers read stop being claims nobody tests.** An independent
238
+ coverage audit found the whole production path behind the peers `placement` column — env gate,
239
+ binary execution, timeout, parse failure — with zero assertions and zero mutants, so a typo in
240
+ `["pane","list"]` or a deleted timeout would have left the deterministic floor green. It has two
241
+ claims now, driven through a stub PROCESS (not a fake herdr: every answer-shaped fact still comes
242
+ from the verbatim 0.9.0 recordings, and what the stub supplies is an exit code, a hang, an
243
+ absence). The doctor's ORPHAN verdict, the inverse's SPLICE arm and its FILE arm gained the
244
+ mutants their behaviour cells never had, and the herdr rail's documented cwd asymmetry — `#` is
245
+ DATA here, because herdr has no format expansion for the tmux refusal to protect against — gained
246
+ a cell that drives a real directory through to the argv.
247
+
248
+ - **The herdr rail's two-step `agent prompt` proposal is closed at the VENDOR SOURCE, not with LIVE
249
+ runs — and not implemented.** §14 proposed starting the agent with no prompt and sending the first
250
+ turn with `herdr agent prompt`, which would remove the tool-list race. The question standing in
251
+ front of "does the child call back" is what that verb SENDS, and the vendor answers it:
252
+ `[source herdr 7505c08]` `api/agents.rs:195` hands the text to `encode_api_submission_parts`,
253
+ `api_helpers.rs:25-32` wraps it in bracketed paste and writes it to the child pane's PTY, and
254
+ `:13` / `:208-212` write an encoded Enter 300 ms later. It runs NO control-character check — only
255
+ a non-empty one, unlike the argv path — so whether a newline pastes or submits is decided by the
256
+ child terminal's paste mode at that instant rather than by a contract, and what is acknowledged is
257
+ that input was written (the vendor's own help: "before any input is sent", "It does not track
258
+ turns"). So the two-step is the keystroke channel §3 already measured and refused, and adopting it
259
+ would put screen input on the FIRST turn — exactly what Hard Rule 16 keeps out of delivery
260
+ evidence. The three LIVE runs budgeted for it were NOT spent characterising a path that cannot
261
+ ship. The remaining legitimate direction for the race is the framing that stands before the tool
262
+ list, the same axis as omp's `--entwurf-bootstrap`.
263
+ - **Two mutant claims the #116 merge moved now aim at what they were about.** Replaying every lane
264
+ the merge touched answered 484/486, and both refusals were the merge's own bookkeeping.
265
+ `FRESHCALL-CWD-SURFACE-PARITY` SURVIVED because the regenerated find was extracted from the FIRST
266
+ `cwd: z` block in the bridge — which belongs to `entwurf_register_native`, not
267
+ `entwurf_fresh_call` — so the mutation deleted a required cwd from a DIFFERENT tool and nothing
268
+ failed; both parity finds are now extracted from inside the `fresh_call` registration span.
269
+ `FRESHCALL-CWD-CALLER-ONLY` came back WRONG-REASON because the mutation now lands in the
270
+ composition leaf while the cell carrying its signature read only the tmux module, so three other
271
+ cells failed instead and the kill could not be attributed.
272
+
273
+ - **The first turn says where the result goes.** `[GLG 직접, 2026-09-18]` a fresh sibling finished
274
+ its task, printed the answer in its own window, and the caller never saw it. The investigation
275
+ found no broken injection: NEITHER framing, the retired one or the current one, had ever said
276
+ this. It used to work because a hand-written briefing named the target and the tool, and when the
277
+ briefing stopped doing that the sentence turned out to live nowhere. The framing now ends with two
278
+ lines — where to send the result (the callback tool, in the same backend dialect) and why sending
279
+ is needed at all, because a sibling with a visible window has no way to know its window is not the
280
+ delivery. It is TOPOLOGY, not a command: nothing watches for completion or sends on the sibling's
281
+ behalf, and a supervisor is what this rail refuses to be.
282
+ - **A LIVE smoke stops taking the operator's `claude` launcher** — #67's failure on a rail born
283
+ after its fence. The vendor resolves the two halves of its install from different roots: the
284
+ version store from `XDG_DATA_HOME`, the launcher from `HOME`. This smoke fenced XDG while keeping
285
+ the real HOME, so each Claude child found an empty store beside a real launcher, installed itself
286
+ into the fixture, and repointed `~/.local/bin/claude` at `<fixture>/claude/versions/2.1.267`.
287
+ **That is not a hazard, it happened** — seven preserved fixture roots each hold that install, and
288
+ the operator's launcher pointed into the newest of them, one `/tmp` sweep away from a `claude`
289
+ that does not start. The fixture's `xdg-data/claude` is now a symlink to the operator's vendor
290
+ data dir, which puts store and launcher back in one install while the rest of the tree stays
291
+ fenced, and the smoke consumes the shared fence: fail-closed preflight, integrity oracle in
292
+ teardown, cleanup verdict. The gate side stopped naming files — `LAUNCHFENCE-EXPOSED-SMOKE-WIRED`
293
+ asks the POPULATION (every LIVE smoke that assigns a fixture `XDG_DATA_HOME` either consumes the
294
+ fence or carries an exemption verified against its own source: four today, three wired, one
295
+ exempt), because the two claims that listed smokes by name are exactly what the newest rail walked
296
+ past. `[LIVE 2026-09-18]` the first run in eight to leave zero version copies behind.
297
+ - **The herdr LIVE ordering oracle reads the join pi actually writes.** The claim asked for ONE
298
+ transcript record carrying both the delivered nonce and the `sent` outcome; pi cannot write that
299
+ record, because the nonce rides the toolCall and the outcome rides a separate toolResult, joined
300
+ by `toolCallId`. So the oracle failed a run whose child did exactly what the claim describes
301
+ (callback 04:15:01.381, `sent` .415, task token .03.728) — the most expensive kind of red: correct
302
+ product, red gate, and a cut that stops for nothing. The join was what the claim was always about:
303
+ the `sent` outcome must belong, by that id, to THE call that carried THIS nonce, and the task
304
+ token must appear after it. Proven on the failing transcript — the real run holds, a forged pair
305
+ whose `sent` belongs to a different call does not, and a run whose token appears only before the
306
+ callback does not.
307
+
308
+ ### Changed
309
+
310
+ - **The two tool descriptions say what the code does.** `entwurf_v2` no longer offers `codex` as an
311
+ example of a backend with no adapter: Codex is a native-push backend, and that sentence pointed a
312
+ caller at the wrong dispatch outcome. `entwurf_fresh_call` says it opens a NEW UNFOCUSED TAB in
313
+ the caller's own herdr workspace rather than "a herdr pane" — the contract has been tab-first
314
+ since it shipped. Both surfaces lost the top-level cwd sentence the `cwd` parameter description
315
+ already owns, which is also what kept them inside the 2048-char host cap.
316
+ - **The herdr rail document describes the first turn that actually ships.** §3 still recorded the
317
+ retired shape — the whole framing wrapped in one JSON literal behind "decode the following" —
318
+ which a Sonnet 5 sibling refused outright. The current shape is plain prose with only the
319
+ operator's task as a literal, and the section now says why: the encoding never bought shell
320
+ safety (herdr single-quotes every argument), and what is left is the vendor's control-character
321
+ refusal, so only the thing that must carry newlines is encoded.
322
+ - **`ROADMAP.md` and `docs/setup-clean-host.md` stop calling accepted work pending.** The Codex
323
+ caller-seat and caller-DIRECTORY re-runs were accepted on 2026-09-16 with 56 and 65 assertions;
324
+ both documents said "pending", and the roadmap's current-package line still read 0.20.1.
325
+ - **`plugins/herdr/README.md` separates an admission floor from a measured supply.** Herdr
326
+ `>=0.9.0` is the manifest floor; Entwurf's reproducible and CI rail is pinned to exactly 0.9.1,
327
+ and a different Herdr inside the admission window carries no receipt of ours. Its install-output
328
+ example uses `<version>` / `<full-commit>` placeholders instead of a frozen `0.21.0` and a frozen
329
+ commit that no longer reproduce. Both READMEs now name the two installation routes and link to
330
+ each other.
331
+
332
+ ### Observations — named rather than carried
333
+
334
+ These were raised by the pre-cut review round and deliberately NOT acted on in this release. Each
335
+ one is recorded here so the next lane inherits the finding instead of rediscovering it.
336
+
337
+ - **The vendor trust receipt is bound by KEY, not by VALUE.** `doctor-codex-birth` and the fresh
338
+ preflight read the receipt at the index our declaration was measured at and check that its value
339
+ has the shape `sha256:<64 hex>` — they do not check that the hash is the one the vendor would
340
+ compute for the CURRENT declaration, so a stale or arbitrary well-shaped value reads as ready.
341
+ Closing it would mean reimplementing the vendor's normalization here, which is exactly the second
342
+ opinion about somebody else's security decision that Hard Rule 9 and the doctor's own prose
343
+ refuse. The live failure mode is bounded and visible: a stale receipt makes the vendor show its
344
+ consent screen again to the human sitting there, and an unattended launch reads as a callback
345
+ timeout.
346
+ - **The typing fence does not detect an assembled typing call** — see its header, and the entry
347
+ above.
348
+ - **The declaration span fixtures do not pin CRLF, BOM or deeply nested arrays by name.** The parser
349
+ and its splice post-condition handled all three correctly in a direct memory-only sample; what is
350
+ missing is a labelled cell, and BOM in particular would read better as an explicit refusal than as
351
+ an incidental one.
352
+ - **Four more coverage gaps were enumerated and left for their own lane:** the uninstall's v1-state
353
+ refusal has a behaviour cell but no mutant of its own, the `trustReceiptKey` composition is
354
+ asserted separately by the doctor and the preflight rather than jointly, and two spawn-seam
355
+ sub-claims were deliberately not split into their own QKs.
356
+ - **The Claude child's `--allowedTools` names only the callback tool**, so the corroboration the
357
+ framing offers is not on the auto-approved list. Measured behaviour is unchanged by this release
358
+ (the bare spelling was not on it either), and widening auto-approval is a decision rather than a
359
+ repair.
360
+ - **`doctor-codex-birth` FOREIGN 축은 이웃 선언의 trust 부재를 판단하지 않지만 vendor 동의 화면은
361
+ 파일 단위로 첫 턴을 막는다.** `smoke-codex-fresh-live` 전제조건은 폴더 trust만 이름짓고 hook
362
+ trust는 timeout으로 읽힌다 — 컷 후 atom.
363
+
364
+ ### Verification
365
+
366
+ - **Land exact-SHA CI.** Push run
367
+ [`35312087142`](https://github.com/junghan0611/entwurf/actions/runs/35312087142) at
368
+ `09aea576a80983709a7e2bb7593d6fb546234eee` concluded green for `check`,
369
+ `install-surface`, `artifact-consumer`, and `macos-install-surface`; the `check` job's
370
+ `check-gate-qualification` body also concluded green.
371
+ - **LIVE Codex acceptance is a focused rerun, not a final `release-gate --cut` exit 0.** First cut
372
+ attempt `/tmp/entwurf-release-gate-0.23.0.Ire9Nr` was red because the vendor's Herdr hook trust
373
+ receipt was absent; the operator answered the vendor consent screen once. Retry
374
+ `/tmp/entwurf-release-gate-0.23.0-retry.17pfMp` recorded `MUST: PASS=23 FAIL=1 SKIP=0`; its only
375
+ red cell was `smoke-codex-fresh-live`, where Luna copied the model label from the callback
376
+ envelope's `from:` line. The frozen-candidate delta afterwards is exactly the two instruction
377
+ sentences in `scripts/lib/codex-fresh-live-protocol.ts` plus these CHANGELOG records. After
378
+ `pnpm run build-bridge` and `./run.sh check-bridge-delivery` (19 PASS), the repaired standalone
379
+ `smoke-codex-fresh-live` passed 66 assertions:
380
+ `/tmp/entwurf-codex-smoke-0.23.0-amend.BHrPTU/smoke-codex-fresh-live.log`.
381
+ - **Deterministic floor and explicit variance.** `pnpm run check:full` passed before the amendment
382
+ at `/tmp/entwurf-check-full-0.23.0.Q6GGeI`; it also passed on the amended working bytes in 579 s
383
+ at `/tmp/entwurf-check-full-0.23.0-amend.zBu9ic/check-full.log`. VERIFY §389 문자 조건 미충족 —
384
+ GLG 명시 승인 하의 편차, 사유: 통과 스텝 재실행은 자원낭비. The prepared-HEAD exact-SHA CI owns
385
+ the final deterministic proof.
386
+
387
+ ## 0.22.0 - 2026-09-17
388
+
389
+ ### Added
390
+
391
+ - **A Codex sibling opened into a directory the vendor has no answer for now SAYS SO
392
+ (`codex-launch-cwd-undecided`).** `entwurf_fresh_call` reads the launch directory against the
393
+ operator's Codex config and prints what it saw — and then opens the window anyway. **It is a
394
+ diagnostic, not a gate, and that is the product decision (GLG, 2026-09-17):** the vendor's
395
+ consent screen is self-repairing for the human this rail exists to put a window in front of, one
396
+ answer teaches the vendor that directory for good, and a refusal would replace that with "no
397
+ window, go run codex yourself, then call again" while having to be right about a decision this
398
+ process cannot fully see. The UNATTENDED case is answered where it belongs —
399
+ `smoke-codex-fresh-live` asserts the same leaf up front, so a release gate with nobody at the
400
+ keyboard reads a named precondition instead of a callback timeout. `[source rust-v0.153.4]` a `--remote` startup — which this
401
+ argv always is — runs `check_directory_trust` on the `-C` value (`tui/src/lib.rs:1699-1725`), and
402
+ nothing on that path consults the approval or sandbox policy
403
+ (`tui/src/onboarding/directory_trust.rs:33-130`): the
404
+ `--dangerously-bypass-approvals-and-sandbox` token this launcher already carries turns off
405
+ approvals, never folder consent. A DIRECT decision is keyed to the exact directory and nothing else — for
406
+ `ProjectTrustHost::Remote` the lookup is `vec![cwd_key]` (`tui/src/config_update.rs:290-296`), with
407
+ no project-root marker, no git root and no parent inheritance. **The question is whether the
408
+ directory has been ANSWERED, not whether it is trusted:** a saved `untrusted` folder is
409
+ explicitly skipped on a remote target (`onboarding/directory_trust.rs:94-96`), so that turn
410
+ starts too and is not noted at all. A cwd inside an explicitly `untrusted` project gets its OWN
411
+ note (`codex-launch-cwd-untrusted-ancestor`) because the vendor answers that one with an error
412
+ rather than a screen — `pass the repository root explicitly with --cd`
413
+ (`config_update.rs:357-371`) — so its repair names a different directory, not a prompt.
414
+
415
+ **The leaf is deliberately NARROWER than the vendor's own judgment and says so rather than
416
+ claiming equivalence.** It reads the operator's `config.toml`; the vendor reads an effective
417
+ config — system, managed and cloud layers merged around that file — through its app-server, and
418
+ an enabled PROJECT LAYER consents on a directory's behalf with no entry at all
419
+ (`config_update.rs:346-354`). That gap is affordable precisely because nothing here refuses:
420
+ everything the leaf cannot see stays silent, and a note that turns out to be unnecessary costs a
421
+ line of stderr. **`[측정 2026-09-16]` this is the
422
+ failure it retires, not a hypothetical:** four `smoke-codex-fresh-live` runs, one scratch directory
423
+ each; the two whose directory carried a trust entry (`…-db65N2`, `…-pBXxOJ`, both answered by a
424
+ human at the keyboard) passed, and the two unattended release-gate runs (`…-2zznHl`, `…-kSsoAn`)
425
+ died with no rollout written after 21:28 at all — the sibling had opened on a consent screen and
426
+ started no turn, so there was no birth, no callback, and nothing to address. The caller then waits
427
+ out its entire timeout on a window that is merely waiting to be answered, which reads as a delivery
428
+ defect and is a folder question. The directory asked about is read back off codex's own `-C` token
429
+ rather than recomputed, so the consent question cannot drift from the thread's real start
430
+ directory, and the inherited default stays resolved in exactly one place.
431
+
432
+ - **A Codex caller opens its sibling beside its own pane (#95 lane B).** Until now the seat a
433
+ Codex citizen got came from the app-server's inherited `TMUX`/`TMUX_PANE`, so a sibling opened
434
+ from Codex landed wherever the operator happened to have started the app-server rather than
435
+ beside the TUI the operator was looking at. `[측정 2026-09-16, codex-cli 0.153.4]` with
436
+ `thread-id` in `[tui].terminal_title` the TUI writes its thread UUID into the OSC 0 title and
437
+ tmux answers with it as `#{pane_title}` — truncated to 32 chars, i.e. 29 graphemes plus `...`
438
+ (`status_surfaces.rs:892-894`, `:1027-1043`; unchanged at `rust-v0.154.0`). So when the
439
+ reconciled sender of a fresh call is a record-backed `codex` citizen and `placement` is
440
+ omitted, that request's own `_meta.threadId` selects the pane whose title carries the token and
441
+ its `$session` becomes the target. **No vendor patch, no new carrier: one config item.**
442
+ - **`entwurf install-codex-terminal-title` / `uninstall-` / `doctor-`** — a fourth Codex
443
+ ownership atom, owning only `thread-id` membership within `tui.terminal_title`. A different key
444
+ and a different axis from `install-codex-statusline`: `status_line` is what a human reads
445
+ inside the TUI, `terminal_title` is what the multiplexer reports back. The seeded list is
446
+ `["activity", "project-name", "thread-id"]` — `activity` leads because the herdr Codex detector
447
+ keys on the spinner and `Action Required` prefix — and an existing operator list is APPENDED to,
448
+ never reordered. `setup` composes it on a detected Codex host.
449
+ - **The seat now follows the CALLER, never the backend being opened, on both fresh-call surfaces.**
450
+ An explicit `placement.tmuxSession` always wins; then a Codex CALLER's own pane
451
+ (`codex-title-anchor`); then the caller's own session. The anchored receipt reports its SOURCE and
452
+ no session name, because a session was observed rather than a name requested.
453
+ - **`entwurf codex-app-server` — the one command that starts the operator's Codex app-server.**
454
+ Every Codex rail needs a server the operator starts themselves, and the command was
455
+ `codex app-server --listen "unix://$CODEX_HOME/app-server-control/app-server-control.sock"`:
456
+ correct, in three documents, and in the text of a refusal — which meant most operators met it
457
+ only AFTER something had already failed. This verb owns that SPELLING and nothing else. It
458
+ `exec`s the vendor in the terminal it was typed in (cwd, pid, tty and exit status are the
459
+ server's; Ctrl-C is the operator's), creates the control directory, forwards the rest of the
460
+ argv byte-identical, and strips `PI_SESSION_ID`/`PI_AGENT_ID` so the server every bridge child
461
+ inherits from cannot carry someone else's identity. The address comes from the SAME leaf
462
+ delivery, preflight and the LIVE smoke read: the launcher ASKS that resolver through the new
463
+ internal `entwurf codex-socket-path` rather than re-deriving the path, so there is exactly one
464
+ spelling of this address instead of two that agree on the inputs somebody thought of.
465
+ Refusals are named and happen before the exec: a live socket (naming the pid whose `/proc`
466
+ cmdline actually carries that socket, and saying plainly when the host holds none — never an
467
+ inferred owner), a resolved address that is not absolute or carries a control character
468
+ (refused before anything is created: a relative socket is a different file for every process
469
+ that resolves it, and the bridge that will look for it is the app-server's MCP child with its
470
+ own cwd), an unidentifiable path, a second
471
+ `--listen`, a recursive launch, a socket classification nobody wrote, no `codex` on PATH. A dead
472
+ socket file is reported and launched over. **It is a managed spelling, not a managed
473
+ lifecycle**: no supervisor, no restart, no daemon, no pid file, and `entwurf_fresh_call` still
474
+ REJECTS a missing app-server rather than starting one. The tmux seat this server gives caller-seat lookups is reported as a fact, not
475
+ enforced as a requirement. Typing the raw vendor command remains equivalent.
476
+
477
+ ### Verification
478
+
479
+ - **Release acceptance for this cut.** `[LIVE 2026-09-17, oracle]` `LIVE=1 ./run.sh release-gate
480
+ /tmp/entwurf-release-gate-0.22.0.royX2L --cut` → **`MUST: PASS=24 FAIL=0 SKIP=0`**, **`BEHAVIOR:
481
+ PASS=1 FAIL=0 SKIP=0`**, **`cut: OK`**, exit 0, 18:47–20:02 KST; log preserved at
482
+ `<scratch>/release-gate.log`. Inside it: `check:full` exit 0 in 521s, `check-gate-qualification`
483
+ **520/520 killed**, `smoke-entwurf-chain-live` 24 assertions, `smoke-codex-fresh-live` **66
484
+ assertions** — one more than lane C's 65, because the launch-directory precondition this release
485
+ adds is asserted before anything launches. Gate env `ENTWURF_CODEX_FRESH_MODEL=gpt-5.6-luna`,
486
+ `ENTWURF_CODEX_FRESH_PI_MODEL=openai-codex/gpt-5.6-luna`, `ENTWURF_CODEX_APP_SERVER_PID` naming an
487
+ operator-started app-server in its own tmux session. Exact-SHA CI at the pre-release commit:
488
+ run [`35202801617`](https://github.com/junghan0611/entwurf/actions/runs/35202801617)
489
+ `event=workflow_dispatch` @ `f5d6e10`, all four required jobs success and the `check` job's
490
+ qualification BODY step success.
491
+ - **The host's Codex hook declaration was measured with the herdr SessionStart group temporarily
492
+ removed, and that is a debt this release does not pay.** `~/.codex/hooks.json` on `oracle` carries
493
+ a second SessionStart group installed by herdr, so `doctor-codex-birth` reports the declaration as
494
+ edited after install and `smoke-codex-fresh-live` cannot run. For this cut the file was moved to
495
+ `hooks.json.herdr-bak` and re-published by `install-codex-birth`; the vendor's trust receipt for
496
+ `…/hooks.json:session_start:0:0` survived that re-publication, measured by a real Codex TUI
497
+ starting with no consent screen. The herdr group is restored after the cut and the doctor goes RED
498
+ again on purpose. **#117 owns the real repair** — entwurf requires a whole-file digest where the
499
+ unit it actually owns is one group.
500
+ - **Two gate defects found by the release gate itself, both repaired in test/gate code with zero
501
+ production bytes.** `check-gate-qualification` scored **518/520** on the first prepared commit and
502
+ both survivors were launch-directory claims whose assertion could not see the defect its mutant
503
+ plants. `CODEX-LAUNCH-CWD-ANCESTOR-PLAIN-PATHS-ONLY` guards two predicates — the cwd side and the
504
+ key side — and every non-plain cell put the encoded character in the KEY, so the key-side filter
505
+ answered alone and deleting the cwd-side guard changed nothing; cells that vary the CWD against a
506
+ plain untrusted key now close it. `FRESHCALL-CODEX-LAUNCH-CWD-NOTES-NEVER-REFUSES` asserted the
507
+ `indexOf("-C")` lookup but not the value read at it, so a note that asked about the REQUESTED cwd
508
+ instead of codex's own `-C` token passed; the three statements are now pinned contiguously.
509
+ Neither mutant was stale — each still matched its production line exactly once, which is what made
510
+ the hole an assertion hole rather than drift.
511
+ - **`smoke-entwurf-chain-live` went red because a model read the operator's policy correctly, and
512
+ the repair moved the authority rather than the wording.** The native Claude Code hop declined to
513
+ forward, citing the operator's global AGENTS.md; B, C and D were all born and addressable and the
514
+ terminus simply received nothing. A first repair put the provenance in the payload and the hop
515
+ named those sentences as the tell — content that vouches for itself is the shape of an injection,
516
+ so no sentence inside the payload can settle it. The payload is therefore byte-identical to
517
+ before, and the fact now rides the disposable world the turn starts in: the gate mints that
518
+ directory, every `ENTWURF_META_*_DIR` resolves inside it, and it exists because the operator ran
519
+ `release-gate`, so an `AGENTS.md`/`CLAUDE.md` written there is a standing operator instruction of
520
+ exactly the kind the operator's own global policy defers to for directory-scoped behaviour. **No
521
+ negative instruction was added**: a "do not check your policy" line would buy the same green by
522
+ asking the model to stop reading, and would keep passing after the chain broke. Accepted on the
523
+ host default model with no model pin.
524
+
525
+ - **The Codex LIVE card now measures WHERE a sibling starts, and the acceptance is recorded.**
526
+ `[LIVE 2026-09-16, Codex 0.153.4, 65 assertions, exit 0]` hop 1 requests a cwd and joins four
527
+ authorities that cannot borrow from each other — the pane's `#{pane_current_path}`, the vendor
528
+ rollout's `session_meta.cwd`, the Entwurf record and the requested scratch; hop 2 requests NEITHER
529
+ placement NOR cwd, so its directory can only come from the Codex caller's own record. Both are
530
+ asserted different from the app-server's live `/proc/<pid>/cwd`, which is the failure mode staying
531
+ visible rather than coincidentally equal. 65 and 56 are different contracts: lane C added the cwd
532
+ axis on top of lane B's seat axis. Receipts and digests in `DELIVERY.md` / `BASELINE.md`.
533
+ - **Caller-seat LIVE accepted on Linux, 2026-09-16 (Codex 0.153.4): 56 assertions, exit 0.** The
534
+ operator-owned app-server stayed alone in session A (`$30`/`@41`/`%45`, pid `737636`) while the
535
+ initial Pi, the omitted-placement Codex it opened, and that Codex's own outbound Pi all landed in a
536
+ different session S (`$2`: `@46`, `@47`, `@48`). A ≠ S is what makes the run decisive: the
537
+ app-server's inherited `TMUX` names A, so the pre-#95 environment fallback would have put the
538
+ outbound Pi there. Its receipt instead records `seat-source=codex-title-anchor`. `DELIVERY.md` owns
539
+ the coordinates, garden ids and digests, including the 43-assertion run kept as the D1 measurement.
540
+ - **`check-codex-app-server-launch` — 29 assertions, hermetic, plus 9 mutants all killed by
541
+ their own claim.** A fake vendor on a sandbox PATH under the real name reports the argv, parent
542
+ and environment it was handed; nothing is asserted by reading the launcher's source.
543
+ `check-pack-install` proves the INSTALLED verb reaches a vendor at the address the INSTALLED
544
+ compiled resolver computes — a file-list assertion cannot tell a shipped script from one that
545
+ resolves its siblings wrongly under `node_modules`.
546
+ - **Independent review caught a false success this gate had certified, and the repair removed the
547
+ class rather than the instance.** The launcher's first version re-derived the socket path in
548
+ bash and the gate compared the two spellings over four ASCII-normal inputs they agreed on.
549
+ `[측정 2026-09-16]` they disagree on `CODEX_HOME=$'\ufeff'` — JS `trim` strips U+FEFF, a POSIX
550
+ `[:space:]` trim does not, so the managed launch would have started a server at
551
+ `<BOM>/app-server-control/app-server-control.sock` while delivery looked at `$HOME/.codex` —
552
+ and on `path.join`'s normalization of a trailing slash or a `..` segment. Widening the matrix
553
+ would only have added the inputs somebody thought of, so the second spelling was deleted: the
554
+ launcher asks, the gate's hostile cells (BOM, trailing slash, `..`) keep it asking, and the
555
+ mutant that used to drop `CODEX_HOME` now re-derives the path in bash instead. The same review
556
+ found the `/proc` holder assertion over-determined (it accepted the fallback text OR a pid, so
557
+ deleting the scan passed) and the socket classifier not fail-closed on an unrecognised reading.
558
+ A second review pass then found that asking one authority for the address does not make the
559
+ ANSWER safe: the resolver returns `CODEX_HOME` faithfully, so
560
+ `CODEX_HOME=$'relative\ncontrol-home'` had the launcher create that directory under its caller's
561
+ cwd and bind a relative socket — a different file for every process that resolves it, while the
562
+ gate still passed. This launcher is the only Codex surface that CREATES and binds rather than
563
+ reads, so it now refuses a non-absolute or control-character address before the first write.
564
+ Every one of those is a discriminating cell with a mutant of its own; the lane is 9.
565
+ - **Two gate defects surfaced on the way and were fixed rather than worked around.** A mutant whose
566
+ `find` no longer matched had been silently untested since a rename — a stale mutant is a claim
567
+ nobody is checking, so `check-gate-qualification` scores it red. And the LIVE source-call oracle
568
+ compared tool arguments with `isDeepStrictEqual`, so a model that omitted the optional `wants_reply`
569
+ rather than passing it explicitly read as drift; it now normalizes that one key to its schema
570
+ default. Both are recorded in `VERIFY.md`'s affected-set rules so the inner loop stops missing them.
571
+
572
+ ### Changed
573
+
574
+ - **`smoke-codex-fresh-live` launches in ONE stable directory instead of a fresh `mkdtemp` per
575
+ run.** Per-run random scratch made every run an undecided folder — a trust prompt per run, an
576
+ entry per run in the operator's `~/.codex/config.toml`, and unattended failure whenever nobody was
577
+ there to answer. The path is now one stable `os.tmpdir()`-rooted directory, answered `Trust`
578
+ once; the smoke PRINTS it as `launch-cwd <dir>` on every run and asserts that consent by name
579
+ BEFORE it launches anything, so a missing answer reads as `codex-launch-cwd-undecided` — with the
580
+ exact `codex -C <dir>` repair command — at the top of the run rather than as a callback timeout
581
+ twenty assertions later. The directory is printed rather than documented as a literal because
582
+ `os.tmpdir()` is a host fact: a doc spelling it `$TMPDIR/...` would send an operator with no
583
+ `TMPDIR` to `/entwurf-codex-fresh-live` in the filesystem root, which is a different directory
584
+ the vendor consents to separately. Lane C is unaffected: its only requirement of that directory is that it differ
585
+ from the app-server's own, which the assertion still proves.
586
+ - **The Codex live-spend default comes down to `gpt-5.6-luna`.** The `entwurf-dev` skill's Codex
587
+ backend default and the `smoke-codex-fresh-live` recommendation in `VERIFY.md` move off
588
+ `gpt-5.6-sol`. **This is the Codex axis alone** — OMP's fresh default is deliberately unchanged at
589
+ `openai-codex/gpt-5.6-sol` (`scripts/smoke-omp-fresh-live.ts:14`), because that is the model its
590
+ two-stage bootstrap callback was actually measured on and moving it would invalidate that
591
+ receipt. A cost decision, not a gate change: no step moved tier, and the acceptance receipt for
592
+ the Codex cell must be the run that actually happened on the new default rather than a `sol` run
593
+ inherited from before.
594
+
595
+ - **The fixed `codex` tmux home is RETIRED (#95 D1).** 0.21.0 shipped it three days earlier: an
596
+ omitted-placement Codex TARGET selected an already-existing session named exactly `codex`, where
597
+ the operator was asked to seat the app-server and every Codex TUI, and the launch receipt labelled
598
+ that source `Codex home`. GLG retired it the moment the caller-pane anchor stood up — *"무조건
599
+ 은퇴야 … 코덱스의 거처를 만들어주자는 말은 그냥 기술이 안돼서 무마한 개념이야. 제대로 가야지."* The
600
+ room was a workaround for a mapping that did not exist yet, and keeping it after the anchor landed
601
+ would have left Codex alone answering "where does a sibling open?" differently from every other
602
+ backend. `CODEX_HOME_TMUX_SESSION`, the `codex-home` receipt source, its renderer branch, its
603
+ deterministic cells and its mutant are gone; an omitted seat is the caller's own session for every
604
+ backend, and the operator seats the app-server wherever they like — Entwurf still never creates,
605
+ moves or supervises it. The 2026-09-12 home acceptance stays recorded in `DELIVERY.md` and
606
+ `docs/setup-clean-host.md` as history, not as a live contract. The reshaped LIVE card now REQUIRES
607
+ the app-server to sit in a different session from the Pi/Codex pair: that separation is what tells
608
+ the caller-pane anchor apart from the app-server's inherited environment.
609
+
610
+ ### Fixed
611
+
612
+ - **An in-band refusal now reaches the sender with the reason the RECEIVER gave it
613
+ (`d49e6cb`, `0c8a42c`).** A completed control-socket RPC that came back `success:false` was
614
+ finalized as `rejected` with no reason at all, so every named receiver refusal — including the
615
+ 0.21.0 compaction guard's own `compacting` / `busy` — arrived at the caller as a reasonless
616
+ rejection indistinguishable from "no live route". The send hand now carries that error verbatim
617
+ when the receiver supplied one, and invents nothing when it did not: receiver evidence is not
618
+ reclassified into the resolver's taxonomy (`dormant-fire-forget-unsupported`,
619
+ `mailbox-undeliverable`, …), and the two remain distinguishable at the surface. The gap was one
620
+ hop wide and entirely invisible to the unit lane — the production `sendOverSocket` closure was
621
+ what dropped `response.error` before `driveSend` could see it — so the guard that holds it is a
622
+ production-wiring assertion with its own mutant (`V2PROD-INBAND-ERROR-WIRED`), not another test
623
+ of the pure function.
624
+ - **A Codex sibling's THREAD now opens where its pane is (#95 lane C).** The seat was right and the
625
+ directory was not: `[측정 2026-09-16]` a pi → Codex → Claude Code chain recorded the app-server's
626
+ `~/repos/gh/entwurf` for all three citizens while the panes sat in `~/repos/gh/agent-config`.
627
+ `[source rust-v0.153.4]` an explicit `--remote <endpoint>` resolves to `AppServerTarget::Remote`
628
+ (`codex-rs/tui/src/lib.rs:875-876`), and a Remote target takes its new thread's cwd from
629
+ `remote_cwd_override` alone (`app_server_session.rs:2022-2033`) — absent, the app-server opens the
630
+ thread in its OWN directory, and the birth hook then records that vendor-supplied cwd honestly.
631
+ The codex argv now always carries that override as `-C <dir>`, the flag the shared interactive
632
+ options already expose beside `--dangerously-bypass-approvals-and-sandbox`
633
+ (`utils/cli/src/shared_options.rs:53-68`), preserved for a remote target at
634
+ `startup_orchestration.rs:191-194`. **One flag, no vendor patch, no new layer.**
635
+ - **A Codex CALLER that names no cwd opens its sibling in its OWN directory.** This bridge runs as
636
+ the operator-owned app-server's MCP child, so the directory its process reports is the
637
+ app-server's rather than the caller's — an omitted `cwd` used to put every sibling of every Codex
638
+ caller in the app-server's repo, whatever backend was being opened. The surface now hands the
639
+ composition that citizen's RECORD cwd under the same condition that already carries its thread
640
+ id, and it is consulted only when the call requested none. One value, two carriers: tmux `-c`
641
+ places the pane, codex `-C` places the thread. Every other caller's argv is byte-identical, and
642
+ the receipt names which rule chose the directory (`requested` / the Codex caller's own record
643
+ directory) instead of calling a caller-record path "requested".
644
+ - **Three named refusals replace a silent wrong seat.** Zero matching panes is
645
+ `codex-caller-seat-unresolved` (the TUI is on another tmux server, its config carries no
646
+ `thread-id`, or that server has `allow-set-title off`, which `[측정]` replaces every pane title
647
+ with the hostname); two or more is `codex-caller-seat-ambiguous`; and a Codex caller whose own
648
+ `[tui].terminal_title` lacks `thread-id` is refused pre-mutation as `codex-caller-title-missing`
649
+ with the installer as its repair. None of them falls back to another session, and none opens a
650
+ window. That caller-side check runs only when the anchor is actually consulted — an explicit
651
+ seat never reads a title.
652
+ - **A pane title is a placement input and nothing else (Hard Rule 16).** Any process in any pane
653
+ can emit the same OSC string, so the leaf's answer may only reach a `-t` target; identity,
654
+ delivery and liveness keep the record + `_meta` join they already had, and the first-turn
655
+ framing still carries the caller's garden id rather than the thread id a title matched. The leaf
656
+ (`pi-extensions/lib/codex-caller-seat.ts`) imports nothing at all, which is how that boundary is
657
+ held by the code rather than by a comment.
658
+ - **The title match is over tokens, not ` | ` segments, and the count is over panes.**
659
+ `title_setup.rs:183-193` joins the `activity` item to its neighbour with a plain space rather
660
+ than ` | `, so on a host whose operator list ends in `activity` a working TUI renders
661
+ `<spinner> <thread-id>` inside one segment and a segment-equality rule would lose the seat
662
+ exactly while the caller is busy. Counting panes rather than tokens keeps a single pane that
663
+ shows the same thread twice (`thread-title` beside `thread-id`) from reading as an ambiguity.
664
+
7
665
  ## 0.21.0 - 2026-09-13
8
666
 
9
667
  ### Added