@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
@@ -0,0 +1,325 @@
1
+ /**
2
+ * meta-bridge-hook-codex — the OpenAI Codex native-session BIRTH entry (#95).
3
+ *
4
+ * WHAT THIS IS, AND WHAT IT DELIBERATELY IS NOT
5
+ *
6
+ * stdin {session_id, transcript_path, cwd, hook_event_name, model, source}
7
+ * -> readCodexBirthEnvelope() strict validation, or REFUSAL
8
+ * -> performCodexBirth() idempotent create/attach of ONE v3 record
9
+ * -> gardenId the session's garden address
10
+ * -> `{}` on stdout, exit 0 always, on every path
11
+ *
12
+ * That is the whole unit. It writes NO pid marker, NO sender marker and NO receiver
13
+ * marker, and each omission has its own reason:
14
+ *
15
+ * 1. NO PID MARKER OF ANY KIND, because on this backend the parent pid is not an
16
+ * identity. `[host]` Measured 2026-09-08 (scripts/raw-codex-measure/README.md,
17
+ * S1b-B): two different TUI windows, two different `session_id`s, and ONE ppid
18
+ * shared by every hook process AND every MCP child of BOTH — the app-server, not
19
+ * the TUI. Neither TUI pid appears in the chain at all. So the Claude/Copilot
20
+ * "shared ancestor is the join key" trick does not merely lack evidence here, it
21
+ * is measurably WRONG: a marker keyed to that pid would name one of two citizens
22
+ * arbitrarily and attribute the other's messages to it. Codex's request-scoped
23
+ * identity rides `_meta.threadId` on the MCP request instead; that is a separate
24
+ * leaf (`lib/meta-sender-identity.ts`), and it needs nothing from this file.
25
+ * 2. NO RECEIVER MARKER, because a receiver marker is a claim that a LIVE process
26
+ * holds a watch. `[source]` Codex has no filesystem-wake vocabulary at all —
27
+ * `watchPaths`/`FileChanged`/`asyncRewake` appear nowhere in its workspace except
28
+ * in a Claude-hooks importer that SKIPS any hook carrying them. Codex receive is
29
+ * native-push and ack-only; nothing about it is armed from here.
30
+ *
31
+ * IDENTITY IS ON THE WIRE, SO NOTHING IS GUESSED. `[host]` The measured 0.153.4
32
+ * `SessionStart` envelope, verbatim:
33
+ *
34
+ * {"session_id":"01a08147-fbad-78d2-b266-e058f322125e",
35
+ * "transcript_path":"<CODEX_HOME>/sessions/2026/09/08/rollout-…-01a08147-….jsonl",
36
+ * "cwd":"/home/junghan/repos/gh/entwurf",
37
+ * "hook_event_name":"SessionStart",
38
+ * "model":"gpt-6-astra",
39
+ * "permission_mode":"default",
40
+ * "source":"startup"}
41
+ *
42
+ * and `[host]` D3 of the same measurement pinned the join: the hook's `session_id`,
43
+ * the app-server's `thread/loaded/list` `threadId`, and the MCP request's
44
+ * `_meta.threadId` are ONE string. Two threads were opened and both matched. That is
45
+ * why this unit stores `session_id` as `nativeSessionId` with no transformation — the
46
+ * send rail (`codex queue --thread <threadId>`) reads that field back verbatim.
47
+ *
48
+ * WHY VALIDATION IS STRICT AND NOT TOLERANT. Every other birth entry in this repo
49
+ * accepts a partially-known envelope because its vendor gave it one (Copilot carries
50
+ * no transcript and no model; omp's shape varies by mode). Codex gives all five axes
51
+ * on every SessionStart, so a MISSING or non-absolute one is not a thin envelope — it
52
+ * is an envelope this unit did not come from, and minting a citizen from it would
53
+ * write a record no live thread can be joined back to. The refusal is cheap and the
54
+ * log names the exact field; a wrong record is neither.
55
+ *
56
+ * THE EVENT NAME IS THE WHOLE TOP-LEVEL PREDICATE, and it is vendor-authoritative.
57
+ * `[source]` A spawned agent does not raise `SessionStart` — it raises `SubagentStart`,
58
+ * and every other subagent source returns before dispatch. `[host]` Measured: exactly
59
+ * ONE `SessionStart` per session, while `SubagentStart` fires per child AND CARRIES THE
60
+ * PARENT'S `session_id` beside its own `agent_id`. So a tolerant reader keyed on
61
+ * `session_id` alone would mint the parent's identity again from a child's event.
62
+ * Hence: `hook_event_name` must be exactly `SessionStart`, and any `agent_id` /
63
+ * `agent_type` key is a refusal even if the event name says otherwise.
64
+ *
65
+ * BIRTH IS ON THE FIRST TURN, NOT AT WINDOW OPEN. `[source]` Session construction only
66
+ * QUEUES the source; `[host]` a TUI sat fully open and idle for ~47s with the hook log
67
+ * EMPTY, then `SessionStart` fired 1.6s after the first prompt. A Codex citizen is born
68
+ * when it is first spoken to. The doctor must therefore read "installed, zero records"
69
+ * as NOT-YET, never as a failure — and never as proof either.
70
+ *
71
+ * FAILURE POLICY, inherited from the Claude/Copilot units: BEST-EFFORT + LOG. Never
72
+ * scream into the operator's terminal, never block the turn. On any error, append a
73
+ * level-tagged line to `<pi-agent-dir>/meta-bridge-hook.log` and exit 0 with `{}` on
74
+ * stdout. The fail-loud surface is the doctor, which reads that log. The four outcomes
75
+ * carry four distinct grep tokens because they need four different fixes:
76
+ *
77
+ * `birth-mint` a new citizen exists. INFO.
78
+ * `birth-attach` an existing citizen was re-seen (resume/clear/compact). INFO.
79
+ * `birth-refused` the envelope was not a trustworthy SessionStart. ERROR — this
80
+ * session did NOT become a citizen.
81
+ * `birth-failed` the envelope was fine and the STORE write broke. ERROR.
82
+ *
83
+ * LAUNCH: never invoked directly by Codex. `[source]` Codex's `command` handler is a
84
+ * SHELL STRING with no exec-form argv variant anywhere in the enum, so entwurf's
85
+ * "no shell-form fallback" rule has no counterpart to bind to here. The declaration in
86
+ * `$CODEX_HOME/hooks.json` therefore names ONE fixed absolute single-quoted launcher,
87
+ * published operator-owned by `scripts/codex-birth-install.sh`, and that launcher `exec`s
88
+ * `node --experimental-strip-types` over a COPY of this file plus its lib closure.
89
+ * Nothing about identity travels in argv or in the environment, so the launcher has no
90
+ * provenance token to stamp (the Claude/Copilot `ENTWURF_META_HOOK_LAUNCH` handshake
91
+ * exists to license a `process.ppid` read, and reason 1 above is why this unit never
92
+ * performs one).
93
+ */
94
+ import * as fs from "node:fs";
95
+ import * as path from "node:path";
96
+ import { defaultMetaSessionsDir, upsertMetaSession } from "./lib/meta-session.js";
97
+ import { resolveCodexDefaultSocketPath, setCodexThreadName } from "./lib/native-push/codex-ws-client.js";
98
+ /** `[source]` `SessionStartSource::{Startup,Resume,Clear,Compact}` — the complete
99
+ * 4-valued matcher input for this event (`hooks/src/events/session_start.rs:25-38`).
100
+ * All four are real births of the same thread id: `Resumed→Resume`, `New|Forked→Startup`,
101
+ * `Cleared→Clear` (`core/src/session/session.rs:1600-1606`), and the upsert is
102
+ * idempotent, so a resume ATTACHES rather than minting a second citizen. */
103
+ const CODEX_SESSION_START_SOURCES = ["startup", "resume", "clear", "compact"];
104
+ /** The one event that means "a top-level thread exists and has spoken once". */
105
+ const CODEX_BIRTH_EVENT = "SessionStart";
106
+ /** A `session_id` travels into a record filename join key and, later, into the exact
107
+ * vendor argv `codex queue --thread <threadId>`. Whitespace or control bytes there
108
+ * would either truncate the field or smuggle argv structure, so the token is bounded
109
+ * at the boundary rather than at each consumer. The measured value is a 36-char UUID;
110
+ * the bound is deliberately looser than that, because the SHAPE of the id is the
111
+ * vendor's business and only its usability as one token is ours. */
112
+ const MAX_THREAD_ID_LEN = 200;
113
+ /** Append a best-effort diagnostic line; swallow even its own failure. Same log file
114
+ * and same LEVEL vocabulary as the Claude/Copilot units, so one doctor grep covers all
115
+ * three. The `[codex]` tag is what makes this unit's lines separable. */
116
+ function logLine(level, message) {
117
+ try {
118
+ const file = path.join(path.dirname(defaultMetaSessionsDir()), "meta-bridge-hook.log");
119
+ fs.mkdirSync(path.dirname(file), { recursive: true });
120
+ fs.appendFileSync(file, `${new Date().toISOString()} ${level} [codex] ${message}\n`);
121
+ }
122
+ catch {
123
+ /* logging is best-effort; a broken log must not break the session */
124
+ }
125
+ }
126
+ function isAbsolutePosix(value) {
127
+ return value.startsWith("/");
128
+ }
129
+ /**
130
+ * Validate a `SessionStart` envelope, or refuse with the exact reason.
131
+ *
132
+ * STRICT ON EVERY AXIS THE VENDOR ALWAYS SENDS, and the refusal messages are written to
133
+ * be read in a log by somebody who cannot re-run the session. Unknown EXTRA keys are
134
+ * tolerated on purpose (`permission_mode` is already one, and a vendor minor may add
135
+ * more); an extra key cannot corrupt an identity, whereas a missing one can.
136
+ *
137
+ * `path.isAbsolute` is not used for the two path axes: it is platform-dependent, and
138
+ * this unit only ever runs from a POSIX declaration (`$CODEX_HOME/hooks.json`). A
139
+ * Windows-shaped absolute path arriving here would mean the envelope came from
140
+ * somewhere this unit was never installed, which is precisely a refusal.
141
+ */
142
+ export function readCodexBirthEnvelope(env) {
143
+ // 1. The event predicate, first and hardest. `SubagentStart` carries the PARENT's
144
+ // session_id, so a reader that checked identity before the event name would mint
145
+ // the parent again from a child's event.
146
+ if (env.hook_event_name !== CODEX_BIRTH_EVENT) {
147
+ return {
148
+ refusal: `hook_event_name is not exactly "${CODEX_BIRTH_EVENT}" (got ${JSON.stringify(env.hook_event_name)}) — only a top-level thread's own start event may mint a citizen`,
149
+ };
150
+ }
151
+ // 2. Belt AND braces on the subagent case: these two keys exist only on the
152
+ // SubagentStart/Stop envelopes, so their presence beside a SessionStart name
153
+ // means the envelope was assembled by something other than the vendor.
154
+ for (const key of ["agent_id", "agent_type"]) {
155
+ if (env[key] !== undefined) {
156
+ return {
157
+ refusal: `a subagent key (${key}) is present on a ${CODEX_BIRTH_EVENT} envelope — a spawned agent raises SubagentStart and must never become a citizen`,
158
+ };
159
+ }
160
+ }
161
+ // 3. The join key.
162
+ if (typeof env.session_id !== "string" || env.session_id.length === 0) {
163
+ return { refusal: "session_id missing or not a non-empty string" };
164
+ }
165
+ const threadId = env.session_id;
166
+ if (threadId.length > MAX_THREAD_ID_LEN) {
167
+ return { refusal: `session_id is longer than ${MAX_THREAD_ID_LEN} characters` };
168
+ }
169
+ if (/[^\x21-\x7e]/.test(threadId)) {
170
+ return {
171
+ refusal: "session_id carries whitespace, control or non-ASCII bytes — it must be one printable token (it becomes the `codex queue --thread` argv value)",
172
+ };
173
+ }
174
+ // 4. The two absolute paths. `cwd` has NO process.cwd() fallback: the record is the
175
+ // only thing this unit writes, and a guessed cwd is a fact nothing later corrects.
176
+ if (typeof env.cwd !== "string" || env.cwd.length === 0) {
177
+ return { refusal: "cwd missing or not a non-empty string" };
178
+ }
179
+ if (!isAbsolutePosix(env.cwd)) {
180
+ return { refusal: `cwd is not an absolute POSIX path (got ${JSON.stringify(env.cwd)})` };
181
+ }
182
+ if (typeof env.transcript_path !== "string" || env.transcript_path.length === 0) {
183
+ return { refusal: "transcript_path missing or not a non-empty string" };
184
+ }
185
+ if (!isAbsolutePosix(env.transcript_path)) {
186
+ return { refusal: `transcript_path is not an absolute POSIX path (got ${JSON.stringify(env.transcript_path)})` };
187
+ }
188
+ // 5. `model` is the one nullable axis: the record schema admits null, and an
189
+ // explicit vendor null is a fact ("unknown"), while an absent key or wrong TYPE
190
+ // is a refusal. The measured SessionStart always carries the key; silently mapping
191
+ // its disappearance to null would hide a vendor envelope regression.
192
+ if (!("model" in env)) {
193
+ return { refusal: "model key is missing — Codex SessionStart must carry it explicitly (string or null)" };
194
+ }
195
+ let model = null;
196
+ if (env.model !== null) {
197
+ if (typeof env.model !== "string" || env.model.length === 0) {
198
+ return { refusal: `model is present but not a non-empty string or null (got ${JSON.stringify(env.model)})` };
199
+ }
200
+ model = env.model;
201
+ }
202
+ // 6. The 4-valued source. Refusing an unknown value is how a vendor minor that adds
203
+ // a fifth source surfaces here as a named log line instead of as a silent mint.
204
+ if (typeof env.source !== "string" || !CODEX_SESSION_START_SOURCES.includes(env.source)) {
205
+ return {
206
+ refusal: `source is not one of ${CODEX_SESSION_START_SOURCES.join("|")} (got ${JSON.stringify(env.source)})`,
207
+ };
208
+ }
209
+ return {
210
+ threadId,
211
+ cwd: env.cwd,
212
+ transcriptPath: env.transcript_path,
213
+ model,
214
+ source: env.source,
215
+ };
216
+ }
217
+ /**
218
+ * Create or attach the ONE record this thread owns, and return the outcome.
219
+ *
220
+ * Exported ahead of `main()` on purpose: a caller that already holds a validated
221
+ * envelope (a gate, or the post-birth `thread/name/set` step that names the thread
222
+ * after its garden id) must be able to reach the gardenId and the create/attach action
223
+ * WITHOUT re-reading stdin — stdin is consumed exactly once, by the hook process.
224
+ * This function performs no logging and no process exit; both belong to `main()`.
225
+ *
226
+ * Both nullable axes are supplied, never omitted: Codex sends them on every
227
+ * SessionStart, so passing `undefined` (the store's "keep existing value" intent) would
228
+ * make a re-attach silently preserve a stale model after `/model`, or a stale transcript
229
+ * path after a compact. A fresh envelope's values are always the newer truth.
230
+ */
231
+ export function performCodexBirth(envelope, opts = {}) {
232
+ const result = upsertMetaSession({
233
+ input: {
234
+ backend: "codex",
235
+ nativeSessionId: envelope.threadId,
236
+ cwd: envelope.cwd,
237
+ model: envelope.model,
238
+ transcriptPath: envelope.transcriptPath,
239
+ },
240
+ dir: opts.dir,
241
+ });
242
+ return {
243
+ action: result.action,
244
+ gardenId: result.record.gardenId,
245
+ recordPath: result.path,
246
+ record: result.record,
247
+ };
248
+ }
249
+ /** Emit the neutral hook response and leave. `{}` claims nothing: Codex's SessionStart
250
+ * output schema is all-optional, and this unit has no context to inject and no decision
251
+ * to make about the turn. */
252
+ function emit() {
253
+ fs.writeSync(1, "{}\n");
254
+ process.exit(0);
255
+ }
256
+ async function main() {
257
+ let raw;
258
+ try {
259
+ raw = fs.readFileSync(0, "utf8"); // fd 0 = stdin (the Codex hook envelope)
260
+ }
261
+ catch (err) {
262
+ logLine("ERROR", `birth-refused stdin read failed: ${err instanceof Error ? err.message : String(err)}`);
263
+ emit();
264
+ }
265
+ let env;
266
+ try {
267
+ const parsed = JSON.parse(raw);
268
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
269
+ throw new Error("envelope is not a JSON object");
270
+ }
271
+ env = parsed;
272
+ }
273
+ catch (err) {
274
+ logLine("ERROR", `birth-refused envelope parse failed: ${err instanceof Error ? err.message : String(err)}`);
275
+ emit();
276
+ }
277
+ const envelope = readCodexBirthEnvelope(env);
278
+ if ("refusal" in envelope) {
279
+ // A record keyed to a guess would be a citizen no live thread can be joined back
280
+ // to, so refuse — and make it ERROR, because this session did NOT become a garden
281
+ // citizen. The key list is printed because the fix depends on WHICH envelope this
282
+ // actually was (a SubagentStart, a hand-run, or a vendor schema change).
283
+ logLine("ERROR", `birth-refused ${envelope.refusal} (keys=${Object.keys(env).sort().join(",")})`);
284
+ emit();
285
+ }
286
+ let born;
287
+ try {
288
+ born = performCodexBirth(envelope);
289
+ logLine("INFO", born.action === "create"
290
+ ? `birth-mint ${born.gardenId} record=${path.basename(born.recordPath)} thread=${envelope.threadId} source=${envelope.source} model=${envelope.model ?? "null"}`
291
+ : `birth-attach ${born.gardenId} record=${path.basename(born.recordPath)} thread=${envelope.threadId} source=${envelope.source} model=${envelope.model ?? "null"}`);
292
+ }
293
+ catch (err) {
294
+ logLine("ERROR", `birth-failed thread=${envelope.threadId} source=${envelope.source}: ${err instanceof Error ? err.message : String(err)}`);
295
+ emit();
296
+ }
297
+ try {
298
+ await setCodexThreadName(resolveCodexDefaultSocketPath(process.env), envelope.threadId, born.gardenId);
299
+ logLine("INFO", `title-set ${born.gardenId} thread=${envelope.threadId}`);
300
+ }
301
+ catch (err) {
302
+ logLine("WARN", `title-unavailable ${born.gardenId} thread=${envelope.threadId}: ${err instanceof Error ? err.message : String(err)}`);
303
+ }
304
+ emit();
305
+ }
306
+ // RUN ONLY AS THE HOOK PROCESS. `main()` consumes stdin and calls `process.exit`, so a
307
+ // gate (or the post-birth `thread/name/set` step) that imports `performCodexBirth` must
308
+ // not trigger it. `import.meta.main` is NOT used for this: it landed in Node 24.2.0 and
309
+ // the package floor is major 24, so on a 24.0/24.1 host it would be `undefined` — the
310
+ // guard would read falsy and the hook would silently mint nothing on every session,
311
+ // which is exactly the failure this file exists to prevent. An argv comparison works on
312
+ // every supported Node.
313
+ const invokedDirectly = (() => {
314
+ const entry = process.argv[1];
315
+ if (typeof entry !== "string" || entry.length === 0)
316
+ return false;
317
+ try {
318
+ return fs.realpathSync(entry) === fs.realpathSync(import.meta.filename);
319
+ }
320
+ catch {
321
+ return false;
322
+ }
323
+ })();
324
+ if (invokedDirectly)
325
+ void main();
@@ -0,0 +1,30 @@
1
+ /**
2
+ * codex-socket-path — print the Codex app-server default control-socket path for THIS
3
+ * environment, and nothing else.
4
+ *
5
+ * WHY A LEAF EXISTS FOR ONE LINE. `entwurf codex-app-server` is a bash launcher, and bash
6
+ * cannot import `resolveCodexDefaultSocketPath`. Its first version re-derived the path
7
+ * instead — `${CODEX_HOME:-$HOME/.codex}` plus a POSIX `[:space:]` trim — and a gate compared
8
+ * the two spellings over four ASCII-normal inputs, which they agreed on.
9
+ *
10
+ * They did not agree everywhere. `[측정 2026-09-16]` with `CODEX_HOME=$''` the TS leaf
11
+ * trims (JS `String.prototype.trim` strips U+FEFF) and falls back to `$HOME/.codex`, while the
12
+ * bash trim keeps the byte and yields `<BOM>/app-server-control/app-server-control.sock`. Same
13
+ * for `path.join`'s normalization of a trailing slash or a `..` segment. Every one of those is
14
+ * a managed launch starting a server at an address delivery and preflight never look at — a
15
+ * false success of exactly the kind this repo refuses.
16
+ *
17
+ * So the second spelling is gone rather than widened. There is ONE implementation of this
18
+ * address, this leaf prints it, and the launcher asks. Matching a transcription against its
19
+ * original can only ever test the inputs somebody thought of.
20
+ *
21
+ * Reads `process.env` directly: the resolver's contract is the ambient environment of whoever
22
+ * is about to launch, and an argv seam here would be a way to redirect the address.
23
+ */
24
+ import { resolveCodexDefaultSocketPath } from "../pi-extensions/lib/native-push/codex-ws-client.js";
25
+ if (process.argv.length > 2) {
26
+ console.error("usage: codex-socket-path (no arguments; the environment is the input)");
27
+ process.exit(2);
28
+ }
29
+ // No trailing newline: the caller substitutes this straight into an address.
30
+ process.stdout.write(resolveCodexDefaultSocketPath(process.env));
@@ -388,7 +388,12 @@ async function inspectNativePushCitizens(storeDir) {
388
388
  try {
389
389
  const probe = await resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId);
390
390
  status = probe.status;
391
- reason = probe.status === "alive" ? `route ${probe.route.lsAddress}` : probe.reason;
391
+ reason =
392
+ probe.status === "alive"
393
+ ? probe.route.backend === "antigravity"
394
+ ? `route ${probe.route.lsAddress}`
395
+ : `socket ${probe.route.socketPath}`
396
+ : probe.reason;
392
397
  }
393
398
  catch (err) {
394
399
  // A probe that cannot run is not a dead conversation.