@junghanacs/entwurf 0.15.1 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +354 -0
  3. package/DELIVERY.md +3 -2
  4. package/README.md +68 -88
  5. package/VERIFY.md +4 -1
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/external-mcp-host.md +147 -9
  9. package/docs/setup-clean-host.md +125 -6
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
  11. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +19 -10
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
  20. package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
  21. package/mcp/entwurf-bridge/src/index.ts +65 -19
  22. package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
  23. package/mcp/tsconfig.json +10 -0
  24. package/package.json +19 -9
  25. package/pi/entwurf-capabilities.json +1 -0
  26. package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
  27. package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
  28. package/pi-extensions/entwurf-control.ts +12 -9
  29. package/pi-extensions/lib/acp/backend-adapter.ts +19 -9
  30. package/pi-extensions/lib/acp/backend.ts +125 -7
  31. package/pi-extensions/lib/acp/claude-acp-launch.js +100 -0
  32. package/pi-extensions/lib/entwurf-self-address.ts +18 -7
  33. package/pi-extensions/lib/meta-sender-identity.ts +1 -1
  34. package/pi-extensions/lib/meta-session.ts +219 -5
  35. package/pi-extensions/lib/mux-fresh-call.ts +171 -17
  36. package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
  37. package/pi-extensions/meta-bridge-omp.ts +1244 -0
  38. package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
  39. package/run.sh +399 -31
  40. package/scripts/check-acp-launch-namespace.ts +127 -0
  41. package/scripts/check-acp-prompt-lifecycle.ts +145 -2
  42. package/scripts/check-copilot-birth-hook.ts +28 -1
  43. package/scripts/check-entwurf-self-address.ts +31 -0
  44. package/scripts/check-gate-qualification.ts +7 -3
  45. package/scripts/check-harness-admission-parity.ts +143 -0
  46. package/scripts/check-meta-doctor-oracle.sh +20 -0
  47. package/scripts/check-omp-birth-hook.ts +1049 -0
  48. package/scripts/check-omp-fresh-preflight.ts +208 -0
  49. package/scripts/check-omp-receive-arm.ts +516 -0
  50. package/scripts/check-setup-qualification.sh +40 -2
  51. package/scripts/copilot-bridge-oracle.sh +14 -6
  52. package/scripts/fake-copilot-vendor.sh +4 -2
  53. package/scripts/inventory-verification-surface.ts +1 -1
  54. package/scripts/meta-bridge-hook-log.sh +9 -1
  55. package/scripts/mutants/acp-launch-namespace.json +34 -0
  56. package/scripts/mutants/acp-prompt-lifecycle.json +67 -2
  57. package/scripts/mutants/copilot-birth.json +10 -10
  58. package/scripts/mutants/mux-fresh-call.json +22 -22
  59. package/scripts/mutants/omp-birth.json +173 -0
  60. package/scripts/mutants/omp-fresh.json +300 -0
  61. package/scripts/mutants/omp-receive.json +135 -0
  62. package/scripts/mutants/pack-install.json +2 -2
  63. package/scripts/mutants/self-address.json +34 -0
  64. package/scripts/mutants/setup-verdict.json +35 -0
  65. package/scripts/omp-bridge-doctor.sh +315 -0
  66. package/scripts/omp-bridge-install.sh +221 -0
  67. package/scripts/omp-bridge-oracle.sh +154 -0
  68. package/scripts/omp-bridge-uninstall.sh +57 -0
  69. package/scripts/omp-config-xdev.py +310 -0
  70. package/scripts/omp-config-xdev.sh +76 -0
  71. package/scripts/omp-mcp-bridge.sh +320 -0
  72. package/scripts/omp-mcp-config.py +392 -0
  73. package/scripts/omp-receive-doctor.sh +246 -0
  74. package/scripts/omp-receive-facts.ts +106 -0
  75. package/scripts/omp-receive-install.sh +228 -0
  76. package/scripts/omp-receive-uninstall.sh +60 -0
  77. package/scripts/omp-tool-surface.py +400 -0
  78. package/scripts/raw-acp-child-exit-measure/README.md +285 -0
  79. package/scripts/raw-acp-child-exit-measure/acp-turn-population.py +89 -0
  80. package/scripts/raw-acp-child-exit-measure/reaper-correlation.py +47 -0
  81. package/scripts/raw-omp-measure/README.md +420 -0
  82. package/scripts/raw-omp-measure/probe-extension.ts +76 -0
  83. package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
  84. package/scripts/raw-omp-measure/source-audit.md +414 -0
  85. package/scripts/smoke-omp-bridge-state.sh +221 -0
  86. package/scripts/smoke-omp-fresh-live.ts +497 -0
  87. package/scripts/smoke-omp-mcp-state.sh +327 -0
  88. package/scripts/smoke-omp-receive-live.ts +340 -0
  89. package/scripts/smoke-omp-receive-state.sh +196 -0
  90. package/scripts/smoke-setup-verdict.sh +48 -3
  91. package/scripts/tsconfig.json +2 -0
