@junghanacs/entwurf 0.15.0 → 0.16.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 (90) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +299 -0
  3. package/DELIVERY.md +4 -3
  4. package/README.md +99 -67
  5. package/VERIFY.md +13 -7
  6. package/docs/acp-backend-rail.md +1 -1
  7. package/docs/external-mcp-host.md +147 -9
  8. package/docs/setup-clean-host.md +221 -16
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
  18. package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
  19. package/mcp/entwurf-bridge/src/index.ts +65 -19
  20. package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
  21. package/mcp/tsconfig.json +10 -0
  22. package/package.json +14 -4
  23. package/pi/entwurf-capabilities.json +1 -0
  24. package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
  25. package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
  26. package/pi-extensions/entwurf-control.ts +12 -9
  27. package/pi-extensions/lib/entwurf-self-address.ts +18 -7
  28. package/pi-extensions/lib/meta-sender-identity.ts +1 -1
  29. package/pi-extensions/lib/meta-session.ts +219 -5
  30. package/pi-extensions/lib/mux-fresh-call.ts +171 -17
  31. package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
  32. package/pi-extensions/meta-bridge-omp.ts +1244 -0
  33. package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
  34. package/run.sh +1041 -174
  35. package/scripts/agy-bridge.sh +4 -2
  36. package/scripts/check-copilot-birth-hook.ts +700 -23
  37. package/scripts/check-entwurf-self-address.ts +31 -0
  38. package/scripts/check-fresh-cut-gate.sh +12 -4
  39. package/scripts/check-gate-qualification.ts +8 -2
  40. package/scripts/check-harness-admission-parity.ts +143 -0
  41. package/scripts/check-meta-doctor-oracle.sh +20 -0
  42. package/scripts/check-omp-birth-hook.ts +1049 -0
  43. package/scripts/check-omp-fresh-preflight.ts +181 -0
  44. package/scripts/check-omp-receive-arm.ts +516 -0
  45. package/scripts/check-setup-qualification.sh +126 -0
  46. package/scripts/copilot-bridge-doctor.sh +104 -71
  47. package/scripts/copilot-bridge-install.sh +180 -20
  48. package/scripts/copilot-bridge-oracle.sh +223 -0
  49. package/scripts/copilot-bridge-uninstall.sh +137 -0
  50. package/scripts/dev-bin.sh +50 -9
  51. package/scripts/fake-copilot-vendor.sh +67 -0
  52. package/scripts/inventory-verification-surface.ts +1 -1
  53. package/scripts/meta-bridge-hook-log.sh +9 -1
  54. package/scripts/mutants/copilot-birth.json +96 -5
  55. package/scripts/mutants/mux-fresh-call.json +22 -22
  56. package/scripts/mutants/omp-birth.json +173 -0
  57. package/scripts/mutants/omp-fresh.json +300 -0
  58. package/scripts/mutants/omp-receive.json +135 -0
  59. package/scripts/mutants/pi-package-ownership.json +92 -0
  60. package/scripts/mutants/self-address.json +34 -0
  61. package/scripts/mutants/setup-verdict.json +148 -0
  62. package/scripts/mutants/source-install.json +32 -0
  63. package/scripts/omp-bridge-doctor.sh +315 -0
  64. package/scripts/omp-bridge-install.sh +221 -0
  65. package/scripts/omp-bridge-oracle.sh +154 -0
  66. package/scripts/omp-bridge-uninstall.sh +57 -0
  67. package/scripts/omp-mcp-bridge.sh +320 -0
  68. package/scripts/omp-mcp-config.py +392 -0
  69. package/scripts/omp-receive-doctor.sh +246 -0
  70. package/scripts/omp-receive-facts.ts +106 -0
  71. package/scripts/omp-receive-install.sh +228 -0
  72. package/scripts/omp-receive-uninstall.sh +60 -0
  73. package/scripts/omp-tool-surface.py +349 -0
  74. package/scripts/pi_settings_io.py +22 -0
  75. package/scripts/raw-omp-measure/README.md +420 -0
  76. package/scripts/raw-omp-measure/probe-extension.ts +76 -0
  77. package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
  78. package/scripts/raw-omp-measure/source-audit.md +414 -0
  79. package/scripts/register-pi-package.py +525 -7
  80. package/scripts/register-pi-provider.py +162 -12
  81. package/scripts/smoke-agy-install-state.sh +86 -30
  82. package/scripts/smoke-agy-statusline-state.sh +9 -6
  83. package/scripts/smoke-omp-bridge-state.sh +221 -0
  84. package/scripts/smoke-omp-fresh-live.ts +497 -0
  85. package/scripts/smoke-omp-mcp-state.sh +327 -0
  86. package/scripts/smoke-omp-receive-live.ts +340 -0
  87. package/scripts/smoke-omp-receive-state.sh +196 -0
  88. package/scripts/smoke-setup-verdict.sh +291 -0
  89. package/scripts/smoke-user-scope-citizen.sh +388 -1
  90. package/scripts/tsconfig.json +2 -0
