@junghanacs/entwurf 0.30.0 → 0.31.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 (69) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +229 -0
  3. package/DELIVERY.md +1 -0
  4. package/README.md +49 -4
  5. package/VERIFY.md +23 -4
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +111 -18
  8. package/docs/setup-clean-host.md +104 -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/acp/backend-adapter.js +43 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
  20. package/mcp/entwurf-bridge/src/index.ts +7 -7
  21. package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
  22. package/package.json +15 -12
  23. package/pi/entwurf-capabilities.json +1 -0
  24. package/pi/pi-durable/bootstrap.mjs +281 -0
  25. package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
  26. package/pi/pi-durable/overlay/upstream-pin.json +16 -0
  27. package/pi-extensions/lib/acp/backend-adapter.ts +68 -0
  28. package/pi-extensions/lib/acp/backend.ts +41 -5
  29. package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
  30. package/pi-extensions/lib/fresh-call-composition.ts +42 -7
  31. package/pi-extensions/lib/meta-sender-identity.ts +7 -1
  32. package/pi-extensions/lib/meta-session.ts +28 -2
  33. package/pi-extensions/lib/mux-fresh-call.ts +34 -2
  34. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
  35. package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
  36. package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
  37. package/run.sh +214 -36
  38. package/scripts/check-fresh-call-dispatch.ts +18 -0
  39. package/scripts/check-gate-qualification.ts +4 -2
  40. package/scripts/check-herdr-plugin-build.ts +159 -57
  41. package/scripts/check-pi-durable-contact.ts +834 -0
  42. package/scripts/check-pi-durable-receive.ts +1116 -0
  43. package/scripts/check-pi-durable-send.ts +1661 -0
  44. package/scripts/fixtures/herdr-supply.json +8 -8
  45. package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
  46. package/scripts/lib/pi-durable-r1.ts +100 -0
  47. package/scripts/lib/pi-durable-reap.ts +27 -0
  48. package/scripts/lib/pi-durable-receive.ts +137 -0
  49. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  50. package/scripts/mutants/acp-vendor-rejection.json +62 -0
  51. package/scripts/mutants/control-send-receipt.json +1 -1
  52. package/scripts/mutants/copilot-birth.json +2 -2
  53. package/scripts/mutants/herdr-plugin-build.json +11 -0
  54. package/scripts/mutants/herdr-supply.json +3 -3
  55. package/scripts/mutants/meta-hook-session-switch.json +4 -2
  56. package/scripts/mutants/mux-fresh-call.json +69 -3
  57. package/scripts/mutants/omp-birth.json +2 -2
  58. package/scripts/mutants/omp-fresh.json +6 -4
  59. package/scripts/mutants/pack-install.json +3 -3
  60. package/scripts/mutants/pi-durable.json +390 -0
  61. package/scripts/mutants/pi-floor.json +2 -2
  62. package/scripts/pi-durable-contact-host.mjs +105 -0
  63. package/scripts/pi-durable-receive-host.mjs +127 -0
  64. package/scripts/pi-durable-send-host.mjs +213 -0
  65. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  66. package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
  67. package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
  68. package/scripts/smoke-setup-verdict.sh +21 -2
  69. package/scripts/tsconfig.json +1 -0