@@ -12,9 +12,10 @@ only on Linux because its strict live-owner join uses `/proc`.
12
12
  | npm/pnpm | npm is bundled with Node; pnpm is required for source setup | package or source installation |
13
13
  | Python 3 | required by `setup`/`install` (project-path normalization + settings writers); `--help`/`check-bridge` stay Python-free | pi/Claude/agy/Copilot wiring writers |
14
14
  | entwurf | global/project-local `@junghanacs/entwurf`, or a source checkout | operator command and garden capability |
15
- | pi | optional-by-presence, `>=0.84.3 <0.85` — absent is an explicit setup SKIP, below-floor is a named FAIL | ACP provider, control sockets |
15
+ | pi | optional-by-presence, `>=0.84.4 <0.85` — absent is an explicit setup SKIP, below-floor is a named FAIL | ACP provider, control sockets |
16
16
  | Claude Code | optional, **`>=2.1.217`** — the exec-form hook floor | Claude ACP auth/runtime and mailbox-backed native citizen |
17
17
  | GitHub Copilot CLI | optional-by-presence, operator-installed and authenticated — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/receiver/footer) | self-fetch citizen and visible fresh |
18
+ | OMP (`omp`) | optional-by-presence, operator-installed — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/`tools.xdev` setting/receiver) | self-fetch citizen and visible fresh (accepted on one host — see §4b) |
18
19
  | Antigravity `agy` | optional, operator-installed and authenticated | native-push citizen |
19
20
  | Cortex Code | optional, operator-installed and authenticated | Cortex ACP backend |
20
21
 
@@ -71,11 +72,12 @@ cd ~/repos/gh/entwurf
71
72
 
72
73
  This owns `~/.local/bin/entwurf` as a symlink to that checkout's `run.sh` and fails if the
73
74
  link is foreign, outside PATH, or shadowed by another command. It detects and wires
74
- pi/Claude/agy/Copilot by presence and prints a computed per-component PASS/SKIP/FAIL summary — a
75
+ pi/Claude/agy/Copilot/OMP by presence and prints a computed per-component PASS/SKIP/FAIL summary — a
75
76
  detected harness that cannot be completed makes setup exit nonzero. A detected `copilot`
76
77
  composes all four native units (birth → MCP → receiver → visible footer) with independent