@@ -0,0 +1,420 @@
1
+ # raw-omp-measure — OMP v18.0.0 vendor measurement (issue #87)
2
+
3
+ Lane: `docs/adding-a-harness.md` step 1 (five measurements) + §3.5 LIVE discriminator +
4
+ one added axis, **pi-rail overlap** (GLG, 2026-08-27). The oracle is the vendor artifact
5
+ or the vendor process, never our assembler.
6
+
7
+ This file was written as a MEASUREMENT-ONLY ledger and stayed one through the audit,
8
+ review, cross-review and first LIVE run. Bundle A of the implementation lane
9
+ (2026-08-27) then closed the slots that could only be closed by writing something —
10
+ the native MCP entry and the installed birth unit. Those receipts are tagged
11
+ **[LIVE …, implementation lane]** so the two phases stay legible: everything without
12
+ that tag was taken before any omp config write or record mint.
13
+
14
+ Evidence-state vocabulary used below: **[source]** = read at `file:line` in the vendor
15
+ checkout; **[host]** = measured on this host (oracle) with the receipt named;
16
+ **[LIVE — pending]** = slot the implementer fills from a real omp process; do not close
17
+ a slot from recollection.
18
+
19
+ Source-layer claims were independently re-verified by a second model:
20
+ [`source-audit.md`](./source-audit.md) (xai/grok-4.6 fresh sibling, 2026-08-27) —
21
+ **20 CONFIRMED / 6 CORRECTED / 0 UNRESOLVED**. Corrections are folded below and marked
22
+ `audited`; the audit file holds the deep receipts.
23
+
24
+ ## Vendor identity
25
+
26
+ - Installed: `omp/18.0.0`, ELF aarch64 at `~/.local/bin/omp` **[host]** (`omp --help` header,
27
+ `file` output; oracle, 2026-08-27).
28
+ - Source checkout used for `[source]` receipts: `~/repos/3rd/oh-my-pi` at tag `v18.0.0`
29
+ (`02696f5b`) + 2 natives lint commits (`50368368…`, `4142f881` HEAD) — no runtime diff
30
+ vs the tag claimed; treat `file:line` as v18.0.0 coordinates and remeasure at any
31
+ vendor upgrade. All `packages/…` paths below are relative to that checkout.
32
+ - State root on this host: `~/.omp/` (`agent/config.yml`, `agent/sessions/<cwd-key>/*.jsonl`,
33
+ `logs/omp.<date>.<pid>.log`, `natives/18.0.0`, `run/daemons`) **[host]**.
34
+
35
+ ## M1 — Hook vocabulary and firing time
36
+
37
+ - **[source]** omp "hooks" are an **in-process extension event bus**, not spawned hook
38
+ commands. `--hook` is an alias of `--extension`; JS/TS hook factories load as extension
39
+ modules and bind via `pi.on(...)` (`docs/hooks.md` "Current status in runtime").
40
+ - **[source]** Event vocabulary (`docs/hooks.md` "Event surfaces",
41
+ `packages/coding-agent/src/extensibility/shared-events.ts`): session_start,
42
+ session_before_switch/switch, session_before_branch/branch, session_before_compact,
43
+ session.compacting, session_compact, session_before_tree/tree, session_shutdown;
44
+ context, before_agent_start, agent_start, agent_end, turn_start, turn_end,
45
+ auto_compaction_start/end, auto_retry_start/end, ttsr_triggered, todo_reminder;
46
+ tool_call (pre) / tool_result (post); mcp_notification; user_bash.
47
+ - **[source]** `session_start` emit sites — the NAME does not scope it to the visible host:
48
+ - TUI host: `packages/coding-agent/src/modes/controllers/extension-ui-controller.ts:311`
49
+ - print/rpc runtime init: `packages/coding-agent/src/modes/runtime-init.ts:147`
50
+ - ACP server mode: `packages/coding-agent/src/modes/acp/acp-agent.ts:2603`
51
+ - **every task subagent**: `packages/coding-agent/src/task/executor.ts:3305`
52
+ - **[source, audited C1/C2]** Firing time: the TUI host fires after first paint, before
53
+ the first prompt, once per process (`interactive-mode.ts:1221`, `:1238`,
54
+ `extension-ui-controller.ts:309-311`). `/new`, fork, and in-TUI resume re-fire as
55
+ `session_before_switch`/`session_switch` (reasons `"new"`/`"fork"`/`"resume"`,
56
+ `agent-session.ts:6910-8074`), **not** as `session_start` — a birth hook must treat
57
+ `session_switch` as a re-birth edge. The `:531` initialize site is test-only.
58
+ - **[LIVE 2026-08-27, observation]** on `/exit`, a `session_shutdown` with a `"print"`
59
+ context fired before the tui one (same run) — shutdown events arrive from non-tui
60
+ contexts too. No fence impact (shutdown is not a birth edge), but a future unit must
61
+ not treat `session_shutdown` as host-scoped.
62
+ - **[LIVE 2026-08-27]** `/tmp/omp-probe-live.jsonl`. Host `session_start` at TUI open with **no model turn** (`2026-08-27T10:53:21.125Z`); `agent_start`/`turn_start` only after the typed prompt (`10:55:41.414Z` / `10:55:41.466Z`):
63
+ `{"event":"session_start","pid":479624,"mode":"tui","hasUI":true,"cwd":"/tmp","sessionId":"01a042da-537a-7770-a275-7b8162eecca4","sessionFile":"/home/junghan/.omp/agent/sessions/-tmp/2026-08-27T10-53-19-610Z_01a042da-537a-7770-a275-7b8162eecca4.jsonl"}`
64
+
65
+ ## M2 — Launch form and envelope
66
+
67
+ - **[source]** Declaration surfaces (`docs/extension-loading.md`): native roots
68
+ `<cwd>/.omp/extensions` and `~/.omp/agent/extensions`; `extensions:` arrays in
69
+ `~/.omp/agent/config.yml`, `<cwd>/.omp/config.yml`, legacy `settings.json`; CLI
70
+ `-e/--extension/--hook`; installed plugin manifests (`omp.extensions`/`pi.extensions`).
71
+ - **[source]** Envelope: **no stdin, no per-event child process.** A module
72
+ default-exports `function (pi: ExtensionAPI)`; handlers receive `(event, ctx)` where
73
+ `ctx: ExtensionContext` carries `mode`, `hasUI`, `cwd`, `sessionManager`
74
+ (`packages/coding-agent/src/extensibility/extensions/types.ts:456`).
75
+ - Consequence for a future birth unit: the payload runs **inside** the omp process
76
+ (`process.pid` == host pid) — unlike Claude Code's exec'd launcher. Step-6 join-key
77
+ design must start from that fact.
78
+ - **[source, audited A5/B1]** Subagent envelope: extension factories are re-executed per
79
+ subagent against a fresh per-session API (`sdk.ts:2000-2028`, `loader.ts:362-443`);
80
+ CLI `-e` paths flow into subagents via `session.extensionPaths` →
81
+ `structured-subagent.ts:442` unless `restrictToolNames`.
82
+ - **[LIVE 2026-08-27]** `/tmp/omp-probe-live.jsonl` `pid:479624` equals `ps` omp pid `479624` (ppid `479023` = tmux-server bash). Extension is in-process.
83
+
84
+ ## M3 — Config writer
85
+
86
+ - **[source]** omp-native writer layer (`docs/mcp-config.md` "Source of truth in code"):
87
+ `packages/coding-agent/src/mcp/config-writer.ts`, targets project `.omp/mcp.json` /
88
+ user `~/.omp/agent/mcp.json` (profile-aware). General settings: `omp config` CLI →
89
+ `~/.omp/agent/config.yml`.
90
+ - **[host, pre-implementation]** `~/.omp/agent/config.yml` exists (modelRoles.default
91
+ `openai-codex/gpt-5.6-terra:high`, statusLine.separator ascii, plan.enabled). **No
92
+ omp-native mcp.json existed anywhere on this host** at measurement time — that absence
93
+ is what made the Claude import the effective source below. Bundle A's writer created
94
+ `~/.omp/agent/mcp.json`; `config.yml` remains untouched by entwurf.
95
+ - **[source]** Import layer (borrowed, not owned): translates Claude Code
96
+ (`~/.claude.json`, `~/.claude/mcp.json`, project `.claude/.mcp.json`/`.claude/mcp.json`),
97
+ Codex, Gemini, OpenCode, Cursor, Windsurf, VS Code (`docs/mcp-config.md` "Imported tool
98
+ configs"); cross-provider precedence puts OMP-native first, Claude Code third
99
+ (`docs/mcp-config.md` "Discovery and precedence").
100
+ - **[host]** The only effective entwurf-bridge source omp can see here is
101
+ `~/.claude.json` user `mcpServers.entwurf-bridge` =
102
+ `bash /home/junghan/repos/gh/entwurf/mcp/entwurf-bridge/start.sh` with
103
+ `ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code`. **Borrowed-config
104
+ finding:** an omp session riding the import would hand the bridge Claude Code's
105
+ external id — wrong provenance for an omp citizen. This is evidence for the issue's
106
+ fence (imported config is borrowed), not a request to clone a second Claude writer.
107
+ - **[LIVE 2026-08-27]** TUI `/mcp list` pane (O1 key): `Claude Code (~/.claude.json):` / `entwurf-bridge ● connected`. Bridge child `479695` ppid `479624`; `/proc/479695/environ` has `ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code`. Server key string is the plain `mcpServers` name **`entwurf-bridge`**.
108
+ - **Design stance (GLG, 2026-08-27):** treat OMP as an independent harness — model the
109
+ operator who never used Claude Code. The Claude import path is never a support
110
+ surface. The future MCP hand is an omp-native writer targeting `~/.omp/agent/mcp.json`
111
+ (vendor writer layer above), whose same-named entry **shadows** the import by vendor
112
+ precedence (OMP-native first, Claude Code third). Vendor-owned controls exist if ever
113
+ needed: user-level `disabledServers` denylist (highest precedence) and
114
+ `mcp.enableProjectConfig` (`docs/mcp-config.md:84-93`, `:426-441`, `:521`); the vendor
115
+ itself never mutates another tool's config (`:441`).
116
+ - **[LIVE 2026-08-27, implementation lane — SHADOWING CLOSED]** after `./run.sh install-omp-mcp`
117
+ wrote the native entry (pinned key `entwurf-bridge`, env `external-mcp/omp`), a fresh omp/18.0.0
118
+ TUI's own `/mcp list` pane reports the source as the NATIVE file and lists the server ONCE:
119
+ `Configured MCP Servers` / `User level (~/.omp/agent/mcp.json):` / `entwurf-bridge ● connected [stdio]`.
120
+ The earlier reading on this same host was `Claude Code (~/.claude.json): entwurf-bridge ● connected`
121
+ (above) — the attribution FLIPPED and the import does not appear beside it. The spawned bridge
122
+ child confirms it from the other side: `/proc/541648/environ` carries
123
+ `ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/omp` (it carried `…/claude-code` before), with
124
+ ppid `541617` = the omp pid. Same-key first-wins, measured, with the vendor as the oracle.
125
+ - **[LIVE 2026-08-27, implementation lane — O2 CLOSED]** `disabledServers: ["entwurf-bridge"]`
126
+ added to `~/.omp/agent/mcp.json`, fresh TUI, `/mcp list`:
127
+ `User level (~/.omp/agent/mcp.json): entwurf-bridge ○ not connected [stdio]` and **no Claude Code
128
+ section at all** — and no MCP child was spawned (`pgrep -P <omp pid>` empty). Denylisting the
129
+ shared name suppresses the native entry AND starves the import, exactly as cross-review (3d)
130
+ predicted from source: it is never the hide-the-import tool. The config was restored afterwards;
131
+ the shipped writer REFUSES to install under such a denylist and `doctor-omp-mcp` is red while one
132
+ exists.
133
+ - **[LIVE 2026-08-27, implementation lane — inverse]** `./run.sh uninstall-omp-mcp` removed the file
134
+ it had created (`created-new` → absent, not an empty `$schema` stub) and `doctor-omp-mcp` then
135
+ reports `the CLAUDE IMPORT is currently the only source` — the honest pre-#87 state, named rather
136
+ than silent. Reinstalling returns the verdict to `native-wins`.
137
+ - **[source, cross-review (3)]** Shadowing contract (external review Defect-1, accepted
138
+ amended): the future native writer must pin the **same server key** as the imported
139
+ entry — the literal `mcpServers` map key `entwurf-bridge` — gate-pinned. Same-key
140
+ first-wins fully suppresses the import even when env differs
141
+ (`capability/index.ts:203-207`; `equivalent` is never consulted on a key hit), which
142
+ is exactly the label swap this stance wants. Never use
143
+ `disabledServers: ["entwurf-bridge"]` to hide the import while a native entry exists —
144
+ suppression is by name and kills **both** (`mcp/config.ts:123-127`,
145
+ `capability/index.ts:191-196`).
146
+ - **[source, audited I1/F1/F4]** Writer shape and precedence in code: user file
147
+ `~/.omp/agent/mcp.json` = `{ $schema, mcpServers: { <name>: { command, args?, env? } } }`
148
+ (`mcp/config-writer.ts:111-143`, `config/mcp-schema.json`); discovery priority
149
+ native=100 > omp-plugins=90 > **claude=80** > … > mcp-json=5 with first-wins dedupe
150
+ (`capability/index.ts:84-91`, `:183`), project-before-user inside each provider; the
151
+ vendor never mutates another tool's config (`config-writer.ts:301-304`) and toggles
152
+ imported servers only via user `disabledServers`/`enabledServers` (`mcp/config.ts:104-127`).
153
+
154
+ ## M4 — Statusline / receive surfaces
155
+
156
+ - **[source — CORRECTED, audited H2/H3]** Visible identity: `setStatus(...)`
157
+ (`types.ts:284`) is the **only** rendering surface — the TUI maps it to hook-status
158
+ lines under the status line (`extension-ui-controller.ts:110`,
159
+ `status-line/component.ts:678-682`, `:2246-2251`), gated by
160
+ `statusLine.showHookStatus` default **true** (`settings-schema.ts:952-956`).
161
+ `setFooter`/`setHeader` are **TUI no-ops** at v18 (`extension-ui-controller.ts:139`).
162
+ The built-in statusLine's segments are a closed enum with **no custom-text/command
163
+ segment** (`status-line/segments.ts:731-757`) — no Copilot-style `statusLine.command`
164
+ route exists. Step-4 visible identity must ride `setStatus`.
165
+ - **[LIVE 2026-08-27, implementation lane]** `ctx.ui.setStatus("entwurf", "🪛 <gid> omp")` renders:
166
+ the TUI's bottom line read `🪛 20260827T211548-68ca9d omp` while idle, and switched to
167
+ `🪛 20260827T211716-487002 omp` after `/new`. Default `statusLine.showHookStatus` was in
168
+ force (untouched operator config). Note the call site is `ctx.ui.setStatus` on the EVENT context
169
+ (`docs/hooks.md` "Status line behavior"), not a method on the `pi` API object.
170
+ - **[source]** Wake surfaces: `pi.sendUserMessage(content, {deliverAs})` — **"idle
171
+ starts a turn"** (`types.ts:1418`); `pi.sendMessage(..., {triggerTurn, deliverAs})`
172
+ (`types.ts:1412`); contained `ctx.setInterval`/`setTimeout` for background watches;
173
+ `mcp_notification` events can be bridged into a turn (`types.ts` McpNotificationEvent).
174
+ Feature gate: extensions are on by default; `--no-extensions` disables discovery.
175
+ - Other surfaces that exist but are NOT measured here: `--mode rpc`, `omp acp`,
176
+ `omp ps` daemons, collab `join`. Naming them is step-1 inventory, not a claim.
177
+ - **[LIVE — pending]** none required this branch — idle-wake demonstration is step 7
178
+ work and needs its own lane; this branch only inventories the surfaces.
179
+
180
+ ## M5 — Parent process topology
181
+
182
+ - **[source]** Extensions are in-process → the "hook process" IS the omp host process.
183
+ - **[source]** MCP stdio servers are spawned by the omp process; on Linux
184
+ `detached: true` → setsid (own session, no controlling terminal) but still a direct
185
+ child (`packages/coding-agent/src/mcp/transports/stdio.ts:41-57`, `:334`).
186
+ - Expected join: `extension process.pid == omp pid == mcp child ppid` — a one-process
187
+ join, structurally simpler than Claude's hook-launcher/pid-marker join.
188
+ - **[source, audited G1/G2]** Child env is `{ ...Bun.env, ...this.config.env }`
189
+ (`transports/stdio.ts:577-581`) — full parent env inherited, entry env merged on top,
190
+ **no omp-injected session/agent id**; setsid does not reparent (ppid stays omp pid);
191
+ teardown signals the process group then SIGKILLs after grace (`stdio.ts:474-494`).
192
+ - **[LIVE 2026-08-27]** same run (`ps -o pid,ppid,sid,args`):
193
+ `479023 185520 479023 -bash` (tmux server child)
194
+ `479624 479023 479023 omp --cwd /tmp -e …/probe-extension.ts`
195
+ `479695 479624 479695 node …/mcp/entwurf-bridge/src/index.ts` (own SID = setsid; ppid = omp).
196
+ Join: probe pid == omp pid == bridge ppid.
197
+ - **[LIVE 2026-08-27, implementation lane — the step-6 join in production]** with the shipped unit:
198
+ `541617 omp` (ppid 185520 = tmux server) and `541648 node …/entwurf-bridge/src/index.ts` with
199
+ ppid `541617`, own SID. The sender marker the extension wrote is
200
+ `~/.pi/agent/meta-senders/omp/541617.json` →
201
+ `{"backend":"omp","gardenId":"20260827T211548-68ca9d","ownerPid":541617,"ownerStartKey":"linux:100960160"}`.
202
+ Marker owner pid == omp pid == bridge child's ppid: the one-process join, measured end to end.
203
+
204
+ ## §3.5 — Host-vs-subagent discriminator
205
+
206
+ - **[source]** `ExtensionMode = "tui" | "rpc" | "json" | "print"` and `ctx.hasUI`
207
+ (`extensibility/extensions/types.ts:453`, `:465`).
208
+ - **[source]** Runner default `#mode = "print"` (`extensibility/extensions/runner.ts:438`,
209
+ `:651`); only the interactive TUI controller passes `"tui"`
210
+ (`modes/controllers/extension-ui-controller.ts:302`, `:531`); rpc passes `"rpc"`
211
+ (`modes/rpc/rpc-mode.ts:961`).
212
+ - **[source]** Task subagents: `hasUI: false` (`task/executor.ts:3115`); their runner is
213
+ initialized **without** a mode argument (`executor.ts:3252`) → `"print"`; each emits its
214
+ own `session_start` (`executor.ts:3305`). Subagents inherit extensions AND parent MCP
215
+ via proxy tools unless `restrictToolNames` (`executor.ts:3021-3024`, `:3096`) — so a
216
+ birth extension WILL fire inside subagents; the discriminator is the only fence.
217
+ - **[source — CORRECTED, audited A1/A2/A4/B2]** Full host-mode matrix: plain TUI →
218
+ `"tui"`; `-p`/`--mode text|json` → `"print"`/`"json"`; `--mode rpc|rpc-ui` and
219
+ `omp acp` → `"rpc"`. **`ExtensionContext.hasUI` is NOT a fence**: rpc, rpc-ui, and ACP
220
+ all pass a real uiContext so `hasUI === true` (`runner.ts:879-881`); the `types.ts:465`
221
+ comment ("false in print/RPC mode") disagrees with code — a live instance of the §1(c)
222
+ docs-vs-code trap. ACP loads discovered extensions and emits `session_start`
223
+ (`main.ts:434-449`, `acp-agent.ts:2603`) but a `mode === "tui"` guard stays silent.
224
+ Bundled task agents do **not** set `restrictToolNames` (it is a spawn option:
225
+ plan-mode/security/compress, `structured-subagent.ts:386`) — default subagents DO load
226
+ extensions, so the discriminator is load-bearing, and it is `mode` **alone**.
227
+ - **[LIVE 2026-08-27, decisive]** `/tmp/omp-probe-live.jsonl`. Discriminator **held**, same pid `479624`, different session ids/files:
228
+ host `10:53:21.125Z` `{"event":"session_start","mode":"tui","hasUI":true,"sessionId":"01a042da-537a-7770-a275-7b8162eecca4","sessionFile":"…/-tmp/2026-08-27T10-53-19-610Z_01a042da-537a-7770-a275-7b8162eecca4.jsonl"}`
229
+ subagent `10:55:47.906Z` `{"event":"session_start","mode":"print","hasUI":false,"sessionId":"01a042dc-9540-7643-b3ef-99d223b459a9","sessionFile":"…/-tmp/2026-08-27T10-53-19-610Z_01a042da-537a-7770-a275-7b8162eecca4/Greeting.jsonl"}`
230
+ Record store `~/.pi/agent/meta-sessions` regular files: **519 before, 519 after**.
231
+ - **[LIVE 2026-08-27, implementation lane — PRODUCTION UNIT]** the same discriminator re-measured
232
+ with the SHIPPED birth extension installed (`~/.omp/agent/extensions/entwurf-meta-omp/index.ts`)
233
+ rather than the `-e` probe. One real TUI, one real task subagent (`GreetingResponder`, 8.5s),
234
+ record store **521 before, 521 after** that subagent.
235
+ `<pi-agent-dir>/meta-bridge-hook.log`:
236
+ `12:15:48.130Z INFO [omp] create record 20260827T211548-68ca9d.meta.json (edge=session_start, native=01a04325-d3ef-70c5-9392-7ac59a19c72e)`
237
+ then `12:16:37.100Z INFO [omp] scope-refused edge=session_start mode=print: not the visible tui host, no record minted`.
238
+ One host record, one refusal, zero subagent citizens — and no receiver marker or mailbox for
239
+ either.
240
+ - **[LIVE 2026-08-27, implementation lane — re-birth edge]** `/new` in that same TUI fired
241
+ `create record 20260827T211716-487002.meta.json (edge=session_switch(new), native=01a04327-2f41-722f-a24a-be5efb5de6cf)`
242
+ and re-pointed the sender marker under the same host pid. The switch edge is a real birth edge
243
+ (audit C2), not a theoretical one: a unit bound to `session_start` alone would have left this
244
+ session unminted with the previous citizen's id still on its status line.
245
+ - **[source, cross-review (4)]** ACP sessions skip on-disk MCP discovery
246
+ (`main.ts:446`) but client-supplied MCP still connects
247
+ (`acp-agent.ts:2608-2669`) — `enableMCP:false` is NOT a second fence; the tui guard
248
+ and the bridge's default send refusal remain the fences.
249
+ - Stop rule (issue #87): if the discriminator flips at any vendor upgrade, stop —
250
+ do not pile heuristics.
251
+
252
+ ## Tool-name dialect (step 5 pre-measurement)
253
+
254
+ - **[source]** Mint: `mcp__${sanitizedServerName}_${normalizedToolName}` capped at 64
255
+ chars (`packages/coding-agent/src/mcp/tool-bridge.ts:396`); sanitizer lowercases,
256
+ replaces `[^a-z_]+` runs with `_`, collapses `_+`, trims edges (`tool-bridge.ts:351`);
257
+ redundant server-prefix stripping documented at `:344-349`.
258
+ - Computed for entwurf-bridge: `entwurf_v2` → `mcp__entwurf_bridge_entwurf_v` (digit
259
+ dropped), `entwurf_peers` → `mcp__entwurf_bridge_entwurf_peers`, etc. **The live omp
260
+ tool list is the acceptance oracle**, not this computation.
261
+ - **[source, audited E1]** All seven bridge tools computed in the audit (unique, ≤64
262
+ chars, no prefix-strip triggers). The approval layer consults the **same minted
263
+ string** — `tools.approval.<minted name>` (`tools/approval.ts:110`, `mcp__`
264
+ special-case `:261`); MCP tools default `approval: "write"` (`tool-bridge.ts:490`).
265
+ **No second Copilot-style permission dialect.**
266
+ - **[LIVE 2026-08-27]** `/tmp/omp-probe-live.jsonl` host `tool_dump_t25` (`10:53:46.128Z`, `mode:"tui"`, `toolCount:28`):
267
+ `["mcp__entwurf_bridge_entwurf_fresh_call","mcp__entwurf_bridge_entwurf_inbox_read","mcp__entwurf_bridge_entwurf_peers","mcp__entwurf_bridge_entwurf_register_native","mcp__entwurf_bridge_entwurf_resume_call","mcp__entwurf_bridge_entwurf_self","mcp__entwurf_bridge_entwurf_v"]`
268
+ Subagent dump (`mode:"print"`, `toolCount:26`) lists the **same seven names** (extensions inherited). Matches source computation; digit in `entwurf_v2` is eaten.
269
+
270
+ ## M6 — pi-rail overlap (added axis, GLG 2026-08-27)
271
+
272
+ omp is a pi fork and keeps pi's env vocabulary. The entwurf pi rail and the omp rail
273
+ must never steer each other through shared knobs.
274
+
275
+ - **[source]** omp honors `PI_CONFIG_DIR` (default `.omp`) and `PI_CODING_AGENT_DIR`
276
+ (`packages/utils/src/dirs.ts:4-5`, `resolvePreProfileAgentDir` `:338`), plus
277
+ `PI_PROFILE`/`OMP_PROFILE` and `PI_SMOL_MODEL`-family role envs (`omp --help`).
278
+ Entwurf's offline verification sandboxes `PI_CODING_AGENT_DIR` (AGENTS.md Hard Rule
279
+ 12) — the same knob now steers TWO harnesses. Any future omp gate/launcher must
280
+ decide explicitly which harness each `PI_*` var is addressing.
281
+ - **[source]** `PI_SESSION_ID`/`PI_AGENT_ID` appear **nowhere** in
282
+ `packages/coding-agent/src` — omp does not mint pi identity carriers itself (only
283
+ `MNEMOPI_SESSION_ID`, `packages/mnemopi/src/mcp-tools.ts:434`). The §6 danger is
284
+ pure **inheritance passthrough**: an omp launched from a pi citizen's shell hands the
285
+ parent's pi garden id to its MCP children (`adding-a-harness.md` §6 sanitization).
286
+ - **[source — refined, audited G4]** Legacy `.pi` survives **only** as a package.json
287
+ manifest alias (`pkg?.omp ?? pkg?.pi`, `discovery/helpers.ts:616-617`,
288
+ `plugins/loader.ts:267-273`); native discovery roots are `.omp`-only
289
+ (`discovery/builtin.ts:58-72`, `utils/src/dirs.ts:23`) — entwurf's pi extensions under
290
+ `~/.pi/` cannot be ambiently loaded by omp.
291
+ - **[source, audited G3]** Identity-adjacent inventory also includes
292
+ `PI_CODING_AGENT_SESSION_DIR` (`cli/args.ts:156`); the full PI_* read inventory
293
+ (40+ non-identity vars included) is in the audit G3 table.
294
+ - **[LIVE 2026-08-27]** clean tmux window (preflight `env | grep PI_SESSION_ID|PI_AGENT_ID` → `CLEAN_NO_PI_IDENTITY`). `/proc/479624/environ` and `/proc/479695/environ` grep `PI_SESSION_ID|PI_AGENT_ID|PI_CODING_AGENT_DIR`: **all absent**. Probe path is only the `-e` file (`…/scripts/raw-omp-measure/probe-extension.ts`); no `~/.pi/` extension path in the JSONL. (Bridge still carries borrowed `ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code` — M3, not a PI_* carrier.)
295
+
296
+ ## M7 — Receive surface (step 7), MEASURED 2026-08-30
297
+
298
+ Probe: `probe-receive-surface.ts`, loaded with `omp -e`. Host: oracle, `omp/18.0.0`
299
+ (the binary reports "New version 18.0.11 is available", so this host had NOT drifted).
300
+ Scratch logs were `/tmp/omp-receive-probe-run{1,2}.jsonl` and `/tmp/omp-order-probe.jsonl`;
301
+ they are gone with the host's `/tmp`, which is exactly why the decisive lines are pasted
302
+ BELOW rather than pointed at. A receipt parked in a host-local path is unreadable to the
303
+ next reviewer. **The vendor source
304
+ checkout named in `source-audit.md` is NOT present on this host**, so every row below is a
305
+ LIVE receipt from the running binary rather than a source read — which is why this section
306
+ exists at all.
307
+
308
+ - **[LIVE] `sendUserMessage` is on the FACTORY object, not the event ctx.**
309
+ `typeof pi.sendUserMessage === "function"`, `typeof pi.sendMessage === "function"`;
310
+ `typeof ctx.sendUserMessage === "undefined"`. This is the exact INVERSE of `setStatus`,
311
+ which lives on `ctx.ui` (M4). Two vendor surfaces, opposite placements, and neither is
312
+ guessable — the call site is a measurement, not a detail.
313
+ - **[LIVE] the idle wake is real.** `pi.sendUserMessage(text, {deliverAs:"user"})` called
314
+ at +12.005s on an idle `mode:"tui"` session with zero typing:
315
+ `agent_start` +31ms, `turn_start` +64ms, `turn_end` +2.45s, and the model answered the
316
+ probe token in the transcript. Returns `undefined` — not a promise, so there is nothing
317
+ to await and no rejection to swallow. This closes the `[LIVE — pending]` slot M4 left open.
318
+ - **[LIVE] `ctx.setInterval` fires while idle, and its canceller is `ctx.clearTimer`.**
319
+ 58 ticks over 29s on a fully idle session with no model turn. **There is NO
320
+ `ctx.clearInterval`**: run 1 called it through `?.`, got a SILENT no-op, and the timer
321
+ ran 29s past its stop condition inside the TUI. Run 2 probed the canceller BY NAME and
322
+ `ctx.clearTimer(handle)` stopped it at exactly 3 ticks. `ctxKeys` also carries `isIdle`,
323
+ `hasPendingMessages`, `getAsyncJobSnapshot`, `abort`, `shutdown`, `invokeTool`.
324
+ - **[LIVE] two extensions in one process both work.** The installed birth unit and the
325
+ `-e` probe ran in the same pid: the birth unit minted a record, wrote its sender marker
326
+ and rendered `🪛 <gid> omp` on the status line while the probe logged its own events.
327
+ - **[LIVE] handler order follows DIRECTORY-NAME COLLATION, and the failure it creates is
328
+ real.** A temporary discovered unit `aa-order-probe` (sorting before `entwurf-meta-omp`)
329
+ ran its `session_start` handler at `04:46:57.310Z` with `markerPresentAtHandler:false`;
330
+ the birth unit wrote its record at `.330Z` and its marker at `.331Z` — **20ms later**.
331
+ `entwurf-receive-omp` sorts AFTER `entwurf-meta-omp`, so a receiver that assumed birth
332
+ had already run would arm on the first try **by accident**, and go green on every host
333
+ until a rename or a vendor scan-order change. That is why the shipped receiver defers on
334
+ a bounded `ctx.setInterval` instead. The probe unit was removed in the same session.
335
+ The three decisive lines, from `/tmp/omp-order-probe.jsonl` and the shared hook log:
336
+
337
+ ```text
338
+ 04:46:57.310Z {"edge":"session_start","unit":"aa-order-probe","pid":3135409,"mode":"tui","markerPresentAtHandler":false}
339
+ 04:46:57.330Z INFO [omp] create record 20260830T134657-f4fbff.meta.json (edge=session_start, native=01a050fd-…)
340
+ 04:46:57.331Z INFO [omp] sender marker 3135409 -> 20260830T134657-f4fbff
341
+ ```
342
+ - **[LIVE] event-based retry cannot substitute for the timer.** Confirmed against M1: the
343
+ host `session_start` fires with no model turn, while `agent_start`/`turn_start` require
344
+ a typed prompt. A Copilot-style retry on turn edges would leave a freshly opened IDLE
345
+ session unaddressable until the operator typed — the one state bundle B exists to wake.
346
+ - **[LIVE] the D6 roundtrip, in the four lines that carry it.** Automated acceptance
347
+ (`smoke-omp-receive-live`, 11 assertions, exit 0), garden `20260830T142524-ab505f`:
348
+
349
+ ```text
350
+ meta-mailbox state.json lastEnqueuedAt 2026-08-30T05:25:25.135Z / lastReadAt 05:25:28.859Z
351
+ receive log doorbell (signal) garden=20260830T142524-ab505f fresh=1 unread=1 → rang … unread=1
352
+ session transcript {"type":"toolCall","name":"mcp__entwurf_bridge_entwurf_inbox_read",
353
+ "arguments":{"gardenId":"20260830T142524-ab505f"}}
354
+ session transcript role:"toolResult" toolName:"mcp__entwurf_bridge_entwurf_inbox_read"
355
+ text: "[entwurf inbox read ⟵] garden: 20260830T142524-ab505f … receipt: lastReadAt=…"
356
+ ```
357
+
358
+ The toolCall is the join that matters: `entwurf_inbox_read` takes a CALLER-SUPPLIED
359
+ garden id, so `lastReadAt` alone proves a read happened, not that THIS session performed
360
+ it. An earlier manual run had the same shape with a real authoritative sender — a
361
+ claude-code citizen to omp `20260830T135551-d9b5d4`, `entwurf_v2 meta-mailbox → enqueued`,
362
+ and the model answering in the same session.
363
+ - **[LIVE] D3 addressed isolation — the cell the Copilot row still lists as PENDING.** Two
364
+ omp citizens armed at once (`20260830T141425-597e5e` pid 3154765,
365
+ `20260830T141428-f8abe4` pid 3154835); one addressed enqueue:
366
+
367
+ ```text
368
+ 05:14:39.604Z INFO [omp-receive] pid=3154765 doorbell (signal) garden=20260830T141425-597e5e fresh=1 unread=1
369
+ 05:14:39.606Z INFO [omp-receive] pid=3154765 rang garden=20260830T141425-597e5e unread=1
370
+ (no line for pid 3154835 anywhere in the log)
371
+ target state.json lastEnqueuedAt 05:14:39.599Z / lastReadAt 05:14:43.019Z
372
+ sibling state.json ABSENT — mailbox directory empty
373
+ sibling transcript FILE DOES NOT EXIST (omp persists lazily; it never took a turn)
374
+ ```
375
+ - **[LIVE] the `/new` unarm, both directions.** Same pid 3140978:
376
+
377
+ ```text
378
+ 04:57:21.319Z INFO [omp-receive] unarmed garden=20260830T135551-d9b5d4 (edge=session_switch(new))
379
+ 04:57:21.320Z INFO [omp-receive] armed garden=20260830T135721-af8973 … edge=session_switch(new)
380
+ ```
381
+
382
+ and dispatch to the retired id then answered
383
+ `entwurf_v2 rejected: mailbox-undeliverable (observed liveness: unsupported)` — the unarm
384
+ is load-bearing, not tidiness.
385
+ - **[LIVE, and it changed an acceptance] the model refuses imperatives inside a mailbox
386
+ body — correctly.** An early draft of `smoke-omp-receive-live` asked the model to echo a
387
+ nonce; it drained the inbox, recorded the receipt, and answered *"Its acceptance
388
+ instruction was unverified, so no token was sent."* It was obeying OUR OWN doorbell,
389
+ which marks bodies untrusted. The acceptance now asserts the read (and the body arriving
390
+ in the drained result), never the model's compliance with an untrusted instruction.
391
+
392
+ ## LIVE procedure (for the implementer sibling)
393
+
394
+ The probe is `scripts/raw-omp-measure/probe-extension.ts` (repo path). Adjust it freely
395
+ against the real API — the file is measurement scaffolding, not product.
396
+
397
+ 1. Fresh tmux window, clean env (no `PI_SESSION_ID`/`PI_AGENT_ID` — verify with `env`).
398
+ `export OMP_PROBE_LOG=<scratch>/omp-probe.jsonl`.
399
+ 2. `omp --cwd <scratch> -e /home/junghan/repos/gh/entwurf/scripts/raw-omp-measure/probe-extension.ts`
400
+ (default model is fine; turns below are tiny).
401
+ 3. While idle at the TUI: capture `ps -eo pid,ppid,args | grep -E 'omp|entwurf'` and the
402
+ two `/proc/*/environ` greps (M5, M6). Wait ≥25s for the tool dumps (dialect).
403
+ 4. One tiny turn: ask the model to spawn exactly one trivial task subagent (e.g. "use
404
+ the task tool to run a subagent that answers 'hi'; nothing else"). This produces the
405
+ §3.5 subagent `session_start` line.
406
+ 5. Exit. Paste the decisive JSONL/ps/environ lines into the `[LIVE — pending]` slots
407
+ above (receipts must travel; host-local paths alone do not). Name the session file
408
+ under `~/.omp/agent/sessions/` as the vendor-side receipt.
409
+ 6. Record store safety: count entwurf meta-records before and after; must be identical.
410
+
411
+ Do NOT: install anything, write any omp config, register a backend, mint a record,
412
+ send via `entwurf_v2`, or demonstrate idle-wake (step 7, separate lane).
413
+
414
+ **That fence belonged to the measurement branch and has been spent.** Bundle A of the
415
+ implementation lane (GLG grant, 2026-08-27) installed the birth unit and the native MCP
416
+ entry on this host under its own acceptance, and the receipts it took are the
417
+ `[LIVE …, implementation lane]` rows above. Idle-wake (step 7) is still out of bounds and
418
+ still has no unit. To re-take any of those rows, the procedure is now
419
+ `./run.sh install-omp-bridge && ./run.sh install-omp-mcp`, a real TUI, and the two
420
+ doctors — not this probe.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * raw-omp-measure probe — OMP v18.0.0 vendor measurement for issue #87.
3
+ *
4
+ * Loaded into a live omp process via `omp -e <this file>`. Logs one JSONL line
5
+ * per extension lifecycle event so the host-vs-subagent discriminator
6
+ * (`mode === "tui"` vs `"print"` + `hasUI:false`, adding-a-harness.md §3.5)
7
+ * and the in-process envelope (pid/ppid) get LIVE receipts from the vendor's
8
+ * own event bus. Measurement only — mints nothing, sends nothing.
9
+ *
10
+ * Log path: $OMP_PROBE_LOG (default /tmp/omp-probe.jsonl), append-only JSONL.
11
+ */
12
+ import { appendFileSync } from "node:fs";
13
+
14
+ const LOG = process.env.OMP_PROBE_LOG ?? "/tmp/omp-probe.jsonl";
15
+
16
+ function log(obj: Record<string, unknown>): void {
17
+ try {
18
+ appendFileSync(LOG, `${JSON.stringify({ ts: new Date().toISOString(), ...obj })}\n`);
19
+ } catch {
20
+ // measurement probe: never take the host down
21
+ }
22
+ }
23
+
24
+ export default function probe(pi: any): void {
25
+ const snap = (event: string, ctx: any, extra?: Record<string, unknown>) => {
26
+ // v18 ReadonlySessionManager (session-manager.ts:359-376): getSessionId / getSessionFile / getSessionDir.
27
+ // There is no getId().
28
+ let sessionFile: unknown;
29
+ let sessionId: unknown;
30
+ let sessionDir: unknown;
31
+ try {
32
+ sessionFile = ctx?.sessionManager?.getSessionFile?.();
33
+ } catch {}
34
+ try {
35
+ sessionId = ctx?.sessionManager?.getSessionId?.();
36
+ } catch {}
37
+ try {
38
+ sessionDir = ctx?.sessionManager?.getSessionDir?.();
39
+ } catch {}
40
+ log({
41
+ event,
42
+ pid: process.pid,
43
+ ppid: process.ppid,
44
+ mode: ctx?.mode,
45
+ hasUI: ctx?.hasUI,
46
+ cwd: ctx?.cwd,
47
+ sessionFile,
48
+ sessionId,
49
+ sessionDir,
50
+ ...extra,
51
+ });
52
+ };
53
+
54
+ const dumpEntwurfTools = (label: string, ctx: any) => {
55
+ try {
56
+ const all = (pi.getAllTools?.() ?? []) as Array<{ name?: string }>;
57
+ const entwurf = all.map((t) => t?.name).filter((n) => typeof n === "string" && n.includes("entwurf"));
58
+ log({ event: label, pid: process.pid, mode: ctx?.mode, toolCount: all.length, entwurfTools: entwurf });
59
+ } catch (err) {
60
+ log({ event: `${label}_error`, error: String(err) });
61
+ }
62
+ };
63
+
64
+ pi.on("session_start", (_e: any, ctx: any) => {
65
+ snap("session_start", ctx);
66
+ // MCP stdio children (imported config included) connect asynchronously;
67
+ // dump twice so a slow bridge start still lands in the receipt.
68
+ try {
69
+ ctx.setTimeout?.(() => dumpEntwurfTools("tool_dump_t10", ctx), 10_000);
70
+ ctx.setTimeout?.(() => dumpEntwurfTools("tool_dump_t25", ctx), 25_000);
71
+ } catch {}
72
+ });
73
+ pi.on("agent_start", (_e: any, ctx: any) => snap("agent_start", ctx));
74
+ pi.on("turn_start", (_e: any, ctx: any) => snap("turn_start", ctx));
75
+ pi.on("session_shutdown", (_e: any, ctx: any) => snap("session_shutdown", ctx));
76
+ }