@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
|
@@ -81,7 +81,7 @@ export class MetaRecordError extends Error {
|
|
|
81
81
|
* every write, including its own. `./run.sh doctor-meta-bridge` already flags a
|
|
82
82
|
* stale deployed writer — run it after the change.
|
|
83
83
|
*/
|
|
84
|
-
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot"] as const;
|
|
84
|
+
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp"] as const;
|
|
85
85
|
export type MetaBackend = (typeof META_BACKENDS)[number];
|
|
86
86
|
|
|
87
87
|
/**
|
|
@@ -170,6 +170,58 @@ export const META_BACKEND_DESCRIPTORS: Record<MetaBackend, MetaBackendDescriptor
|
|
|
170
170
|
deliveryLevel: "D6",
|
|
171
171
|
nativeIdLabel: "sessionId",
|
|
172
172
|
},
|
|
173
|
+
// SELF-FETCH citizen since #87 bundle B (oh-my-pi v18.0.0). An omp "hook" is an
|
|
174
|
+
// in-process EXTENSION (`--hook` aliases `--extension`), so BOTH units run inside the
|
|
175
|
+
// omp host: the birth unit mints on `session_start`/`session_switch`, and the receiver
|
|
176
|
+
// unit arms a mailbox watch in the same process and rings the doorbell.
|
|
177
|
+
// self-fetch The doorbell announces and the model drains its own inbox with
|
|
178
|
+
// `entwurf_inbox_read` — that read is the receipt, and the notice never
|
|
179
|
+
// carries the body. `[LIVE 2026-08-30, oracle, omp 18.0.0]` the vendor's
|
|
180
|
+
// own wake surface was measured rather than inherited:
|
|
181
|
+
// `pi.sendUserMessage(text,{deliverAs:"user"})` (on the FACTORY object, not
|
|
182
|
+
// the event ctx) called on an IDLE tui session with zero typing started a
|
|
183
|
+
// real turn — `agent_start` +31ms, `turn_start` +64ms, `turn_end` +2.45s,
|
|
184
|
+
// model reply in the transcript. It was `direct-inject` while no process
|
|
185
|
+
// held a watch; that label was the honest "not a drainable mailbox" bucket,
|
|
186
|
+
// and it stopped being true when the receiver unit landed.
|
|
187
|
+
// Replyability is still a fact about a LIVE marker, never a constant of this
|
|
188
|
+
// backend: with no armed receiver every dispatch is refused
|
|
189
|
+
// `mailbox-undeliverable`, which is exactly what an omp host with the birth
|
|
190
|
+
// unit and no receiver unit still gets.
|
|
191
|
+
// D6 the PRODUCT grade, earned by the LIVE roundtrip of 2026-08-30 on oracle
|
|
192
|
+
// (omp 18.0.0). The code landing was NOT the evidence — this number moved
|
|
193
|
+
// when the receipt did (`adding-a-harness.md` step 8(c)):
|
|
194
|
+
// garden `20260830T140819-116f6a`, `lastEnqueuedAt 05:08:20.555Z` /
|
|
195
|
+
// `lastReadAt 05:08:23.958Z`, doorbell rung on an idle session with zero
|
|
196
|
+
// typing, and the citizen's OWN transcript carrying
|
|
197
|
+
// `mcp__entwurf_bridge_entwurf_inbox_read` for its own garden id — the join
|
|
198
|
+
// that makes the drain a fact about THAT session rather than about the clock.
|
|
199
|
+
// D3 is PROVEN here rather than pending: with two live omp citizens armed
|
|
200
|
+
// (pids 3154765 / 3154835) a single addressed enqueue rang exactly one
|
|
201
|
+
// doorbell, and the sibling persisted no transcript and kept an empty
|
|
202
|
+
// mailbox. That is the cell the Copilot row still lists as PENDING.
|
|
203
|
+
// D7 is PARTIAL, deliberately: `lastReadAt` is observable without scraping,
|
|
204
|
+
// but the reply itself is only visible in the transcript, and no completion
|
|
205
|
+
// taxonomy or long-haul operation was measured.
|
|
206
|
+
// D8 is PARTIAL: fresh-only ring (dedupe), `/new` unarm, watch-error unarm,
|
|
207
|
+
// vanished-signal unarm and the identity-guarded teardown are implemented and
|
|
208
|
+
// pinned hermetically by `check-omp-receive-arm`; ordering under load, loop
|
|
209
|
+
// guards and crash recovery are not measured.
|
|
210
|
+
// Replyability stays a fact about a LIVE marker: with no armed receiver every
|
|
211
|
+
// dispatch is still refused `mailbox-undeliverable` — re-proved on the same
|
|
212
|
+
// day against a garden id whose session had just been replaced by `/new`.
|
|
213
|
+
// sessionId the native join key: `ReadonlySessionManager.getSessionId()`
|
|
214
|
+
// (`session-manager.ts:1946-1948`), a UUIDv7 minted at
|
|
215
|
+
// `mintSessionId()` `:95-97` — NOT the transcript filename, which is
|
|
216
|
+
// `<iso>_<uuidv7>.jsonl` (`:1134-1137`). Measured LIVE 2026-08-27:
|
|
217
|
+
// host `01a042da-537a-7770-a275-7b8162eecca4`
|
|
218
|
+
// (`scripts/raw-omp-measure/README.md` M1).
|
|
219
|
+
omp: {
|
|
220
|
+
backend: "omp",
|
|
221
|
+
wakeMode: "self-fetch",
|
|
222
|
+
deliveryLevel: "D6",
|
|
223
|
+
nativeIdLabel: "sessionId",
|
|
224
|
+
},
|
|
173
225
|
};
|
|
174
226
|
|
|
175
227
|
// ---------------------------------------------------------------------------
|
|
@@ -253,7 +305,7 @@ function isoNow(now: Date): string {
|
|
|
253
305
|
export const META_SCHEMA_VERSION_V3 = 3 as const;
|
|
254
306
|
|
|
255
307
|
/** Every backend admitted by the one V3 record-citizen schema. */
|
|
256
|
-
export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "pi"] as const;
|
|
308
|
+
export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp", "pi"] as const;
|
|
257
309
|
export type MetaCitizenBackend = (typeof META_CITIZEN_BACKENDS)[number];
|
|
258
310
|
|
|
259
311
|
/**
|
|
@@ -1234,12 +1286,16 @@ export function decideUpsert(
|
|
|
1234
1286
|
// stays strip-types clean (see module header for why this is not a sibling file).
|
|
1235
1287
|
// ---------------------------------------------------------------------------
|
|
1236
1288
|
|
|
1237
|
-
function
|
|
1238
|
-
if (p === "~") return
|
|
1239
|
-
if (p.startsWith("~/")) return path.join(
|
|
1289
|
+
function expandTildeIn(p: string, home: string): string {
|
|
1290
|
+
if (p === "~") return home;
|
|
1291
|
+
if (p.startsWith("~/")) return path.join(home, p.slice(2));
|
|
1240
1292
|
return p;
|
|
1241
1293
|
}
|
|
1242
1294
|
|
|
1295
|
+
function expandTilde(p: string): string {
|
|
1296
|
+
return expandTildeIn(p, os.homedir());
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1243
1299
|
/**
|
|
1244
1300
|
* The pi agent dir — the persistence root pi owns. `PI_CODING_AGENT_DIR` lets an
|
|
1245
1301
|
* isolated install / test relocate it (symmetric with how pi's own sessions
|
|
@@ -1320,6 +1376,164 @@ export function defaultMetaReceiversDir(): string {
|
|
|
1320
1376
|
return path.join(piAgentDir(), "meta-receivers");
|
|
1321
1377
|
}
|
|
1322
1378
|
|
|
1379
|
+
// ---------------------------------------------------------------------------
|
|
1380
|
+
// OMP meta-root policy (#87 B1) — ADDITIVE, and only for backend `omp`.
|
|
1381
|
+
//
|
|
1382
|
+
// THE COLLISION THIS CLOSES. `PI_CODING_AGENT_DIR` has two owners. To entwurf it
|
|
1383
|
+
// means "pi's persistence root", and `piAgentDir()` above derives all four garden
|
|
1384
|
+
// roots from it. To the OMP vendor it means "my agent dir": `setProfile` exports it
|
|
1385
|
+
// in-process for every named profile (`oh-my-pi` v18.0.0 `packages/utils/src/dirs.ts:452-473`,
|
|
1386
|
+
// reached from `cli.ts:342-360`), so a plain `omp --profile work` sets it. An OMP
|
|
1387
|
+
// birth that resolved its roots through `piAgentDir()` would therefore mint its record
|
|
1388
|
+
// and marker into a DIFFERENT garden store — and if the value came from a pi sandbox,
|
|
1389
|
+
// into that sandbox. Record-authority violation (Hard Rules 2 and 7), reachable on an
|
|
1390
|
+
// ordinary vendor workflow rather than an exotic one.
|
|
1391
|
+
//
|
|
1392
|
+
// WHY A BUNDLE OF FOUR AND NOT TWO. Splitting mailbox from receivers is the dangerous
|
|
1393
|
+
// cell: dispatch can trust an armed receiver marker in one root and enqueue into the
|
|
1394
|
+
// mailbox of the other while the real watcher drains the first — FALSE DELIVERABILITY.
|
|
1395
|
+
// So the four move together or not at all. Birth (bundle A) writes only sessions and
|
|
1396
|
+
// senders; receive (bundle B) consumes mailbox and receivers from this same object.
|
|
1397
|
+
//
|
|
1398
|
+
// WHAT IT DELIBERATELY DOES NOT DO. It does not change `piAgentDir()` or any
|
|
1399
|
+
// `defaultMeta*Dir()` — every existing backend keeps byte-identical behaviour, and pi's
|
|
1400
|
+
// own sandboxing still relocates through `PI_CODING_AGENT_DIR`. It honours the four
|
|
1401
|
+
// entwurf-owned `ENTWURF_META_*_DIR` overrides INDEPENDENTLY, so Hard Rule 12 sandboxing
|
|
1402
|
+
// stays usable. It bakes no absolute path into any record, marker, install-state or MCP
|
|
1403
|
+
// entry, and it introduces no new authority carrier: the roots are computed, never stored.
|
|
1404
|
+
// ---------------------------------------------------------------------------
|
|
1405
|
+
|
|
1406
|
+
/** The four Entwurf-owned meta roots, resolved as ONE indivisible bundle. */
|
|
1407
|
+
export interface MetaRootBundle {
|
|
1408
|
+
sessionsDir: string;
|
|
1409
|
+
mailboxDir: string;
|
|
1410
|
+
sendersDir: string;
|
|
1411
|
+
receiversDir: string;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/** The env shape these pure resolvers read. `process.env` satisfies it. */
|
|
1415
|
+
export type MetaRootEnv = Record<string, string | undefined>;
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* The exact provenance label the omp-native MCP entry carries
|
|
1419
|
+
* (`scripts/omp-mcp-config.py` `EXTERNAL_AGENT_ID`). It is what lets a bridge CHILD know
|
|
1420
|
+
* it is an OMP child — the same string both sides pin, never re-spelled.
|
|
1421
|
+
*/
|
|
1422
|
+
export const OMP_BRIDGE_PROVENANCE_LABEL = "external-mcp/omp";
|
|
1423
|
+
|
|
1424
|
+
/** `HOME` as the process that owns this env sees it; `os.homedir()` returns exactly this
|
|
1425
|
+
* on POSIX when HOME is set, so a real process and a composed child env agree. */
|
|
1426
|
+
function metaRootHome(env: MetaRootEnv): string {
|
|
1427
|
+
const home = env.HOME;
|
|
1428
|
+
return home !== undefined && home.length > 0 ? home : os.homedir();
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* THE shared OMP root policy — one pure function, two consumers (the in-process birth
|
|
1433
|
+
* extension and the OMP-labeled bridge child). Agreement is by CONSTRUCTION, not by two
|
|
1434
|
+
* places computing the same thing: both read this leaf, and both see the same HOME and the
|
|
1435
|
+
* same four overrides because the vendor composes the child env as parent + entry env
|
|
1436
|
+
* (`oh-my-pi` `packages/coding-agent/src/mcp/transports/stdio.ts:575-607`) and that entry
|
|
1437
|
+
* env is provenance-only.
|
|
1438
|
+
*/
|
|
1439
|
+
export class MetaRootPolicyError extends Error {
|
|
1440
|
+
constructor(message: string) {
|
|
1441
|
+
super(message);
|
|
1442
|
+
this.name = "MetaRootPolicyError";
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* The unambiguous base the OMP bundle hangs off when no override is set. Separate from
|
|
1448
|
+
* {@link ompMetaRoots} because it must NEVER throw: the hook log is a diagnostic, and a
|
|
1449
|
+
* policy refusal still has to be visible somewhere.
|
|
1450
|
+
*/
|
|
1451
|
+
export function ompMetaRootBase(env: MetaRootEnv = process.env, home: string = metaRootHome(env)): string {
|
|
1452
|
+
// NEVER `piAgentDir()` here: for backend omp that variable is the VENDOR's agent dir.
|
|
1453
|
+
return path.join(home, ".pi", "agent");
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* THE OVERRIDE GRAMMAR IS NARROW ON PURPOSE: absolute, or `~` / `~/…`. Anything else —
|
|
1458
|
+
* a relative path, or `~user` — is a NAMED REFUSAL, not a resolution.
|
|
1459
|
+
*
|
|
1460
|
+
* Why fail closed rather than resolve: this bundle has to mean the same thing in two
|
|
1461
|
+
* processes that do not share a working directory. `path.resolve` on a relative value
|
|
1462
|
+
* silently makes CWD an authority, and the OMP extension's cwd is whatever the operator
|
|
1463
|
+
* launched omp from while the doctor's is the repository (`run_ts` cd's there). `[측정]`
|
|
1464
|
+
* the same `ENTWURF_META_SESSIONS_DIR=relative-records` resolved to two different stores
|
|
1465
|
+
* that way, so the doctor could report NOT-YET off an empty directory while the extension's
|
|
1466
|
+
* real store held records (#87 A2, Terra review). Refusing is what keeps cwd from becoming
|
|
1467
|
+
* a garden-root carrier. `~` is allowed because it expands from HOME, which both halves
|
|
1468
|
+
* share by construction.
|
|
1469
|
+
*
|
|
1470
|
+
* The refusal is shared: extension and OMP-labeled bridge child both reach it through this
|
|
1471
|
+
* one leaf, so neither can proceed on a value the other would read differently.
|
|
1472
|
+
*/
|
|
1473
|
+
export function ompMetaRoots(env: MetaRootEnv = process.env, home: string = metaRootHome(env)): MetaRootBundle {
|
|
1474
|
+
const base = ompMetaRootBase(env, home);
|
|
1475
|
+
const surface = (key: string, leaf: string): string => {
|
|
1476
|
+
const override = env[key];
|
|
1477
|
+
if (override === undefined || override === "") return path.join(base, leaf);
|
|
1478
|
+
if (override === "~" || override.startsWith("~/")) return path.resolve(expandTildeIn(override, home));
|
|
1479
|
+
if (path.isAbsolute(override)) return path.resolve(override);
|
|
1480
|
+
throw new MetaRootPolicyError(
|
|
1481
|
+
`omp meta-root policy refuses ${key}=${JSON.stringify(override)}: a garden root must be absolute or ~-rooted ` +
|
|
1482
|
+
"(`~` or `~/…`). A relative value would resolve against each process's own working directory, and the omp " +
|
|
1483
|
+
"extension and its doctor do not share one — set an absolute path, or unset it to use the default under HOME.",
|
|
1484
|
+
);
|
|
1485
|
+
};
|
|
1486
|
+
return {
|
|
1487
|
+
sessionsDir: surface("ENTWURF_META_SESSIONS_DIR", "meta-sessions"),
|
|
1488
|
+
mailboxDir: surface("ENTWURF_META_MAILBOX_DIR", "meta-mailbox"),
|
|
1489
|
+
sendersDir: surface("ENTWURF_META_SENDERS_DIR", "meta-senders"),
|
|
1490
|
+
receiversDir: surface("ENTWURF_META_RECEIVERS_DIR", "meta-receivers"),
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* The bridge CHILD half of the same policy, applied to that child's own environment.
|
|
1496
|
+
*
|
|
1497
|
+
* A bridge child selects this by the exact provenance label its managed entry carries —
|
|
1498
|
+
* no probing, no guessing, and no effect on any other harness's child. It then does two
|
|
1499
|
+
* things, in this order and before any lazy default-root consumer has run:
|
|
1500
|
+
*
|
|
1501
|
+
* 1. removes the foreign `PI_CODING_AGENT_DIR` from THIS PROCESS only. The OMP HOST
|
|
1502
|
+
* keeps it — there it is the vendor's own agent dir and deleting it would rewrite
|
|
1503
|
+
* vendor path semantics — but the child has no vendor lookup left to do.
|
|
1504
|
+
* 2. pins the four `ENTWURF_META_*_DIR` overrides to this leaf's answer, so every
|
|
1505
|
+
* `defaultMeta*Dir()` consumer in the bridge reads the OMP bundle literally rather
|
|
1506
|
+
* than recomputing something that merely happens to match. Idempotent: an override
|
|
1507
|
+
* the operator already set was honoured by the leaf and is written back unchanged.
|
|
1508
|
+
*
|
|
1509
|
+
* These four are entwurf's OWN documented override vocabulary, set in our own process —
|
|
1510
|
+
* not a new carrier, not a record field, not a marker.
|
|
1511
|
+
*
|
|
1512
|
+
* The in-process birth extension cannot use this and must not try: it runs INSIDE the omp
|
|
1513
|
+
* host, so mutating that process's env is exactly the thing forbidden above. It passes
|
|
1514
|
+
* explicit directories from {@link ompMetaRoots} instead.
|
|
1515
|
+
*/
|
|
1516
|
+
export function applyOmpBridgeChildRootPolicy(
|
|
1517
|
+
env: MetaRootEnv = process.env,
|
|
1518
|
+
home?: string,
|
|
1519
|
+
): { applied: boolean; roots: MetaRootBundle | null } {
|
|
1520
|
+
// EXACT equality on the RAW label (#87 A3). Trimming was a courtesy that let a
|
|
1521
|
+
// whitespace-drifted entry — one `doctor-omp-mcp` and the writer both call foreign —
|
|
1522
|
+
// still select OMP root mutation in its child. The writer emits the literal and the
|
|
1523
|
+
// doctor compares the literal; this compares the literal too, so all three agree on
|
|
1524
|
+
// what "our entry" means.
|
|
1525
|
+
if (env.ENTWURF_BRIDGE_EXTERNAL_AGENT_ID !== OMP_BRIDGE_PROVENANCE_LABEL) {
|
|
1526
|
+
return { applied: false, roots: null };
|
|
1527
|
+
}
|
|
1528
|
+
const roots = ompMetaRoots(env, home ?? metaRootHome(env));
|
|
1529
|
+
delete env.PI_CODING_AGENT_DIR;
|
|
1530
|
+
env.ENTWURF_META_SESSIONS_DIR = roots.sessionsDir;
|
|
1531
|
+
env.ENTWURF_META_MAILBOX_DIR = roots.mailboxDir;
|
|
1532
|
+
env.ENTWURF_META_SENDERS_DIR = roots.sendersDir;
|
|
1533
|
+
env.ENTWURF_META_RECEIVERS_DIR = roots.receiversDir;
|
|
1534
|
+
return { applied: true, roots };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1323
1537
|
/**
|
|
1324
1538
|
* A boot-unique identity for a live process: pid is reused, but pid + start-time
|
|
1325
1539
|
* is unique within a boot. Linux reads `/proc/<pid>/stat` field 22 (starttime in
|
|
@@ -70,10 +70,14 @@ import {
|
|
|
70
70
|
runTmux,
|
|
71
71
|
type WindowHandle,
|
|
72
72
|
} from "./mux-placement.ts";
|
|
73
|
+
import { OMP_PREFLIGHT_HINT, type OmpPreflightRejectReason, ompFreshPreflight } from "./omp-fresh-preflight.ts";
|
|
73
74
|
|
|
74
75
|
/** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
|
|
75
|
-
* not a config entry. `copilot` was added by #82 RAIL 9 under the step 9 admission contract
|
|
76
|
-
|
|
76
|
+
* not a config entry. `copilot` was added by #82 RAIL 9 under the step 9 admission contract, and
|
|
77
|
+
* `omp` by #87 Bundle C under the same one. The set is joined to the citizen backends by
|
|
78
|
+
* `check-harness-admission-parity`: a harness that mints records but is missing HERE is not an
|
|
79
|
+
* unwired convenience, it is a release blocker. */
|
|
80
|
+
export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp"] as const;
|
|
77
81
|
export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
|
|
78
82
|
|
|
79
83
|
/**
|
|
@@ -89,11 +93,20 @@ export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
|
|
|
89
93
|
* flag, its recursion fence and its receiver precondition; fresh call reaches Copilot only
|
|
90
94
|
* through it. The cost is named: a Copilot fresh call needs a current `entwurf` on PATH, the
|
|
91
95
|
* way a pi fresh call needs `pi`.
|
|
96
|
+
*
|
|
97
|
+
* `omp` resolves the BARE vendor, and that difference is a measured one rather than an
|
|
98
|
+
* inconsistency. Copilot needs a managed wrapper because the bare CLI starts without
|
|
99
|
+
* `COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS` and skips its extension scan silently — there
|
|
100
|
+
* is a flag only a launcher can carry. omp has no such flag: it always scans its extensions
|
|
101
|
+
* directory, and the one thing it needs beyond that (`tools.xdev: false`) lives in the operator
|
|
102
|
+
* config, which is a PREFLIGHT fact and not something a launcher could supply. Inventing an
|
|
103
|
+
* `entwurf omp` verb here would add a managed surface with nothing to manage.
|
|
92
104
|
*/
|
|
93
105
|
export const FRESH_CALL_RUNTIME: Record<FreshCallBackend, string> = {
|
|
94
106
|
pi: "pi",
|
|
95
107
|
"claude-code": "claude",
|
|
96
108
|
copilot: "entwurf",
|
|
109
|
+
omp: "omp",
|
|
97
110
|
};
|
|
98
111
|
|
|
99
112
|
/**
|
|
@@ -107,11 +120,21 @@ export const FRESH_CALL_RUNTIME: Record<FreshCallBackend, string> = {
|
|
|
107
120
|
* `tool.execution_start.toolName` both carry `entwurf-bridge-entwurf_v2`, with
|
|
108
121
|
* `mcpServerName`/`mcpToolName` beside them as the parts. Derive-and-measure, never copy a
|
|
109
122
|
* sibling's spelling (`docs/adding-a-harness.md` step 5).
|
|
123
|
+
*
|
|
124
|
+
* `[측정]` omp 18.0.0 is the sharpest case for that rule: it mints
|
|
125
|
+
* `mcp__${sanitizedServerName}_${normalizedToolName}` with a sanitizer whose charset is
|
|
126
|
+
* `[a-z_]` (`mcp/tool-bridge.ts:351-357`, `:396`), so the DIGIT IN `entwurf_v2` IS EATEN and the
|
|
127
|
+
* hyphen in the server key becomes an underscore — the model-facing name is
|
|
128
|
+
* `mcp__entwurf_bridge_entwurf_v`, not `..._entwurf_v2` and not Claude's double-underscore form.
|
|
129
|
+
* Confirmed against a live tool dump of all seven bridge tools and a real session transcript
|
|
130
|
+
* (`scripts/raw-omp-measure/README.md` "Tool-name dialect"). Unlike Copilot there is no second
|
|
131
|
+
* permission dialect: omp's approval layer consults the same minted string (`source-audit.md`).
|
|
110
132
|
*/
|
|
111
133
|
export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
|
|
112
134
|
pi: "entwurf_v2",
|
|
113
135
|
"claude-code": "mcp__entwurf-bridge__entwurf_v2",
|
|
114
136
|
copilot: "entwurf-bridge-entwurf_v2",
|
|
137
|
+
omp: "mcp__entwurf_bridge_entwurf_v",
|
|
115
138
|
};
|
|
116
139
|
|
|
117
140
|
/** Mirrors the `entwurf_v2` message bound. This is an INTERFACE cap for symmetry with the
|
|
@@ -119,7 +142,7 @@ export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
|
|
|
119
142
|
* that the OS refuses is a launch failure and fails loud — it never reads as a delivered task. */
|
|
120
143
|
export const TASK_MAX_CHARS = 16000;
|
|
121
144
|
export const MODEL_MAX_CHARS = 200;
|
|
122
|
-
const MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._
|
|
145
|
+
const MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/:[\]-]*$/;
|
|
123
146
|
|
|
124
147
|
/** A model is an explicit launch input, not ambient process state. The grammar admits canonical
|
|
125
148
|
* pi provider/model ids, Claude model ids/aliases, and bracketed context variants, while refusing
|
|
@@ -140,6 +163,11 @@ export function isSafeFreshCallModel(model: string): boolean {
|
|
|
140
163
|
* copilot — the managed VERB first, then the prompt as the value of `-i/--interactive`,
|
|
141
164
|
* `--model`, value as two tokens, and the policy as the explicit `--yolo`
|
|
142
165
|
* token. Measured from `copilot --help` (1.0.80).
|
|
166
|
+
* omp — NO positional prompt at all. The task rides `--entwurf-bootstrap`, a fixed
|
|
167
|
+
* flag the installed birth extension registers, then `--model`, value as two
|
|
168
|
+
* tokens, then the policy as `--approval-mode`, `yolo`. `-p/--print` remains
|
|
169
|
+
* the flag this argv must never carry — it processes a prompt and EXITS,
|
|
170
|
+
* closing the window on a sibling that has to stay open to be delivered to.
|
|
143
171
|
*
|
|
144
172
|
* Both pi/claude failures looked identical from outside: window open, record and socket minted,
|
|
145
173
|
* no turn.
|
|
@@ -172,18 +200,108 @@ export function isSafeFreshCallModel(model: string): boolean {
|
|
|
172
200
|
* `--allow-tool` takes `<mcp-server-name>(tool-name?)`, a different dialect from the
|
|
173
201
|
* model-facing tool name — stays recorded in `docs/adding-a-harness.md` step 9's worked
|
|
174
202
|
* example; it is a measured vendor fact even though this argv no longer uses it.
|
|
203
|
+
*
|
|
204
|
+
* OMP'S POLICY TOKEN IS THE ONE MOST EASILY ARGUED AWAY, SO READ THIS BEFORE DELETING IT.
|
|
205
|
+
* `[측정]` omp 18.0.0's schema default for `tools.approvalMode` IS ALREADY `yolo`
|
|
206
|
+
* (vendor doc `omp://approval-mode.md`; `omp config get tools.approvalMode` → `yolo` on the
|
|
207
|
+
* acceptance host). So dropping `--approval-mode yolo` changes NOTHING observable: the callback
|
|
208
|
+
* still fires, the LIVE smoke still passes, and the argv silently starts depending on a vendor
|
|
209
|
+
* default and on whatever the operator's config happens to say. That is exactly the drift step 9
|
|
210
|
+
* clause 2 forbids — "carry the chosen width as an explicit argv token rather than relying on a
|
|
211
|
+
* launcher's injected default" — and the reason the width is stated here even though the host
|
|
212
|
+
* would have granted it anyway. The width itself (task-wide, not callback-only) is a GLG
|
|
213
|
+
* operator decision of 2026-08-30, taken with the Copilot measurement in hand: a callback-only
|
|
214
|
+
* sibling names itself and then stops at the first tool its TASK needs. omp offers no argv
|
|
215
|
+
* grammar for a narrower grant at all — `tools.approval.<tool>` is a config axis, not a flag —
|
|
216
|
+
* so the honest choice was between `write` and `yolo`, and `yolo` matches what a human-typed
|
|
217
|
+
* `omp` gets on this host. `--approval-mode` takes both the space and equals form (measured);
|
|
218
|
+
* the space form is used for symmetry with `--model`.
|
|
219
|
+
*
|
|
220
|
+
* WHY OMP ALONE CARRIES NO PROMPT, AND WHY THAT IS A MEASUREMENT RATHER THAN A PREFERENCE.
|
|
221
|
+
* `[LIVE 2026-08-30]` the first public fresh call at omp DID pass the full framing as a bare
|
|
222
|
+
* positional. The window opened, the record minted (garden `20260830T181342-452167`), the
|
|
223
|
+
* prompt arrived byte-identical as a user message at `09:13:42.413Z` — and the model answered
|
|
224
|
+
* the literal text `ACK` with ZERO tool calls, because the callback tool did not exist yet.
|
|
225
|
+
* `[source]` the interactive UI defers MCP discovery and only refreshes the tool list once
|
|
226
|
+
* `discoverAndConnect()` settles (`sdk.ts:1847-1855`, `:1881-1905`), while the positional
|
|
227
|
+
* `initialMessage` prompts immediately after `await mode.init()` (`main.ts:540-565`,
|
|
228
|
+
* `595-610`). `[측정]` a `/tmp` observer on the same runtime: `turn_start` at +654ms with the
|
|
229
|
+
* entwurf tools ABSENT, callback tool present only at +1484ms — the turn began ~830ms before
|
|
230
|
+
* the tool it was told to call existed. No argv can close that gap, because the gap is a race
|
|
231
|
+
* inside the host. So the composition hands omp a PAYLOAD instead of a turn, and the
|
|
232
|
+
* in-process birth extension — which can see when the tool becomes callable — owns the first
|
|
233
|
+
* two messages (`pi-extensions/meta-bridge-omp.ts`, "THE TWO-STAGE FRESH BOOTSTRAP").
|
|
234
|
+
*
|
|
235
|
+
* The flag is fixed and one-purpose ON PURPOSE. `[측정 2026-08-30]` a normal discovered
|
|
236
|
+
* extension that registers a flag receives the operator's argv value byte-identical — quotes,
|
|
237
|
+
* `$VAR`, backticks and a semicolon all survived a 137-byte JSON payload — because extensions
|
|
238
|
+
* load before argv classification and the reparse writes the registered map
|
|
239
|
+
* (`main.ts:1799-1810`, `cli/extension-flags.ts:36-43`). An env carrier or a temp file would
|
|
240
|
+
* have needed its own quoting, its own lifetime and its own refusal rules; argv already owns
|
|
241
|
+
* all three. This is deliberately NOT a general `--flag value` passthrough — an arbitrary
|
|
242
|
+
* carrier would hand callers the launch-shaping power this rail exists to refuse.
|
|
175
243
|
*/
|
|
176
|
-
export function buildBackendArgs(
|
|
244
|
+
export function buildBackendArgs(
|
|
245
|
+
backend: FreshCallBackend,
|
|
246
|
+
composition: FreshCallComposition,
|
|
247
|
+
model: string,
|
|
248
|
+
): string[] {
|
|
177
249
|
switch (backend) {
|
|
178
250
|
case "pi":
|
|
179
|
-
return [prompt, "--entwurf-control", "--model", model];
|
|
251
|
+
return [composition.prompt, "--entwurf-control", "--model", model];
|
|
180
252
|
case "claude-code":
|
|
181
|
-
return [prompt, `--allowedTools=${FRESH_CALL_CALLBACK_TOOL["claude-code"]}`, `--model=${model}`];
|
|
253
|
+
return [composition.prompt, `--allowedTools=${FRESH_CALL_CALLBACK_TOOL["claude-code"]}`, `--model=${model}`];
|
|
182
254
|
case "copilot":
|
|
183
|
-
return ["copilot", "--interactive", prompt, "--model", model, "--yolo"];
|
|
255
|
+
return ["copilot", "--interactive", composition.prompt, "--model", model, "--yolo"];
|
|
256
|
+
case "omp":
|
|
257
|
+
return [`--${OMP_BOOTSTRAP_FLAG}`, composition.bootstrapPayload, "--model", model, "--approval-mode", "yolo"];
|
|
184
258
|
}
|
|
185
259
|
}
|
|
186
260
|
|
|
261
|
+
/**
|
|
262
|
+
* What a launch has to say, in the two shapes the four backends need. Three of them are
|
|
263
|
+
* handed a first-turn PROMPT; omp is handed a bootstrap PAYLOAD its own installed extension
|
|
264
|
+
* unpacks. Both are always built, because building one is cheap and a backend switch must
|
|
265
|
+
* never be able to reach a field that was not composed.
|
|
266
|
+
*/
|
|
267
|
+
export interface FreshCallComposition {
|
|
268
|
+
prompt: string;
|
|
269
|
+
bootstrapPayload: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The omp bootstrap flag, spelled WITHOUT dashes — the vendor's flag map is keyed by bare
|
|
274
|
+
* name (`extensions/loader.ts:221-228`) and this composition adds the `--` itself.
|
|
275
|
+
*
|
|
276
|
+
* Held equal to the installed extension's own constant by
|
|
277
|
+
* `test/omp-fresh-bootstrap.contract.test.ts`. The two copies exist because the extension
|
|
278
|
+
* ships INSIDE the omp agent dir carrying only its own small closure and cannot import this
|
|
279
|
+
* module; the gate is what keeps the duplication from becoming drift.
|
|
280
|
+
*/
|
|
281
|
+
export const OMP_BOOTSTRAP_FLAG = "entwurf-bootstrap";
|
|
282
|
+
|
|
283
|
+
/** Payload grammar version, matched exactly by the decoder. A bump means a stale installed
|
|
284
|
+
* unit, which is the one thing `doctor-omp-bridge` exists to say out loud. */
|
|
285
|
+
export const OMP_BOOTSTRAP_VERSION = 1;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The whole of what a fresh omp sibling is launched with.
|
|
289
|
+
*
|
|
290
|
+
* THREE FIELDS, CLOSED. The decoder refuses an unknown key, so this object is the entire
|
|
291
|
+
* contract: who to call back, the nonce that proves it is this call, and the task that is
|
|
292
|
+
* released only after that callback succeeds. There is no command here, no path, no env name
|
|
293
|
+
* and no model — the model is already an explicit argv token, and a second copy of it inside
|
|
294
|
+
* a payload would be a second place for it to disagree with the launch.
|
|
295
|
+
*/
|
|
296
|
+
export function buildOmpBootstrapPayload(params: { callerGardenId: string; nonce: string; task: string }): string {
|
|
297
|
+
return JSON.stringify({
|
|
298
|
+
v: OMP_BOOTSTRAP_VERSION,
|
|
299
|
+
target: params.callerGardenId,
|
|
300
|
+
nonce: params.nonce,
|
|
301
|
+
task: params.task,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
187
305
|
/**
|
|
188
306
|
* The first-turn framing. Order is the contract: the callback is the FIRST action and the task
|
|
189
307
|
* follows it, so a sibling that gets stuck in the task has already told the caller who it is.
|
|
@@ -224,6 +342,7 @@ export type FreshCallRejectReason =
|
|
|
224
342
|
| LaunchRejectReason
|
|
225
343
|
| TmuxCwdRejectReason
|
|
226
344
|
| CopilotPreflightRejectReason
|
|
345
|
+
| OmpPreflightRejectReason
|
|
227
346
|
| "caller-identity-unavailable"
|
|
228
347
|
| "model-empty"
|
|
229
348
|
| "model-invalid"
|
|
@@ -256,9 +375,35 @@ function defaultRandomHex(): string {
|
|
|
256
375
|
return randomBytes(12).toString("hex");
|
|
257
376
|
}
|
|
258
377
|
|
|
259
|
-
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
378
|
+
/**
|
|
379
|
+
* The pi identity carrier, scrubbed at the launch seam for EVERY backend (#87 Bundle C).
|
|
380
|
+
*
|
|
381
|
+
* `[측정]` 2026-08-30, private tmux server: a `new-window` pane inherits the tmux SERVER's
|
|
382
|
+
* environment, not the caller's. A server started from a shell that exported `PI_SESSION_ID`
|
|
383
|
+
* hands that value to every window it will ever open — the control run printed
|
|
384
|
+
* `SID=[leaked-uuid]` in a pane the caller never touched. Nothing about the fresh call creates
|
|
385
|
+
* that leak and nothing about it notices: the sibling's own MCP child would read the STALE pair
|
|
386
|
+
* as its authoritative identity and call home as a citizen it is not (`mcp/entwurf-bridge/
|
|
387
|
+
* src/index.ts:692-698` keeps the measured incident — a fresh cell answering with the uuidv7 it
|
|
388
|
+
* found in the environment, confidently and wrong).
|
|
389
|
+
*
|
|
390
|
+
* `-e VAR=` sets the variable EMPTY rather than unsetting it, which tmux has no per-window form
|
|
391
|
+
* for. That is sufficient and not a compromise: every reader of the carrier trims and tests
|
|
392
|
+
* truthiness (`index.ts:212-217`), so empty and absent are the same answer by construction.
|
|
393
|
+
*
|
|
394
|
+
* It is applied to all four backends because the leak is a property of tmux, not of a vendor. A
|
|
395
|
+
* scrub only on the backend whose measurement surfaced it would encode the claim that the other
|
|
396
|
+
* three are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
|
|
397
|
+
* authoritative when the process that owns it exported it ITSELF, and a fresh `pi` sibling does
|
|
398
|
+
* exactly that after this argv has run. This is a fixed two-variable seam and deliberately NOT a
|
|
399
|
+
* general env carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping
|
|
400
|
+
* power this rail exists to refuse.
|
|
401
|
+
*/
|
|
402
|
+
const SCRUBBED_INHERITED_ENV = ["PI_SESSION_ID=", "PI_AGENT_ID="] as const;
|
|
403
|
+
|
|
404
|
+
/** Launch argv: the leaf's detached-append shape, the identity scrub, optionally `-c` at the
|
|
405
|
+
* resume-symmetric token position (after `-t`, before `-P -F`), the runtime, then the backend's
|
|
406
|
+
* dialect. An omitted cwd adds no `-c` carrier at all. */
|
|
262
407
|
export function buildFreshCallArgs(
|
|
263
408
|
placement: Placement,
|
|
264
409
|
runtimePath: string,
|
|
@@ -275,6 +420,7 @@ export function buildFreshCallArgs(
|
|
|
275
420
|
"new-window",
|
|
276
421
|
"-d",
|
|
277
422
|
"-a",
|
|
423
|
+
...SCRUBBED_INHERITED_ENV.flatMap((assignment) => ["-e", assignment]),
|
|
278
424
|
"-t",
|
|
279
425
|
`${placement.sessionId}:{end}`,
|
|
280
426
|
...(cwd === undefined ? [] : ["-c", cwd]),
|
|
@@ -337,20 +483,27 @@ export function freshCall(
|
|
|
337
483
|
const missing = copilotFreshPreflight(env);
|
|
338
484
|
if (missing) return { ok: false, reason: missing };
|
|
339
485
|
}
|
|
486
|
+
if (params.backend === "omp") {
|
|
487
|
+
const missing = ompFreshPreflight(env);
|
|
488
|
+
if (missing) return { ok: false, reason: missing };
|
|
489
|
+
}
|
|
340
490
|
|
|
341
491
|
const inspected = inspectPlacement(env);
|
|
342
492
|
if (!inspected.ok) return { ok: false, reason: inspected.reason };
|
|
343
493
|
const placement = inspected.placement;
|
|
344
494
|
requireSameContext("freshCall", placement, env);
|
|
345
495
|
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
496
|
+
const composition: FreshCallComposition = {
|
|
497
|
+
prompt: buildFreshCallPrompt({
|
|
498
|
+
backend: params.backend,
|
|
499
|
+
task,
|
|
500
|
+
callerGardenId: params.callerGardenId,
|
|
501
|
+
nonce,
|
|
502
|
+
}),
|
|
503
|
+
bootstrapPayload: buildOmpBootstrapPayload({ callerGardenId: params.callerGardenId, nonce, task }),
|
|
504
|
+
};
|
|
352
505
|
const run = runTmux(
|
|
353
|
-
buildFreshCallArgs(placement, runtimePath, buildBackendArgs(params.backend,
|
|
506
|
+
buildFreshCallArgs(placement, runtimePath, buildBackendArgs(params.backend, composition, model), cwd),
|
|
354
507
|
env,
|
|
355
508
|
);
|
|
356
509
|
assertTmuxOk("new-window", run);
|
|
@@ -390,6 +543,7 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
|
|
|
390
543
|
// The Copilot capability reasons keep their repair text on the leaf that decides them, so
|
|
391
544
|
// the sentence an operator reads cannot drift away from the predicate that produced it.
|
|
392
545
|
...COPILOT_PREFLIGHT_HINT,
|
|
546
|
+
...OMP_PREFLIGHT_HINT,
|
|
393
547
|
"no-tmux-context": "this agent is not running inside tmux, so there is no session to open a sibling beside",
|
|
394
548
|
"anchor-malformed": "TMUX_PANE is not a native pane id",
|
|
395
549
|
"anchor-unresolved": "tmux resolved no pane for this agent's anchor",
|