@junghanacs/entwurf 0.20.1 → 0.22.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 (111) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +6 -3
  3. package/CHANGELOG.md +473 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +332 -60
  6. package/README.md +100 -22
  7. package/VERIFY.md +93 -7
  8. package/docs/acp-backend-rail.md +0 -1
  9. package/docs/external-mcp-host.md +64 -33
  10. package/docs/setup-clean-host.md +151 -17
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  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/entwurf-v2-surface.js +7 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  29. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  30. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  31. package/mcp/entwurf-bridge/src/index.ts +154 -116
  32. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  33. package/package.json +13 -4
  34. package/pi-extensions/entwurf-control.ts +71 -19
  35. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  36. package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
  37. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  38. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  39. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  41. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  42. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  43. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  44. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  46. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  47. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  48. package/pi-extensions/lib/mux-fresh-call.ts +233 -29
  49. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  50. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  51. package/pi-extensions/lib/native-push/register.ts +7 -9
  52. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  53. package/run.sh +342 -28
  54. package/scripts/check-agy-sender-identity.ts +1 -1
  55. package/scripts/check-codex-app-server-launch.ts +445 -0
  56. package/scripts/check-codex-birth-hook.ts +264 -0
  57. package/scripts/check-codex-bridge-identity.ts +179 -0
  58. package/scripts/check-codex-native-push.ts +386 -0
  59. package/scripts/check-codex-sender-identity.ts +495 -0
  60. package/scripts/check-compaction-send-guard.ts +130 -0
  61. package/scripts/check-copilot-receive-arm.ts +4 -1
  62. package/scripts/check-entwurf-fact-provider.ts +38 -0
  63. package/scripts/check-entwurf-peers-surface.ts +13 -1
  64. package/scripts/check-entwurf-self-address.ts +15 -16
  65. package/scripts/check-entwurf-v2-contract.ts +4 -3
  66. package/scripts/check-entwurf-v2-decider.ts +7 -5
  67. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  68. package/scripts/check-entwurf-v2-production.ts +245 -12
  69. package/scripts/check-entwurf-v2-runner.ts +1 -1
  70. package/scripts/check-entwurf-v2-send.ts +26 -7
  71. package/scripts/check-entwurf-v2-surface.ts +1 -1
  72. package/scripts/check-gate-qualification.ts +8 -3
  73. package/scripts/check-harness-admission-parity.ts +0 -1
  74. package/scripts/check-mux-launch-tmux.ts +345 -4
  75. package/scripts/check-native-push-adapter.ts +20 -16
  76. package/scripts/check-native-push-register.ts +5 -1
  77. package/scripts/check-release-gate-outcomes.ts +47 -1
  78. package/scripts/check-setup-qualification.sh +3 -1
  79. package/scripts/codex-app-server-launch.sh +275 -0
  80. package/scripts/codex-birth-doctor.sh +276 -0
  81. package/scripts/codex-birth-install.sh +414 -0
  82. package/scripts/codex-birth-uninstall.sh +170 -0
  83. package/scripts/codex-mcp-config.py +435 -0
  84. package/scripts/codex-socket-path.ts +33 -0
  85. package/scripts/codex-statusline-config.py +434 -0
  86. package/scripts/codex-terminal-title-config.py +500 -0
  87. package/scripts/codex_toml_io.py +653 -0
  88. package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
  89. package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
  90. package/scripts/lib/launch-receipt-windows.ts +46 -0
  91. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  92. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  93. package/scripts/mutants/codex-app-server-launch.json +157 -0
  94. package/scripts/mutants/codex-caller-seat.json +336 -0
  95. package/scripts/mutants/codex-native.json +838 -0
  96. package/scripts/mutants/compaction-send-guard.json +103 -0
  97. package/scripts/mutants/entwurf-peers.json +19 -0
  98. package/scripts/mutants/mux-fresh-call.json +93 -10
  99. package/scripts/mutants/omp-fresh.json +6 -4
  100. package/scripts/mutants/release-gate.json +13 -0
  101. package/scripts/mutants/v2-surface.json +75 -1
  102. package/scripts/raw-async-delivery/README.md +2 -1
  103. package/scripts/raw-codex-measure/README.md +114 -46
  104. package/scripts/smoke-agy-native-push-live.ts +3 -1
  105. package/scripts/smoke-codex-birth.sh +347 -0
  106. package/scripts/smoke-codex-config-state.sh +700 -0
  107. package/scripts/smoke-codex-fresh-live.ts +1426 -0
  108. package/scripts/smoke-codex-native-push-live.ts +75 -0
  109. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  110. package/scripts/smoke-setup-verdict.sh +125 -10
  111. package/scripts/tsconfig.json +1 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,467 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.22.0 - 2026-09-17
