@junghanacs/entwurf 0.30.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +194 -0
  3. package/DELIVERY.md +1 -0
  4. package/README.md +64 -14
  5. package/VERIFY.md +36 -1
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/setup-clean-host.md +121 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +28 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +236 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +746 -0
  19. package/mcp/entwurf-bridge/src/index.ts +7 -7
  20. package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
  21. package/package.json +19 -19
  22. package/pi/entwurf-capabilities.json +1 -0
  23. package/pi/pi-durable/bootstrap.mjs +404 -0
  24. package/pi/pi-durable/carrier/LICENSE +21 -0
  25. package/pi/pi-durable/carrier/harness-setup.js +123 -0
  26. package/pi/pi-durable/carrier/prompt.js +74 -0
  27. package/pi/pi-durable/carrier/runtime.js +402 -0
  28. package/pi/pi-durable/carrier/sessions.js +56 -0
  29. package/pi/pi-durable/carrier/subagent.js +55 -0
  30. package/pi/pi-durable/carrier/tui.js +655 -0
  31. package/pi/pi-durable/carrier-relocation.mjs +160 -0
  32. package/pi/pi-durable/carrier-resolver.mjs +38 -0
  33. package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
  34. package/pi/pi-durable/overlay/upstream-pin.json +108 -0
  35. package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
  36. package/pi-extensions/lib/fresh-call-composition.ts +42 -7
  37. package/pi-extensions/lib/meta-sender-identity.ts +7 -1
  38. package/pi-extensions/lib/meta-session.ts +28 -2
  39. package/pi-extensions/lib/mux-fresh-call.ts +32 -2
  40. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
  41. package/pi-extensions/lib/pi-durable-runtime.ts +318 -0
  42. package/pi-extensions/meta-bridge-pi-durable.ts +1024 -0
  43. package/run.sh +624 -184
  44. package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
  45. package/scripts/check-fresh-call-dispatch.ts +18 -0
  46. package/scripts/check-gate-qualification.ts +87 -34
  47. package/scripts/check-herdr-activation.ts +5 -4
  48. package/scripts/check-pi-durable-contact.ts +767 -0
  49. package/scripts/check-pi-durable-receive.ts +1079 -0
  50. package/scripts/check-pi-durable-send.ts +1821 -0
  51. package/scripts/emit-pi-durable-carrier.ts +355 -0
  52. package/scripts/lib/mutation-qualify.ts +21 -45
  53. package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
  54. package/scripts/lib/pi-durable-operator-guard.ts +89 -0
  55. package/scripts/lib/pi-durable-r1.ts +100 -0
  56. package/scripts/lib/pi-durable-reap.ts +27 -0
  57. package/scripts/lib/pi-durable-receive.ts +137 -0
  58. package/scripts/lib/qualification-receipt.ts +7 -11
  59. package/scripts/mutants/control-send-receipt.json +1 -1
  60. package/scripts/mutants/copilot-birth.json +2 -2
  61. package/scripts/mutants/gate-qualification.json +13 -0
  62. package/scripts/mutants/meta-hook-session-switch.json +4 -2
  63. package/scripts/mutants/mux-fresh-call.json +69 -3
  64. package/scripts/mutants/omp-birth.json +2 -2
  65. package/scripts/mutants/omp-fresh.json +6 -4
  66. package/scripts/mutants/pack-install.json +3 -3
  67. package/scripts/mutants/pi-durable.json +723 -0
  68. package/scripts/mutants/pi-floor.json +24 -2
  69. package/scripts/pi-durable-contact-host.mjs +105 -0
  70. package/scripts/pi-durable-receive-host.mjs +127 -0
  71. package/scripts/pi-durable-send-host.mjs +284 -0
  72. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  73. package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
  74. package/scripts/smoke-setup-verdict.sh +22 -3
  75. package/scripts/smoke-user-scope-citizen.sh +1 -1
  76. package/scripts/tsconfig.json +1 -0
@@ -28,7 +28,8 @@ evidence split, and its three states must not collapse into "macOS is supported"
28
28
  | npm/pnpm | npm is bundled with Node; pnpm is required for source setup | package or source installation |
29
29
  | Python 3 | required by `setup`/`install` (project-path normalization + settings writers); `--help`/`check-bridge` stay Python-free | pi/Claude/agy/Copilot/Codex wiring writers |
30
30
  | entwurf | global/project-local `@junghanacs/entwurf`, or a source checkout | operator command and garden capability |
31
- | pi | optional-by-presence, `>=1.0.0 <1.1` — absent is an explicit setup SKIP, below-floor is a named FAIL | ACP provider, control sockets |
31
+ | pi | optional-by-presence, `>=1.0.4 <1.1` — absent is an explicit setup SKIP, below-floor is a named FAIL | ACP provider, control sockets |
32
+ | Native durable (`pi-durable`) | supplied by the Entwurf package (#130): a pinned upstream v1.0.4 app carrier plus the exact 1.0.4 pi SDK set as production dependencies — setup PASS when they verify, a named FAIL otherwise (never SKIP) | the `entwurf pi-durable` contact; native birth/LIVE on this supply surface is a separate release proof (§2b) |
32
33
  | Claude Code | optional, **`>=2.1.217`** — the exec-form hook floor | Claude ACP auth/runtime and mailbox-backed native citizen |
33
34
  | 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 |
34
35
  | OMP (`omp`) | optional-by-presence, operator-installed — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/`tools.xdev` setting/receiver) | self-fetch citizen and visible fresh (accepted on one host — see §4b) |
