@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
@@ -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 on named refusals of its installed carrier or exact SDK set, 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.1",
3
+ "version": "0.32.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,12 @@
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/carrier-relocation.mjs",
57
+ "pi/pi-durable/carrier-resolver.mjs",
58
+ "pi/pi-durable/carrier/",
59
+ "pi/pi-durable/overlay/upstream-pin.json",
60
+ "pi/pi-durable/overlay/runtime-contacts.patch",
55
61
  "pi/meta-bridge/.claude-plugin/marketplace.json",
56
62
  "pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json",
57
63
  "pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json",
@@ -87,22 +93,25 @@
87
93
  "@agentclientprotocol/claude-agent-acp": "0.85.1",
88
94
  "@agentclientprotocol/sdk": "1.6.0",
89
95
  "@anthropic-ai/sdk": "0.100.1",
96
+ "@earendil-works/chord": "1.0.4",
97
+ "@earendil-works/pi-agent-core": "1.0.4",
98
+ "@earendil-works/pi-ai": "1.0.4",
99
+ "@earendil-works/pi-codemode": "1.0.4",
100
+ "@earendil-works/pi-coding-agent": "1.0.4",
101
+ "@earendil-works/pi-durable": "1.0.4",
102
+ "@earendil-works/pi-mcp": "1.0.4",
103
+ "@earendil-works/pi-telemetry": "1.0.4",
104
+ "@earendil-works/pi-tui": "1.0.4",
90
105
  "@modelcontextprotocol/sdk": "^1.12.1",
106
+ "proper-lockfile": "4.1.2",
91
107
  "smol-toml": "^1.8.0",
92
108
  "zod": "^3.25.0 || ^4.0.0"
93
109
  },
94
110
  "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
111
  "typebox": "*"
99
112
  },
100
113
  "devDependencies": {
101
114
  "@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",
106
115
  "@types/node": "^24.3.0",
107
116
  "husky": "^9.1.7",
108
117
  "rregex": "1.13.1",
@@ -113,7 +122,7 @@
113
122
  "prepare": "husky 1>&2 || true; npm run --silent build-bridge 1>&2",
114
123
  "postinstall": "node scripts/postinstall-chmod.cjs",
115
124
  "build-bridge": "bash scripts/with-dist-lock.sh bash scripts/build-bridge.sh",
116
- "prepack": "pnpm --silent run build-bridge",
125
+ "prepack": "pnpm run build-bridge 1>&2",
117
126
  "prepublishOnly": "pnpm run check:full && pnpm run check-pack-install",
118
127
  "setup": "./run.sh setup",
119
128
  "check-shell-quote": "./run.sh check-shell-quote",
@@ -177,7 +186,7 @@
177
186
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
178
187
  "check:toolchain": "pnpm lint && pnpm typecheck",
179
188
  "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-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
189
+ "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
190
  "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
191
  "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
192
  },
@@ -198,15 +207,6 @@
198
207
  "entwurf-copilot-statusline": "./scripts/copilot-statusline.sh"
199
208
  },