8
+
9
+ ### Added
10
+
11
+ - **A Codex sibling opened into a directory the vendor has no answer for now SAYS SO
12
+ (`codex-launch-cwd-undecided`).** `entwurf_fresh_call` reads the launch directory against the
13
+ operator's Codex config and prints what it saw — and then opens the window anyway. **It is a
14
+ diagnostic, not a gate, and that is the product decision (GLG, 2026-09-17):** the vendor's
15
+ consent screen is self-repairing for the human this rail exists to put a window in front of, one
16
+ answer teaches the vendor that directory for good, and a refusal would replace that with "no
17
+ window, go run codex yourself, then call again" while having to be right about a decision this
18
+ process cannot fully see. The UNATTENDED case is answered where it belongs —
19
+ `smoke-codex-fresh-live` asserts the same leaf up front, so a release gate with nobody at the
20
+ keyboard reads a named precondition instead of a callback timeout. `[source rust-v0.153.4]` a `--remote` startup — which this
21
+ argv always is — runs `check_directory_trust` on the `-C` value (`tui/src/lib.rs:1699-1725`), and
22
+ nothing on that path consults the approval or sandbox policy
23
+ (`tui/src/onboarding/directory_trust.rs:33-130`): the
24
+ `--dangerously-bypass-approvals-and-sandbox` token this launcher already carries turns off
25
+ approvals, never folder consent. A DIRECT decision is keyed to the exact directory and nothing else — for
26
+ `ProjectTrustHost::Remote` the lookup is `vec![cwd_key]` (`tui/src/config_update.rs:290-296`), with
27
+ no project-root marker, no git root and no parent inheritance. **The question is whether the
28
+ directory has been ANSWERED, not whether it is trusted:** a saved `untrusted` folder is
29
+ explicitly skipped on a remote target (`onboarding/directory_trust.rs:94-96`), so that turn
30
+ starts too and is not noted at all. A cwd inside an explicitly `untrusted` project gets its OWN
31
+ note (`codex-launch-cwd-untrusted-ancestor`) because the vendor answers that one with an error
32
+ rather than a screen — `pass the repository root explicitly with --cd`
33
+ (`config_update.rs:357-371`) — so its repair names a different directory, not a prompt.
34
+
35
+ **The leaf is deliberately NARROWER than the vendor's own judgment and says so rather than
36
+ claiming equivalence.** It reads the operator's `config.toml`; the vendor reads an effective
37
+ config — system, managed and cloud layers merged around that file — through its app-server, and
38
+ an enabled PROJECT LAYER consents on a directory's behalf with no entry at all
39
+ (`config_update.rs:346-354`). That gap is affordable precisely because nothing here refuses:
40
+ everything the leaf cannot see stays silent, and a note that turns out to be unnecessary costs a
41
+ line of stderr. **`[측정 2026-09-16]` this is the
42
+ failure it retires, not a hypothetical:** four `smoke-codex-fresh-live` runs, one scratch directory
43
+ each; the two whose directory carried a trust entry (`…-db65N2`, `…-pBXxOJ`, both answered by a
44
+ human at the keyboard) passed, and the two unattended release-gate runs (`…-2zznHl`, `…-kSsoAn`)
45
+ died with no rollout written after 21:28 at all — the sibling had opened on a consent screen and
46
+ started no turn, so there was no birth, no callback, and nothing to address. The caller then waits
47
+ out its entire timeout on a window that is merely waiting to be answered, which reads as a delivery
48
+ defect and is a folder question. The directory asked about is read back off codex's own `-C` token
49
+ rather than recomputed, so the consent question cannot drift from the thread's real start
50
+ directory, and the inherited default stays resolved in exactly one place.
51
+
52
+ - **A Codex caller opens its sibling beside its own pane (#95 lane B).** Until now the seat a
53
+ Codex citizen got came from the app-server's inherited `TMUX`/`TMUX_PANE`, so a sibling opened
54
+ from Codex landed wherever the operator happened to have started the app-server rather than
55
+ beside the TUI the operator was looking at. `[측정 2026-09-16, codex-cli 0.153.4]` with
56
+ `thread-id` in `[tui].terminal_title` the TUI writes its thread UUID into the OSC 0 title and
57
+ tmux answers with it as `#{pane_title}` — truncated to 32 chars, i.e. 29 graphemes plus `...`
58
+ (`status_surfaces.rs:892-894`, `:1027-1043`; unchanged at `rust-v0.154.0`). So when the
59
+ reconciled sender of a fresh call is a record-backed `codex` citizen and `placement` is
60
+ omitted, that request's own `_meta.threadId` selects the pane whose title carries the token and
61
+ its `$session` becomes the target. **No vendor patch, no new carrier: one config item.**
62
+ - **`entwurf install-codex-terminal-title` / `uninstall-` / `doctor-`** — a fourth Codex
63
+ ownership atom, owning only `thread-id` membership within `tui.terminal_title`. A different key
64
+ and a different axis from `install-codex-statusline`: `status_line` is what a human reads
65
+ inside the TUI, `terminal_title` is what the multiplexer reports back. The seeded list is
66
+ `["activity", "project-name", "thread-id"]` — `activity` leads because the herdr Codex detector
67
+ keys on the spinner and `Action Required` prefix — and an existing operator list is APPENDED to,
68
+ never reordered. `setup` composes it on a detected Codex host.
69
+ - **The seat now follows the CALLER, never the backend being opened, on both fresh-call surfaces.**
70
+ An explicit `placement.tmuxSession` always wins; then a Codex CALLER's own pane
71
+ (`codex-title-anchor`); then the caller's own session. The anchored receipt reports its SOURCE and
72
+ no session name, because a session was observed rather than a name requested.
73
+ - **`entwurf codex-app-server` — the one command that starts the operator's Codex app-server.**
74
+ Every Codex rail needs a server the operator starts themselves, and the command was
75
+ `codex app-server --listen "unix://$CODEX_HOME/app-server-control/app-server-control.sock"`:
76
+ correct, in three documents, and in the text of a refusal — which meant most operators met it
77
+ only AFTER something had already failed. This verb owns that SPELLING and nothing else. It
78
+ `exec`s the vendor in the terminal it was typed in (cwd, pid, tty and exit status are the
79
+ server's; Ctrl-C is the operator's), creates the control directory, forwards the rest of the
80
+ argv byte-identical, and strips `PI_SESSION_ID`/`PI_AGENT_ID` so the server every bridge child
81
+ inherits from cannot carry someone else's identity. The address comes from the SAME leaf
82
+ delivery, preflight and the LIVE smoke read: the launcher ASKS that resolver through the new
83
+ internal `entwurf codex-socket-path` rather than re-deriving the path, so there is exactly one
84
+ spelling of this address instead of two that agree on the inputs somebody thought of.
85
+ Refusals are named and happen before the exec: a live socket (naming the pid whose `/proc`
86
+ cmdline actually carries that socket, and saying plainly when the host holds none — never an
87
+ inferred owner), a resolved address that is not absolute or carries a control character
88
+ (refused before anything is created: a relative socket is a different file for every process
89
+ that resolves it, and the bridge that will look for it is the app-server's MCP child with its
90
+ own cwd), an unidentifiable path, a second
91
+ `--listen`, a recursive launch, a socket classification nobody wrote, no `codex` on PATH. A dead
92
+ socket file is reported and launched over. **It is a managed spelling, not a managed
93
+ lifecycle**: no supervisor, no restart, no daemon, no pid file, and `entwurf_fresh_call` still
94
+ REJECTS a missing app-server rather than starting one. The tmux seat this server gives caller-seat lookups is reported as a fact, not
95
+ enforced as a requirement. Typing the raw vendor command remains equivalent.
96
+
97
+ ### Verification
98
+
99
+ - **Release acceptance for this cut.** `[LIVE 2026-09-17, oracle]` `LIVE=1 ./run.sh release-gate
100
+ /tmp/entwurf-release-gate-0.22.0.royX2L --cut` → **`MUST: PASS=24 FAIL=0 SKIP=0`**, **`BEHAVIOR:
101
+ PASS=1 FAIL=0 SKIP=0`**, **`cut: OK`**, exit 0, 18:47–20:02 KST; log preserved at
102
+ `<scratch>/release-gate.log`. Inside it: `check:full` exit 0 in 521s, `check-gate-qualification`
103
+ **520/520 killed**, `smoke-entwurf-chain-live` 24 assertions, `smoke-codex-fresh-live` **66
104
+ assertions** — one more than lane C's 65, because the launch-directory precondition this release
105
+ adds is asserted before anything launches. Gate env `ENTWURF_CODEX_FRESH_MODEL=gpt-5.6-luna`,
106
+ `ENTWURF_CODEX_FRESH_PI_MODEL=openai-codex/gpt-5.6-luna`, `ENTWURF_CODEX_APP_SERVER_PID` naming an
107
+ operator-started app-server in its own tmux session. Exact-SHA CI at the pre-release commit:
108
+ run [`35202801617`](https://github.com/junghan0611/entwurf/actions/runs/35202801617)
109
+ `event=workflow_dispatch` @ `f5d6e10`, all four required jobs success and the `check` job's
110
+ qualification BODY step success.
111
+ - **The host's Codex hook declaration was measured with the herdr SessionStart group temporarily
112
+ removed, and that is a debt this release does not pay.** `~/.codex/hooks.json` on `oracle` carries
113
+ a second SessionStart group installed by herdr, so `doctor-codex-birth` reports the declaration as
114
+ edited after install and `smoke-codex-fresh-live` cannot run. For this cut the file was moved to
115
+ `hooks.json.herdr-bak` and re-published by `install-codex-birth`; the vendor's trust receipt for
116
+ `…/hooks.json:session_start:0:0` survived that re-publication, measured by a real Codex TUI
117
+ starting with no consent screen. The herdr group is restored after the cut and the doctor goes RED
118
+ again on purpose. **#117 owns the real repair** — entwurf requires a whole-file digest where the
119
+ unit it actually owns is one group.
120
+ - **Two gate defects found by the release gate itself, both repaired in test/gate code with zero
121
+ production bytes.** `check-gate-qualification` scored **518/520** on the first prepared commit and
122
+ both survivors were launch-directory claims whose assertion could not see the defect its mutant
123
+ plants. `CODEX-LAUNCH-CWD-ANCESTOR-PLAIN-PATHS-ONLY` guards two predicates — the cwd side and the
124
+ key side — and every non-plain cell put the encoded character in the KEY, so the key-side filter
125
+ answered alone and deleting the cwd-side guard changed nothing; cells that vary the CWD against a
126
+ plain untrusted key now close it. `FRESHCALL-CODEX-LAUNCH-CWD-NOTES-NEVER-REFUSES` asserted the
127
+ `indexOf("-C")` lookup but not the value read at it, so a note that asked about the REQUESTED cwd
128
+ instead of codex's own `-C` token passed; the three statements are now pinned contiguously.
129
+ Neither mutant was stale — each still matched its production line exactly once, which is what made
130
+ the hole an assertion hole rather than drift.
131
+ - **`smoke-entwurf-chain-live` went red because a model read the operator's policy correctly, and
132
+ the repair moved the authority rather than the wording.** The native Claude Code hop declined to
133
+ forward, citing the operator's global AGENTS.md; B, C and D were all born and addressable and the
134
+ terminus simply received nothing. A first repair put the provenance in the payload and the hop
135
+ named those sentences as the tell — content that vouches for itself is the shape of an injection,
136
+ so no sentence inside the payload can settle it. The payload is therefore byte-identical to
137
+ before, and the fact now rides the disposable world the turn starts in: the gate mints that
138
+ directory, every `ENTWURF_META_*_DIR` resolves inside it, and it exists because the operator ran
139
+ `release-gate`, so an `AGENTS.md`/`CLAUDE.md` written there is a standing operator instruction of
140
+ exactly the kind the operator's own global policy defers to for directory-scoped behaviour. **No
141
+ negative instruction was added**: a "do not check your policy" line would buy the same green by
142
+ asking the model to stop reading, and would keep passing after the chain broke. Accepted on the
143
+ host default model with no model pin.
144
+
145
+ - **The Codex LIVE card now measures WHERE a sibling starts, and the acceptance is recorded.**
146
+ `[LIVE 2026-09-16, Codex 0.153.4, 65 assertions, exit 0]` hop 1 requests a cwd and joins four
147
+ authorities that cannot borrow from each other — the pane's `#{pane_current_path}`, the vendor
148
+ rollout's `session_meta.cwd`, the Entwurf record and the requested scratch; hop 2 requests NEITHER
149
+ placement NOR cwd, so its directory can only come from the Codex caller's own record. Both are
150
+ asserted different from the app-server's live `/proc/<pid>/cwd`, which is the failure mode staying
151
+ visible rather than coincidentally equal. 65 and 56 are different contracts: lane C added the cwd
152
+ axis on top of lane B's seat axis. Receipts and digests in `DELIVERY.md` / `BASELINE.md`.
153
+ - **Caller-seat LIVE accepted on Linux, 2026-09-16 (Codex 0.153.4): 56 assertions, exit 0.** The
154
+ operator-owned app-server stayed alone in session A (`$30`/`@41`/`%45`, pid `737636`) while the
155
+ initial Pi, the omitted-placement Codex it opened, and that Codex's own outbound Pi all landed in a
156
+ different session S (`$2`: `@46`, `@47`, `@48`). A ≠ S is what makes the run decisive: the
157
+ app-server's inherited `TMUX` names A, so the pre-#95 environment fallback would have put the
158
+ outbound Pi there. Its receipt instead records `seat-source=codex-title-anchor`. `DELIVERY.md` owns
159
+ the coordinates, garden ids and digests, including the 43-assertion run kept as the D1 measurement.
160
+ - **`check-codex-app-server-launch` — 29 assertions, hermetic, plus 9 mutants all killed by
161
+ their own claim.** A fake vendor on a sandbox PATH under the real name reports the argv, parent
162
+ and environment it was handed; nothing is asserted by reading the launcher's source.
163
+ `check-pack-install` proves the INSTALLED verb reaches a vendor at the address the INSTALLED
164
+ compiled resolver computes — a file-list assertion cannot tell a shipped script from one that
165
+ resolves its siblings wrongly under `node_modules`.
166
+ - **Independent review caught a false success this gate had certified, and the repair removed the
167
+ class rather than the instance.** The launcher's first version re-derived the socket path in
168
+ bash and the gate compared the two spellings over four ASCII-normal inputs they agreed on.
169
+ `[측정 2026-09-16]` they disagree on `CODEX_HOME=$'\ufeff'` — JS `trim` strips U+FEFF, a POSIX
170
+ `[:space:]` trim does not, so the managed launch would have started a server at
171
+ `<BOM>/app-server-control/app-server-control.sock` while delivery looked at `$HOME/.codex` —
172
+ and on `path.join`'s normalization of a trailing slash or a `..` segment. Widening the matrix
173
+ would only have added the inputs somebody thought of, so the second spelling was deleted: the
174
+ launcher asks, the gate's hostile cells (BOM, trailing slash, `..`) keep it asking, and the
175
+ mutant that used to drop `CODEX_HOME` now re-derives the path in bash instead. The same review
176
+ found the `/proc` holder assertion over-determined (it accepted the fallback text OR a pid, so
177
+ deleting the scan passed) and the socket classifier not fail-closed on an unrecognised reading.
178
+ A second review pass then found that asking one authority for the address does not make the
179
+ ANSWER safe: the resolver returns `CODEX_HOME` faithfully, so
180
+ `CODEX_HOME=$'relative\ncontrol-home'` had the launcher create that directory under its caller's
181
+ cwd and bind a relative socket — a different file for every process that resolves it, while the
182
+ gate still passed. This launcher is the only Codex surface that CREATES and binds rather than
183
+ reads, so it now refuses a non-absolute or control-character address before the first write.
184
+ Every one of those is a discriminating cell with a mutant of its own; the lane is 9.
185
+ - **Two gate defects surfaced on the way and were fixed rather than worked around.** A mutant whose
186
+ `find` no longer matched had been silently untested since a rename — a stale mutant is a claim
187
+ nobody is checking, so `check-gate-qualification` scores it red. And the LIVE source-call oracle
188
+ compared tool arguments with `isDeepStrictEqual`, so a model that omitted the optional `wants_reply`
189
+ rather than passing it explicitly read as drift; it now normalizes that one key to its schema
190
+ default. Both are recorded in `VERIFY.md`'s affected-set rules so the inner loop stops missing them.
191
+
192
+ ### Changed
193
+
194
+ - **`smoke-codex-fresh-live` launches in ONE stable directory instead of a fresh `mkdtemp` per
195
+ run.** Per-run random scratch made every run an undecided folder — a trust prompt per run, an
196
+ entry per run in the operator's `~/.codex/config.toml`, and unattended failure whenever nobody was
197
+ there to answer. The path is now one stable `os.tmpdir()`-rooted directory, answered `Trust`
198
+ once; the smoke PRINTS it as `launch-cwd <dir>` on every run and asserts that consent by name
199
+ BEFORE it launches anything, so a missing answer reads as `codex-launch-cwd-undecided` — with the
200
+ exact `codex -C <dir>` repair command — at the top of the run rather than as a callback timeout
201
+ twenty assertions later. The directory is printed rather than documented as a literal because
202
+ `os.tmpdir()` is a host fact: a doc spelling it `$TMPDIR/...` would send an operator with no
203
+ `TMPDIR` to `/entwurf-codex-fresh-live` in the filesystem root, which is a different directory
204
+ the vendor consents to separately. Lane C is unaffected: its only requirement of that directory is that it differ
205
+ from the app-server's own, which the assertion still proves.
206
+ - **The Codex live-spend default comes down to `gpt-5.6-luna`.** The `entwurf-dev` skill's Codex
207
+ backend default and the `smoke-codex-fresh-live` recommendation in `VERIFY.md` move off
208
+ `gpt-5.6-sol`. **This is the Codex axis alone** — OMP's fresh default is deliberately unchanged at
209
+ `openai-codex/gpt-5.6-sol` (`scripts/smoke-omp-fresh-live.ts:14`), because that is the model its
210
+ two-stage bootstrap callback was actually measured on and moving it would invalidate that
211
+ receipt. A cost decision, not a gate change: no step moved tier, and the acceptance receipt for
212
+ the Codex cell must be the run that actually happened on the new default rather than a `sol` run
213
+ inherited from before.
214
+
215
+ - **The fixed `codex` tmux home is RETIRED (#95 D1).** 0.21.0 shipped it three days earlier: an
216
+ omitted-placement Codex TARGET selected an already-existing session named exactly `codex`, where
217
+ the operator was asked to seat the app-server and every Codex TUI, and the launch receipt labelled
218
+ that source `Codex home`. GLG retired it the moment the caller-pane anchor stood up — *"무조건
219
+ 은퇴야 … 코덱스의 거처를 만들어주자는 말은 그냥 기술이 안돼서 무마한 개념이야. 제대로 가야지."* The
220
+ room was a workaround for a mapping that did not exist yet, and keeping it after the anchor landed
221
+ would have left Codex alone answering "where does a sibling open?" differently from every other
222
+ backend. `CODEX_HOME_TMUX_SESSION`, the `codex-home` receipt source, its renderer branch, its
223
+ deterministic cells and its mutant are gone; an omitted seat is the caller's own session for every
224
+ backend, and the operator seats the app-server wherever they like — Entwurf still never creates,
225
+ moves or supervises it. The 2026-09-12 home acceptance stays recorded in `DELIVERY.md` and
226
+ `docs/setup-clean-host.md` as history, not as a live contract. The reshaped LIVE card now REQUIRES
227
+ the app-server to sit in a different session from the Pi/Codex pair: that separation is what tells
228
+ the caller-pane anchor apart from the app-server's inherited environment.
229
+
230
+ ### Fixed
231
+
232
+ - **An in-band refusal now reaches the sender with the reason the RECEIVER gave it
233
+ (`d49e6cb`, `0c8a42c`).** A completed control-socket RPC that came back `success:false` was
234
+ finalized as `rejected` with no reason at all, so every named receiver refusal — including the
235
+ 0.21.0 compaction guard's own `compacting` / `busy` — arrived at the caller as a reasonless
236
+ rejection indistinguishable from "no live route". The send hand now carries that error verbatim
237
+ when the receiver supplied one, and invents nothing when it did not: receiver evidence is not
238
+ reclassified into the resolver's taxonomy (`dormant-fire-forget-unsupported`,
239
+ `mailbox-undeliverable`, …), and the two remain distinguishable at the surface. The gap was one
240
+ hop wide and entirely invisible to the unit lane — the production `sendOverSocket` closure was
241
+ what dropped `response.error` before `driveSend` could see it — so the guard that holds it is a
242
+ production-wiring assertion with its own mutant (`V2PROD-INBAND-ERROR-WIRED`), not another test
243
+ of the pure function.
244
+ - **A Codex sibling's THREAD now opens where its pane is (#95 lane C).** The seat was right and the
245
+ directory was not: `[측정 2026-09-16]` a pi → Codex → Claude Code chain recorded the app-server's
246
+ `~/repos/gh/entwurf` for all three citizens while the panes sat in `~/repos/gh/agent-config`.
247
+ `[source rust-v0.153.4]` an explicit `--remote <endpoint>` resolves to `AppServerTarget::Remote`
248
+ (`codex-rs/tui/src/lib.rs:875-876`), and a Remote target takes its new thread's cwd from
249
+ `remote_cwd_override` alone (`app_server_session.rs:2022-2033`) — absent, the app-server opens the
250
+ thread in its OWN directory, and the birth hook then records that vendor-supplied cwd honestly.
251
+ The codex argv now always carries that override as `-C <dir>`, the flag the shared interactive
252
+ options already expose beside `--dangerously-bypass-approvals-and-sandbox`
253
+ (`utils/cli/src/shared_options.rs:53-68`), preserved for a remote target at
254
+ `startup_orchestration.rs:191-194`. **One flag, no vendor patch, no new layer.**
255
+ - **A Codex CALLER that names no cwd opens its sibling in its OWN directory.** This bridge runs as
256
+ the operator-owned app-server's MCP child, so the directory its process reports is the
257
+ app-server's rather than the caller's — an omitted `cwd` used to put every sibling of every Codex
258
+ caller in the app-server's repo, whatever backend was being opened. The surface now hands the
259
+ composition that citizen's RECORD cwd under the same condition that already carries its thread
260
+ id, and it is consulted only when the call requested none. One value, two carriers: tmux `-c`
261
+ places the pane, codex `-C` places the thread. Every other caller's argv is byte-identical, and
262
+ the receipt names which rule chose the directory (`requested` / the Codex caller's own record
263
+ directory) instead of calling a caller-record path "requested".
264
+ - **Three named refusals replace a silent wrong seat.** Zero matching panes is
265
+ `codex-caller-seat-unresolved` (the TUI is on another tmux server, its config carries no
266
+ `thread-id`, or that server has `allow-set-title off`, which `[측정]` replaces every pane title
267
+ with the hostname); two or more is `codex-caller-seat-ambiguous`; and a Codex caller whose own
268
+ `[tui].terminal_title` lacks `thread-id` is refused pre-mutation as `codex-caller-title-missing`
269
+ with the installer as its repair. None of them falls back to another session, and none opens a
270
+ window. That caller-side check runs only when the anchor is actually consulted — an explicit
271
+ seat never reads a title.
272
+ - **A pane title is a placement input and nothing else (Hard Rule 16).** Any process in any pane
273
+ can emit the same OSC string, so the leaf's answer may only reach a `-t` target; identity,
274
+ delivery and liveness keep the record + `_meta` join they already had, and the first-turn
275
+ framing still carries the caller's garden id rather than the thread id a title matched. The leaf
276
+ (`pi-extensions/lib/codex-caller-seat.ts`) imports nothing at all, which is how that boundary is
277
+ held by the code rather than by a comment.
278
+ - **The title match is over tokens, not ` | ` segments, and the count is over panes.**
279
+ `title_setup.rs:183-193` joins the `activity` item to its neighbour with a plain space rather
280
+ than ` | `, so on a host whose operator list ends in `activity` a working TUI renders
281
+ `<spinner> <thread-id>` inside one segment and a segment-equality rule would lose the seat
282
+ exactly while the caller is busy. Counting panes rather than tokens keeps a single pane that
283
+ shows the same thread twice (`thread-title` beside `thread-id`) from reading as an ambiguity.
284
+
285
+ ## 0.21.0 - 2026-09-13
286
+
287
+ ### Added
288
+
289
+ - **This is a minor release because Codex support adds a new visible citizen axis, not merely a
290
+ patch to an existing rail.** The operator-owned exact `codex` tmux home gives the app-server and
291
+ supported Codex TUIs a stable visible place without making Entwurf their lifecycle manager. UX
292
+ symmetry means each backend remains a sibling with its own transcript, auth, native tools, and
293
+ visible seat—not that unlike transports are relabeled as identical. The release also carries the
294
+ independent #111 and #112 bug repairs.
295
+ - **OpenAI Codex CLI becomes an app-server-backed native-push citizen (#95).**
296
+ A user-scope `SessionStart` hook, declared in the operator's own `$CODEX_HOME/hooks.json`,
297
+ mints the V3 record on the first turn and sets the visible thread title. It needs no root:
298
+ the installer refuses to run as uid 0, and the declaration is trust-gated rather than
299
+ prompt-free — the operator answers the vendor's prompt with "Trust all" ONCE in their own
300
+ visible Codex, and the vendor records that decision itself. Entwurf never computes, writes,
301
+ pre-seeds or reads a `trusted_hash`, and never touches `config.toml`. Because the vendor
302
+ hashes the declaration identity (event, matcher, handler path) and not the launcher's
303
+ contents, the payload closure can be upgraded on later releases without asking again. The bridge strictly joins each request's Codex metadata to
304
+ that record, so multiple visible TUIs may share one app-server without sharing a
305
+ process marker. The purpose is to preserve Codex's native tools, delegation, and work
306
+ context as a citizen — not to add an ACP Codex backend or duplicate GPT access.
307
+ - **Codex gets three independently owned install/doctor/inverse atoms.**
308
+ `install-codex-birth` owns only the fixed `$CODEX_HOME/hooks.json` declaration plus its
309
+ launcher/payload closure under `${XDG_DATA_HOME:-~/.local/share}/entwurf/codex-birth`, with a
310
+ digest inventory that licenses an exact inverse and refuses drifted bytes;
311
+ `install-codex-mcp` owns only `[mcp_servers.entwurf-bridge]`; and
312
+ `install-codex-statusline` owns only `thread-title`. The MCP atom also owns the exact
313
+ `env_vars` names that forward `CODEX_HOME`, Entwurf garden/control roots, and the
314
+ operator-owned app-server's `TMUX`/`TMUX_PANE`. `setup` never escalates or starts it.
315
+ - **`entwurf_fresh_call` accepts `backend: "codex"` on both public tool surfaces and the
316
+ operator skill.** It requires an explicit model and uses the measured
317
+ `codex --remote unix://<default-socket> --model … --dangerously-bypass-approvals-and-sandbox`
318
+ shape. The birth unit, MCP/env boundary, visible title, and the operator-owned app-server
319
+ socket are checked before tmux mutation. With placement omitted, Codex resolves the exact
320
+ existing `codex` tmux home; a missing home rejects without creating one. Explicit placement
321
+ remains an expert override. This does not infer an attached TUI seat from request identity.
322
+ - **Omitted placement becomes a backend-selected policy, and Codex is its first implementation.**
323
+ Every backend could already be sent to an operator-owned existing seat through explicit
324
+ `placement.tmuxSession`; that is unchanged, and named `claude`/`pi` seats stay available that
325
+ way without becoming omitted-placement defaults. What is new is that omitting placement no
326
+ longer means one thing for every backend: `pi`, `claude-code`, `copilot` and `omp` still open
327
+ in the caller's seat, while `codex` resolves its exact operator-owned home. tmux remains the
328
+ visible launch seat, never an address — the garden id is the address and arrives in the
329
+ callback envelope. Sibling symmetry is transcript, auth, native tools and visibility; it is
330
+ not a claim that the transports are the same.
331
+ - **Why a client-side home rather than a vendor seat carrier.** Upstream
332
+ [openai/codex#44774](https://github.com/openai/codex/issues/44774) records the maintainer's
333
+ preference for client-side terminal/window management, citing multi-client steer semantics and
334
+ network propagation. Rather than wait for a request→pane carrier or guess one, 0.21.0 takes the
335
+ client-side route the exact `rust-v0.153.4` source supports: an operator-owned exact home plus
336
+ explicit placement, with the wider attached-TUI topology left unsupported and unclaimed.
337
+
338
+ ### Changed
339
+
340
+ - **The native-push rail is adapter-specific instead of Antigravity-shaped.** Codex liveness
341
+ is `thread/loaded/list` over the app-server WebSocket UDS; delivery is the measured one-shot
342
+ `codex queue --remote … --thread … --message …`. It has no mailbox, receiver marker, or
343
+ resume surface. Unlike Antigravity, Codex delivery is never retried because vendor acceptance
344
+ may precede the CLI receipt; replay would duplicate the user's message.
345
+ - **Codex remains outside ACP and Entwurf remains outside Codex lifecycle ownership.**
346
+ Entwurf does not install Codex, supply auth, expose a generic app-server API/manager, or
347
+ start/supervise/restart the app-server or create its tmux home. The supported topology is
348
+ one operator-owned existing session named `codex` containing the app-server and supported
349
+ Codex TUIs. N arbitrary attached TUIs across sessions still have no request→TUI seat join;
350
+ that wider topology is unsupported and unclaimed rather than a blocker to the explicit home.
351
+ - **`entwurf_peers` bounds expensive historical observation to its 32 rendered rows (#112).**
352
+ Full-store parsing, duplicate/conflict authority, socket liveness, diagnostics, and all 1,012
353
+ measured payload rows remain intact; older receiver/transcript cells say `unobserved` rather
354
+ than fabricating facts. On the 1,012-record fixture, repeated warm composition fell from about
355
+ 3.2s to 70–103ms. Generic fact-provider callers remain unbounded unless they explicitly opt in.
356
+ - **Version-coherence gates no longer require concise `AGENTS.md` to duplicate package pins.**
357
+ The owning runtime/package documents remain checked; removing redundant pin prose from the
358
+ maintainer guide no longer makes an otherwise coherent floor fail.
359
+
360
+ ### Upgrade note
361
+
362
+ Codex native delivery requires three user-scope atoms and one operator trust answer. No step
363
+ needs root:
364
+
365
+ ```bash
366
+ entwurf install-codex-birth
367
+ entwurf install-codex-mcp
368
+ entwurf install-codex-statusline
369
+
370
+ # Then, ONCE, in a visible plain Codex: answer the hook prompt with "Trust all and continue"
371
+ # and send one first turn. Until the vendor records that receipt the hook is declared and
372
+ # never runs, and `doctor-codex-birth` / `setup` stay non-green and say so.
373
+
374
+ # First create/open the operator-owned tmux session named exactly `codex`.
375
+ # From a pane in that session:
376
+ CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
377
+ mkdir -p "$CODEX_HOME/app-server-control"
378
+ codex app-server --listen "unix://$CODEX_HOME/app-server-control/app-server-control.sock"
379
+ # Seat supported Codex TUIs in this same `codex` session.
380
+ ```
381
+
382
+ Then restart/open Codex and send its first turn. `entwurf setup <project>` never runs sudo and
383
+ never starts the app-server.
384
+
385
+ ### Verification
386
+
387
+ - The GPT-6 review reopened five surfaces in the candidate: terminal parser, explicit
388
+ env-name boundary, setup acceptance, fresh preflight safety, and clause-7 composition.
389
+ The prior “source-complete; only host ownership remains” wording is withdrawn.
390
+ - The 2026-09-11 Codex CLI 0.153.4 run remains pre-amendment evidence: native-push and a
391
+ public MCP call woke one visible loaded thread; authenticated request identity resolved
392
+ `agentId=meta-session/codex`, `replyable=true`; and a source hook minted a record in an
393
+ isolated store. It does not qualify the amended candidate.
394
+ - The 2026-09-12 Codex 0.153.4 explicit-home LIVE passed 57 assertions. A real initial Pi
395
+ ran at `$150/@397`; the operator app-server remained at `$158/@390/%390`; omitted-placement
396
+ Codex opened at `$158/@398`; and Codex opened outbound Pi at `$158/@399`. Exact callbacks
397
+ and addressed delivery succeeded both ways. `DELIVERY.md` carries the garden ids, thread,
398
+ nonces, preserved log path, and SHA-256.
399
+ - Two red attempts repaired the acceptance itself. “Wait” had let Codex call one-hour
400
+ `wait_agent`; the task now ends its turn and forbids wait tools. A model also reformatted
401
+ a copied LAUNCH receipt; the gate compares source-owned tool results rather than treating that prose
402
+ as raw evidence. Failure cleanup interrupts only the exact smoke-owned active Codex turn and removes
403
+ only source-receipt-named windows.
404
+ - The second full prepare P5 remained honestly red at **MUST 23 PASS / 1 FAIL / 0 SKIP**. The initial
405
+ Pi's source transcript proved Codex launch, exact callback, native-push delivery, outbound Pi launch
406
+ and callback, and Codex's final mailbox enqueue; it also showed the missing observation's direct
407
+ cause: the model shortened expected launch-report token `…1G5XEKQH` to `…1G5XEKQ`. Enqueue did not
408
+ prove the fixture read, and cleanup had removed the mailbox needed to distinguish every downstream
409
+ observer hypothesis. The amendment removes intermediate model coordinate reports entirely: Pi JSONL
410
+ call/result rows are joined by call id, exact tool name/arguments, non-error result, and uniqueness;
411
+ Codex uses the corresponding structured app-server MCP items; raw callback events supply garden ids.
412
+ Mailbox prose can never authorize a window kill. Every drain now persists filename, byte length,
413
+ SHA-256, observed token candidates, phase-specific selection/exclusion reason and a 0600 body artifact
414
+ immediately, plus resolved-root/run/source-path manifest and a mandatory fixture/record/transcript/thread
415
+ snapshot before destructive cleanup on every outcome. That snapshot uses the record owner's `.meta.json`
416
+ filename, records missing expected sources, and still copies known raw files when optional identity recovery
417
+ throws; final acceptance also requires known `completed` Codex turn/items state and logs payload validation
418
+ separately from token/sender candidate selection. The preserved failed-rollout extraction
419
+ (`c3a28938…`) confirms raw outbound launch → callback → final enqueue but still not fixture read; its tracked
420
+ positive fixture is explicitly labelled a vendor-source-derived v2 projection, not a captured `thread/read`
421
+ response. A second defect the amendment itself introduced was then measured and closed: the smoke read the
422
+ initial Pi's `transcriptPath` once at callback correlation, and a record that gained that path 34s later
423
+ left every subsequent source observation permanently empty — one 300s red whose chain had in fact run to
424
+ completion, proven by the Codex thread's own completed final `entwurf_v2` result. The resolution is a pure
425
+ leaf that re-reads the record while its cached path is empty (an unreadable record stays fail-loud; only
426
+ "no path yet" is pending), its own `CODEX-LIVE-SOURCE-PATH-RE-RESOLVED` claim and mutant, and an explicit
427
+ `no unverified child window killed` line so a run that never obtains source evidence names its
428
+ unrecoverable windows instead of leaking them. Declared inventory is now 475 mutants across 43 lanes.
429
+ The amended standalone LIVE then passed: **48 assertions, exit 0**, final source audit `initial-pi=3/3`
430
+ and `codex=3/3` completed exact, all three opened windows reclaimed by their own receipts, artifact
431
+ `.probe-artifacts/codex-fresh-live-fZccoK/`. Assertion count is not comparable with the pre-amendment 57:
432
+ that contract counted model coordinate reports this one removes.
433
+ - **Release acceptance for this candidate: `check:full` exit 0 in 502s, then the LIVE release gate
434
+ `--cut` at MUST PASS=24 FAIL=0 SKIP=0, BEHAVIOR PASS=1 FAIL=0 SKIP=0, `cut: OK`.** Its
435
+ `check-gate-qualification` MUST step killed 475/475 across 43 lanes with the origin HEAD and
436
+ work-surface hash identical before and after. The aggregate's own Codex leg repeated the
437
+ 48-assertion source-owned acceptance (artifact `.probe-artifacts/codex-fresh-live-2oId4C/`), and the
438
+ mux lifecycle MUST passed in the same run. Scratch log sha256 `942d5fa0…`.
439
+ - **Observation, not a repaired defect.** The first acceptance attempt at the identical source
440
+ fingerprint failed two model-in-loop MUST steps: a callback nonce arrived one character short
441
+ (`…c70cfb` for `…c70cfb2`) and an addressed instruction carried the wrong payload. Both oracles
442
+ refused fail-closed and named their cause, and the joined source receipts proved delivery and rails
443
+ were correct, so nothing in the product was changed. The same candidate, model, and environment then
444
+ passed. That leaves an intermittent exact-byte transfer failure in the model lane as a recorded
445
+ observation; the retry does not establish a cause for it.
446
+ - The first 460-mutant qualification honestly stopped at 454/460. One wait-task mutant was
447
+ stale after the prompt repair; the birth safe-umask mutant was killed earlier by the new
448
+ package-root mode doctor but carried the wrong assertion label; and four setup claims shared
449
+ one control-red because `smoke-setup-verdict` ran real pnpm inside the qualifier's
450
+ node_modules-symlink snapshot. The manifest was re-pinned, safe-umask attribution now has one
451
+ QK helper at both valid failure leaves, and the setup behavior gate uses a strict fixture
452
+ pnpm accepting only a no-op `install --frozen-lockfile` or the source-owned `build-bridge`
453
+ emit, whose snapshot-local dist is removed on exit. The next run killed all 460 mutants but
454
+ correctly rejected its own impure snapshot: the fixture had called the production dist-lock
455
+ wrapper, which removed its lock but left the ignored `.tmp-verify` parent. The fixture now
456
+ invokes the process-exclusive snapshot emit directly; a focused pre/post tree-manifest replay
457
+ was byte-identical. The fresh run then killed **460/460** mutants across 43 lanes in 53m42s
458
+ with the origin and snapshot surfaces pure. Biome only collapsed the new manifest arrays onto
459
+ one line afterward (claim/subject/find/replace bytes and semantics unchanged), and the frozen
460
+ full floor passed in 506s. The receipt-bearing final candidate passed the full floor again
461
+ in 501s before the implementation commit and local-main merge.
462
+
463
+ ### Fixed
464
+
465
+ - **The Copilot receiver parent-death mutant no longer leaves one PID-1 fixture child per qualification run.** P9 found ten reparented Node stubs from ten qualifications. The gate did detect the mutant, but its failing assertion threw before the cleanup statement below it; cleanup now kills the surviving mutant child before asserting, preserving the same attributed red while leaving no process behind.
466
+ - **Control-socket send during Pi compaction is no longer a false `delivered:true` (#111).** A compacting Pi citizen is neither idle nor an ordinary streaming turn, but the receiver only sampled `ctx.isIdle()` and forwarded every non-idle send into `pi.sendMessage({ triggerTurn: true })`. Installed Pi 0.85.1 `sendCustomMessage` then starts `_runAgentPrompt` when not streaming — racing `compact()`'s session rewrite, which is the field path that can make compaction "풀린다". There is no public `ExtensionContext.isCompacting()`. The resident arms on `session_before_compact` and refuses as `compacting`; quiet unknown non-idle (`!idle && ctx.signal === undefined`) is fail-closed as `busy`, not `compacting`, because `ctx.signal` is `agent.activeRun?.abortController.signal` and is already gone during `_handlePostAgentRun` retry/continuation while `_isAgentRunActive` is still true — that cell also covers the manual-compact start race and branch summary, which the public API cannot tell apart. Either token: no `pi.sendMessage`, no `delivered:true`. Idle and live-run steer/followUp are unchanged. Frozen v2 reject taxonomy is untouched — this is an in-band RPC refusal.
467
+
7
468
  ## 0.20.1 - 2026-09-10
8
469
 
9
470
  ### Fixed
@@ -124,17 +585,15 @@ Hosts with no detected harness, and every Linux host, see no new prompt.
124
585
  by the promotion above; it is where a borrowed Mac produces the physical-host
125
586
  evidence the CI runner cannot.
126
587
  - **`scripts/raw-codex-measure/` — Codex 0.153.4 vendor measurement (#95 step 1).**
127
- Nothing is installed: no birth hook, no record, no marker, no doctor, and
128
- `entwurf_fresh_call` still cannot open a codex sibling. GLG reversed the
129
- 2026-08-01 decline on 2026-09-08; live claims that still said otherwise
130
- were repaired. The ledger closes step 1: SessionStart fires on the first
131
- turn, not at window open; a fourth callback spelling
132
- (`mcp__entwurf_bridge__entwurf_v2`); the delivery-capable app-server join
133
- cannot use parent-pid as a citizen key; hook-trust is an ownership cost
134
- (`/etc/codex`, root-owned), not a capability gap; clause 4 is
135
- `thread/name/set` plus a status_line config. Step 2 is not opened.
136
- `PIN_CODEX_MINOR` 0.144 → 0.153 is a re-verification of the archived drift
137
- probe, not a product pin.
588
+ **At the measurement landing on 2026-09-08**, no Entwurf Codex unit had been
589
+ installed or built, no product record/doctor existed, and fresh did not include
590
+ Codex. Those are dated step-1 facts, not the current candidate state. The ledger
591
+ established first-turn `SessionStart`, callback spelling
592
+ `mcp__entwurf_bridge__entwurf_v2`, the shared-app-server refutation of a pid-keyed
593
+ citizen join, root-owned prompt-free hooks, and `thread/name/set`. Later #95 work
594
+ added the record/native-push/fresh candidate described under Unreleased.
595
+ `PIN_CODEX_MINOR` 0.144 0.153 was a re-verification of the archived drift probe,
596
+ not a product pin.
138
597
 
139
598
  ### Changed
140
599
 
@@ -261,9 +720,9 @@ Lane receipts (oracle, 2026-09-09) plus prepare P4/P5 on this host. There is
261
720
  CommandLineTools stub. Both are cells the borrowed-Mac probe
262
721
  (`scripts/raw-macos-measure/probe.sh`, M6 and M3) is built to answer. Do
263
722
  not fill them from a CI image spec.
264
- - **Codex is not a garden backend in this cut.** Step 1 of #95 closed as
265
- measurement; step 2 is not opened. `entwurf_fresh_call` still cannot open a
266
- codex sibling.
723
+ - **On the 0.20.0 release date (2026-09-09), Codex was not a garden backend in that
724
+ cut.** Step 1 of #95 was measurement only and `entwurf_fresh_call` did not include
725
+ Codex. This is release archaeology; 0.21.0 support supersedes it.
267
726
 
268
727
  ## 0.19.0 - 2026-09-07
269
728
 
package/CONTRIBUTING.md CHANGED
@@ -4,7 +4,7 @@ This is a daily-driver bridge. Correctness beats feature breadth. Read this befo
4
4
 
5
5
  ## What this repo is
6
6
 
7
- `entwurf` is a **garden-citizen dispatch bridge** — entwurf-core (v2 dispatch) + a meta-bridge + a pi adapter + an **ACP plugin on a two-backend adapter rail** — that lets already-running harnesses address one another by garden id; pi is one adapter, not the subject. The ACP plugin borrows the backend's identity (system prompt preset, model behavior, tool implementations) and shapes the *operating surface* — tools, MCP, skills, permissions — to match pi's own policy **wherever that backend exposes a knob for it**. Where it does not, the plugin does not fake one: a cortex session keeps its own native tool surface and receives MCP through an overlay projection, so "shaped to pi's policy" is a Claude-strength claim, not a universal one. Claude is the reference ACP backend and Snowflake Cortex Code is the second (landed 0.13.0, `cortex-` prefixed ids — [docs/acp-backend-rail.md](./docs/acp-backend-rail.md#cortex-code-audit-d1d10)); Codex has native delivery-probe evidence but no managed citizen lane, and Gemini is not a shipped backend. That is the entire scope.
7
+ `entwurf` is a **garden-citizen dispatch bridge** — entwurf-core (v2 dispatch) + a meta-bridge + a pi adapter + an **ACP plugin on a two-backend adapter rail** — that lets already-running harnesses address one another by garden id; pi is one adapter, not the subject. The ACP plugin borrows the backend's identity (system prompt preset, model behavior, tool implementations) and shapes the *operating surface* — tools, MCP, skills, permissions — to match pi's own policy **wherever that backend exposes a knob for it**. Where it does not, the plugin does not fake one: a cortex session keeps its own native tool surface and receives MCP through an overlay projection, so "shaped to pi's policy" is a Claude-strength claim, not a universal one. Claude is the reference ACP backend and Snowflake Cortex Code is the second (landed 0.13.0, `cortex-` prefixed ids — [docs/acp-backend-rail.md](./docs/acp-backend-rail.md#cortex-code-audit-d1d10)); Codex is not on this rail at all — it is an unreleased native-push candidate with record birth and delivery-probe evidence (see AGENTS.md, DELIVERY.md), never ACP-managed and Gemini is not a shipped backend. That is the entire ACP-rail scope.
8
8
 
9
9
  **How a backend joins the rail** (the shape a PR must take): one adapter object in `pi-extensions/lib/acp/backend-adapter.ts` + its own curated rows/overlay modules + its own `check-acp-*` gate and mutant lane. Backend-specific *behavior* must stay behind the adapter, and backend-specific settings ride the opaque `adapterSettings` seam rather than growing the common config. That is not a ban on ever touching the common layer (`backend.ts`'s turn loop, `acp-client.ts`, `event-mapper.ts`, `session-store.ts`, `config.ts`) — cortex's landing did change `backend.ts` to pass the authoritative session key through the generic `ensureOverlay` seam. The rule is narrower and stricter: a common-layer change must be **backend-invariant** (it reads no backend name and branches on no backend) and **separately gated**. A common file that grows an `if (backend === …)` is the thing to reject.
10
10