@@ -190,7 +191,7 @@ on Darwin; that is wiring, not a rail receipt. If `pi` is on PATH,
190
191
  Install the exact release floor, then wire the project:
191
192
 
192
193
  ```bash
193
- npm install -g @earendil-works/pi-coding-agent@1.0.0
194
+ npm install -g @earendil-works/pi-coding-agent@1.0.4
194
195
  pi --version
195
196
 
196
197
  cd ~/entwurf-smoke
@@ -198,7 +199,7 @@ entwurf install .
198
199
  pi -e "$(npm root -g)/@junghanacs/entwurf" --list-models entwurf
199
200
  ```
200
201
 
201
- The supported range is `>=1.0.0 <1.1`. It is a hard minimum: installing this
202
+ The supported range is `>=1.0.4 <1.1`. It is a hard minimum: installing this
202
203
  release onto a 0.83.x pi host upgrades the runtime rather than keeping the older
203
204
  minor. A host using only the external MCP bridge can skip pi until it needs a
204
205
  control socket; no delivery rail launches a pi process.
@@ -282,6 +283,122 @@ emacsclient -s "${PI_EMACS_AGENT_SOCKET:-server}" --eval '(...)'
282
283
 
283
284
  </details>
284
285
 
286
+ ## 2b. Optional native durable (`pi-durable`)
287
+
288
+ **#130 / 0.32 installation contract: installing Entwurf supplies pi-durable.** The
289
+ operator clones, patches, builds and copies nothing. 0.31.0 shipped the earlier
290
+ source-runtime route (an operator checkout of Pi 1.0.2/source `cd32f77` at a fixed XDG
291
+ place); that route is retired, not kept as a fallback, and its runtime directory is never
292
+ read. Durable is a separate native harness contact, not an option automatically enabled
293
+ by `entwurf pi` or by registering ordinary Pi's adapter.
294
+
295
+ What the Entwurf package supplies:
296
+
297
+ 1. **The pi SDK set** — `@earendil-works/{pi-coding-agent, pi-ai, pi-tui, pi-agent-core,
298
+ pi-durable, chord, pi-mcp, pi-codemode, pi-telemetry}` at exactly `1.0.4`, plus
299
+ `proper-lockfile`, as Entwurf's own production dependencies (declared once in
300
+ `pi/pi-durable/overlay/upstream-pin.json` `sdk`). Model data rides the published
301
+ `pi-ai`; there is no catalog copy.
302
+ 2. **The native app/TUI carrier** — upstream does not publish the experimental durable
303
+ app (`pi-coding-agent` excludes `dist/experimental`; `pi-durable` on npm is the
304
+ library). Entwurf ships the six pinned app files from upstream tag `v1.0.4`
305
+ (`7c10bd4337495ee613f2224843ecdf349b80d1df`), its narrow runtime-contact overlay
306
+ applied to `runtime.ts`, type-stripped and relocated, in `pi/pi-durable/carrier/`
307
+ with upstream's MIT license. Each file's source blob and emitted digest are in the pin.
308
+ The carrier reaches pi-coding-agent's internals only through its resolver
309
+ (`pi/pi-durable/carrier-resolver.mjs`), which maps them to the published package's
310
+ own `dist/` as installed — never through a source checkout.
311
+
312
+ The upstream [library guide](https://github.com/earendil-works/pi/blob/v1.0.4/packages/durable/README.md)
313
+ and [experimental app guide](https://github.com/earendil-works/pi/blob/v1.0.4/packages/coding-agent/src/experimental/durable/README.md)
314
+ describe what the carrier is. Running the bare upstream app does not attach Entwurf's
315
+ garden contact. Entwurf does not rebuild the controller/model/auth/storage system or
316
+ proxy a subscription; native execution, models, auth, queues, SQLite and recovery stay
317
+ upstream-owned.
318
+
319
+ ### What Entwurf checks
320
+
321
+ The carrier is runnable only when, read from where Entwurf is installed: every pinned
322
+ carrier file, the license, the resolver and its relocation grammar are present
323
+ (`pi-durable-package-incomplete`); each carrier file carries its pinned digest
324
+ (`pi-durable-carrier-drift`); every declared SDK package resolves from the carrier
325
+ (`pi-durable-sdk-absent`); and every reachable physical edge — from the carrier to each package,
326
+ and from every package copy it reaches, at any depth, to its own `@earendil-works` dependencies —
327
+ lands on a package of the declared name at exactly the pinned version (`pi-durable-sdk-mismatch`). The members declare one another with caret
328
+ ranges, so semver satisfaction is not the contract; the bytes an import binds are. Sharing
329
+ one copy with an aligned host install of the same version is an ordinary layout. A mixed
330
+ tree (for example an override that binds pi-durable's `pi-ai` one patch behind) still
331
+ *imports* — which is why the edge check exists — and is refused by name.
332
+
333
+ - `entwurf setup` reports a `pi-durable` row from that check and writes no durable wiring:
334
+ **PASS** when it verifies, **FAIL** with the named refusal otherwise — never SKIP, because
335
+ nothing is optional to detect: a missing piece is an incomplete installation, repaired by
336
+ reinstalling Entwurf.
337
+ - `entwurf pi-durable [args…]` is the managed launch. It verifies first (exit 3 on any
338
+ refusal), then runs the packaged bootstrap `pi/pi-durable/bootstrap.mjs` under the carrier
339
+ resolver, owning this terminal:
340
+
341
+ ```bash
342
+ entwurf pi-durable --provider <provider> --model <exact catalog id> --width task-wide
343
+ entwurf pi-durable --continue # this directory's newest durable session
344
+ ```
345
+
346
+ The bootstrap imports only the compiled closure shipped beside it
347
+ (`mcp/entwurf-bridge/dist/…`); a source checkout needs a built bridge
348
+ (`pnpm run build-bridge`) and its development dependencies installed first.
349
+ `plugins/pi-durable/bootstrap.mjs` is only the checkout path that re-exports it.
350
+
351
+ - **One explicit native module (optional).** Add `--native-module <absolute file>` to either
352
+ launch to load ONE module of your own. It is an ordinary ES module: its top-level code (top-level
353
+ await included) runs first, before the TUI and the durable runtime load, so it can prepare the
354
+ environment the provider runtime and tool children will read; its default export is a native
355
+ durable `Extension` (`{ name, tools?, sections?, hooks?, … }`, the upstream durable API, not
356
+ ordinary Pi's `ExtensionAPI`), installed after Entwurf's contact. An env-only module exports
357
+ just `{ name }`. Refused by name before anything opens: a relative or non-file path
358
+ (`native-module-path-*`), a module that throws (`native-module-import-failed`), a function or
359
+ nameless default (`native-module-export-invalid`), an extension named like one the app installs
360
+ itself (`entwurf`, `coding-tools`, `pi-prompt`, `subagent`) or a tool named like one of Entwurf's
361
+ contact tools (`native-module-name-reserved`; any other duplicate is the native registry's to
362
+ judge), and a module whose initialization changes
363
+ `PI_SESSION_ID`, `PI_CODING_AGENT_DIR`, `HOME` or any `ENTWURF_*` (`native-module-env-identity-changed`)
364
+ or the process directory (`native-module-cwd-changed`). Your module is trusted code; those
365
+ checks catch mistakes, they do not sandbox it. Nothing else names a module — no directory,
366
+ setting or profile — and a sibling opened by `entwurf_fresh_call` starts without one.
367
+
368
+ - **Known warning.** Ordinary Pi may print `Host-provided extension packages must be declared in
369
+ peerDependencies with a "*" range, not dependencies: @earendil-works/…` for Entwurf's
370
+ `package.json`. Entwurf declares the exact pi SDK set as production dependencies on purpose:
371
+ the installed copies serve the separate pi-durable native process. Pi's check reads that
372
+ manifest field; it is a known constraint of supplying both from one package, accepted for 0.32,
373
+ not a claim that every load path is harmless.
374
+
375
+ The ordinary Pi CLI on PATH is a separate subject (§2, range `>=1.0.4 <1.1`): a host Pi is
376
+ not needed to run pi-durable, and a host inside the range need not be the same copy as the
377
+ carrier's. Native Pi owns credentials, model settings and durable SQLite sessions; keep them
378
+ in place rather than copying credentials or migrating ordinary Pi transcripts. Native
379
+ recovery does not prove exactly-once Entwurf dispatch or inbox read recovery. The contact
380
+ provides garden attachment, visible identity and announce-only root-directed self-fetch.
381
+ Package gates prove the installed carrier's load, its physical edges and the public launch up
382
+ to the app's own model resolution; the checkout contact/send/receive gates run the same carrier
383
+ with scripted S plus native H; Pi ↔ pi-durable LIVE on this supply surface is a separate release
384
+ proof ([#130](https://github.com/junghan0611/entwurf/issues/130)).
385
+
386
+ ### Maintainer: re-emitting the carrier
387
+
388
+ Only a maintainer moving the pin runs `scripts/emit-pi-durable-carrier.ts`. It reads the
389
+ pinned commit's objects (`<commit>:<path>`, `--no-replace-objects`) from a repository the
390
+ maintainer names, applies the overlay patch with `git apply` in a private scratch tree,
391
+ type-strips with Node's own strip-only TypeScript, relocates, and checks that the six files
392
+ import exactly the pin's declared `distTargets`:
393
+
394
+ ```bash
395
+ node scripts/emit-pi-durable-carrier.ts --source <clone holding the pinned commit> --write
396
+ node scripts/emit-pi-durable-carrier.ts --source <clone holding the pinned commit> --check # rebuild equality
397
+ ```
398
+
399
+ `--check` without such a repository is a named SKIP, never rebuild acceptance; the emitter
400
+ version (Node) is part of the pin.
401
+
285
402
  ## 3. Optional Claude Code native citizen
286
403
 
287
404
  This rail is CERTIFIED on Linux; on macOS it is NOT CERTIFIED — pending physical
@@ -445,7 +562,7 @@ as a note rather than a fault.
445
562
 
446
563
  **entwurf sets no OMP version floor in code, and will not grow one on schedule.** Detection is
447
564
  presence-only (`command -v "${OMP_BIN:-omp}"`); there is no `entwurf.ompFloor`, no coherence
448
- gate, and no exact pin — unlike Node (`engines.node`), pi (`>=1.0.0 <1.1`) and Claude Code
565
+ gate, and no exact pin — unlike Node (`engines.node`), pi (`>=1.0.4 <1.1`) and Claude Code
449
566
  (`entwurf.claudeCodeFloor`), each of which has an enforcement point. That asymmetry is a
450
567
  decision, not an omission. A floor is the answer to a vendor that fails SILENTLY — Claude Code
451
568
  earned one because an older binary validates the exec manifest, drops `args` at runtime, and
@@ -630,8 +630,8 @@ server.tool("entwurf_register_native", "Register an ALREADY-RUNNING native conve
630
630
  server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and hand it a first task. WHERE it opens is decided by where THIS " +
631
631
  "agent runs, never by a parameter: inside herdr (HERDR_ENV=1) it opens a NEW UNFOCUSED TAB in your own " +
632
632
  "herdr workspace, pi and claude-code " +
633
- "only; everywhere else it opens in the operator's tmux with all five backends (pi, " +
634
- "claude-code, copilot, omp, codex). No fallback: an incomplete herdr context is refused by name, " +
633
+ "only; everywhere else it opens in the operator's tmux with all six backends (pi, " +
634
+ "claude-code, copilot, omp, codex, pi-durable). No fallback: an incomplete herdr context is refused by name, " +
635
635
  "never by a tmux window you cannot see from herdr. The sibling's FIRST action " +
636
636
  "is a callback to you carrying a nonce, whose sender envelope is its garden id — that is how you learn the " +
637
637
  "address of a thing that did not exist a moment ago. This returns a LAUNCH receipt (the owner's " +
@@ -639,8 +639,8 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and
639
639
  "the task was delivered. Those coordinates are a VIEW, never an address — a herdr tab/pane id can change under " +
640
640
  "a running sibling. Nothing polls. " +
641
641
  "For EXISTING citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the chosen " +
642
- "runtime CLI (`provider/model` for pi, an id/alias for Claude Code, a pattern for the rest). On tmux, copilot/omp/codex are refused BEFORE any window " +
643
- "opens when their birth, MCP, receive or visible-identity units are absent, and codex also needs the " +
642
+ "runtime CLI (`provider/model` for pi, an id/alias for Claude Code, `provider/model id` exactly as the catalog names it for pi-durable, a pattern for the rest). On tmux, copilot/omp/codex/pi-durable are refused BEFORE any window " +
643
+ "opens when their birth, MCP, receive or visible-identity units are absent, pi-durable also on named refusals of its installed carrier or exact SDK set, and codex also needs the " +
644
644
  "operator-owned app-server socket entwurf never starts; in herdr they are refused by name first. " +
645
645
  "Optional placement.tmuxSession is a TMUX-ONLY seat naming ONE EXISTING session and " +
646
646
  "ALWAYS wins; omitted, the seat follows the CALLER, never the backend opened: a CODEX CALLER opens beside " +
@@ -648,8 +648,8 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and
648
648
  "NOTHING is created. In herdr the field is refused by name. Do not put secrets in the task — model and task argv are visible to same-user " +
649
649
  "processes.", {
650
650
  backend: z
651
- .enum(["pi", "claude-code", "copilot", "omp", "codex"])
652
- .describe("Which fixed runtime to open. Only these five, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command."),
651
+ .enum(["pi", "claude-code", "copilot", "omp", "codex", "pi-durable"])
652
+ .describe("Which fixed runtime to open. Only these six, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command."),
653
653
  model: z
654
654
  .string()
655
655
  .min(1)
@@ -663,7 +663,7 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling beside you and
663
663
  // as an unused suppression while the escape goes back to being fixable.
664
664
  // biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above
665
665
  .regex(/^[A-Za-z0-9][A-Za-z0-9._/:\[\]-]*$/)
666
- .describe("Required runtime model: canonical provider/model for pi, a Claude Code model id/alias, or a Copilot/OMP/Codex model name."),
666
+ .describe("Required runtime model: canonical provider/model for pi, provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name."),
667
667
  task: z
668
668
  .string()
669
669
  .min(1)
@@ -6,6 +6,7 @@
6
6
  "codex": { "wakeMode": "direct-inject", "deliveryLevel": "D6", "nativeIdLabel": "threadId" },
7
7
  "copilot": { "wakeMode": "self-fetch", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" },
8
8
  "omp": { "wakeMode": "self-fetch", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" },
9
+ "pi-durable": { "wakeMode": "self-fetch", "deliveryLevel": "D2", "nativeIdLabel": "sessionId" },
9
10
  "pi": { "wakeMode": "direct-inject", "deliveryLevel": "D6", "nativeIdLabel": "sessionId" }
10
11
  }
11
12
  }
@@ -105,7 +105,13 @@ export function receiverMarkerMatchesIdentity(marker, identity) {
105
105
  // previous garden in-process on its `/new` edge (the unarm the claude hook lacked),
106
106
  // so the cell this join closes has a different owner there. Admitting it needs its
107
107
  // own measurement, not this file's optimism.
108
- export const SENDER_JOINED_RECEIVER_OWNER_KINDS = ["claude-code-cli"];
108
+ //
109
+ // `pi-durable-host` is admitted because the join holds by construction: the durable contact
110
+ // writes its sender marker under the host's own pid and arms its watch in that same process
111
+ // (`meta-bridge-pi-durable.ts`), so the receiver marker's `ownerPid` IS the sender-marker key.
112
+ // Inside the scope a host whose sender marker is gone or names another garden is a retired
113
+ // watch, which is the refusal this admission exists to make reachable.
114
+ export const SENDER_JOINED_RECEIVER_OWNER_KINDS = ["claude-code-cli", "pi-durable-host"];
109
115
  /** Does this watch owner share its pid with the backend's sender marker? */
110
116
  export function receiverOwnerKindJoinsSender(ownerKind) {
111
117
  return SENDER_JOINED_RECEIVER_OWNER_KINDS.includes(ownerKind);
@@ -23,11 +23,12 @@
23
23
  */
24
24
  import { randomBytes } from "node:crypto";
25
25
  /** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
26
- * not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, and `codex`
27
- * by #95 after its system birth and app-server rails were measured. The set is joined to the
26
+ * not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, `codex`
27
+ * by #95 after its system birth and app-server rails were measured, and `pi-durable` by #129
28
+ * (the durable app this package carries, through the managed `entwurf pi-durable`). The set is joined to the
28
29
  * citizen backends by `check-harness-admission-parity`: a harness that mints records but is
29
30
  * missing HERE is not an unwired convenience, it is a release blocker. */
30
- export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex"];
31
+ export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex", "pi-durable"];
31
32
  /**
32
33
  * The callback tool NAME differs per backend and that is not cosmetic: every session reaches the
33
34
  * capability under whatever name its harness composes for the `entwurf-bridge` MCP server, and
@@ -64,6 +65,11 @@ export const FRESH_CALL_CALLBACK_TOOL = {
64
65
  // Codex's tool process is the operator app-server, not the pane: the no-arg
65
66
  // verb refuses there by name. First action stays the delivery verb with args.
66
67
  codex: "mcp__entwurf_bridge__entwurf_v2",
68
+ // The durable contact registers the bridge verbs as its OWN tools under their bare names, and the
69
+ // app offers a tool to the model by its registered name (`durable/harness/registry.ts:99`
70
+ // @cd32f77). Measured for `entwurf_v2` on the S+native-H wire (receipt 4012c066); the callback
71
+ // spelling follows the same rule and is owed its first LIVE observation.
72
+ "pi-durable": "entwurf_callback",
67
73
  };
68
74
  /** Where a sibling SENDS the task result. Always the delivery verb, never the birth callback. */
69
75
  export const FRESH_CALL_DELIVERY_TOOL = {
@@ -72,6 +78,7 @@ export const FRESH_CALL_DELIVERY_TOOL = {
72
78
  copilot: "entwurf-bridge-entwurf_v2",
73
79
  omp: "mcp__entwurf_bridge_entwurf_v",
74
80
  codex: "mcp__entwurf_bridge__entwurf_v2",
81
+ "pi-durable": "entwurf_v2",
75
82
  };
76
83
  /**
77
84
  * The same dialects, for the tool the framing OFFERS rather than requires.
@@ -93,6 +100,7 @@ export const FRESH_CALL_PEERS_TOOL = {
93
100
  copilot: "entwurf-bridge-entwurf_peers",
94
101
  omp: "mcp__entwurf_bridge_entwurf_peers",
95
102
  codex: "mcp__entwurf_bridge__entwurf_peers",
103
+ "pi-durable": "entwurf_peers",
96
104
  };
97
105
  /**
98
106
  * ONE BACKEND NEEDS TO BE TOLD HOW TO REACH ITS OWN CALLBACK TOOL `[측정 2026-09-18, n=5 + n=3]`.
@@ -132,6 +140,7 @@ export const FRESH_CALL_TOOL_LOAD_HINT = {
132
140
  copilot: [],
133
141
  omp: [],
134
142
  codex: [],
143
+ "pi-durable": [],
135
144
  };
136
145
  /**
137
146
  * The omp bootstrap flag, spelled WITHOUT dashes — the vendor's flag map is keyed by bare
@@ -311,6 +320,28 @@ export function composeBackendArgs(backend, composition, model, resolveCodexSock
311
320
  "--dangerously-bypass-approvals-and-sandbox",
312
321
  composition.prompt,
313
322
  ];
323
+ case "pi-durable": {
324
+ // The managed verb (`entwurf pi-durable`) carries the explicit model as pi's own
325
+ // `--provider`/`--model` pair, split at the FIRST `/` (the preflight already refused a model
326
+ // with no provider), the explicit width, and the WHOLE framing as the closed `{v,task}`
327
+ // payload — the durable bootstrap submits it to the root once, after the birth. The
328
+ // caller's address rides the env pair, never this payload.
329
+ const at = model.indexOf("/");
330
+ if (at <= 0 || at === model.length - 1) {
331
+ throw new Error(`fresh-call composition: pi-durable model ${JSON.stringify(model)} is not <provider>/<model id>`);
332
+ }
333
+ return [
334
+ "pi-durable",
335
+ "--provider",
336
+ model.slice(0, at),
337
+ "--model",
338
+ model.slice(at + 1),
339
+ "--width",
340
+ "task-wide",
341
+ `--${OMP_BOOTSTRAP_FLAG}`,
342
+ JSON.stringify({ v: OMP_BOOTSTRAP_VERSION, task: composition.prompt }),
343
+ ];
344
+ }
314
345
  }
315
346
  }
316
347
  /**
@@ -57,7 +57,13 @@ import { resolveNativePushAdapter } from "./native-push/adapter.js";
57
57
  * exactly the #46 defect on agy, and copilot joined the list only once its own hook wrote one
58
58
  * (#82 RAIL 5b). Membership says a marker may EXIST, never that a reply can land: the reply
59
59
  * rail is chosen from `nativePushSupported` at the bridge, not from this list. */
60
- export const META_SENDER_BACKENDS = ["claude-code", "antigravity", "copilot", "omp"];
60
+ export const META_SENDER_BACKENDS = [
61
+ "claude-code",
62
+ "antigravity",
63
+ "copilot",
64
+ "omp",
65
+ "pi-durable",
66
+ ];
61
67
  /**
62
68
  * Two live native sessions claim this MCP process as their own. We can SEE both identities but
63
69
  * 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", "omp"];
79
+ export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp", "pi-durable"];
80
80
  export const META_BACKEND_DESCRIPTORS = {
81
81
  "claude-code": {
82
82
  backend: "claude-code",
@@ -189,6 +189,24 @@ export const META_BACKEND_DESCRIPTORS = {
189
189
  deliveryLevel: "D6",
190
190
  nativeIdLabel: "sessionId",
191
191
  },
192
+ // The long-running durable app (#129). Graded only as far as its RECORDED receipts reach.
193
+ // sessionId the durable session directory name `<13-digit ms>-<uuid>` that
194
+ // `selectSession` creates and `--continue` reopens
195
+ // (`experimental/durable/sessions.ts:18-55` @cd32f77), surfaced as
196
+ // `view.current().session.id`. Not `pi.provider.sessionId`, which a
197
+ // fork changes.
198
+ // D2 receiver armed: the contact mints the record, names the host as sender, and arms
199
+ // a root doorbell (the receiver marker is owned by the host pid as `pi-durable-host`,
200
+ // which the owner join admits). The addressed-enqueue → idle-wake → root-notice → drain
201
+ // chain has ONE recorded receipt, on the scripted-model + native-harness lab cell
202
+ // (`check-pi-durable-receive`, S+H, V not run) — lab evidence, not a vendor-turn D4–D6
203
+ // acceptance, and no sibling-isolation (D3) or LIVE fresh receipt exists yet.
204
+ "pi-durable": {
205
+ backend: "pi-durable",
206
+ wakeMode: "self-fetch",
207
+ deliveryLevel: "D2",
208
+ nativeIdLabel: "sessionId",
209
+ },
192
210
  };
193
211
  // ---------------------------------------------------------------------------
194
212
  // Validation helpers (crash, don't warn)
@@ -261,7 +279,15 @@ function isoNow(now) {
261
279
  /** The one live identity schema number. */
262
280
  export const META_SCHEMA_VERSION_V3 = 3;
263
281
  /** Every backend admitted by the one V3 record-citizen schema. */
264
- export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp", "pi"];
282
+ export const META_CITIZEN_BACKENDS = [
283
+ "claude-code",
284
+ "antigravity",
285
+ "codex",
286
+ "copilot",
287
+ "omp",
288
+ "pi-durable",
289
+ "pi",
290
+ ];
265
291
  export function requireCitizenBackend(value) {
266
292
  if (typeof value !== "string" || !META_CITIZEN_BACKENDS.includes(value)) {
267
293
  throw new MetaRecordError(`meta-record "backend" must be one of ${META_CITIZEN_BACKENDS.join(" | ")} (got ${describe(value)}).`);
@@ -95,6 +95,8 @@ import { assertLaunchTarget, LaunchPreconditionError, resolveRuntimeOnPath, } fr
95
95
  import { APPEND_FORMAT, assertSelector, assertTmuxOk, inspectPlacement, parseWindowFields, requireSameContext, runTmux, } from "./mux-placement.js";
96
96
  import { resolveCodexDefaultSocketPath } from "./native-push/codex-ws-client.js";
97
97
  import { OMP_PREFLIGHT_HINT, ompFreshPreflight } from "./omp-fresh-preflight.js";
98
+ import { PI_DURABLE_PREFLIGHT_HINT, piDurableFreshPreflight, } from "./pi-durable-fresh-preflight.js";
99
+ import { checkPiDurableRuntime, PI_DURABLE_RUNTIME_HINT, piDurablePackageLayout } from "./pi-durable-runtime.js";
98
100
  import { classifyTmuxSessionName, resolveTmuxSessionId } from "./resolve-tmux-session.js";
99
101
  /**
100
102
  * THE COMPOSITION MOVED, THE RAIL DID NOT (#116 S2-a). Backend argv, the first-turn framing and
@@ -154,6 +156,10 @@ export const FRESH_CALL_RUNTIME = {
154
156
  copilot: "entwurf",
155
157
  omp: "omp",
156
158
  codex: "codex",
159
+ // #129/#130: like copilot, a MANAGED verb and never the bare app. The durable app is this
160
+ // package's carrier and runs under the carrier's resolver; `entwurf pi-durable` is the one fixed
161
+ // invocation that verifies the carrier and its SDK set and execs the packaged bootstrap.
162
+ "pi-durable": "entwurf",
157
163
  };
158
164
  /**
159
165
  * WHICH RULE PICKS THE TARGET SESSION. Three in strict order, and the order is the contract:
@@ -197,9 +203,9 @@ export function selectFreshCallSeat(placement) {
197
203
  * for. That is sufficient and not a compromise: every reader of the carrier trims and tests
198
204
  * truthiness (`index.ts:212-217`), so empty and absent are the same answer by construction.
199
205
  *
200
- * It is applied to all five backends because the leak is a property of tmux, not of a vendor. A
206
+ * It is applied to all six backends because the leak is a property of tmux, not of a vendor. A
201
207
  * scrub only on the backend whose measurement surfaced it would encode the claim that the other
202
- * four are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
208
+ * five are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
203
209
  * authoritative when the process that owns it exported it ITSELF, and a fresh `pi` sibling does
204
210
  * exactly that after this argv has run. This is a fixed seam and deliberately NOT a general env
205
211
  * carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping power this
@@ -337,6 +343,24 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
337
343
  if (missing)
338
344
  return { ok: false, reason: missing };
339
345
  }
346
+ if (params.backend === "pi-durable") {
347
+ // The framed first input is computed here only to MEASURE it against the bootstrap payload
348
+ // cap; the same pure function composes it again below. The carrier is verified from THIS
349
+ // process's package closure, before anything is placed. The window runs whatever `entwurf`
350
+ // PATH names, and the managed verb re-verifies its own package before it execs anything, so
351
+ // the window never runs an unverified carrier — but when the two package closures differ, this
352
+ // verdict and the window's can disagree, and the window is left showing its own refusal.
353
+ const layout = piDurablePackageLayout();
354
+ const runtime = layout === null ? null : checkPiDurableRuntime(layout);
355
+ const missing = piDurableFreshPreflight({
356
+ model,
357
+ firstInput: buildFreshCallPrompt({ backend: params.backend, task, callerGardenId, nonce }),
358
+ layout,
359
+ runtime: runtime === null || runtime.ok ? null : runtime.reason,
360
+ });
361
+ if (missing)
362
+ return { ok: false, reason: missing };
363
+ }
340
364
  const inspected = inspectPlacement(env);
341
365
  if (!inspected.ok)
342
366
  return { ok: false, reason: inspected.reason };
@@ -448,6 +472,8 @@ const REJECT_HINT = {
448
472
  // the sentence an operator reads cannot drift away from the predicate that produced it.
449
473
  ...COPILOT_PREFLIGHT_HINT,
450
474
  ...OMP_PREFLIGHT_HINT,
475
+ ...PI_DURABLE_RUNTIME_HINT,
476
+ ...PI_DURABLE_PREFLIGHT_HINT,
451
477
  ...CODEX_PREFLIGHT_HINT,
452
478
  ...CODEX_CALLER_PREFLIGHT_HINT,
453
479
  ...CODEX_CALLER_SEAT_HINT,
@@ -0,0 +1,92 @@
1
+ /**
2
+ * pi-durable-fresh-preflight — the pre-mutation check a pi-durable fresh call needs (#129 W, L6;
3
+ * `docs/adding-a-harness.md` step 9 clauses 2, 3 and 5).
4
+ *
5
+ * Same question as `omp-fresh-preflight.ts`, at the same moment: before `mux-fresh-call` mutates the
6
+ * operator's tmux session, is everything the fresh contract needs actually in place? A refusal HERE,
7
+ * before placement, leaves no window behind. A pass here does not promise the window's own pass: the
8
+ * managed verb re-verifies inside the window, whose env is the tmux SERVER's and whose `entwurf` is
9
+ * whatever PATH names, so under env or package-closure skew it can still refuse after the window
10
+ * exists — and that refusal is left showing in it.
11
+ *
12
+ * ── The inputs, first ──
13
+ *
14
+ * model the fresh-call model grammar already admits `/`, and a durable launch needs the
15
+ * provider spelled out (the bootstrap refuses a model resolved by inference), so
16
+ * the model here is `<provider>/<model id>` split at the FIRST `/` — a catalog id
17
+ * that itself contains `/` keeps it. A model with no provider part is refused.
18
+ * first input the whole framing (callback first, then the task) rides the bootstrap's closed
19
+ * `{v:2,task}` payload, whose decoder caps `task` at 16000 characters. A framed
20
+ * task over that cap would open a window whose bootstrap refuses its own argv.
21
+ *
22
+ * ── Then the carrier, then the four units ──
23
+ *
24
+ * carrier the package's own pi-durable carrier and its declared SDK set, verified
25
+ * (`pi-durable-runtime.ts`); passed in as that check's verdict.
26
+ * birth the compiled contact in this package's closure — it mints the record.
27
+ * MCP hand the compiled entwurf-bridge entry in the same closure — the contact spawns it.
28
+ * receive the compiled owner join in the same closure admits `pi-durable-host`; without
29
+ * it every dispatch to the citizen is the honest `mailbox-undeliverable`.
30
+ * visible identity the packaged bootstrap — the one that hands the TUI the view whose conversation
31
+ * labels carry the garden id.
32
+ *
33
+ * ── What this deliberately does NOT claim ──
34
+ *
35
+ * Filesystem truth about ONE package closure and the SDK it resolves. It does not prove the durable
36
+ * app opens, the bridge connects, the doorbell arms or a garden id renders — that is runtime truth for
37
+ * the clause 7 LIVE receipt. No spawn, no network, no mutation; every path arrives as an argument, so
38
+ * this leaf imports nothing but types.
39
+ */
40
+ import { existsSync, readFileSync } from "node:fs";
41
+ /** The bootstrap decoder's task cap (`meta-bridge-omp.ts` OMP_BOOTSTRAP_TASK_MAX_CHARS); the beside
42
+ * test holds the two equal. */
43
+ export const PI_DURABLE_FIRST_INPUT_MAX_CHARS = 16000;
44
+ /** `<provider>/<model id>` at the FIRST `/`, or `null` when either side is empty. */
45
+ export function splitPiDurableModel(model) {
46
+ const at = model.indexOf("/");
47
+ if (at <= 0 || at === model.length - 1)
48
+ return null;
49
+ return { provider: model.slice(0, at), model: model.slice(at + 1) };
50
+ }
51
+ /** The compiled owner join, read from the emitted bytes the bridge actually loads. */
52
+ function ownerJoinAdmitsDurableHost(file) {
53
+ try {
54
+ const join = /SENDER_JOINED_RECEIVER_OWNER_KINDS\s*=\s*(\[[^\]]*\])/.exec(readFileSync(file, "utf8"))?.[1];
55
+ return join?.includes('"pi-durable-host"') ?? false;
56
+ }
57
+ catch {
58
+ // Bounded environment probe: an unreadable file is a missing unit.
59
+ return false;
60
+ }
61
+ }
62
+ /** The FIRST missing thing, in repair order, or `null` when a fresh launch may proceed. */
63
+ export function piDurableFreshPreflight(params) {
64
+ if (splitPiDurableModel(params.model) === null)
65
+ return "pi-durable-model-not-provider-qualified";
66
+ if (params.firstInput.length > PI_DURABLE_FIRST_INPUT_MAX_CHARS)
67
+ return "pi-durable-first-input-too-long";
68
+ if (params.layout === null)
69
+ return "pi-durable-package-incomplete";
70
+ if (params.runtime !== null)
71
+ return params.runtime;
72
+ const layout = params.layout;
73
+ if (!existsSync(layout.adapter))
74
+ return "pi-durable-birth-unit-missing";
75
+ if (!existsSync(layout.bridgeEntry))
76
+ return "pi-durable-mcp-hand-missing";
77
+ if (!ownerJoinAdmitsDurableHost(layout.deliverability))
78
+ return "pi-durable-receive-unit-missing";
79
+ if (!existsSync(layout.bootstrap))
80
+ return "pi-durable-visible-identity-missing";
81
+ return null;
82
+ }
83
+ /** Repair text beside the predicate that decides it. The runtime reasons keep theirs on the runtime
84
+ * leaf; `mux-fresh-call` spreads both tables. */
85
+ export const PI_DURABLE_PREFLIGHT_HINT = {
86
+ "pi-durable-model-not-provider-qualified": "a pi-durable model is `<provider>/<model id>` — the durable bootstrap resolves no provider by inference",
87
+ "pi-durable-first-input-too-long": "the framed first input exceeds the 16000-character bootstrap payload cap — shorten the task",
88
+ "pi-durable-birth-unit-missing": "this Entwurf package has no compiled pi-durable contact (mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js), so the sibling would mint no record — reinstall or rebuild Entwurf",
89
+ "pi-durable-mcp-hand-missing": "this Entwurf package has no compiled entwurf-bridge entry, so the sibling's callback tool would not exist — reinstall or rebuild Entwurf",
90
+ "pi-durable-receive-unit-missing": "this Entwurf package's compiled owner join does not admit pi-durable-host, so nothing could ever be delivered to the sibling — reinstall or rebuild Entwurf",
91
+ "pi-durable-visible-identity-missing": "this Entwurf package has no packaged pi-durable bootstrap (pi/pi-durable/bootstrap.mjs), so no TUI would carry the garden id — reinstall Entwurf",
92
+ };