@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.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +299 -0
- package/DELIVERY.md +4 -3
- package/README.md +99 -67
- package/VERIFY.md +13 -7
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +147 -9
- package/docs/setup-clean-host.md +221 -16
- 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 +14 -4
- 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 +1041 -174
- package/scripts/agy-bridge.sh +4 -2
- package/scripts/check-copilot-birth-hook.ts +700 -23
- package/scripts/check-entwurf-self-address.ts +31 -0
- package/scripts/check-fresh-cut-gate.sh +12 -4
- package/scripts/check-gate-qualification.ts +8 -2
- 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/check-setup-qualification.sh +126 -0
- package/scripts/copilot-bridge-doctor.sh +104 -71
- package/scripts/copilot-bridge-install.sh +180 -20
- package/scripts/copilot-bridge-oracle.sh +223 -0
- package/scripts/copilot-bridge-uninstall.sh +137 -0
- package/scripts/dev-bin.sh +50 -9
- package/scripts/fake-copilot-vendor.sh +67 -0
- package/scripts/inventory-verification-surface.ts +1 -1
- package/scripts/meta-bridge-hook-log.sh +9 -1
- package/scripts/mutants/copilot-birth.json +96 -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/pi-package-ownership.json +92 -0
- package/scripts/mutants/self-address.json +34 -0
- package/scripts/mutants/setup-verdict.json +148 -0
- package/scripts/mutants/source-install.json +32 -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/pi_settings_io.py +22 -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/register-pi-package.py +525 -7
- package/scripts/register-pi-provider.py +162 -12
- package/scripts/smoke-agy-install-state.sh +86 -30
- package/scripts/smoke-agy-statusline-state.sh +9 -6
- 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/smoke-setup-verdict.sh +291 -0
- package/scripts/smoke-user-scope-citizen.sh +388 -1
- package/scripts/tsconfig.json +2 -0
|
@@ -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
|
|
@@ -76,7 +76,7 @@ export class MetaRecordError extends Error {
|
|
|
76
76
|
* every write, including its own. `./run.sh doctor-meta-bridge` already flags a
|
|
77
77
|
* stale deployed writer — run it after the change.
|
|
78
78
|
*/
|
|
79
|
-
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot"];
|
|
79
|
+
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp"];
|
|
80
80
|
export const META_BACKEND_DESCRIPTORS = {
|
|
81
81
|
"claude-code": {
|
|
82
82
|
backend: "claude-code",
|
|
@@ -137,6 +137,58 @@ export const META_BACKEND_DESCRIPTORS = {
|
|
|
137
137
|
deliveryLevel: "D6",
|
|
138
138
|
nativeIdLabel: "sessionId",
|
|
139
139
|
},
|
|
140
|
+
// SELF-FETCH citizen since #87 bundle B (oh-my-pi v18.0.0). An omp "hook" is an
|
|
141
|
+
// in-process EXTENSION (`--hook` aliases `--extension`), so BOTH units run inside the
|
|
142
|
+
// omp host: the birth unit mints on `session_start`/`session_switch`, and the receiver
|
|
143
|
+
// unit arms a mailbox watch in the same process and rings the doorbell.
|
|
144
|
+
// self-fetch The doorbell announces and the model drains its own inbox with
|
|
145
|
+
// `entwurf_inbox_read` — that read is the receipt, and the notice never
|
|
146
|
+
// carries the body. `[LIVE 2026-08-30, oracle, omp 18.0.0]` the vendor's
|
|
147
|
+
// own wake surface was measured rather than inherited:
|
|
148
|
+
// `pi.sendUserMessage(text,{deliverAs:"user"})` (on the FACTORY object, not
|
|
149
|
+
// the event ctx) called on an IDLE tui session with zero typing started a
|
|
150
|
+
// real turn — `agent_start` +31ms, `turn_start` +64ms, `turn_end` +2.45s,
|
|
151
|
+
// model reply in the transcript. It was `direct-inject` while no process
|
|
152
|
+
// held a watch; that label was the honest "not a drainable mailbox" bucket,
|
|
153
|
+
// and it stopped being true when the receiver unit landed.
|
|
154
|
+
// Replyability is still a fact about a LIVE marker, never a constant of this
|
|
155
|
+
// backend: with no armed receiver every dispatch is refused
|
|
156
|
+
// `mailbox-undeliverable`, which is exactly what an omp host with the birth
|
|
157
|
+
// unit and no receiver unit still gets.
|
|
158
|
+
// D6 the PRODUCT grade, earned by the LIVE roundtrip of 2026-08-30 on oracle
|
|
159
|
+
// (omp 18.0.0). The code landing was NOT the evidence — this number moved
|
|
160
|
+
// when the receipt did (`adding-a-harness.md` step 8(c)):
|
|
161
|
+
// garden `20260830T140819-116f6a`, `lastEnqueuedAt 05:08:20.555Z` /
|
|
162
|
+
// `lastReadAt 05:08:23.958Z`, doorbell rung on an idle session with zero
|
|
163
|
+
// typing, and the citizen's OWN transcript carrying
|
|
164
|
+
// `mcp__entwurf_bridge_entwurf_inbox_read` for its own garden id — the join
|
|
165
|
+
// that makes the drain a fact about THAT session rather than about the clock.
|
|
166
|
+
// D3 is PROVEN here rather than pending: with two live omp citizens armed
|
|
167
|
+
// (pids 3154765 / 3154835) a single addressed enqueue rang exactly one
|
|
168
|
+
// doorbell, and the sibling persisted no transcript and kept an empty
|
|
169
|
+
// mailbox. That is the cell the Copilot row still lists as PENDING.
|
|
170
|
+
// D7 is PARTIAL, deliberately: `lastReadAt` is observable without scraping,
|
|
171
|
+
// but the reply itself is only visible in the transcript, and no completion
|
|
172
|
+
// taxonomy or long-haul operation was measured.
|
|
173
|
+
// D8 is PARTIAL: fresh-only ring (dedupe), `/new` unarm, watch-error unarm,
|
|
174
|
+
// vanished-signal unarm and the identity-guarded teardown are implemented and
|
|
175
|
+
// pinned hermetically by `check-omp-receive-arm`; ordering under load, loop
|
|
176
|
+
// guards and crash recovery are not measured.
|
|
177
|
+
// Replyability stays a fact about a LIVE marker: with no armed receiver every
|
|
178
|
+
// dispatch is still refused `mailbox-undeliverable` — re-proved on the same
|
|
179
|
+
// day against a garden id whose session had just been replaced by `/new`.
|
|
180
|
+
// sessionId the native join key: `ReadonlySessionManager.getSessionId()`
|
|
181
|
+
// (`session-manager.ts:1946-1948`), a UUIDv7 minted at
|
|
182
|
+
// `mintSessionId()` `:95-97` — NOT the transcript filename, which is
|
|
183
|
+
// `<iso>_<uuidv7>.jsonl` (`:1134-1137`). Measured LIVE 2026-08-27:
|
|
184
|
+
// host `01a042da-537a-7770-a275-7b8162eecca4`
|
|
185
|
+
// (`scripts/raw-omp-measure/README.md` M1).
|
|
186
|
+
omp: {
|
|
187
|
+
backend: "omp",
|
|
188
|
+
wakeMode: "self-fetch",
|
|
189
|
+
deliveryLevel: "D6",
|
|
190
|
+
nativeIdLabel: "sessionId",
|
|
191
|
+
},
|
|
140
192
|
};
|
|
141
193
|
// ---------------------------------------------------------------------------
|
|
142
194
|
// Validation helpers (crash, don't warn)
|
|
@@ -209,7 +261,7 @@ function isoNow(now) {
|
|
|
209
261
|
/** The one live identity schema number. */
|
|
210
262
|
export const META_SCHEMA_VERSION_V3 = 3;
|
|
211
263
|
/** Every backend admitted by the one V3 record-citizen schema. */
|
|
212
|
-
export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "pi"];
|
|
264
|
+
export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp", "pi"];
|
|
213
265
|
export function requireCitizenBackend(value) {
|
|
214
266
|
if (typeof value !== "string" || !META_CITIZEN_BACKENDS.includes(value)) {
|
|
215
267
|
throw new MetaRecordError(`meta-record "backend" must be one of ${META_CITIZEN_BACKENDS.join(" | ")} (got ${describe(value)}).`);
|
|
@@ -1015,13 +1067,16 @@ export function decideUpsert(existing, input, now = new Date()) {
|
|
|
1015
1067
|
// core. Only node builtins beyond the pure layer, so the deterministic gate
|
|
1016
1068
|
// stays strip-types clean (see module header for why this is not a sibling file).
|
|
1017
1069
|
// ---------------------------------------------------------------------------
|
|
1018
|
-
function
|
|
1070
|
+
function expandTildeIn(p, home) {
|
|
1019
1071
|
if (p === "~")
|
|
1020
|
-
return
|
|
1072
|
+
return home;
|
|
1021
1073
|
if (p.startsWith("~/"))
|
|
1022
|
-
return path.join(
|
|
1074
|
+
return path.join(home, p.slice(2));
|
|
1023
1075
|
return p;
|
|
1024
1076
|
}
|
|
1077
|
+
function expandTilde(p) {
|
|
1078
|
+
return expandTildeIn(p, os.homedir());
|
|
1079
|
+
}
|
|
1025
1080
|
/**
|
|
1026
1081
|
* The pi agent dir — the persistence root pi owns. `PI_CODING_AGENT_DIR` lets an
|
|
1027
1082
|
* isolated install / test relocate it (symmetric with how pi's own sessions
|
|
@@ -1101,6 +1156,118 @@ export function defaultMetaReceiversDir() {
|
|
|
1101
1156
|
return path.resolve(expandTilde(process.env.ENTWURF_META_RECEIVERS_DIR));
|
|
1102
1157
|
return path.join(piAgentDir(), "meta-receivers");
|
|
1103
1158
|
}
|
|
1159
|
+
/**
|
|
1160
|
+
* The exact provenance label the omp-native MCP entry carries
|
|
1161
|
+
* (`scripts/omp-mcp-config.py` `EXTERNAL_AGENT_ID`). It is what lets a bridge CHILD know
|
|
1162
|
+
* it is an OMP child — the same string both sides pin, never re-spelled.
|
|
1163
|
+
*/
|
|
1164
|
+
export const OMP_BRIDGE_PROVENANCE_LABEL = "external-mcp/omp";
|
|
1165
|
+
/** `HOME` as the process that owns this env sees it; `os.homedir()` returns exactly this
|
|
1166
|
+
* on POSIX when HOME is set, so a real process and a composed child env agree. */
|
|
1167
|
+
function metaRootHome(env) {
|
|
1168
|
+
const home = env.HOME;
|
|
1169
|
+
return home !== undefined && home.length > 0 ? home : os.homedir();
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* THE shared OMP root policy — one pure function, two consumers (the in-process birth
|
|
1173
|
+
* extension and the OMP-labeled bridge child). Agreement is by CONSTRUCTION, not by two
|
|
1174
|
+
* places computing the same thing: both read this leaf, and both see the same HOME and the
|
|
1175
|
+
* same four overrides because the vendor composes the child env as parent + entry env
|
|
1176
|
+
* (`oh-my-pi` `packages/coding-agent/src/mcp/transports/stdio.ts:575-607`) and that entry
|
|
1177
|
+
* env is provenance-only.
|
|
1178
|
+
*/
|
|
1179
|
+
export class MetaRootPolicyError extends Error {
|
|
1180
|
+
constructor(message) {
|
|
1181
|
+
super(message);
|
|
1182
|
+
this.name = "MetaRootPolicyError";
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* The unambiguous base the OMP bundle hangs off when no override is set. Separate from
|
|
1187
|
+
* {@link ompMetaRoots} because it must NEVER throw: the hook log is a diagnostic, and a
|
|
1188
|
+
* policy refusal still has to be visible somewhere.
|
|
1189
|
+
*/
|
|
1190
|
+
export function ompMetaRootBase(env = process.env, home = metaRootHome(env)) {
|
|
1191
|
+
// NEVER `piAgentDir()` here: for backend omp that variable is the VENDOR's agent dir.
|
|
1192
|
+
return path.join(home, ".pi", "agent");
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* THE OVERRIDE GRAMMAR IS NARROW ON PURPOSE: absolute, or `~` / `~/…`. Anything else —
|
|
1196
|
+
* a relative path, or `~user` — is a NAMED REFUSAL, not a resolution.
|
|
1197
|
+
*
|
|
1198
|
+
* Why fail closed rather than resolve: this bundle has to mean the same thing in two
|
|
1199
|
+
* processes that do not share a working directory. `path.resolve` on a relative value
|
|
1200
|
+
* silently makes CWD an authority, and the OMP extension's cwd is whatever the operator
|
|
1201
|
+
* launched omp from while the doctor's is the repository (`run_ts` cd's there). `[측정]`
|
|
1202
|
+
* the same `ENTWURF_META_SESSIONS_DIR=relative-records` resolved to two different stores
|
|
1203
|
+
* that way, so the doctor could report NOT-YET off an empty directory while the extension's
|
|
1204
|
+
* real store held records (#87 A2, Terra review). Refusing is what keeps cwd from becoming
|
|
1205
|
+
* a garden-root carrier. `~` is allowed because it expands from HOME, which both halves
|
|
1206
|
+
* share by construction.
|
|
1207
|
+
*
|
|
1208
|
+
* The refusal is shared: extension and OMP-labeled bridge child both reach it through this
|
|
1209
|
+
* one leaf, so neither can proceed on a value the other would read differently.
|
|
1210
|
+
*/
|
|
1211
|
+
export function ompMetaRoots(env = process.env, home = metaRootHome(env)) {
|
|
1212
|
+
const base = ompMetaRootBase(env, home);
|
|
1213
|
+
const surface = (key, leaf) => {
|
|
1214
|
+
const override = env[key];
|
|
1215
|
+
if (override === undefined || override === "")
|
|
1216
|
+
return path.join(base, leaf);
|
|
1217
|
+
if (override === "~" || override.startsWith("~/"))
|
|
1218
|
+
return path.resolve(expandTildeIn(override, home));
|
|
1219
|
+
if (path.isAbsolute(override))
|
|
1220
|
+
return path.resolve(override);
|
|
1221
|
+
throw new MetaRootPolicyError(`omp meta-root policy refuses ${key}=${JSON.stringify(override)}: a garden root must be absolute or ~-rooted ` +
|
|
1222
|
+
"(`~` or `~/…`). A relative value would resolve against each process's own working directory, and the omp " +
|
|
1223
|
+
"extension and its doctor do not share one — set an absolute path, or unset it to use the default under HOME.");
|
|
1224
|
+
};
|
|
1225
|
+
return {
|
|
1226
|
+
sessionsDir: surface("ENTWURF_META_SESSIONS_DIR", "meta-sessions"),
|
|
1227
|
+
mailboxDir: surface("ENTWURF_META_MAILBOX_DIR", "meta-mailbox"),
|
|
1228
|
+
sendersDir: surface("ENTWURF_META_SENDERS_DIR", "meta-senders"),
|
|
1229
|
+
receiversDir: surface("ENTWURF_META_RECEIVERS_DIR", "meta-receivers"),
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* The bridge CHILD half of the same policy, applied to that child's own environment.
|
|
1234
|
+
*
|
|
1235
|
+
* A bridge child selects this by the exact provenance label its managed entry carries —
|
|
1236
|
+
* no probing, no guessing, and no effect on any other harness's child. It then does two
|
|
1237
|
+
* things, in this order and before any lazy default-root consumer has run:
|
|
1238
|
+
*
|
|
1239
|
+
* 1. removes the foreign `PI_CODING_AGENT_DIR` from THIS PROCESS only. The OMP HOST
|
|
1240
|
+
* keeps it — there it is the vendor's own agent dir and deleting it would rewrite
|
|
1241
|
+
* vendor path semantics — but the child has no vendor lookup left to do.
|
|
1242
|
+
* 2. pins the four `ENTWURF_META_*_DIR` overrides to this leaf's answer, so every
|
|
1243
|
+
* `defaultMeta*Dir()` consumer in the bridge reads the OMP bundle literally rather
|
|
1244
|
+
* than recomputing something that merely happens to match. Idempotent: an override
|
|
1245
|
+
* the operator already set was honoured by the leaf and is written back unchanged.
|
|
1246
|
+
*
|
|
1247
|
+
* These four are entwurf's OWN documented override vocabulary, set in our own process —
|
|
1248
|
+
* not a new carrier, not a record field, not a marker.
|
|
1249
|
+
*
|
|
1250
|
+
* The in-process birth extension cannot use this and must not try: it runs INSIDE the omp
|
|
1251
|
+
* host, so mutating that process's env is exactly the thing forbidden above. It passes
|
|
1252
|
+
* explicit directories from {@link ompMetaRoots} instead.
|
|
1253
|
+
*/
|
|
1254
|
+
export function applyOmpBridgeChildRootPolicy(env = process.env, home) {
|
|
1255
|
+
// EXACT equality on the RAW label (#87 A3). Trimming was a courtesy that let a
|
|
1256
|
+
// whitespace-drifted entry — one `doctor-omp-mcp` and the writer both call foreign —
|
|
1257
|
+
// still select OMP root mutation in its child. The writer emits the literal and the
|
|
1258
|
+
// doctor compares the literal; this compares the literal too, so all three agree on
|
|
1259
|
+
// what "our entry" means.
|
|
1260
|
+
if (env.ENTWURF_BRIDGE_EXTERNAL_AGENT_ID !== OMP_BRIDGE_PROVENANCE_LABEL) {
|
|
1261
|
+
return { applied: false, roots: null };
|
|
1262
|
+
}
|
|
1263
|
+
const roots = ompMetaRoots(env, home ?? metaRootHome(env));
|
|
1264
|
+
delete env.PI_CODING_AGENT_DIR;
|
|
1265
|
+
env.ENTWURF_META_SESSIONS_DIR = roots.sessionsDir;
|
|
1266
|
+
env.ENTWURF_META_MAILBOX_DIR = roots.mailboxDir;
|
|
1267
|
+
env.ENTWURF_META_SENDERS_DIR = roots.sendersDir;
|
|
1268
|
+
env.ENTWURF_META_RECEIVERS_DIR = roots.receiversDir;
|
|
1269
|
+
return { applied: true, roots };
|
|
1270
|
+
}
|
|
1104
1271
|
/**
|
|
1105
1272
|
* A boot-unique identity for a live process: pid is reused, but pid + start-time
|
|
1106
1273
|
* is unique within a boot. Linux reads `/proc/<pid>/stat` field 22 (starttime in
|