@@ -752,8 +752,8 @@ server.tool(
752
752
  "Open ONE fresh visible sibling beside you and hand it a first task. WHERE it opens is decided by where THIS " +
753
753
  "agent runs, never by a parameter: inside herdr (HERDR_ENV=1) it opens a NEW UNFOCUSED TAB in your own " +
754
754
  "herdr workspace, pi and claude-code " +
755
- "only; everywhere else it opens in the operator's tmux with all five backends (pi, " +
756
- "claude-code, copilot, omp, codex). No fallback: an incomplete herdr context is refused by name, " +
755
+ "only; everywhere else it opens in the operator's tmux with all six backends (pi, " +
756
+ "claude-code, copilot, omp, codex, pi-durable). No fallback: an incomplete herdr context is refused by name, " +
757
757
  "never by a tmux window you cannot see from herdr. The sibling's FIRST action " +
758
758
  "is a callback to you carrying a nonce, whose sender envelope is its garden id — that is how you learn the " +
759
759
  "address of a thing that did not exist a moment ago. This returns a LAUNCH receipt (the owner's " +
@@ -761,8 +761,8 @@ server.tool(
761
761
  "the task was delivered. Those coordinates are a VIEW, never an address — a herdr tab/pane id can change under " +
762
762
  "a running sibling. Nothing polls. " +
763
763
  "For EXISTING citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the chosen " +
764
- "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 " +
765
- "opens when their birth, MCP, receive or visible-identity units are absent, and codex also needs the " +
764
+ "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 " +
765
+ "opens when their birth, MCP, receive or visible-identity units are absent, pi-durable also when its operator-provided pinned runtime is absent or drifted, and codex also needs the " +
766
766
  "operator-owned app-server socket entwurf never starts; in herdr they are refused by name first. " +
767
767
  "Optional placement.tmuxSession is a TMUX-ONLY seat naming ONE EXISTING session and " +
768
768
  "ALWAYS wins; omitted, the seat follows the CALLER, never the backend opened: a CODEX CALLER opens beside " +
@@ -771,9 +771,9 @@ server.tool(
771
771
  "processes.",
772
772
  {
773
773
  backend: z
774
- .enum(["pi", "claude-code", "copilot", "omp", "codex"])
774
+ .enum(["pi", "claude-code", "copilot", "omp", "codex", "pi-durable"])
775
775
  .describe(
776
- "Which fixed runtime to open. Only these five, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command.",
776
+ "Which fixed runtime to open. Only these six, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command.",
777
777
  ),
778
778
  model: z
779
779
  .string()
@@ -789,7 +789,7 @@ server.tool(
789
789
  // biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above
790
790
  .regex(/^[A-Za-z0-9][A-Za-z0-9._/:\[\]-]*$/)
791
791
  .describe(
792
- "Required runtime model: canonical provider/model for pi, a Claude Code model id/alias, or a Copilot/OMP/Codex model name.",
792
+ "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.",
793
793
  ),
794
794
  task: z
795
795
  .string()
@@ -122,6 +122,12 @@
122
122
  // twin would make the one-command launch die before its first line. Its only repo import is
123
123
  // native-push/codex-ws-client.ts, ALREADY emitted for meta-bridge-hook-codex, so this adds ONE
124
124
  // leaf JS and ZERO new deps.
125
+ // WHY meta-bridge-pi-durable rides THIS build (#129 W, L4): the packaged durable bootstrap
126
+ // (pi/pi-durable/bootstrap.mjs) imports the contact from THIS closure — the same closure whose
127
+ // bridge entry the contact spawns and whose meta-bridge-omp.js decodes the first-input payload —
128
+ // so an installed Entwurf never imports checkout TypeScript. Its only repo import is
129
+ // meta-session, ALREADY emitted for the bridge; it is never in index.ts's boot closure. The runtime locator/verifier and the fresh preflight arrive
130
+ // through the import graph (mux-fresh-call) and need no entry here.
125
131
  // WHY meta-bridge-omp rides THIS build (#87): `install-omp-bridge` explicitly SELECTS
126
132
  // `dist/pi-extensions/meta-bridge-omp.js` when REPO_DIR is under node_modules — the same
127
133
  // strip-types fence the Claude and Copilot birth entries cross — and
@@ -149,6 +155,7 @@
149
155
  "../../pi-extensions/meta-bridge-hook-copilot.ts",
150
156
  "../../pi-extensions/meta-bridge-hook-codex.ts",
151
157
  "../../pi-extensions/meta-bridge-omp.ts",
152
- "../../pi-extensions/meta-bridge-receive-omp.ts"
158
+ "../../pi-extensions/meta-bridge-receive-omp.ts",
159
+ "../../pi-extensions/meta-bridge-pi-durable.ts"
153
160
  ]
154
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for pi, Claude Code, Copilot CLI, OMP, Antigravity, and Codex CLI; Claude and Snowflake Cortex Code ACP backends.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -52,6 +52,9 @@
52
52
  "docs/assets/entwurf-entwurf.gif",
53
53
  "pi/entwurf-capabilities.json",
54
54
  "pi/settings.reference.json",
55
+ "pi/pi-durable/bootstrap.mjs",
56
+ "pi/pi-durable/overlay/upstream-pin.json",
57
+ "pi/pi-durable/overlay/runtime-contacts.patch",
55
58
  "pi/meta-bridge/.claude-plugin/marketplace.json",
56
59
  "pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json",
57
60
  "pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json",
@@ -84,25 +87,25 @@
84
87
  "claudeCodeFloor": ">=2.1.217"
85
88
  },
86
89
  "dependencies": {
87
- "@agentclientprotocol/claude-agent-acp": "0.79.0",
88
- "@agentclientprotocol/sdk": "1.4.0",
90
+ "@agentclientprotocol/claude-agent-acp": "0.85.1",
91
+ "@agentclientprotocol/sdk": "1.6.0",
89
92
  "@anthropic-ai/sdk": "0.100.1",
90
93
  "@modelcontextprotocol/sdk": "^1.12.1",
91
94
  "smol-toml": "^1.8.0",
92
95
  "zod": "^3.25.0 || ^4.0.0"
93
96
  },
94
97
  "peerDependencies": {
95
- "@earendil-works/pi-ai": ">=1.0.0 <1.1",
96
- "@earendil-works/pi-coding-agent": ">=1.0.0 <1.1",
97
- "@earendil-works/pi-tui": ">=1.0.0 <1.1",
98
+ "@earendil-works/pi-ai": ">=1.0.2 <1.1",
99
+ "@earendil-works/pi-coding-agent": ">=1.0.2 <1.1",
100
+ "@earendil-works/pi-tui": ">=1.0.2 <1.1",
98
101
  "typebox": "*"
99
102
  },
100
103
  "devDependencies": {
101
104
  "@biomejs/biome": "^2.4.13",
102
- "@earendil-works/pi-agent-core": "1.0.0",
103
- "@earendil-works/pi-ai": "1.0.0",
104
- "@earendil-works/pi-coding-agent": "1.0.0",
105
- "@earendil-works/pi-tui": "1.0.0",
105
+ "@earendil-works/pi-agent-core": "1.0.2",
106
+ "@earendil-works/pi-ai": "1.0.2",
107
+ "@earendil-works/pi-coding-agent": "1.0.2",
108
+ "@earendil-works/pi-tui": "1.0.2",
106
109
  "@types/node": "^24.3.0",
107
110
  "husky": "^9.1.7",
108
111
  "rregex": "1.13.1",
@@ -113,7 +116,7 @@
113
116
  "prepare": "husky 1>&2 || true; npm run --silent build-bridge 1>&2",
114
117
  "postinstall": "node scripts/postinstall-chmod.cjs",
115
118
  "build-bridge": "bash scripts/with-dist-lock.sh bash scripts/build-bridge.sh",
116
- "prepack": "pnpm --silent run build-bridge",
119
+ "prepack": "pnpm run build-bridge 1>&2",
117
120
  "prepublishOnly": "pnpm run check:full && pnpm run check-pack-install",
118
121
  "setup": "./run.sh setup",
119
122
  "check-shell-quote": "./run.sh check-shell-quote",
@@ -177,7 +180,7 @@
177
180
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
178
181
  "check:toolchain": "pnpm lint && pnpm typecheck",
179
182
  "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior",
180
- "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
183
+ "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside",
181
184
  "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-pi-bridge-sender && ./run.sh check-pi-mcp-register && ./run.sh check-pi-mcp-bridge && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
182
185
  "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-pack-consumer-roots && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
183
186
  },
@@ -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
  }
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ // Durable host bootstrap for Entwurf's pi-durable contact (#129). PACKAGED: this file ships in the
3
+ // package `files`, and every Entwurf module it imports is the COMPILED closure that ships beside it
4
+ // (`mcp/entwurf-bridge/dist/…`) — an installed package never imports checkout TypeScript. The bridge
5
+ // the contact spawns is derived from that same closure, never from argv. A checkout runs it the same
6
+ // way, after `pnpm run build-bridge`; `plugins/pi-durable/bootstrap.mjs` re-exports this file.
7
+ //
8
+ // The same shape as upstream `experimental/durable/main.ts` (parse, open, run the TUI, close),
9
+ // with these differences:
10
+ // - Entwurf's tools-only extension is handed to `openDurable`;
11
+ // - a NEW session is opened with an explicit model and an explicit width, never a default (below);
12
+ // - the citizen is born once `openDurable` returns, with the overlay's `submitToRoot` so its
13
+ // doorbell can announce mail to the root conversation;
14
+ // - a launcher's first input, when given, is admitted to the root ONCE, after that birth;
15
+ // - the TUI is handed the native view with the garden id on every conversation label
16
+ // (`withGardenIdentity`).
17
+ // Nothing of the native registry, controller or TUI is rebuilt here.
18
+ //
19
+ // It runs under the operator-provided runtime's own source resolver, which is what selects the
20
+ // runtime — this file carries no path into it. `entwurf pi-durable` (run.sh) locates that runtime
21
+ // at its one fixed place, verifies it against the pin, and execs:
22
+ //
23
+ // node --import <runtime>/packages/coding-agent/src/experimental/source-resolver.ts \
24
+ // <entwurf>/pi/pi-durable/bootstrap.mjs \
25
+ // --provider <provider> --model <model id> --width task-wide [--entwurf-bootstrap <json>]
26
+ // ... bootstrap.mjs --continue
27
+ //
28
+ // Explicit means the native API, not a vendor argv: upstream's durable main takes only
29
+ // `--continue`, so `--provider`/`--model` reach pi's own `findInitialAgentModel` through the
30
+ // overlay's `model` option, for a new session only, and an unresolvable pair fails the open
31
+ // before any citizen is born (the native session directory may already exist by then). The model
32
+ // must be one registered catalog model of that provider, named by its canonical id or by
33
+ // `id:<level>` as pi's own parser reads a thinking level: a typo, a partial or case-variant match, a
34
+ // provider-prefixed id or an unregistered custom-id fallback is refused by the overlay as
35
+ // `explicit-model-not-exact`, before the birth and the first input.
36
+ // `--width task-wide` is the one width: no tool filter, so the model is offered exactly the tools
37
+ // the installed extensions register. It is a statement of what is OFFERED, not an approval or
38
+ // sandbox policy; the native app has no approval prompt, so a narrower width would be a sibling
39
+ // that can never reach its task's tools, and none is offered.
40
+ // `--entwurf-bootstrap` is the existing fixed `{v,task}` payload grammar (meta-bridge-omp.ts,
41
+ // read-only reuse): unknown keys — a target or a nonce included — are refused; the caller's
42
+ // address rides the process env only.
43
+ // `--continue` opens the newest session for this directory as upstream does (not a chosen
44
+ // record), so every flag that only a new session can honour is refused beside it by name.
45
+ //
46
+ // Without that resolver the two pi-coding-agent experimental subpath imports below fail: the
47
+ // published package ships no experimental source, and that failure is the honest one. They are the
48
+ // one sanctioned pi private-subpath exception (check-pi-import-surface, #129).
49
+ import { realpathSync } from "node:fs";
50
+ import { fileURLToPath } from "node:url";
51
+ import { decodeOmpBootstrapPayload } from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js";
52
+ import {
53
+ closeAll,
54
+ createPiDurableContact,
55
+ failAfterCleanup,
56
+ withGardenIdentity,
57
+ } from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js";
58
+
59
+ const RUNTIME = "@earendil-works/pi-coding-agent/experimental/durable/runtime.ts";
60
+ const TUI = "@earendil-works/pi-coding-agent/experimental/durable/tui.ts";
61
+
62
+ /** The entwurf-bridge of THIS package closure: the one the contact spawns as its MCP hand. */
63
+ export const PACKAGED_BRIDGE_ENTRY = fileURLToPath(
64
+ new URL("../../mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js", import.meta.url),
65
+ );
66
+
67
+ /** Flags that take a value. Anything else but `--continue`/`-c` is an unknown argument — a bridge
68
+ * entry included: the launch argv cannot point the contact at another bridge. */
69
+ const VALUE_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap"];
70
+ /** Flags only a NEW session can honour. */
71
+ const FRESH_ONLY_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap"];
72
+
73
+ /** A refusal with a stable name first, so a launcher and an operator can tell the repairs apart. */
74
+ function refuse(reason, detail) {
75
+ return Object.assign(new Error(`${reason}: ${detail}`), { reason });
76
+ }
77
+
78
+ const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
79
+
80
+ /**
81
+ * Open the durable app with the contact installed, then birth the citizen and arm its doorbell on
82
+ * the root conversation, then admit the first input (when given) to the root once. On any failure
83
+ * the contact is told why (waiting tools reject by name), everything opened is closed again, and a
84
+ * cleanup failure is reported beside the cause instead of replacing it.
85
+ *
86
+ * `model` and `firstInput` apply to a new session only; with `continueSession` they are refused
87
+ * before anything opens. `contact` carries extra `createPiDurableContact` options (explicit garden
88
+ * roots; tests also pass a connector). `open` replaces the runtime's `openDurable` for tests.
89
+ * `bridgeEntry` defaults to this package's own compiled bridge; only a programmatic caller (the
90
+ * checkout-only gates, which drive a private bundle) passes another one — never the launch argv.
91
+ *
92
+ * @param {{
93
+ * bridgeEntry?: string,
94
+ * continueSession?: boolean,
95
+ * cwd?: string,
96
+ * model?: { provider: string, model: string },
97
+ * firstInput?: string,
98
+ * contact?: Record<string, unknown>,
99
+ * open?: (options: { cwd: string, continueSession: boolean, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
100
+ * }} options
101
+ * @returns {Promise<{ durable: any, contact: any, attachment: { gardenId: string } & Record<string, any>, firstInput?: any, close(): Promise<void> }>}
102
+ * (stated, because the compiled closure carries no declarations: a cleanup path that rethrows
103
+ * through `failAfterCleanup` never resolves)
104
+ */
105
+ export async function openDurableCitizen({
106
+ bridgeEntry = PACKAGED_BRIDGE_ENTRY,
107
+ continueSession = false,
108
+ cwd = process.cwd(),
109
+ model,
110
+ firstInput,
111
+ contact: contactOptions = {},
112
+ open,
113
+ }) {
114
+ if (continueSession && (model !== undefined || firstInput !== undefined)) {
115
+ throw refuse(
116
+ "fresh-only-argument-with-continue",
117
+ "an explicit model and a first input apply only to a new session; a continued session is reopened as it was saved",
118
+ );
119
+ }
120
+ if (model !== undefined && !(nonEmpty(model.provider) && nonEmpty(model.model))) {
121
+ throw refuse("explicit-model-incomplete", "an explicit model names both its provider and its model id");
122
+ }
123
+ if (firstInput !== undefined && !nonEmpty(firstInput)) {
124
+ throw refuse("first-input-empty", "a first input is a non-empty task");
125
+ }
126
+ const openDurable = open ?? (await import(RUNTIME)).openDurable;
127
+ const contact = createPiDurableContact({ ...contactOptions, bridgeEntry });
128
+ let durable;
129
+ try {
130
+ durable = await openDurable({
131
+ cwd,
132
+ continueSession,
133
+ extensions: [contact.extension],
134
+ ...(model === undefined ? {} : { model }),
135
+ });
136
+ } catch (error) {
137
+ contact.fail(error);
138
+ throw error;
139
+ }
140
+ let attachment;
141
+ try {
142
+ attachment = await contact.attach(durable.view.current().session, { submitToRoot: durable.submitToRoot });
143
+ } catch (error) {
144
+ return failAfterCleanup(
145
+ error,
146
+ () => contact.close(),
147
+ () => durable.close(),
148
+ );
149
+ }
150
+ // After the birth, so a callback the input asks for finds a citizen. Admission, not the answer:
151
+ // the turn runs under the TUI. A refused admission is the cause, never a notice.
152
+ let first;
153
+ if (firstInput !== undefined) {
154
+ try {
155
+ first = await durable.submitToRoot({
156
+ type: "input",
157
+ content: firstInput,
158
+ requestId: `entwurf-first-input:${attachment.gardenId}`,
159
+ });
160
+ } catch (error) {
161
+ return failAfterCleanup(
162
+ error,
163
+ () => contact.close(),
164
+ () => durable.close(),
165
+ );
166
+ }
167
+ }
168
+ let closing;
169
+ return {
170
+ durable,
171
+ contact,
172
+ attachment,
173
+ ...(first === undefined ? {} : { firstInput: first }),
174
+ close() {
175
+ closing ??= closeAll(
176
+ () => contact.close(),
177
+ () => durable.close(),
178
+ );
179
+ return closing;
180
+ },
181
+ };
182
+ }
183
+
184
+ function parseArgs(argv) {
185
+ let continueSession = false;
186
+ const values = new Map();
187
+ for (let i = 0; i < argv.length; i++) {
188
+ const arg = argv[i];
189
+ if (arg === "--continue" || arg === "-c") {
190
+ continueSession = true;
191
+ continue;
192
+ }
193
+ if (!VALUE_FLAGS.includes(arg)) throw new Error(`Unknown argument: ${arg}`);
194
+ if (values.has(arg)) throw refuse("argument-repeated", `${arg} is given more than once`);
195
+ const value = argv[++i];
196
+ if (value === undefined || value === "" || value.startsWith("-")) {
197
+ throw refuse("argument-value-missing", `${arg} needs a value`);
198
+ }
199
+ values.set(arg, value);
200
+ }
201
+ const fresh = FRESH_ONLY_FLAGS.filter((flag) => values.has(flag));
202
+ if (continueSession && fresh.length > 0) {
203
+ throw refuse(
204
+ "fresh-only-argument-with-continue",
205
+ `${fresh.join(", ")} apply only to a new session; --continue reopens this directory's newest session as it was saved`,
206
+ );
207
+ }
208
+ if (continueSession) return { continueSession };
209
+ const provider = values.get("--provider");
210
+ if (provider === undefined) {
211
+ throw refuse(
212
+ "explicit-provider-required",
213
+ "a new session names its provider with --provider; a model is never resolved to a provider by inference",
214
+ );
215
+ }
216
+ const model = values.get("--model");
217
+ if (model === undefined) throw refuse("explicit-model-required", "a new session names its model with --model");
218
+ const width = values.get("--width");
219
+ if (width !== "task-wide") {
220
+ throw refuse(
221
+ width === undefined ? "width-required" : "width-unsupported",
222
+ "a new session is opened with --width task-wide: every tool the installed extensions register is offered",
223
+ );
224
+ }
225
+ const payload = values.get("--entwurf-bootstrap");
226
+ let firstInput;
227
+ if (payload !== undefined) {
228
+ const decoded = decodeOmpBootstrapPayload(payload);
229
+ if (!decoded.ok) throw refuse("bootstrap-payload-rejected", decoded.reason);
230
+ firstInput = decoded.value.task;
231
+ }
232
+ return {
233
+ continueSession,
234
+ model: { provider, model },
235
+ ...(firstInput === undefined ? {} : { firstInput }),
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Parse, load the TUI, open, run, close. `loadTui`, `open` and `contact` are test seams.
241
+ *
242
+ * @param {readonly string[]} argv
243
+ * @param {{
244
+ * loadTui?: () => Promise<{ runDurableTui: (...args: any[]) => Promise<void> }>,
245
+ * open?: (options: any) => Promise<any>,
246
+ * contact?: Record<string, unknown>,
247
+ * }} [seams]
248
+ */
249
+ export async function main(argv, { loadTui = () => import(TUI), open, contact } = {}) {
250
+ // Every refusal is decided here, before the TUI loads and before anything opens.
251
+ const args = parseArgs(argv);
252
+ // Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot
253
+ // load opens no session, mints no citizen and leaves nothing to clean up.
254
+ const { runDurableTui } = await loadTui();
255
+ const citizen = await openDurableCitizen({ ...args, open, contact });
256
+ try {
257
+ await runDurableTui(
258
+ withGardenIdentity(citizen.durable.view, citizen.attachment.gardenId),
259
+ citizen.durable.controller,
260
+ citizen.durable.settings,
261
+ );
262
+ } catch (error) {
263
+ return failAfterCleanup(error, () => citizen.close());
264
+ }
265
+ await citizen.close();
266
+ }
267
+
268
+ // Run only when THIS file is the process entry (`node --import <resolver> bootstrap.mjs …`), decided
269
+ // by an argv realpath as in meta-bridge-hook-codex.ts — not by the `main` flag on import.meta, which
270
+ // Node gained in 24.2.0, above the package's major-24 floor. Importing this file starts nothing.
271
+ const invokedDirectly = (() => {
272
+ const entry = process.argv[1];
273
+ if (typeof entry !== "string" || entry.length === 0) return false;
274
+ try {
275
+ return realpathSync(entry) === realpathSync(import.meta.filename);
276
+ } catch {
277
+ // Bounded environment probe: an entry that cannot be resolved is not this file.
278
+ return false;
279
+ }
280
+ })();
281
+ if (invokedDirectly) await main(process.argv.slice(2));
@@ -0,0 +1,91 @@
1
+ diff --git a/packages/coding-agent/src/experimental/durable/runtime.ts b/packages/coding-agent/src/experimental/durable/runtime.ts
2
+ index fcd6e523..929dbc79 100644
3
+ --- a/packages/coding-agent/src/experimental/durable/runtime.ts
4
+ +++ b/packages/coding-agent/src/experimental/durable/runtime.ts
5
+ @@ -8,7 +8,9 @@ import {
6
+ type ConversationView,
7
+ type Cursor,
8
+ type EntryRecord,
9
+ + type Extension,
10
+ Harness,
11
+ + type InputSubmissionDraft,
12
+ type ModelRef,
13
+ ROOT_CONVERSATION_ID,
14
+ type Submission,
15
+ @@ -81,6 +83,10 @@ export interface DurableController {
16
+ export interface OpenDurableOptions {
17
+ readonly cwd?: string;
18
+ readonly continueSession?: boolean;
19
+ + /** Installed after the built-in extensions and before the Harness opens, so recovered work finds their tools. */
20
+ + readonly extensions?: readonly Extension[];
21
+ + /** A new session's explicit model, resolved as pi's `--provider`/`--model`; unresolved, the open fails. */
22
+ + readonly model?: { readonly provider: string; readonly model: string };
23
+ }
24
+
25
+ export interface OpenDurableResult {
26
+ @@ -88,6 +94,8 @@ export interface OpenDurableResult {
27
+ readonly controller: DurableController;
28
+ /** pi's settings, for the TUI's theme and terminal capabilities. */
29
+ readonly settings: SettingsManager;
30
+ + /** Admits input to the root conversation, whichever one is on screen; resolves at admission, not at the answer. */
31
+ + submitToRoot(draft: InputSubmissionDraft): Promise<Submission>;
32
+ close(): Promise<void>;
33
+ }
34
+
35
+ @@ -122,6 +130,9 @@ function titleOf(entry: EntryRecord | undefined): { title?: string } {
36
+ }
37
+
38
+ export async function openDurable(options: OpenDurableOptions = {}): Promise<OpenDurableResult> {
39
+ + if (options.model !== undefined && options.continueSession) {
40
+ + throw new Error("An explicit model applies only to a new session, not to a continued one");
41
+ + }
42
+ const location = await selectSession(options.cwd ?? process.cwd(), options.continueSession ?? false);
43
+ const envs = new ExecutionEnvs(location.cwd);
44
+ let harness: Harness | undefined;
45
+ @@ -132,6 +143,7 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
46
+ const settings = createHarnessSettings(settingsManager);
47
+ const registry = createCodingRegistry(settingsManager, location.cwd);
48
+ registry.install(Subagent);
49
+ + for (const extension of options.extensions ?? []) registry.install(extension);
50
+
51
+ const pendingReports: unknown[] = [];
52
+ let report: (error: unknown) => void = (error) => pendingReports.push(error);
53
+ @@ -146,7 +158,29 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
54
+ },
55
+ context,
56
+ );
57
+ - const initial = location.created ? await findInitialAgentModel(settingsManager, modelRuntime) : undefined;
58
+ + const initial = location.created
59
+ + ? await findInitialAgentModel(settingsManager, modelRuntime, options.model)
60
+ + : undefined;
61
+ + if (options.model !== undefined) {
62
+ + // pi resolves case-insensitive, partial and unknown ids too (a custom-id fallback with only a warning);
63
+ + // an explicit model must be exactly one registered catalog model, named by its canonical id.
64
+ + const asked = options.model;
65
+ + const ref = initial?.model;
66
+ + const thinking = initial?.thinkingLevel;
67
+ + const registered = ref !== undefined && modelRuntime.getModel(ref.provider, ref.modelId) !== undefined;
68
+ + const named =
69
+ + ref !== undefined &&
70
+ + (asked.model === ref.modelId || (thinking !== undefined && asked.model === `${ref.modelId}:${thinking}`));
71
+ + if (ref === undefined || ref.provider !== asked.provider || !registered || !named) {
72
+ + const resolved =
73
+ + ref === undefined
74
+ + ? "none"
75
+ + : `${ref.provider}/${ref.modelId}${thinking === undefined ? "" : `:${thinking}`}`;
76
+ + throw new Error(
77
+ + `explicit-model-not-exact: asked ${asked.provider}/${asked.model}, resolved ${resolved} (registered=${registered})`,
78
+ + );
79
+ + }
80
+ + }
81
+ const root = await harness.root(context, {
82
+ agent: {
83
+ cwd: location.cwd,
84
+ @@ -342,6 +376,7 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
85
+ },
86
+ controller,
87
+ settings: settingsManager,
88
+ + submitToRoot: (draft) => root.submit(draft, context),
89
+ close() {
90
+ closing ??= (async () => {
91
+ unsubscribe();
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "comment": "The one upstream commit this overlay is maintained against, and the patch it applies. The experimental durable app is source-only upstream, so its runtime is a pinned checkout plus this patch, provisioned by the operator; Entwurf ships neither. A checkout's TRACKED tree counts as this overlay only when HEAD equals commit, `git diff` equals the patch bytes exactly, and no non-ignored file is untracked. Git-ignored runtime inputs are outside that equality and are described separately below.",
4
+ "repository": "https://github.com/earendil-works/pi",
5
+ "commit": "cd32f7725fdbddbaecdff5b1e68491563394e0ca",
6
+ "patches": ["runtime-contacts.patch"],
7
+ "modelData": {
8
+ "comment": "Git-ignored generated model data the runtime imports at open. Upstream hydrates it by calling vendor model-list endpoints; this overlay instead takes the bytes the v1.0.2 release built from this commit, from the published package below, with no network. aggregateSha256 is sha256 over one `<sha256 of file> <file name>\\n` line per *.json file (dotfiles excluded), names in C byte order. upstream `packages/ai/scripts/check-model-data.ts` validates these bytes against the manifest.",
9
+ "path": "packages/ai/src/providers/data",
10
+ "source": "@earendil-works/pi-ai@1.0.2 dist/providers/data",
11
+ "integrity": "sha512-JP59xGlSAQ/HhQ6EHN5qmneBlWfDyu6FPPryAvAaSo8A38ubAsEpS9YisCM5Jbbns0srrDffLZonE0OXorHI5A==",
12
+ "files": 42,
13
+ "aggregateSha256": "e71a1965a81c29fbe83d6e90dc1f4595602588281374603dee9e3bcbb7f46fa4",
14
+ "manifestSha256": "d373b99c0860f84281d786d493e8d8237c0a9fd98f142ef31c6db187c5089bc5"
15
+ }
16
+ }