200
209
  "peerDependenciesMeta": {
201
- "@earendil-works/pi-ai": {
202
- "optional": true
203
- },
204
- "@earendil-works/pi-coding-agent": {
205
- "optional": true
206
- },
207
- "@earendil-works/pi-tui": {
208
- "optional": true
209
- },
210
210
  "typebox": {
211
211
  "optional": true
212
212
  }
@@ -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,404 @@
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
+ // The durable app itself is this package's CARRIER (./carrier/, the pinned upstream files emitted by
20
+ // the maintainer — upstream publishes no experimental app), and it runs under the carrier's resolver,
21
+ // which maps the app's pi-coding-agent internals to the published package the carrier resolves.
22
+ // `entwurf pi-durable` (run.sh) verifies the carrier and its SDK set (pi-durable-runtime.ts) and
23
+ // execs:
24
+ //
25
+ // node --import <entwurf>/pi/pi-durable/carrier-resolver.mjs \
26
+ // <entwurf>/pi/pi-durable/bootstrap.mjs \
27
+ // --provider <provider> --model <model id> --width task-wide [--entwurf-bootstrap <json>]
28
+ // ... bootstrap.mjs --continue
29
+ //
30
+ // Explicit means the native API, not a vendor argv: upstream's durable main takes only
31
+ // `--continue`, so `--provider`/`--model` reach pi's own `findInitialAgentModel` through the
32
+ // overlay's `model` option, for a new session only, and an unresolvable pair fails the open
33
+ // before any citizen is born (the native session directory may already exist by then). The model
34
+ // must be one registered catalog model of that provider, named by its canonical id or by
35
+ // `id:<level>` as pi's own parser reads a thinking level: a typo, a partial or case-variant match, a
36
+ // provider-prefixed id or an unregistered custom-id fallback is refused by the overlay as
37
+ // `explicit-model-not-exact`, before the birth and the first input.
38
+ // `--width task-wide` is the one width: no tool filter, so the model is offered exactly the tools
39
+ // the installed extensions register. It is a statement of what is OFFERED, not an approval or
40
+ // sandbox policy; the native app has no approval prompt, so a narrower width would be a sibling
41
+ // that can never reach its task's tools, and none is offered.
42
+ // `--entwurf-bootstrap` is the existing fixed `{v,task}` payload grammar (meta-bridge-omp.ts,
43
+ // read-only reuse): unknown keys — a target or a nonce included — are refused; the caller's
44
+ // address rides the process env only.
45
+ // `--continue` opens the newest session for this directory as upstream does (not a chosen
46
+ // record), so every flag that only a new session can honour is refused beside it by name.
47
+ //
48
+ // `--native-module <absolute file>` (#130 P2) names ONE operator module, for a new or a continued
49
+ // session. It is an ordinary ES module: its own top-level evaluation (top-level await included) is
50
+ // its initialization, and its default export is a native durable Extension, handed to `openDurable`
51
+ // by reference after the contact. Initialization runs before the TUI or the runtime is imported —
52
+ // the runtime creates the provider runtime before it installs any extension (carrier/runtime.js
53
+ // 140-146), so an extension object cannot carry an environment phase. Entwurf checks only what the
54
+ // ingress needs: a default export object with a name, no extension name the app installs itself (a
55
+ // same-name install REPLACES), and no tool name of the contact. Every other field is the native
56
+ // registry's to validate. The module is trusted operator code: the identity carriers and the
57
+ // process directory are compared across its initialization and a change is refused by name — a
58
+ // mistake guard, not a sandbox, and nothing is rolled back. Nothing is discovered: no directory,
59
+ // setting, environment variable or profile names a module.
60
+ //
61
+ // Without that resolver the carrier's first pi-coding-agent internal import fails by name (an
62
+ // unsupported entwurf-pi-dist: URL scheme), and that failure is the honest one.
63
+ import { realpathSync, statSync } from "node:fs";
64
+ import { isAbsolute } from "node:path";
65
+ import { fileURLToPath, pathToFileURL } from "node:url";
66
+ import { decodeOmpBootstrapPayload } from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js";
67
+ import {
68
+ closeAll,
69
+ createPiDurableContact,
70
+ failAfterCleanup,
71
+ PI_DURABLE_EXTENSION_NAME,
72
+ PI_DURABLE_TOOL_NAMES,
73
+ withGardenIdentity,
74
+ } from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js";
75
+
76
+ const RUNTIME = "./carrier/runtime.js";
77
+ const TUI = "./carrier/tui.js";
78
+
79
+ /** The entwurf-bridge of THIS package closure: the one the contact spawns as its MCP hand. */
80
+ export const PACKAGED_BRIDGE_ENTRY = fileURLToPath(
81
+ new URL("../../mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js", import.meta.url),
82
+ );
83
+
84
+ /** Flags that take a value. Anything else but `--continue`/`-c` is an unknown argument — a bridge
85
+ * entry included: the launch argv cannot point the contact at another bridge. */
86
+ const VALUE_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap", "--native-module"];
87
+ /** Flags only a NEW session can honour. */
88
+ const FRESH_ONLY_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap"];
89
+
90
+ /**
91
+ * Extension names the durable app installs itself — the contact, then the carrier's own
92
+ * (`coding-tools` pi-durable/tools, `pi-prompt` carrier/prompt.js:70, `subagent`
93
+ * carrier/subagent.js:25). The native registry REPLACES an installed extension of the same name.
94
+ */
95
+ const RESERVED_EXTENSION_NAMES = [PI_DURABLE_EXTENSION_NAME, "coding-tools", "pi-prompt", "subagent"];
96
+ /** The identity carriers a module's initialization must leave as it found them, every ENTWURF_* included. */
97
+ const IDENTITY_KEYS = ["PI_SESSION_ID", "PI_CODING_AGENT_DIR", "HOME"];
98
+
99
+ /** A refusal with a stable name first, so a launcher and an operator can tell the repairs apart. */
100
+ function refuse(reason, detail) {
101
+ return Object.assign(new Error(`${reason}: ${detail}`), { reason });
102
+ }
103
+
104
+ const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
105
+
106
+ /** The identity carriers present now: names and values, compared in memory and never printed. */
107
+ function identitySnapshot() {
108
+ return Object.fromEntries(
109
+ Object.entries(process.env).filter(([key]) => IDENTITY_KEYS.includes(key) || key.startsWith("ENTWURF_")),
110
+ );
111
+ }
112
+
113
+ /** Keys changed, added or removed between two snapshots, sorted. */
114
+ function identityChanges(before, after) {
115
+ return [...new Set([...Object.keys(before), ...Object.keys(after)])]
116
+ .filter((key) => before[key] !== after[key])
117
+ .sort();
118
+ }
119
+
120
+ /** The operator's module path: absolute, resolving to a regular file. Decided before anything loads. */
121
+ function nativeModuleFile(value) {
122
+ if (!isAbsolute(value))
123
+ throw refuse("native-module-path-not-absolute", `--native-module takes an absolute path, got ${value}`);
124
+ let file;
125
+ try {
126
+ file = realpathSync(value);
127
+ } catch (error) {
128
+ throw Object.assign(refuse("native-module-path-invalid", `${value} does not resolve`), { cause: error });
129
+ }
130
+ if (!statSync(file).isFile()) throw refuse("native-module-path-invalid", `${value} is not a regular file`);
131
+ return file;
132
+ }
133
+
134
+ /** The module's default export, checked only for what the ingress needs and returned BY REFERENCE. */
135
+ function nativeExtensionOf(extension, file) {
136
+ if (typeof extension === "function") {
137
+ throw refuse(
138
+ "native-module-export-invalid",
139
+ `${file} exports a function by default — a classic ExtensionAPI factory is not a native durable extension; export the Extension object ({ name, tools?, … })`,
140
+ );
141
+ }
142
+ if (typeof extension !== "object" || extension === null || Array.isArray(extension)) {
143
+ throw refuse("native-module-export-invalid", `${file} has no default export object`);
144
+ }
145
+ if (!nonEmpty(extension.name)) throw refuse("native-module-export-invalid", `${file}'s default export has no name`);
146
+ if (RESERVED_EXTENSION_NAMES.includes(extension.name)) {
147
+ throw refuse(
148
+ "native-module-name-reserved",
149
+ `${file} names its extension ${extension.name}, which the durable app installs itself; the same name would replace it`,
150
+ );
151
+ }
152
+ const taken = (Array.isArray(extension.tools) ? extension.tools : [])
153
+ .map((tool) => tool?.name)
154
+ .filter((name) => PI_DURABLE_TOOL_NAMES.includes(name));
155
+ if (taken.length > 0) {
156
+ throw refuse("native-module-name-reserved", `${file} registers ${taken.join(", ")}, the contact's own tool name`);
157
+ }
158
+ return extension;
159
+ }
160
+
161
+ /**
162
+ * Initialize the operator's module and return its native Extension. The identity carriers and the
163
+ * process directory are read BEFORE its first line runs and compared once its evaluation completes.
164
+ */
165
+ async function loadNativeModule(file) {
166
+ const before = identitySnapshot();
167
+ const cwd = process.cwd();
168
+ let namespace;
169
+ try {
170
+ namespace = await import(pathToFileURL(file).href);
171
+ } catch (error) {
172
+ throw Object.assign(
173
+ refuse("native-module-import-failed", `${file}: ${error instanceof Error ? error.message : String(error)}`),
174
+ { cause: error },
175
+ );
176
+ }
177
+ const changed = identityChanges(before, identitySnapshot());
178
+ if (changed.length > 0) {
179
+ throw refuse(
180
+ "native-module-env-identity-changed",
181
+ `initializing ${file} changed ${changed.join(", ")}; a module sets its own environment, never an identity carrier`,
182
+ );
183
+ }
184
+ if (process.cwd() !== cwd) {
185
+ throw refuse("native-module-cwd-changed", `initializing ${file} moved the process out of ${cwd}`);
186
+ }
187
+ return nativeExtensionOf(namespace.default, file);
188
+ }
189
+
190
+ /**
191
+ * Open the durable app with the contact installed, then birth the citizen and arm its doorbell on
192
+ * the root conversation, then admit the first input (when given) to the root once. On any failure
193
+ * the contact is told why (waiting tools reject by name), everything opened is closed again, and a
194
+ * cleanup failure is reported beside the cause instead of replacing it.
195
+ *
196
+ * `model` and `firstInput` apply to a new session only; with `continueSession` they are refused
197
+ * before anything opens. `contact` carries extra `createPiDurableContact` options (explicit garden
198
+ * roots; tests also pass a connector). `open` replaces the runtime's `openDurable` for tests.
199
+ * `bridgeEntry` defaults to this package's own compiled bridge; only a programmatic caller (the
200
+ * checkout-only gates, which drive a private bundle) passes another one — never the launch argv.
201
+ * `extensions` are the operator's native extensions (`--native-module`), installed after the
202
+ * contact, by reference.
203
+ *
204
+ * @param {{
205
+ * bridgeEntry?: string,
206
+ * continueSession?: boolean,
207
+ * cwd?: string,
208
+ * model?: { provider: string, model: string },
209
+ * firstInput?: string,
210
+ * extensions?: readonly object[],
211
+ * contact?: Record<string, unknown>,
212
+ * open?: (options: { cwd: string, continueSession: boolean, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
213
+ * }} options
214
+ * @returns {Promise<{ durable: any, contact: any, attachment: { gardenId: string } & Record<string, any>, firstInput?: any, close(): Promise<void> }>}
215
+ * (stated, because the compiled closure carries no declarations: a cleanup path that rethrows
216
+ * through `failAfterCleanup` never resolves)
217
+ */
218
+ export async function openDurableCitizen({
219
+ bridgeEntry = PACKAGED_BRIDGE_ENTRY,
220
+ continueSession = false,
221
+ cwd = process.cwd(),
222
+ model,
223
+ firstInput,
224
+ extensions = [],
225
+ contact: contactOptions = {},
226
+ open,
227
+ }) {
228
+ if (continueSession && (model !== undefined || firstInput !== undefined)) {
229
+ throw refuse(
230
+ "fresh-only-argument-with-continue",
231
+ "an explicit model and a first input apply only to a new session; a continued session is reopened as it was saved",
232
+ );
233
+ }
234
+ if (model !== undefined && !(nonEmpty(model.provider) && nonEmpty(model.model))) {
235
+ throw refuse("explicit-model-incomplete", "an explicit model names both its provider and its model id");
236
+ }
237
+ if (firstInput !== undefined && !nonEmpty(firstInput)) {
238
+ throw refuse("first-input-empty", "a first input is a non-empty task");
239
+ }
240
+ const openDurable = open ?? (await import(RUNTIME)).openDurable;
241
+ const contact = createPiDurableContact({ ...contactOptions, bridgeEntry });
242
+ let durable;
243
+ try {
244
+ durable = await openDurable({
245
+ cwd,
246
+ continueSession,
247
+ extensions: [contact.extension, ...extensions],
248
+ ...(model === undefined ? {} : { model }),
249
+ });
250
+ } catch (error) {
251
+ contact.fail(error);
252
+ throw error;
253
+ }
254
+ let attachment;
255
+ try {
256
+ attachment = await contact.attach(durable.view.current().session, { submitToRoot: durable.submitToRoot });
257
+ } catch (error) {
258
+ return failAfterCleanup(
259
+ error,
260
+ () => contact.close(),
261
+ () => durable.close(),
262
+ );
263
+ }
264
+ // After the birth, so a callback the input asks for finds a citizen. Admission, not the answer:
265
+ // the turn runs under the TUI. A refused admission is the cause, never a notice.
266
+ let first;
267
+ if (firstInput !== undefined) {
268
+ try {
269
+ first = await durable.submitToRoot({
270
+ type: "input",
271
+ content: firstInput,
272
+ requestId: `entwurf-first-input:${attachment.gardenId}`,
273
+ });
274
+ } catch (error) {
275
+ return failAfterCleanup(
276
+ error,
277
+ () => contact.close(),
278
+ () => durable.close(),
279
+ );
280
+ }
281
+ }
282
+ let closing;
283
+ return {
284
+ durable,
285
+ contact,
286
+ attachment,
287
+ ...(first === undefined ? {} : { firstInput: first }),
288
+ close() {
289
+ closing ??= closeAll(
290
+ () => contact.close(),
291
+ () => durable.close(),
292
+ );
293
+ return closing;
294
+ },
295
+ };
296
+ }
297
+
298
+ function parseArgs(argv) {
299
+ let continueSession = false;
300
+ const values = new Map();
301
+ for (let i = 0; i < argv.length; i++) {
302
+ const arg = argv[i];
303
+ if (arg === "--continue" || arg === "-c") {
304
+ continueSession = true;
305
+ continue;
306
+ }
307
+ if (!VALUE_FLAGS.includes(arg)) throw new Error(`Unknown argument: ${arg}`);
308
+ if (values.has(arg)) throw refuse("argument-repeated", `${arg} is given more than once`);
309
+ const value = argv[++i];
310
+ if (value === undefined || value === "" || value.startsWith("-")) {
311
+ throw refuse("argument-value-missing", `${arg} needs a value`);
312
+ }
313
+ values.set(arg, value);
314
+ }
315
+ const fresh = FRESH_ONLY_FLAGS.filter((flag) => values.has(flag));
316
+ if (continueSession && fresh.length > 0) {
317
+ throw refuse(
318
+ "fresh-only-argument-with-continue",
319
+ `${fresh.join(", ")} apply only to a new session; --continue reopens this directory's newest session as it was saved`,
320
+ );
321
+ }
322
+ const moduleArg = values.get("--native-module");
323
+ const nativeModule = moduleArg === undefined ? {} : { nativeModule: nativeModuleFile(moduleArg) };
324
+ if (continueSession) return { continueSession, ...nativeModule };
325
+ const provider = values.get("--provider");
326
+ if (provider === undefined) {
327
+ throw refuse(
328
+ "explicit-provider-required",
329
+ "a new session names its provider with --provider; a model is never resolved to a provider by inference",
330
+ );
331
+ }
332
+ const model = values.get("--model");
333
+ if (model === undefined) throw refuse("explicit-model-required", "a new session names its model with --model");
334
+ const width = values.get("--width");
335
+ if (width !== "task-wide") {
336
+ throw refuse(
337
+ width === undefined ? "width-required" : "width-unsupported",
338
+ "a new session is opened with --width task-wide: every tool the installed extensions register is offered",
339
+ );
340
+ }
341
+ const payload = values.get("--entwurf-bootstrap");
342
+ let firstInput;
343
+ if (payload !== undefined) {
344
+ const decoded = decodeOmpBootstrapPayload(payload);
345
+ if (!decoded.ok) throw refuse("bootstrap-payload-rejected", decoded.reason);
346
+ firstInput = decoded.value.task;
347
+ }
348
+ return {
349
+ continueSession,
350
+ model: { provider, model },
351
+ ...(firstInput === undefined ? {} : { firstInput }),
352
+ ...nativeModule,
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Parse, initialize the operator's module, load the TUI, open, run, close. `loadTui`, `open` and
358
+ * `contact` are test seams; `bridgeEntry` is the checkout-only gates' private bundle, as in
359
+ * `openDurableCitizen` — never the launch argv.
360
+ *
361
+ * @param {readonly string[]} argv
362
+ * @param {{
363
+ * loadTui?: () => Promise<{ runDurableTui: (...args: any[]) => Promise<void> }>,
364
+ * open?: (options: any) => Promise<any>,
365
+ * contact?: Record<string, unknown>,
366
+ * bridgeEntry?: string,
367
+ * }} [seams]
368
+ */
369
+ export async function main(argv, { loadTui = () => import(TUI), open, contact, bridgeEntry } = {}) {
370
+ // Every argument refusal is decided here, before any module, the TUI or the app loads.
371
+ const { nativeModule, ...args } = parseArgs(argv);
372
+ // The caller's directory, fixed before any operator code runs and handed to the app explicitly.
373
+ const cwd = process.cwd();
374
+ const extensions = nativeModule === undefined ? [] : [await loadNativeModule(nativeModule)];
375
+ // Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot
376
+ // load opens no session, mints no citizen and leaves nothing to clean up.
377
+ const { runDurableTui } = await loadTui();
378
+ const citizen = await openDurableCitizen({ ...args, cwd, extensions, bridgeEntry, open, contact });
379
+ try {
380
+ await runDurableTui(
381
+ withGardenIdentity(citizen.durable.view, citizen.attachment.gardenId),
382
+ citizen.durable.controller,
383
+ citizen.durable.settings,
384
+ );
385
+ } catch (error) {
386
+ return failAfterCleanup(error, () => citizen.close());
387
+ }
388
+ await citizen.close();
389
+ }
390
+
391
+ // Run only when THIS file is the process entry (`node --import <resolver> bootstrap.mjs …`), decided
392
+ // by an argv realpath as in meta-bridge-hook-codex.ts — not by the `main` flag on import.meta, which
393
+ // Node gained in 24.2.0, above the package's major-24 floor. Importing this file starts nothing.
394
+ const invokedDirectly = (() => {
395
+ const entry = process.argv[1];
396
+ if (typeof entry !== "string" || entry.length === 0) return false;
397
+ try {
398
+ return realpathSync(entry) === realpathSync(import.meta.filename);
399
+ } catch {
400
+ // Bounded environment probe: an entry that cannot be resolved is not this file.
401
+ return false;
402
+ }
403
+ })();
404
+ if (invokedDirectly) await main(process.argv.slice(2));
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mario Zechner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.