@junghanacs/entwurf 0.15.1 → 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.
- package/CHANGELOG.md +229 -0
- package/DELIVERY.md +3 -2
- package/README.md +70 -87
- package/VERIFY.md +4 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +147 -9
- package/docs/setup-clean-host.md +105 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
- package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
- package/mcp/entwurf-bridge/src/index.ts +65 -19
- package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
- package/mcp/tsconfig.json +10 -0
- package/package.json +13 -3
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
- package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
- package/pi-extensions/entwurf-control.ts +12 -9
- package/pi-extensions/lib/entwurf-self-address.ts +18 -7
- package/pi-extensions/lib/meta-sender-identity.ts +1 -1
- package/pi-extensions/lib/meta-session.ts +219 -5
- package/pi-extensions/lib/mux-fresh-call.ts +171 -17
- package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
- package/pi-extensions/meta-bridge-omp.ts +1244 -0
- package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
- package/run.sh +296 -4
- package/scripts/check-entwurf-self-address.ts +31 -0
- package/scripts/check-gate-qualification.ts +4 -1
- package/scripts/check-harness-admission-parity.ts +143 -0
- package/scripts/check-meta-doctor-oracle.sh +20 -0
- package/scripts/check-omp-birth-hook.ts +1049 -0
- package/scripts/check-omp-fresh-preflight.ts +181 -0
- package/scripts/check-omp-receive-arm.ts +516 -0
- package/scripts/inventory-verification-surface.ts +1 -1
- package/scripts/meta-bridge-hook-log.sh +9 -1
- package/scripts/mutants/copilot-birth.json +7 -5
- package/scripts/mutants/mux-fresh-call.json +22 -22
- package/scripts/mutants/omp-birth.json +173 -0
- package/scripts/mutants/omp-fresh.json +300 -0
- package/scripts/mutants/omp-receive.json +135 -0
- package/scripts/mutants/self-address.json +34 -0
- package/scripts/omp-bridge-doctor.sh +315 -0
- package/scripts/omp-bridge-install.sh +221 -0
- package/scripts/omp-bridge-oracle.sh +154 -0
- package/scripts/omp-bridge-uninstall.sh +57 -0
- package/scripts/omp-mcp-bridge.sh +320 -0
- package/scripts/omp-mcp-config.py +392 -0
- package/scripts/omp-receive-doctor.sh +246 -0
- package/scripts/omp-receive-facts.ts +106 -0
- package/scripts/omp-receive-install.sh +228 -0
- package/scripts/omp-receive-uninstall.sh +60 -0
- package/scripts/omp-tool-surface.py +349 -0
- package/scripts/raw-omp-measure/README.md +420 -0
- package/scripts/raw-omp-measure/probe-extension.ts +76 -0
- package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
- package/scripts/raw-omp-measure/source-audit.md +414 -0
- package/scripts/smoke-omp-bridge-state.sh +221 -0
- package/scripts/smoke-omp-fresh-live.ts +497 -0
- package/scripts/smoke-omp-mcp-state.sh +327 -0
- package/scripts/smoke-omp-receive-live.ts +340 -0
- package/scripts/smoke-omp-receive-state.sh +196 -0
- package/scripts/tsconfig.json +2 -0
package/VERIFY.md
CHANGED
|
@@ -43,7 +43,7 @@ Verification here is not a benchmark. In production we exchange short turns and
|
|
|
43
43
|
- **Deterministic floor — two tiers (#70):** `pnpm check` is the everyday CORE — toolchain (lint + typecheck), the vitest lanes, and the pure-unit / behavioral-contract / source-topology gates plus the cheap static coherence checks. It prints its own total wall time; acceptance is ≤60s on the reference host `oracle` (an operator measurement, never a hard wall-clock gate on arbitrary hosts). `pnpm run check:full` is the FULL deterministic floor — the core plus the hermetic-integration and package/install tiers — and is what the frozen-candidate protocol, push CI, release-gate, and `prepublishOnly` run. Exact membership is the named `check:*` group scripts in `package.json` (the executable SSOT — this document records meaning and principles, not the command list); a gate changes tier by semantic-class decision, never by getting faster or slower. Both tiers exclude the separately scheduled `check-gate-qualification`.
|
|
44
44
|
- **Discriminating power of that floor:** `./run.sh check-gate-qualification` (scheduled, not ambient: standalone when a lane changes a gate/mutant/matrix, in the CI `check` job on every push, and as a release-gate MUST step — no longer inside the default check chains) re-plants committed defect mutants (`scripts/mutants/*.json`, one per closed defect class) in an isolated snapshot repo and requires each to turn its gate red **bounded and at its claimed `[QK:<claim>]` signature** — a wrong-reason red fails, a baseline-red control voids the whole group, and the runner is negative-controlled on every run (zero-match/multi-match/survived/wrong-reason/hang/control-red/impurity). This measures whether the deterministic gates still *block* what they claim to block; it is **not a new evidence level** (L0–L5 are untouched) and never substitutes for LIVE evidence. Per-cut records cite claim IDs + killed mutant IDs — "N checks passed" alone is not evidence. `check-agy-permission-matrix` complements it with the enumerated permission contract space (literal cells + stated exclusion rules, oracle independent of the SUT).
|
|
45
45
|
- **Live floor:** `LIVE=1 ./run.sh release-gate <scratch-project-dir> --cut` — `pnpm run check:full` + the v2-native live gates + the ACP plugin acceptance floor. It reports a **two-tier summary**:
|
|
46
|
-
- **MUST tier** (release-blocking — owns the exit code; "green" applies only here): `pnpm run check:full`, `check-gate-qualification`, `smoke-entwurf-v2-matrix-live`, `check-bridge`, `doctor-pi-provider` (#81 — `check-bridge` proves the launcher this checkout SHIPS; this proves the invocation the operator's pi provider actually EXECS, by booting it and requiring the entwurf verb set back, because `command -v` answers yes for a command that exits 127; wired as a step on 2026-08-19 after a relocated pnpm cmd-shim cost a cut sixteen LIVE steps before the same fact surfaced at `smoke-acp-bundled-mcp-live`), the resident-garden-guard zero-token half (record birth / record-keyed socket / attach-on-reopen), the `smoke-acp-*-live` ACP plugin smokes (socket-citizen / raw-turn / overlay / provider / session-reuse / carrier-augment / memory-containment / rgg / mcp / skill / bundled-mcp / v2-send), the axis wired in on 2026-07-31 that the aggregate had simply never listed (`smoke-claude-native-resume-live`),
|
|
46
|
+
- **MUST tier** (release-blocking — owns the exit code; "green" applies only here): `pnpm run check:full`, `check-gate-qualification`, `smoke-entwurf-v2-matrix-live`, `check-bridge`, `doctor-pi-provider` (#81 — `check-bridge` proves the launcher this checkout SHIPS; this proves the invocation the operator's pi provider actually EXECS, by booting it and requiring the entwurf verb set back, because `command -v` answers yes for a command that exits 127; wired as a step on 2026-08-19 after a relocated pnpm cmd-shim cost a cut sixteen LIVE steps before the same fact surfaced at `smoke-acp-bundled-mcp-live`), the resident-garden-guard zero-token half (record birth / record-keyed socket / attach-on-reopen), the `smoke-acp-*-live` ACP plugin smokes (socket-citizen / raw-turn / overlay / provider / session-reuse / carrier-augment / memory-containment / rgg / mcp / skill / bundled-mcp / v2-send), the axis wired in on 2026-07-31 that the aggregate had simply never listed (`smoke-claude-native-resume-live`), `smoke-entwurf-chain-live` — the cross-harness delivery chain (native Claude Code → pi GPT → pi ACP Sonnet → mailbox terminus) proving sender identity and replyability at every hop plus a real read receipt at the end — the integrated `smoke-mux-lifecycle-live` (its own note below), and the two OMP steps 0.16.0 wired: `smoke-omp-receive-live` (the addressed roundtrip into a live omp citizen; it reads the capability registry and decides its own outcome rather than passing by default) and `smoke-omp-fresh-live` (the clause 7 receipt the release stop now requires). (`smoke-session-id-name` is gone — #50 C3: its `--session-id`/`--name` substrate has no entwurf consumer anymore.)
|
|
47
47
|
- **BEHAVIOR tier** (advisory, non-blocking): the resident-garden-guard positive (a model-in-loop `entwurf_self` turn). A BEHAVIOR FAIL is surfaced with its artifact path but **never blocks the cut**. The lane holds what the model *chooses*, never what our wiring fails to deliver — a gate that TELLS the model which tool to call stays MUST, because its failure is ours — measured 2026-07-24, when the tool turned out to be absent from the session schema in both observed failures (the bundled-MCP readiness gap recorded in `scripts/smoke-acp-v2-send-live.ts`).
|
|
48
48
|
- **Every MUST step is invoked and reports its own outcome.** Prerequisite-bearing LIVE MUST steps ride the P1 STEP OUTCOME protocol (`scripts/lib/step-outcome.sh`): exit 0 = PASS, exit 97 = SKIP (a prerequisite the step does not have, printed as an `[entwurf:skip]` line), anything else = FAIL. The two non-skippable static steps (`pnpm run check:full`, `check-gate-qualification`) are inline: 0 = PASS, nonzero = FAIL, no SKIP arm — they have no prerequisite to decline. A skip is never counted as a pass — that hole is what let a cortex-less host read as cortex acceptance. Without `--cut` this is the unattended diagnostic: SKIPs are reported and the run still exits 0. **`--cut` makes it acceptance and any MUST SKIP is red**, which is how "a real cut needs `LIVE=1` with `SKIP=0`" stopped being prose. A green MUST gate is **necessary, not sufficient** — GLG authorizes the cut.
|
|
49
49
|
- **When cost-bearing MUST gates run (fixed 2026-07-23, the F6/F7 lesson):** a commit that touches a rail a MUST-tier live gate covers runs that gate **before cross-review is requested** — never parked behind "run it at approval time". Deferring a wired gate to a human decision is what let F6/F7 ship reviewed-and-approved; the wiring exists so the verdict never depends on who pressed enter. "배선이 없어 못 한 것은 OK, 배선이 있는데 안 돌린 것은 우리가 남긴 구멍이다." Model-in-loop cost is spent via the subscription-backed `entwurf` provider where the gate allows it, a free-tier native model otherwise; cost is a reason to pick the cheap target, not to skip the gate.
|
|
@@ -59,6 +59,8 @@ Verification here is not a benchmark. In production we exchange short turns and
|
|
|
59
59
|
> Be honest about its cost, because it is a MUST: it spends real model turns on the operator's configured runtimes (two pi siblings, each resumed once, plus one Claude Code sibling), it READS the operator's real runtime config, and the siblings' native session transcripts stay in the real pi agent dir — they are not cleaned up, and they are part of the evidence a resume was real. The fence is per-axis rather than absolute: the four meta roots (records, mailbox, receivers, senders) are fixture for every cell, so no garden record is ever minted outside it; the native pi cell's control socket and v2 lock are fixture too; the ACP pi cell's are REAL and transient (see the paragraph above); and the Claude cell runs its runtime under the real HOME while its mailbox and meta stay fixture. The run ends by proving the operator's SIX real roots — the four meta roots plus `~/.pi/entwurf-control` and `~/.pi/entwurf-v2-locks` — hold the entry sets they started with, with no fixture garden id anywhere, and that proof now runs on the failure path as well as the green one. Missing prerequisites decline with the protocol SKIP (97), which `release-gate --cut` refuses rather than rounding up.
|
|
60
60
|
>
|
|
61
61
|
> **Fresh-call LIVE is on-demand, not part of `release-gate`.** `LIVE=1 ./run.sh smoke-mux-fresh-call-live` opens configured Pi and Claude Code siblings, spends two model turns, and preserves their native transcripts; run it directly whenever a cut changes mux fresh-call/launch behavior. Its private tmux servers and fixture-bound entwurf write axes make it deterministic about garden-record/socket residue, but those real native turns should not appear unexpectedly in every aggregate cut. **Both this smoke and `smoke-mux-lifecycle-live` enumerate `pi` and `claude-code` only — that exclusion is intentional.** A Copilot visible-fresh (step 9 clause 7) was accepted on 2026-08-25 as an operator-metered LIVE (receipts in `DELIVERY.md`'s Copilot matrix row and the issue #82 thread). It still spends Copilot premium quota, still needs its own grant to re-run, and is never folded into those loops or into the release-gate MUST tier.
|
|
62
|
+
|
|
63
|
+
> **A visible-fresh receipt outside the MUST tier is no longer the general rule — it is Copilot's preserved exception.** #87 measured what that wording permitted: because clause 7 sat outside every aggregate, a harness could be admitted as a full D6 citizen that `entwurf_fresh_call` cannot open, carry an honest "not a supported harness" sentence in the delivery matrix, and still pass the whole floor toward a cut. From OMP onward, the FIRST release of a harness admitted under the #82 contract owes its clause 7 receipt as a release-gate MUST step (`smoke-omp-fresh-live`), and the deterministic half of the same rule (`check-harness-admission-parity`, inside `check:full`) makes a citizen backend missing from `FRESH_CALL_BACKENDS` a red floor rather than a documented one. Forward-only by decision: Copilot's row is not reopened. The contract lives in `docs/adding-a-harness.md` step 9, "The release stop"; this note exists so the cost discipline above is not read as permission.
|
|
62
64
|
>
|
|
63
65
|
> The aggregate release gate does not own a live agy conversation id, so agy's real native-push round trip is a separate acceptance axis: three fail-loud doctors plus `LIVE=1 AGY_CONVERSATION_ID=<id> ./run.sh smoke-agy-native-push-live`, followed by a fresh-conversation sender/reply check after package install. Its deterministic install/sender gates are already inside `pnpm run check:full`; do not misreport the aggregate gate as live agy evidence. **Cost fence:** the agy conversation this smoke drives runs on a free account, so open it on `gemini-3.6-flash` — never a Pro tier. The model is the operator's choice at conversation-open time; entwurf never selects it, and no assertion reads it (see the shipped-lane note: model display is not part of the agy contract).
|
|
64
66
|
>
|
|
@@ -285,6 +287,7 @@ The minimum passing bar:
|
|
|
285
287
|
7. **agy shipped lane accepted:** all three agy doctors are green; automatic birth/statusline/sender identity and same-gid native-push reply are confirmed in a fresh conversation. `agentId=meta-session/antigravity` is correct; model display is not part of that contract. Same-pid concurrent conversation invocation is not claimed.
|
|
286
288
|
8. **Boundary preservation across backends/machines:** for every shipped or explicitly probed backend, regardless of install path or host, no cross-backend tool-surface contamination and no confabulation about pi internals.
|
|
287
289
|
9. **Hygiene:** no orphan ACP children; no unexpected persisted session garbage (a turn-scoped `cwd:` fallback is never a persisted reuse).
|
|
290
|
+
10. **New-harness admission closed:** a release that introduces a native harness admitted under the #82 contract has that harness in `FRESH_CALL_BACKENDS` on all three public surfaces, with `check-harness-admission-parity` green and its clause 7 visible-fresh LIVE step green in the MUST tier. That release also owes the cross-harness leg the same release stop names — two dispatch receipts, an existing citizen's live turn delivered into the new citizen and the new citizen's live turn delivered into an existing one — recorded in `DELIVERY.md`; its deterministic half is an owed follow-up, so until that gate lands this half of the condition is prose and is judged by the recorded receipts. Partial evidence is a branch state; an `unsupported` note in `DELIVERY.md` is a description, never a permit.
|
|
288
291
|
|
|
289
292
|
Passing establishes a **release verification floor**, not an 8-hour/day operational guarantee. The floor says: gates hold, the agent honestly recognizes its environment, no tool surface is normalized away, no identity leaks, no orphans. It does **not** say a real-day workload (50–100+ turns, tool bursts, partial MCP failures, auth/version drift) survives — that needs L3–L5 evidence (appendix).
|
|
290
293
|
|
package/docs/acp-backend-rail.md
CHANGED
|
@@ -92,7 +92,7 @@ undifferentiated "supported" column is what let a Claude PASS read as if it also
|
|
|
92
92
|
|
|
93
93
|
| Surface | Declaration | Class | What a green actually says |
|
|
94
94
|
|---|---|---|---|
|
|
95
|
-
| Entwurf package | `0.15.
|
|
95
|
+
| Entwurf package | `0.15.1` | shipped baseline | the package contract these rows belong to |
|
|
96
96
|
| pi runtime | devDep exact `0.84.3`, peer `>=0.84.3 <0.85` | **exact** oracle + **closed range** | built and certified against 0.84.3; hosts inside the range are accepted, and the ceiling moves only on measurement |
|
|
97
97
|
| ACP wire SDK | `@agentclientprotocol/sdk 1.3.0` | **exact** | the shared wire oracle both adapters speak |
|
|
98
98
|
| Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.70.0` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
# Wiring `entwurf-bridge` into an external MCP host
|
|
2
2
|
|
|
3
|
-
Per-harness registration for Claude Code, Codex CLI,
|
|
4
|
-
PATH/env boundary and the external vs garden-native semantics. `README.md` keeps only
|
|
3
|
+
Per-harness registration for Claude Code, Codex CLI, Antigravity, GitHub Copilot CLI and OMP,
|
|
4
|
+
plus the PATH/env boundary and the external vs garden-native semantics. `README.md` keeps only
|
|
5
5
|
the one-line registration; everything an operator needs to actually wire a host is here.
|
|
6
6
|
|
|
7
|
-
`entwurf-bridge` can also be registered in a separate MCP-aware harness (Claude Code, Codex CLI, Antigravity/`agy`, …). That host does **not** become a pi session and does **not** need to be ACP-backed. There are now two honest cases:
|
|
7
|
+
`entwurf-bridge` can also be registered in a separate MCP-aware harness (Claude Code, Codex CLI, Antigravity/`agy`, GitHub Copilot CLI, OMP, …). That host does **not** become a pi session and does **not** need to be ACP-backed. There are now two honest cases:
|
|
8
8
|
|
|
9
9
|
- **plain external MCP host**: no garden meta-record / sender marker. It can call the read surfaces (`entwurf_peers`, `entwurf_inbox_read`), but `entwurf_v2` sends are **refused by default** (#50 C4: "if we don't know who sent it, we don't send it"). The operator may wire the explicit hatch below; the send then goes out external/non-replyable.
|
|
10
|
-
- **garden-native native session**: a trusted lifecycle hook minted a garden id and sender marker — `SessionStart` for Claude Code, `PreInvocation` for agy, `userPromptSubmitted`/`sessionStart` for GitHub Copilot CLI. It is not a pi control-socket session, but it can be replyable by garden id when its own mailbox/probe rail says so.
|
|
10
|
+
- **garden-native native session**: a trusted lifecycle hook minted a garden id and sender marker — `SessionStart` for Claude Code, `PreInvocation` for agy, `userPromptSubmitted`/`sessionStart` for GitHub Copilot CLI, and for OMP an in-process extension on `session_start`/`session_switch` that mints only a `mode === "tui"` host. It is not a pi control-socket session, but it can be replyable by garden id when its own mailbox/probe rail says so.
|
|
11
11
|
|
|
12
12
|
**Being garden-native is not the same as being replyable, and Copilot is the case that separates them.** Its hook writes a sender marker, so an `entwurf_v2` send carries its own garden id and the receiver learns who wrote — measured 2026-08-21 on Copilot CLI 1.0.80, where a live send arrived under its own garden id with `origin: "meta-session"` and `replyable: false`. Replyability arrived later and through a different process: a first-party extension (`run.sh install-copilot-receive`) that the CLI forks, which binds to the same V3 record and writes a receiver marker owned by its own pid (#82 RAIL 5). So a Copilot citizen is `replyable: true` exactly while that extension is armed for it, and `replyable: false` — honestly, with a real garden identity — when it is not installed, not launched with `COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS` (which is what `entwurf copilot` sets for one invocation), not yet born, or gone. Read the two facts off different rails: the sender marker answers *who sent this*; the receiver marker answers *can a reply land*. The onboarding obligations are in [`adding-a-harness.md`](./adding-a-harness.md) step 7, and the evidence boundary — the managed LIVE acceptance that moved receive to D6, and what D7-partial / D3-pending still exclude — is in [`DELIVERY.md`](../DELIVERY.md).
|
|
13
13
|
|
|
14
|
-
**Which verb an external agent should reach for:** to deliver to / reply to a garden id, use **`entwurf_v2`** — it is the canonical delivery surface and the only one that reads whether the target is live pi, dormant pi, mailbox-backed Claude Code, or native-push Antigravity and routes correctly. Discover targets with `entwurf_peers`, confirm your own identity with `entwurf_self`, drain a mailbox with `entwurf_inbox_read`, and use `entwurf_register_native` only as the explicit/manual fallback for binding an already-running agy conversation (normal agy birth is automatic through the installed hook). Open a NEW sibling with **`entwurf_fresh_call {backend, model, task, cwd?}`**, and reopen a DORMANT pi citizen under its own garden id with **`entwurf_resume_call {target}`**. Fresh call accepts one optional literal absolute `cwd`: omit it or pass `""` for the caller's cwd; otherwise it must name an existing directory and may not contain `#`. Use that input for a new cross-repository sibling — resume preserves a dormant Pi citizen's recorded continuity and is not a cwd substitute. Fresh call needs its selected runtime on the server's PATH (`pi`, `claude`, or `
|
|
14
|
+
**Which verb an external agent should reach for:** to deliver to / reply to a garden id, use **`entwurf_v2`** — it is the canonical delivery surface and the only one that reads whether the target is live pi, dormant pi, a mailbox-backed self-fetch citizen (Claude Code, Copilot, OMP), or native-push Antigravity and routes correctly. Discover targets with `entwurf_peers`, confirm your own identity with `entwurf_self`, drain a mailbox with `entwurf_inbox_read`, and use `entwurf_register_native` only as the explicit/manual fallback for binding an already-running agy conversation (normal agy birth is automatic through the installed hook). Open a NEW sibling with **`entwurf_fresh_call {backend, model, task, cwd?}`**, and reopen a DORMANT pi citizen under its own garden id with **`entwurf_resume_call {target}`**. Fresh call accepts one optional literal absolute `cwd`: omit it or pass `""` for the caller's cwd; otherwise it must name an existing directory and may not contain `#`. Use that input for a new cross-repository sibling — resume preserves a dormant Pi citizen's recorded continuity and is not a cwd substitute. Fresh call needs its selected runtime on the server's PATH (`pi`, `claude`, `entwurf` or `omp` — Copilot opens through the managed `entwurf copilot` invocation, never the bare vendor, while OMP is opened as the bare `omp` runtime); resume call always needs `pi`. Both start a runtime, while delivery does not. (The old v1 verbs `entwurf` / `entwurf_resume` / `entwurf_send` are gone.)
|
|
15
15
|
|
|
16
|
-
Observed: Claude Code, Codex CLI, Antigravity CLI
|
|
16
|
+
Observed: Claude Code, Codex CLI, Antigravity CLI, GitHub Copilot CLI and OMP all reach the read surfaces through this MCP bridge from a plain external host — `entwurf_peers` is a pure fact projection, while `entwurf_inbox_read` is a **mutating drain** (it archives the messages and stamps the read-receipt), so "read" here does not mean side-effect-free; **sending** needs an identity lane. Claude, Copilot and OMP become symmetric/replyable through a mailbox-backed meta-session — Claude's watch armed by its own hook, Copilot's by the forked extension it installs, OMP's by an extension running inside the operator's own TUI process; agy becomes symmetric/replyable through its record-backed sender marker plus live native-push probe. Codex has no managed citizen lifecycle yet, so a Codex host cannot send without the explicit anonymous hatch below.
|
|
17
17
|
|
|
18
18
|
Prerequisites on the host running the external MCP client:
|
|
19
19
|
|
|
20
20
|
- A live pi session launched with `--entwurf-control` populates `~/.pi/entwurf-control/<gardenId>.sock` — the key is the **record's** garden id, never a transcript/session id (`PI_SESSION_ID` only carries the id record birth already established). Required for `entwurf_v2` control-socket dispatch and `entwurf_peers`.
|
|
21
21
|
|
|
22
|
-
> **PATH boundary.** MCP servers are often launched by GUI/editor daemons and may not inherit the interactive shell's PATH. No `entwurf_v2` rail launches a process, so this does not affect delivery — but `entwurf_fresh_call` and `entwurf_resume_call` do open a fixed runtime. If that runtime works in your terminal but an external-host call fails with `spawn pi ENOENT`, `spawn claude ENOENT`, or `spawn
|
|
22
|
+
> **PATH boundary.** MCP servers are often launched by GUI/editor daemons and may not inherit the interactive shell's PATH. No `entwurf_v2` rail launches a process, so this does not affect delivery — but `entwurf_fresh_call` and `entwurf_resume_call` do open a fixed runtime. If that runtime works in your terminal but an external-host call fails with `spawn pi ENOENT`, `spawn claude ENOENT`, `spawn entwurf ENOENT`, or `spawn omp ENOENT`, pass a full PATH in the MCP server `env`, set `ENTWURF_BRIDGE_ENV_FILE` to a small shell file that exports PATH, or point the host at a wrapper that can find the runtime. `start.sh` sources only the explicit `ENTWURF_BRIDGE_ENV_FILE`; it never reads personal dotfiles automatically.
|
|
23
23
|
|
|
24
24
|
> **Identity-carrier boundary.** A complete `PI_SESSION_ID` + `PI_AGENT_ID` pair wins before a
|
|
25
25
|
> native sender marker in the bridge's authoritative-self resolution. Those variables are correct
|
|
@@ -40,6 +40,22 @@ Prerequisites on the host running the external MCP client:
|
|
|
40
40
|
> and still speak under the parent's garden id, so the flag verdict must not absorb it. The other
|
|
41
41
|
> native launchers have NOT been certified against this failure mode; do not read Copilot's cells
|
|
42
42
|
> as coverage for them.
|
|
43
|
+
>
|
|
44
|
+
> **OMP has both halves, and the strip one is not an omp launcher** (#87 Bundle C). entwurf still
|
|
45
|
+
> owns no managed omp invocation — the bare vendor runtime IS this harness's clause 1 answer, and
|
|
46
|
+
> nothing here argues for a wrapper. The strip sits one level up instead, at the shared launch seam
|
|
47
|
+
> every `entwurf_fresh_call` backend passes through: `SCRUBBED_INHERITED_ENV` empties
|
|
48
|
+
> `PI_SESSION_ID` and `PI_AGENT_ID` on the `new-window` argv itself
|
|
49
|
+
> (`pi-extensions/lib/mux-fresh-call.ts:402`), for all four backends rather than only the one whose
|
|
50
|
+
> measurement surfaced it, because the leak is a property of tmux and not of a vendor. `-e VAR=`
|
|
51
|
+
> sets the variable empty rather than unsetting it — tmux has no per-window unset — and every
|
|
52
|
+
> carrier reader trims and tests truthiness, so empty and absent are the same answer by
|
|
53
|
+
> construction. Pinned by `[QK:FRESHCALL-IDENTITY-SCRUB]` in the `check-mux-fresh-call` vitest lane,
|
|
54
|
+
> inside `check:full`. The DETECT half is unchanged and still load-bearing, because a seam covers
|
|
55
|
+
> only the launches that pass through it: `doctor-omp-bridge` reads `/proc/<pid>/environ` for every
|
|
56
|
+
> live `omp` process and goes RED on its own axis when one carries either carrier. An omp the
|
|
57
|
+
> operator started from a pi citizen's bash never touched the seam and is still unsupported, exactly
|
|
58
|
+
> as this boundary says.
|
|
43
59
|
|
|
44
60
|
Example env file:
|
|
45
61
|
|
|
@@ -66,7 +82,7 @@ Emergency/manual workaround when the MCP server environment is wrong but an exis
|
|
|
66
82
|
External/meta-session semantics:
|
|
67
83
|
|
|
68
84
|
- `entwurf_v2` from a plain external host is **refused by default** (no authoritative sender — #50 C4). With the explicit `ENTWURF_BRIDGE_ALLOW_ANONYMOUS_SENDER=1` hatch it delivers with `origin: "external-mcp"` / `replyable: false`; there is still no reply address.
|
|
69
|
-
- `entwurf_v2` from a trusted meta-session delivers with `origin: "meta-session"`, and `replyable` is **derived from that sender's own rail — not granted by being trusted**: a self-fetch sender (Claude Code) is replyable only while its receiver is live and armed, and a native-push sender (Antigravity) only while its adapter probe finds the live conversation. Identity survives either way; only `replyable` drops to `false`. When it is `true`, `wants_reply: true` is allowed and the receiver can reply to the sender's garden id.
|
|
85
|
+
- `entwurf_v2` from a trusted meta-session delivers with `origin: "meta-session"`, and `replyable` is **derived from that sender's own rail — not granted by being trusted**: a self-fetch sender (Claude Code, Copilot, OMP) is replyable only while its receiver is live and armed, and a native-push sender (Antigravity) only while its adapter probe finds the live conversation. Identity survives either way; only `replyable` drops to `false`. When it is `true`, `wants_reply: true` is allowed and the receiver can reply to the sender's garden id.
|
|
70
86
|
- `entwurf_v2` never launches a process, so no delivery path needs `pi` on PATH. A dormant pi target is refused as `dormant-fire-forget-unsupported`: the hidden background resume that used to answer there was withdrawn under the visible-first rule, and re-opening the session is the separate `entwurf_resume_call` verb — which DOES need `pi` on PATH, because it starts one.
|
|
71
87
|
- `entwurf_self` returns the same authoritative identity for pi sessions **and** trusted meta-sessions. A plain external host with no pi env and no trusted sender marker still fails because there is no reply address to report.
|
|
72
88
|
|
|
@@ -143,9 +159,131 @@ Unrelated servers, permissions, settings, and hooks are preserved; every adapter
|
|
|
143
159
|
|
|
144
160
|
The **global** MCP config live agy reads is `~/.gemini/config/mcp_config.json`. `~/.gemini/antigravity-cli/mcp_config.json` is not the global MCP root; the bridge installer one-way cleans only a stale entwurf-owned entry there. After the first model invocation, the imprint hook binds the native `conversationId` to a garden id, the statusline shows `🪛 <garden-id> agy`, and sends from that MCP child carry `agentId=meta-session/antigravity` with `replyable:true` only when the record exists and the live native-push probe succeeds.
|
|
145
161
|
|
|
162
|
+
#### OMP (`omp`, oh-my-pi)
|
|
163
|
+
|
|
164
|
+
Use the managed install surface rather than editing omp's files by hand:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
entwurf install-omp-bridge # the BIRTH extension (a garden id per visible TUI session)
|
|
168
|
+
entwurf install-omp-mcp # the MCP hand (this section)
|
|
169
|
+
entwurf install-omp-receive # the RECEIVER extension (mailbox watch + announce-only doorbell)
|
|
170
|
+
|
|
171
|
+
entwurf doctor-omp-bridge
|
|
172
|
+
entwurf doctor-omp-mcp
|
|
173
|
+
entwurf doctor-omp-receive
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Three units, not two. The receiver is what makes the citizen answerable at all — without it omp
|
|
177
|
+
sends under its own garden id and every reply is refused as `mailbox-undeliverable` — and it is
|
|
178
|
+
also a prerequisite of visible fresh: the fresh preflight refuses this host before touching tmux
|
|
179
|
+
when the receive unit is missing, rather than opening a window that can never be reached.
|
|
180
|
+
|
|
181
|
+
`install-omp-mcp` writes ONE server into omp's own user MCP file,
|
|
182
|
+
`<omp agent dir>/mcp.json` (`~/.omp/agent/mcp.json`, profile-aware), in omp's own writer
|
|
183
|
+
shape — `{command, args?, env?}` with `type` omitted, since stdio is the default:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
|
|
188
|
+
"mcpServers": {
|
|
189
|
+
"entwurf-bridge": {
|
|
190
|
+
"command": "bash",
|
|
191
|
+
"args": ["/absolute/path/to/entwurf/mcp/entwurf-bridge/start.sh"],
|
|
192
|
+
"env": { "ENTWURF_BRIDGE_EXTERNAL_AGENT_ID": "external-mcp/omp" }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**The server key is a pinned literal, and that is the whole point.** omp translates Claude
|
|
199
|
+
Code's `~/.claude.json` as an import provider, so a host that ever used Claude Code already
|
|
200
|
+
has an `entwurf-bridge` — carrying `external-mcp/claude-code`. An omp session riding that
|
|
201
|
+
import introduces itself to the bridge under Claude Code's name. Writing the native entry
|
|
202
|
+
under the byte-identical key shadows it: native provider priority 100 beats claude 80,
|
|
203
|
+
dedupe is first-wins on the server NAME, and on a key hit the equivalence check is never
|
|
204
|
+
consulted, so an entry whose env deliberately differs still suppresses the import outright
|
|
205
|
+
— not both-loaded, not merged, no warning. A different key would load BOTH.
|
|
206
|
+
`[측정]` 2026-08-27, omp/18.0.0: the vendor's own `/mcp list` pane flipped from
|
|
207
|
+
`Claude Code (~/.claude.json): entwurf-bridge ● connected` to
|
|
208
|
+
`User level (~/.omp/agent/mcp.json): entwurf-bridge ● connected [stdio]`, and the spawned
|
|
209
|
+
bridge child's environ flipped with it.
|
|
210
|
+
|
|
211
|
+
**`disabledServers` is never the way to hide the import.** Suppression is by name and a
|
|
212
|
+
suppressed item still claims the dedupe key, so denylisting `entwurf-bridge` kills the
|
|
213
|
+
native entry and the import together. `[측정]` with that denylist the pane shows
|
|
214
|
+
`entwurf-bridge ○ not connected` and no Claude Code section at all, and no MCP child is
|
|
215
|
+
spawned. The installer refuses to write into a config that denylists its own key, and
|
|
216
|
+
`doctor-omp-mcp` is red while one exists.
|
|
217
|
+
|
|
218
|
+
**The tool names are omp's dialect, not Claude's.** omp mints `mcp__<server>_<tool>` after
|
|
219
|
+
lowercasing and replacing every `[^a-z_]+` run with `_`, collapsing runs and trimming edges,
|
|
220
|
+
so `entwurf_v2` surfaces as `mcp__entwurf_bridge_entwurf_v` — the trailing digit is eaten by
|
|
221
|
+
the charset, not by the length cap. The live tool list is the acceptance oracle; a live
|
|
222
|
+
session mounts all seven.
|
|
223
|
+
|
|
224
|
+
**And the NAME is not the invocation. Under omp's default settings an MCP tool is not a
|
|
225
|
+
function the model calls — it is a virtual file it writes to.** `tools.xdev` (boolean,
|
|
226
|
+
**default on**) mounts "discoverable" tools as `xd://<tool>` devices and DROPS them from the
|
|
227
|
+
top-level toolset; the model then reads `xd://<tool>` for the schema and *writes* the JSON
|
|
228
|
+
argument object to `xd://<tool>` to execute it. `tools.xdevDocs` (**default `builtins`**)
|
|
229
|
+
keeps built-in docs inline while MCP and extension schemas stay off-prompt until read. That
|
|
230
|
+
default costs a real capability. `[측정]` 2026-08-28, omp/18.0.0: with the defaults, a plain
|
|
231
|
+
"send this message to garden id X" produced a `write` to `xd://…entwurf_peers` (a LISTING)
|
|
232
|
+
and then the sentence "보냈습니다" — no `entwurf_v2` call, nothing enqueued, `lastEnqueuedAt`
|
|
233
|
+
unchanged. Discovery and delivery share one verb (`write`) and neither schema was in the
|
|
234
|
+
prompt. The vendor has hit the same shape in its own toolset: its changelog records
|
|
235
|
+
`web_search` becoming unreachable under `tools.xdev: true` because the mount dropped it from
|
|
236
|
+
top-level (`Tool web_search not found`, upstream #5973), fixed by pinning it via
|
|
237
|
+
`XDEV_KEEP_TOP_LEVEL` — a pin no MCP tool has.
|
|
238
|
+
|
|
239
|
+
`read xd://` reports exactly what the default hides. `[측정]` on a host with only this
|
|
240
|
+
bridge registered, **11 devices**: omp's own `ast_edit`, `debug`, `lsp`, `browser`, plus all
|
|
241
|
+
seven `entwurf_*`. So the default does not merely wrap entwurf — it wraps omp's own LSP and
|
|
242
|
+
debugger too.
|
|
243
|
+
|
|
244
|
+
**Set this on any omp host that is meant to work as a citizen:**
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
# ~/.omp/agent/config.yml
|
|
248
|
+
tools:
|
|
249
|
+
xdev: false # every enabled tool top-level — MCP is MCP again
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Nothing is disabled by that: the setting's own text is *"Disable to expose every enabled tool
|
|
253
|
+
top-level"*, and it moves tools rather than removing them. `[측정]` with `xdev: false` the
|
|
254
|
+
same plain-language request produced a first-try `mcp__entwurf_bridge_entwurf_v` function
|
|
255
|
+
call carrying a correct `intent`, the marker landed in the target mailbox, `read xd://`
|
|
256
|
+
answered `xd:// is not mounted in this session.`, and `lsp` / `debug` / `browser` /
|
|
257
|
+
`ast_edit` were all present top-level. The cost is prompt size: the system prompt's
|
|
258
|
+
non-message tokens went 18,707 → 21,834 (+3,127, +17%) on that host.
|
|
259
|
+
|
|
260
|
+
Two boundaries worth carrying:
|
|
261
|
+
|
|
262
|
+
- **`xd://` resolution devices survive the switch.** omp's plan mode and every staged-action
|
|
263
|
+
finalization write to `xd://propose` / `xd://resolve` / `xd://reject`, and its plan prompt
|
|
264
|
+
names them unconditionally — so "turn xdev off" looks like it should break planning. It
|
|
265
|
+
does not: the write dispatcher matches the resolution devices BEFORE the mount check.
|
|
266
|
+
`[측정]` with `xdev: false`, plan mode reached `write xd://propose` and the approval dialog
|
|
267
|
+
normally.
|
|
268
|
+
- **The narrow alternative keeps the wrapper.** `tools.xdevInlineDevices:
|
|
269
|
+
["mcp__entwurf_bridge_*"]` inlines only our schemas (+1,013 tokens instead of +3,127) and
|
|
270
|
+
also fixed the send in the same measurement — but the 11 devices stay off top-level, `lsp`
|
|
271
|
+
included, and the listing/delivery verb stays shared. Prefer it only on a host carrying so many MCP servers
|
|
272
|
+
that the full top-level toolset is the larger problem.
|
|
273
|
+
|
|
274
|
+
All of the above is measured against omp 18.0.0 and is a setting, not a contract: re-measure
|
|
275
|
+
at a vendor upgrade.
|
|
276
|
+
|
|
277
|
+
Registration is tools, not identity: sending needs the birth extension
|
|
278
|
+
(`install-omp-bridge`), whose sender marker is keyed to the omp host's OWN pid — omp runs
|
|
279
|
+
its extensions in-process, so the marker's owner, the host, and the MCP child's parent are
|
|
280
|
+
one pid rather than the two-process join Claude and Copilot have. An omp session is a
|
|
281
|
+
citizen only in the operator-visible TUI; task subagents borrow its tools under its garden
|
|
282
|
+
id and never receive a second address.
|
|
283
|
+
|
|
146
284
|
#### External-host skills and commands
|
|
147
285
|
|
|
148
|
-
MCP registration gives the external harness the tools; the host still needs workflow guidance. Put the Mitsein-over-MCP (cross-harness collaboration) rules in that host's instruction file or, when supported, as a host-native skill. Do not assume pi slash commands are portable across external hosts — if a workflow must work across Claude Code, Codex CLI, Antigravity, and future hosts, make it a skill or MCP tool rather than a command shortcut.
|
|
286
|
+
MCP registration gives the external harness the tools; the host still needs workflow guidance. Put the Mitsein-over-MCP (cross-harness collaboration) rules in that host's instruction file or, when supported, as a host-native skill. Do not assume pi slash commands are portable across external hosts — if a workflow must work across Claude Code, Codex CLI, Antigravity, OMP, and future hosts, make it a skill or MCP tool rather than a command shortcut.
|
|
149
287
|
|
|
150
288
|
For the maintained multi-harness setup and skill/command packaging details, see
|
|
151
289
|
[agent-config](https://github.com/junghan0611/agent-config). See also the
|
package/docs/setup-clean-host.md
CHANGED
|
@@ -15,6 +15,7 @@ only on Linux because its strict live-owner join uses `/proc`.
|
|
|
15
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 |
|
|
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, operator-installed; three units (birth/MCP/receiver) plus `tools: xdev: false` in the agent config | 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
|
|
|
@@ -185,6 +186,110 @@ scanning for that process, checks the receiver, removes inherited pi identity ca
|
|
|
185
186
|
the model/permission defaults. Birth occurs on the first prompt. `entwurf_fresh_call` uses this
|
|
186
187
|
same managed invocation and requires the birth, MCP, receiver, and visible-identity preflight.
|
|
187
188
|
|
|
189
|
+
## 4b. Optional OMP (`omp`) native citizen — accepted on one host
|
|
190
|
+
|
|
191
|
+
Three independently owned surfaces, and a boundary that is part of the instructions rather
|
|
192
|
+
than a footnote. Birth, visible identity, who-sent, the MCP hand and RECEIVE are landed, and
|
|
193
|
+
`entwurf_fresh_call` opens an omp sibling on all three public surfaces. The clause 7 LIVE
|
|
194
|
+
receipt has been taken: `smoke-omp-fresh-live` went green on 2026-08-30 (omp 18.0.0, one
|
|
195
|
+
model, one accepted run), which is what `docs/adding-a-harness.md` step 9 asks for and why
|
|
196
|
+
that smoke is wired as a release-gate MUST — the label was never allowed to move ahead of the
|
|
197
|
+
evidence, and it moved only once the evidence existed. Read the receipt itself in DELIVERY.md's
|
|
198
|
+
OMP row; what it does NOT establish is multi-host, multi-model, or repeated fresh calls in one
|
|
199
|
+
process. Open omp yourself and it is a two-way citizen either way — it sends under its own
|
|
200
|
+
garden id and a reply lands on it.
|
|
201
|
+
|
|
202
|
+
The first turn of a fresh omp sibling is a TWO-STAGE BOOTSTRAP, not a positional prompt: the
|
|
203
|
+
launcher carries `{v,target,nonce,task}` on the fixed registered flag `--entwurf-bootstrap`,
|
|
204
|
+
and the installed birth extension waits until the callback tool is actually callable, sends a
|
|
205
|
+
callback-only prompt, and delivers the operator's task only after that exact call succeeds.
|
|
206
|
+
That is a measured correction — the interactive host defers MCP discovery, so a positional
|
|
207
|
+
first turn began before the tool it named existed.
|
|
208
|
+
|
|
209
|
+
**One operator setting is load-bearing for fresh, not just for receive.** `tools: xdev: false`
|
|
210
|
+
in the omp agent config is checked by the fresh preflight BEFORE any window opens, because the
|
|
211
|
+
vendor default mounts MCP tools as `xd://` devices whose schemas never reach the prompt: a
|
|
212
|
+
sibling launched onto a default-config host would start, look healthy, and be unable to call
|
|
213
|
+
the callback tool at all. A refusal there names `omp-callback-tool-uncallable` and opens
|
|
214
|
+
nothing.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
entwurf install-omp-bridge # the birth extension, into <omp agent dir>/extensions/
|
|
218
|
+
entwurf install-omp-mcp # the omp-native entwurf-bridge server
|
|
219
|
+
entwurf install-omp-receive # the receiver extension: mailbox watch + doorbell
|
|
220
|
+
|
|
221
|
+
entwurf doctor-omp-bridge
|
|
222
|
+
entwurf doctor-omp-mcp
|
|
223
|
+
entwurf doctor-omp-receive
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Order matters only in one direction: the receiver JOINS the citizen birth mints, and
|
|
227
|
+
announces a tool the MCP hand provides. Install it without them and it will log
|
|
228
|
+
`arm-deferred`, give up after ~20s, and `doctor-omp-receive` will name the missing sibling
|
|
229
|
+
as a note rather than a fault.
|
|
230
|
+
|
|
231
|
+
**The receiver arms per session, and only for the visible TUI host.** Opening omp arms it;
|
|
232
|
+
`/new` re-arms it for the replacement citizen and retires the previous one; closing omp
|
|
233
|
+
retires it. A task subagent arms nothing. While nothing is armed, dispatch to that garden
|
|
234
|
+
id is the honest `mailbox-undeliverable` refusal — an unarmed receiver is a legible state,
|
|
235
|
+
not a broken one.
|
|
236
|
+
|
|
237
|
+
The admission work has landed and `setup` still does not compose these: the three omp
|
|
238
|
+
units stay operator-selectable installs, so run them yourself on an omp host.
|
|
239
|
+
|
|
240
|
+
Both installers resolve the omp agent directory the way omp itself does, and REFUSE rather
|
|
241
|
+
than guess when an inherited `PI_CODING_AGENT_DIR`, `PI_CONFIG_DIR` or `PI_PROFILE` makes it
|
|
242
|
+
ambiguous: omp is a pi fork and reads pi's env vocabulary, so those names no longer say which
|
|
243
|
+
harness they address. Pass `ENTWURF_OMP_AGENT_DIR` if you genuinely mean a non-default one.
|
|
244
|
+
|
|
245
|
+
Two things the installers deliberately will NOT do. They never adopt an artifact already
|
|
246
|
+
sitting at their path without entwurf's own ownership state — a directory that merely looks
|
|
247
|
+
like our unit could be yours, and adopting it would overwrite it with no way back — so a
|
|
248
|
+
no-state path is a named refusal you resolve by hand. And the MCP writer's target is exactly
|
|
249
|
+
`<resolved omp agent dir>/mcp.json`; there is no path override, so it can never be aimed at
|
|
250
|
+
another tool's config.
|
|
251
|
+
|
|
252
|
+
**Where an omp citizen's garden artifacts live.** Under `$HOME/.pi/agent/meta-*`, the same
|
|
253
|
+
garden every other citizen uses — and that stays true under `omp --profile work`. omp is a pi
|
|
254
|
+
fork, so the vendor exports `PI_CODING_AGENT_DIR` for every named profile; for entwurf that
|
|
255
|
+
name means pi's persistence root, so honouring it here would put an omp session's record in a
|
|
256
|
+
different store (or in a pi sandbox). For backend omp it is read as the VENDOR's agent dir
|
|
257
|
+
only, and never as a garden root. Its presence on a live omp is normal and is not a fault.
|
|
258
|
+
The four `ENTWURF_META_*` variables remain the way to relocate the garden roots, and for
|
|
259
|
+
backend omp each one must be **absolute or `~`-rooted** (`~` or `~/…`). A relative value is
|
|
260
|
+
refused by name rather than resolved: it would resolve against each process's own working
|
|
261
|
+
directory, and the omp extension and `doctor-omp-bridge` do not share one — the doctor would
|
|
262
|
+
then report on a directory the extension never writes to. A refused value mints nothing and
|
|
263
|
+
turns the doctor's runtime axis red.
|
|
264
|
+
|
|
265
|
+
**One vendor setting is required, and the default is wrong for a citizen.** omp's
|
|
266
|
+
`tools.xdev` (default ON) mounts MCP tools as `xd://<tool>` devices and removes them from the
|
|
267
|
+
model's top-level toolset, so `entwurf_v2` is reached by WRITING JSON to a virtual file rather
|
|
268
|
+
than by calling a tool — and with `tools.xdevDocs` at its `builtins` default its schema is not
|
|
269
|
+
in the prompt at all. On that default a plain "send this to garden id X" was measured to list
|
|
270
|
+
peers and then CLAIM the send without ever dispatching. Put this in `~/.omp/agent/config.yml`
|
|
271
|
+
and restart the session:
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
tools:
|
|
275
|
+
xdev: false
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
It disables nothing — it exposes every enabled tool top-level, omp's own `lsp`, `debug`,
|
|
279
|
+
`browser` and `ast_edit` included, which the default was hiding as well. Plan mode and staged
|
|
280
|
+
`xd://resolve` / `xd://propose` finalization keep working. The rationale, the numbers and the
|
|
281
|
+
narrower `tools.xdevInlineDevices` alternative are in
|
|
282
|
+
[`external-mcp-host.md`](./external-mcp-host.md).
|
|
283
|
+
|
|
284
|
+
Birth happens when the TUI OPENS (not on the first prompt, unlike Copilot), and the garden id
|
|
285
|
+
appears on omp's status line as `🪛 <garden-id> omp`. `/new`, fork and in-TUI resume mint the
|
|
286
|
+
replacement session's own record. Task subagents of that session are refused by design — they
|
|
287
|
+
borrow the host's tools under the host's garden id and never get a second address.
|
|
288
|
+
|
|
289
|
+
The MCP entry deliberately uses the same server key as any Claude Code import so that it
|
|
290
|
+
SHADOWS it; see [`external-mcp-host.md`](./external-mcp-host.md) for why that key is pinned and
|
|
291
|
+
why `disabledServers` is never the way to hide an import.
|
|
292
|
+
|
|
188
293
|
## 5. Optional Antigravity native citizen
|
|
189
294
|
|
|
190
295
|
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 (보정①).
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
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)
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
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
|
|
574
|
-
"BEFORE any window opens if this host lacks the Copilot birth, MCP, receiver or
|
|
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
|
|
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
|
}
|
|
@@ -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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
//
|
|
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
|