77
- per-unit verdicts (#86 C3b); §4 keeps the explicit per-unit install/doctor/inverse surfaces for
78
- repair.
78
+ per-unit verdicts (#86 C3b), and a detected `omp` composes its own four (birth → MCP →
79
+ `tools.xdev` setting → receiver) the same way; §4 and §4b keep the explicit per-unit
80
+ install/doctor/inverse surfaces for repair.
79
81
 
80
82
  ### 1.1 User-scope ownership (one shared registration, one recorded owner)
81
83
 
@@ -108,7 +110,7 @@ packageRoot↔installerRoot coupling mismatch and a package/provider managed-pat
108
110
  Install the exact release floor, then wire the project:
109
111
 
110
112
  ```bash
111
- npm install -g @earendil-works/pi-coding-agent@0.84.3
113
+ npm install -g @earendil-works/pi-coding-agent@0.84.4
112
114
  pi --version
113
115
 
114
116
  cd ~/entwurf-smoke
@@ -116,7 +118,7 @@ entwurf install .
116
118
  pi -e "$(npm root -g)/@junghanacs/entwurf" --list-models entwurf
117
119
  ```
118
120
 
119
- The supported range is `>=0.84.3 <0.85`. It is a hard minimum: installing this
121
+ The supported range is `>=0.84.4 <0.85`. It is a hard minimum: installing this
120
122
  release onto a 0.83.x pi host upgrades the runtime rather than keeping the older
121
123
  minor. A host using only the external MCP bridge can skip pi until it needs a
122
124
  control socket; no delivery rail launches a pi process.
@@ -185,6 +187,123 @@ scanning for that process, checks the receiver, removes inherited pi identity ca
185
187
  the model/permission defaults. Birth occurs on the first prompt. `entwurf_fresh_call` uses this
186
188
  same managed invocation and requires the birth, MCP, receiver, and visible-identity preflight.
187
189
 
190
+ ## 4b. Optional OMP (`omp`) native citizen — accepted on one host
191
+
192
+ Three independently owned surfaces, and a boundary that is part of the instructions rather
193
+ than a footnote. Birth, visible identity, who-sent, the MCP hand and RECEIVE are landed, and
194
+ `entwurf_fresh_call` opens an omp sibling on all three public surfaces. The clause 7 LIVE
195
+ receipt has been taken: `smoke-omp-fresh-live` went green on 2026-08-30 (omp 18.0.0, one
196
+ model, one accepted run), which is what `docs/adding-a-harness.md` step 9 asks for and why
197
+ that smoke is wired as a release-gate MUST — the label was never allowed to move ahead of the
198
+ evidence, and it moved only once the evidence existed. Read the receipt itself in DELIVERY.md's
199
+ OMP row; what it does NOT establish is multi-host, multi-model, or repeated fresh calls in one
200
+ process. Open omp yourself and it is a two-way citizen either way — it sends under its own
201
+ garden id and a reply lands on it.
202
+
203
+ The first turn of a fresh omp sibling is a TWO-STAGE BOOTSTRAP, not a positional prompt: the
204
+ launcher carries `{v,target,nonce,task}` on the fixed registered flag `--entwurf-bootstrap`,
205
+ and the installed birth extension waits until the callback tool is actually callable, sends a
206
+ callback-only prompt, and delivers the operator's task only after that exact call succeeds.
207
+ That is a measured correction — the interactive host defers MCP discovery, so a positional
208
+ first turn began before the tool it named existed.
209
+
210
+ **One operator setting is load-bearing for fresh, not just for receive.** `tools: xdev: false`
211
+ in the omp agent config is checked by the fresh preflight BEFORE any window opens, because the
212
+ vendor default mounts MCP tools as `xd://` devices whose schemas never reach the prompt: a
213
+ sibling launched onto a default-config host would start, look healthy, and be unable to call
214
+ the callback tool at all. A refusal there names `omp-callback-tool-uncallable` and opens
215
+ nothing.
216
+
217
+ `setup` composes all four omp units when `omp` is on PATH, and the setting is one of them —
218
+ `entwurf setup` writes `tools: xdev: false` itself. The verbs below are the REPAIR path, not
219
+ the install:
220
+
221
+ ```bash
222
+ entwurf install-omp-bridge # the birth extension, into <omp agent dir>/extensions/
223
+ entwurf install-omp-mcp # the omp-native entwurf-bridge server
224
+ entwurf install-omp-config # the operator setting: tools.xdev: false
225
+ entwurf install-omp-receive # the receiver extension: mailbox watch + doorbell
226
+
227
+ entwurf doctor-omp-bridge
228
+ entwurf doctor-omp-mcp # also owns the tools.xdev runtime axis
229
+ entwurf doctor-omp-receive
230
+ ```
231
+
232
+ The setting writer owns exactly the lines it adds and records them, so `uninstall-omp-config`
233
+ takes back its own bytes and nothing else. It refuses a symlinked config, a config it cannot
234
+ parse, and — deliberately — an EXPLICIT `tools: xdev: true`: that is your decision, not drift,
235
+ so setup names it as a component FAIL for you to resolve instead of overwriting it.
236
+
237
+ Order matters only in one direction: the receiver JOINS the citizen birth mints, and
238
+ announces a tool the MCP hand provides. Install it without them and it will log
239
+ `arm-deferred`, give up after ~20s, and `doctor-omp-receive` will name the missing sibling
240
+ as a note rather than a fault.
241
+
242
+ **The receiver arms per session, and only for the visible TUI host.** Opening omp arms it;
243
+ `/new` re-arms it for the replacement citizen and retires the previous one; closing omp
244
+ retires it. A task subagent arms nothing. While nothing is armed, dispatch to that garden
245
+ id is the honest `mailbox-undeliverable` refusal — an unarmed receiver is a legible state,
246
+ not a broken one.
247
+
248
+ `[측정]` This section used to end by saying `setup` did not compose these and the verbs had to
249
+ be run by hand. That is what v0.16.0 actually shipped, and on an operator host it printed a
250
+ green `setup` summary with OMP entirely absent — no extension, no MCP entry, no visible garden
251
+ id. The composition landed afterwards; `docs/adding-a-harness.md` step 10 is the rule that
252
+ keeps the next harness from repeating it.
253
+
254
+ Both installers resolve the omp agent directory the way omp itself does, and REFUSE rather
255
+ than guess when an inherited `PI_CODING_AGENT_DIR`, `PI_CONFIG_DIR` or `PI_PROFILE` makes it
256
+ ambiguous: omp is a pi fork and reads pi's env vocabulary, so those names no longer say which
257
+ harness they address. Pass `ENTWURF_OMP_AGENT_DIR` if you genuinely mean a non-default one.
258
+
259
+ Two things the installers deliberately will NOT do. They never adopt an artifact already
260
+ sitting at their path without entwurf's own ownership state — a directory that merely looks
261
+ like our unit could be yours, and adopting it would overwrite it with no way back — so a
262
+ no-state path is a named refusal you resolve by hand. And the MCP writer's target is exactly
263
+ `<resolved omp agent dir>/mcp.json`; there is no path override, so it can never be aimed at
264
+ another tool's config.
265
+
266
+ **Where an omp citizen's garden artifacts live.** Under `$HOME/.pi/agent/meta-*`, the same
267
+ garden every other citizen uses — and that stays true under `omp --profile work`. omp is a pi
268
+ fork, so the vendor exports `PI_CODING_AGENT_DIR` for every named profile; for entwurf that
269
+ name means pi's persistence root, so honouring it here would put an omp session's record in a
270
+ different store (or in a pi sandbox). For backend omp it is read as the VENDOR's agent dir
271
+ only, and never as a garden root. Its presence on a live omp is normal and is not a fault.
272
+ The four `ENTWURF_META_*` variables remain the way to relocate the garden roots, and for
273
+ backend omp each one must be **absolute or `~`-rooted** (`~` or `~/…`). A relative value is
274
+ refused by name rather than resolved: it would resolve against each process's own working
275
+ directory, and the omp extension and `doctor-omp-bridge` do not share one — the doctor would
276
+ then report on a directory the extension never writes to. A refused value mints nothing and
277
+ turns the doctor's runtime axis red.
278
+
279
+ **One vendor setting is required, and the default is wrong for a citizen.** omp's
280
+ `tools.xdev` (default ON) mounts MCP tools as `xd://<tool>` devices and removes them from the
281
+ model's top-level toolset, so `entwurf_v2` is reached by WRITING JSON to a virtual file rather
282
+ than by calling a tool — and with `tools.xdevDocs` at its `builtins` default its schema is not
283
+ in the prompt at all. On that default a plain "send this to garden id X" was measured to list
284
+ peers and then CLAIM the send without ever dispatching. Put this in `~/.omp/agent/config.yml`
285
+ and restart the session:
286
+
287
+ ```yaml
288
+ tools:
289
+ xdev: false
290
+ ```
291
+
292
+ It disables nothing — it exposes every enabled tool top-level, omp's own `lsp`, `debug`,
293
+ `browser` and `ast_edit` included, which the default was hiding as well. Plan mode and staged
294
+ `xd://resolve` / `xd://propose` finalization keep working. The rationale, the numbers and the
295
+ narrower `tools.xdevInlineDevices` alternative are in
296
+ [`external-mcp-host.md`](./external-mcp-host.md).
297
+
298
+ Birth happens when the TUI OPENS (not on the first prompt, unlike Copilot), and the garden id
299
+ appears on omp's status line as `🪛 <garden-id> omp`. `/new`, fork and in-TUI resume mint the
300
+ replacement session's own record. Task subagents of that session are refused by design — they
301
+ borrow the host's tools under the host's garden id and never get a second address.
302
+
303
+ The MCP entry deliberately uses the same server key as any Claude Code import so that it
304
+ SHADOWS it; see [`external-mcp-host.md`](./external-mcp-host.md) for why that key is pinned and
305
+ why `disabledServers` is never the way to hide an import.
306
+
188
307
  ## 5. Optional Antigravity native citizen
189
308
 
190
309
  Install the three independently owned surfaces:
@@ -69,14 +69,37 @@ import { listEntwurfFacts } from "../../../pi-extensions/lib/entwurf-fact-provid
69
69
  import { renderEntwurfPeers } from "../../../pi-extensions/lib/entwurf-peers-render.js";
70
70
  import { computeSelfAddressability } from "../../../pi-extensions/lib/entwurf-self-address.js";
71
71
  import { nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contract.js";
72
+ import { resolveMailboxWakeModeCapability } from "../../../pi-extensions/lib/entwurf-v2-decider.js";
72
73
  import { runAndRenderEntwurfV2FromSurface } from "../../../pi-extensions/lib/entwurf-v2-surface.js";
73
74
  import { makeVisibleResumeDeps, renderVisibleResume, visibleResume, } from "../../../pi-extensions/lib/entwurf-v2-visible-resume.js";
74
75
  import { probeNativeSenderAlive, resolveTrustedMetaSenderIdentity, } from "../../../pi-extensions/lib/meta-sender-identity.js";
75
- import { defaultMetaMailboxDir, defaultMetaSessionsDir, makeStoreRecordReader, readActiveStoreEntries, readMetaInbox, readMetaReceiverMarker, } from "../../../pi-extensions/lib/meta-session.js";
76
+ import { applyOmpBridgeChildRootPolicy, defaultMetaMailboxDir, defaultMetaSessionsDir, makeStoreRecordReader, readActiveStoreEntries, readMetaInbox, readMetaReceiverMarker, } from "../../../pi-extensions/lib/meta-session.js";
76
77
  import { freshCall, renderFreshCall } from "../../../pi-extensions/lib/mux-fresh-call.js";
77
78
  import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.js";
78
79
  import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.js";
79
80
  const HOME = os.homedir();
81
+ // ============================================================================
82
+ // OMP root policy — FIRST, before any lazy default-root consumer (#87 B1).
83
+ //
84
+ // This runs at module load for one reason: every meta-root consumer below resolves its
85
+ // directory lazily inside a tool handler, and the whole point of the policy is that none
86
+ // of them may ever see the foreign value. It is a no-op for every other child — it fires
87
+ // only when this process carries the exact `external-mcp/omp` provenance label its managed
88
+ // entry writes, and then it removes `PI_CODING_AGENT_DIR` from THIS process alone (the omp
89
+ // HOST keeps it: there the variable is the vendor's own agent dir) and pins the four
90
+ // entwurf-owned meta roots to the shared leaf's answer. See `applyOmpBridgeChildRootPolicy`.
91
+ // ============================================================================
92
+ try {
93
+ applyOmpBridgeChildRootPolicy(process.env, HOME);
94
+ }
95
+ catch (err) {
96
+ // FAIL CLOSED, LOUDLY. The policy refuses an ambiguous garden root (a relative
97
+ // `ENTWURF_META_*` override, #87 A2), and a bridge child that cannot say which store it
98
+ // is addressing must not boot with a guess — the extension half received the identical
99
+ // refusal, so proceeding here is exactly the split the policy exists to prevent.
100
+ console.error(`[entwurf-bridge] fatal: ${err instanceof Error ? err.message : String(err)}`);
101
+ process.exit(1);
102
+ }
80
103
  // Directory SOURCE is this adapter's own policy — the bridge honours an explicit
81
104
  // ENTWURF_DIR override the pi side does not. The path GRAMMAR is the shared leaf.
82
105
  const ENTWURF_DIR = process.env.ENTWURF_DIR ?? defaultControlSocketDir(HOME);
@@ -192,11 +215,17 @@ async function buildTrustedMetaSenderEnvelope(cwd = process.cwd()) {
192
215
  return null;
193
216
  const { marker, identity } = trusted;
194
217
  // Identity is trusted — but `replyable` is a SEPARATE fact, and WHICH fact depends on the
195
- // rail a reply would ride (보정①). The domain comes from nativePushSupported(backend), not
196
- // from wakeMode: `direct-inject` also covers codex/pi, which have no native-push adapter.
197
- // self-fetch (claude-code): can this citizen's own inbox wake? → the receiver presence
198
- // marker (readMetaReceiverMarker folds a dead/reused owner to null, so a match means a
199
- // live, ARMED receiver the sender marker proves identity, never an armed watch).
218
+ // rail a reply would ride (보정①). THREE values, not a native-push-or-self-fetch binary:
219
+ // native-push nativePushSupported(backend). NOT wakeMode: `direct-inject` also covers
220
+ // codex/pi, which have no native-push adapter.
221
+ // self-fetch ← resolveMailboxWakeModeCapability (the decider's mailbox seam one owner
222
+ // with dispatch). A new hardcoded backend list would drift the moment the registry
223
+ // admits another self-fetch citizen.
224
+ // none ← neither. omp today: no mailbox drain, no native-push adapter. Rendering
225
+ // this as self-fetch printed a mailboxPath nothing drains.
226
+ // self-fetch (claude-code/copilot): can this citizen's own inbox wake? → the receiver
227
+ // presence marker (readMetaReceiverMarker folds a dead/reused owner to null, so a match
228
+ // means a live, ARMED receiver — the sender marker proves identity, never an armed watch).
200
229
  // native-push (antigravity): there is no inbox and no watch. A reply is injected into a
201
230
  // live app-server conversation, so only an adapter probe can answer. Composing the
202
231
  // receiver atom here would demand `watchArmed` from a backend that never arms one, and
@@ -205,7 +234,11 @@ async function buildTrustedMetaSenderEnvelope(cwd = process.cwd()) {
205
234
  // survive; degrading to null would erase the sender) — only with replyable:false.
206
235
  // The rail, named ONCE and reused for both the predicate and the caller's rendering —
207
236
  // so entwurf_self can never re-derive it differently from what decided `replyable`.
208
- const metaDeliveryDomain = nativePushSupported(identity.backend) ? "native-push" : "self-fetch";
237
+ const metaDeliveryDomain = nativePushSupported(identity.backend)
238
+ ? "native-push"
239
+ : resolveMailboxWakeModeCapability(identity)
240
+ ? "self-fetch"
241
+ : "none";
209
242
  const facts = metaDeliveryDomain === "native-push"
210
243
  ? {
211
244
  origin: "meta-session",
@@ -213,17 +246,23 @@ async function buildTrustedMetaSenderEnvelope(cwd = process.cwd()) {
213
246
  recordBacked: true,
214
247
  probeAlive: await probeNativeSenderAlive(identity),
215
248
  }
216
- : (() => {
217
- const receiver = readMetaReceiverMarker({ gardenId: identity.gardenId });
218
- const active = receiverMarkerMatchesIdentity(receiver, identity);
219
- return {
249
+ : metaDeliveryDomain === "self-fetch"
250
+ ? (() => {
251
+ const receiver = readMetaReceiverMarker({ gardenId: identity.gardenId });
252
+ const active = receiverMarkerMatchesIdentity(receiver, identity);
253
+ return {
254
+ origin: "meta-session",
255
+ metaDeliveryDomain,
256
+ recordBacked: true,
257
+ ownerAlive: active,
258
+ watchArmed: active,
259
+ };
260
+ })()
261
+ : {
220
262
  origin: "meta-session",
221
263
  metaDeliveryDomain,
222
264
  recordBacked: true,
223
- ownerAlive: active,
224
- watchArmed: active,
225
265
  };
226
- })();
227
266
  const self = computeSelfAddressability(facts);
228
267
  return {
229
268
  envelope: {
@@ -428,6 +467,9 @@ server.tool("entwurf_self", "Return this caller's authoritative identity envelop
428
467
  else if (rail === "native-push") {
429
468
  lines.push("mailbox: none — native-push has no inbox; a reply direct-injects only while the adapter probe is alive");
430
469
  }
470
+ else if (rail === "none") {
471
+ lines.push("mailbox: none — no inbound rail (no mailbox, no native-push adapter)");
472
+ }
431
473
  else {
432
474
  // Fail-closed, matching computeSelfAddressability's own unsupplied-domain row:
433
475
  // with no rail we cannot say how a reply would travel, so we claim no transport.
@@ -562,24 +604,27 @@ server.tool("entwurf_register_native", "Register an ALREADY-RUNNING native conve
562
604
  // its own garden id, a fresh cell answered with its uuidv7 `PI_SESSION_ID` value read out of the
563
605
  // environment by an MCP server it had spawned itself — confidently, and wrong. A sibling launched
564
606
  // against that answer would call home to a garden id nobody holds.
565
- server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operator's own tmux session and hand it a first task. Three fixed " +
566
- "backends only: pi, claude-code, copilot. The sibling's FIRST action is a callback to you carrying a nonce, and the " +
607
+ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operator's own tmux session and hand it a first task. Four fixed " +
608
+ "backends only: pi, claude-code, copilot, omp. The sibling's FIRST action is a callback to you carrying a nonce, and the " +
567
609
  "sender envelope of that callback is its garden id — that is how you learn the address of something that " +
568
610
  "did not exist a moment ago. This returns a LAUNCH receipt (tmux window/pane plus that nonce) and nothing " +
569
611
  "else: it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls " +
570
612
  "for the callback; if it never arrives the window is visible and can be read directly. For EXISTING " +
571
613
  "citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and " +
572
614
  "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code; a model name " +
573
- "or `auto` for copilot). A copilot launch goes through entwurf's own managed invocation and is refused " +
574
- "BEFORE any window opens if this host lacks the Copilot birth, MCP, receiver or visible-footer units. An optional " +
615
+ "or `auto` for copilot; a fuzzy model pattern for omp). A copilot launch goes through entwurf's own managed " +
616
+ "invocation and is refused BEFORE any window opens if this host lacks the Copilot birth, MCP, receiver or " +
617
+ "visible-footer units; an omp launch is refused the same way if this host lacks the OMP birth, MCP, receiver or " +
618
+ "visible-status units, or if omp's tools.xdev is not false (the vendor default hides MCP tool schemas from the " +
619
+ "prompt, so the sibling could not call you back at all). An optional " +
575
620
  "cwd starts the sibling in ONE literal absolute existing directory (cross-repo fresh) — never pick resume " +
576
621
  "for a dormant record's cwd; resume is continuity-only. Omitted/empty cwd means the caller's own directory. " +
577
622
  "There are no arbitrary command/env knobs. Do not put secrets in the task — model and task argv are visible to " +
578
623
  "same-user processes on this host. Requires that this agent itself runs " +
579
624
  "inside tmux: without a pane anchor there is no session to open a sibling beside.", {
580
625
  backend: z
581
- .enum(["pi", "claude-code", "copilot"])
582
- .describe("Which fixed runtime to open. Only these three; there is no arbitrary command."),
626
+ .enum(["pi", "claude-code", "copilot", "omp"])
627
+ .describe("Which fixed runtime to open. Only these four; there is no arbitrary command."),
583
628
  model: z
584
629
  .string()
585
630
  .min(1)
@@ -5,6 +5,7 @@
5
5
  "antigravity": { "wakeMode": "direct-inject", "deliveryLevel": "D6", "nativeIdLabel": "conversationId" },
6
6
  "codex": { "wakeMode": "direct-inject", "deliveryLevel": "D6", "nativeIdLabel": "threadId" },
7
7
  "copilot": { "wakeMode": "self-fetch", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" },
8
+ "omp": { "wakeMode": "self-fetch", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" },
8
9
  "pi": { "wakeMode": "direct-inject", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" }
9
10
  }
10
11
  }
@@ -17,10 +17,9 @@
17
17
  //
18
18
  // Fence: imported by the root program with `.js` suffixes, same as the sibling
19
19
  // lib/acp modules — no new strip-types fence.
20
- import { readFileSync } from "node:fs";
21
20
  import { createRequire } from "node:module";
22
21
  import { homedir } from "node:os";
23
- import { dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
24
23
  import { enrichMcpServersWithEnvelope } from "./config.js";
25
24
  import { loadEngraving } from "./engraving.js";
26
25
  import { CORTEX_MODEL_PREFIX, curatedClaudeModels, curatedCortexModels, SUPPORTED_ANTHROPIC_MODEL_IDS, SUPPORTED_CORTEX_MODEL_IDS, } from "./models.js";
@@ -39,19 +38,29 @@ function shellQuote(value) {
39
38
  // claude adapter — the first implementation (the rail's reference backend)
40
39
  // ---------------------------------------------------------------------------
41
40
  const SUPPORTED_CLAUDE_IDS = new Set(SUPPORTED_ANTHROPIC_MODEL_IDS);
42
- /** Resolve the claude-agent-acp launch — package bin (resolve), env override for debug.
43
- * This is the single source for the claude launch spec; backend.ts holds no private copy. */
41
+ /**
42
+ * Resolve the claude launch an ENTWURF-OWNED launcher, or the env override for debug.
43
+ * This is the single source for the claude launch spec; backend.ts holds no private copy.
44
+ *
45
+ * The default no longer names the vendor bin directly. `claude-acp-launch.js`
46
+ * imports it in-process; that file's header carries the reason (#72: a janitor
47
+ * for another harness selects `claude-agent-acp` by argv substring and SIGTERMs
48
+ * it by age, and the vendor's own handler erases the signal into exit 0).
49
+ *
50
+ * `CLAUDE_AGENT_ACP_COMMAND` is an EXPLICIT operator override and is deliberately
51
+ * NOT routed through the launcher: an operator who names their own command owns
52
+ * the result, including the loss of the name split and the signal observation.
53
+ */
44
54
  function resolveClaudeLaunch() {
45
55
  const override = process.env.CLAUDE_AGENT_ACP_COMMAND?.trim();
46
56
  if (override)
47
57
  return { command: "bash", args: ["-lc", override] };
58
+ // Resolved here (not inside the launcher's own directory lookup) so a missing
59
+ // vendor package still fails at launch resolution, where it always failed.
48
60
  const require = createRequire(import.meta.url);
49
- const pkgJsonPath = require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
50
- const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
51
- const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.["claude-agent-acp"];
52
- if (!binPath)
53
- throw new Error("@agentclientprotocol/claude-agent-acp resolved but exposes no bin entry");
54
- return { command: process.execPath, args: [join(dirname(pkgJsonPath), binPath)] };
61
+ require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
62
+ const launcher = fileURLToPath(new URL("./claude-acp-launch.js", import.meta.url));
63
+ return { command: process.execPath, args: [launcher] };
55
64
  }
56
65
  export const claudeAdapter = {
57
66
  backend: "claude",
@@ -33,10 +33,13 @@
33
33
  * - external-mcp: never replyable — no authoritative reply address.
34
34
  *
35
35
  * `origin` stays sender-carrier PROVENANCE, never the citizen identity authority and never a rail.
36
- * Which rail a meta citizen's reply rides is a SECOND axis — `metaDeliveryDomain`, derived
37
- * by the caller from `nativePushSupported(backend)`, not from `wakeMode` (direct-inject also
38
- * covers codex/pi, which have no native-push adapter). Fail-closed: an unsupplied domain is
39
- * not replyable.
36
+ * Which rail a meta citizen's reply rides is a SECOND axis — `metaDeliveryDomain`.
37
+ * The caller derives it as three values, never as a native-push-or-self-fetch binary:
38
+ * native-push `nativePushSupported(backend)` (the adapter list; NOT wakeMode —
39
+ * `direct-inject` also covers codex/pi, which have no native-push adapter)
40
+ * self-fetch ← `resolveMailboxWakeModeCapability` (the decider's own mailbox seam)
41
+ * none ← neither (omp today: no mailbox drain, no native-push adapter)
42
+ * Fail-closed: an unsupplied domain is not replyable.
40
43
  */
41
44
  import { computeMetaReceiverActive, nativePushDeliverable } from "./entwurf-deliverability.js";
42
45
  /**
@@ -65,9 +68,11 @@ export function computeSelfAddressability(facts) {
65
68
  };
66
69
  }
67
70
  case "meta-session": {
68
- // TWO rails, pinned apart (보정①). Each branch composes the predicate that OWNS its
71
+ // THREE rails, pinned apart (보정①). Each branch composes the predicate that OWNS its
69
72
  // axis — the mailbox receiver atom and the native-push predicate share nothing, so a
70
- // mailbox liveness fact can never leak into a backend that has no mailbox.
73
+ // mailbox liveness fact can never leak into a backend that has no mailbox. `none` is
74
+ // the remainder: no inbound rail at all (not an unsupplied domain — that stays the
75
+ // default fail-closed row).
71
76
  switch (facts.metaDeliveryDomain) {
72
77
  case "native-push": {
73
78
  const push = nativePushDeliverable({ recordBacked: facts.recordBacked, probeAlive: facts.probeAlive });
@@ -93,6 +98,12 @@ export function computeSelfAddressability(facts) {
93
98
  reason: recv.active ? `meta receiver active (${recv.reason})` : `meta receiver inactive — ${recv.reason}`,
94
99
  };
95
100
  }
101
+ case "none":
102
+ return {
103
+ replyable: false,
104
+ socketState: "none",
105
+ reason: "no inbound rail — this backend has no mailbox and no native-push adapter",
106
+ };
96
107
  default:
97
108
  return {
98
109
  replyable: false,
@@ -46,7 +46,7 @@ import { resolveNativePushAdapter } from "./native-push/adapter.js";
46
46
  * exactly the #46 defect on agy, and copilot joined the list only once its own hook wrote one
47
47
  * (#82 RAIL 5b). Membership says a marker may EXIST, never that a reply can land: the reply
48
48
  * rail is chosen from `nativePushSupported` at the bridge, not from this list. */
49
- export const META_SENDER_BACKENDS = ["claude-code", "antigravity", "copilot"];
49
+ export const META_SENDER_BACKENDS = ["claude-code", "antigravity", "copilot", "omp"];
50
50
  /**
51
51
  * Two live native sessions claim this MCP process as their own. We can SEE both identities but
52
52
  * cannot say which one is calling, so we send under neither. Choosing by lookup order, backend