@junghanacs/entwurf 0.13.0 → 0.14.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 (193) hide show
  1. package/AGENTS.md +52 -17
  2. package/BASELINE.md +60 -206
  3. package/CHANGELOG.md +64 -0
  4. package/CONTRIBUTING.md +14 -10
  5. package/DELIVERY.md +118 -261
  6. package/README.md +91 -431
  7. package/VERIFY.md +74 -95
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +236 -0
  12. package/docs/external-mcp-host.md +131 -0
  13. package/docs/fresh-cut-policy.md +99 -0
  14. package/docs/setup-clean-host.md +124 -328
  15. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  37. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  38. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  39. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  40. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  41. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  42. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  43. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  44. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  45. package/mcp/entwurf-bridge/src/index.ts +176 -67
  46. package/mcp/entwurf-bridge/start.sh +2 -2
  47. package/mcp/entwurf-bridge/test.sh +23 -9
  48. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  49. package/package.json +25 -11
  50. package/pi-extensions/entwurf-control.ts +220 -71
  51. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  52. package/pi-extensions/lib/acp/augment.ts +42 -3
  53. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  54. package/pi-extensions/lib/acp/backend.ts +366 -38
  55. package/pi-extensions/lib/acp/config.ts +1 -1
  56. package/pi-extensions/lib/acp/engraving.ts +43 -1
  57. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  58. package/pi-extensions/lib/acp/models.ts +4 -4
  59. package/pi-extensions/lib/acp/overlay.ts +11 -9
  60. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  61. package/pi-extensions/lib/entwurf-core.ts +15 -15
  62. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  63. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  64. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  65. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  66. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  67. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  68. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  69. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  70. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  71. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  72. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  73. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  74. package/pi-extensions/lib/meta-session.ts +93 -5
  75. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  76. package/pi-extensions/lib/mux-launch.ts +267 -0
  77. package/pi-extensions/lib/mux-placement.ts +387 -0
  78. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  79. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  80. package/pi-extensions/lib/session-id.js +8 -5
  81. package/pi-extensions/lib/socket-discovery.ts +3 -3
  82. package/prompts/engraving.md +13 -5
  83. package/run.sh +688 -282
  84. package/scripts/agy-bridge-config.py +5 -1
  85. package/scripts/check-acp-backend-preflight.ts +1 -1
  86. package/scripts/check-acp-carrier-augment.ts +246 -10
  87. package/scripts/check-acp-cortex.ts +5 -5
  88. package/scripts/check-acp-overlay.ts +13 -3
  89. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  90. package/scripts/check-acp-stop-reason.ts +342 -0
  91. package/scripts/check-acp-stream-hooks.ts +504 -0
  92. package/scripts/check-elapsed.sh +25 -0
  93. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  94. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  95. package/scripts/check-entwurf-control-rpc.ts +4 -3
  96. package/scripts/check-entwurf-resume-args.ts +72 -70
  97. package/scripts/check-entwurf-session-identity.ts +14 -10
  98. package/scripts/check-entwurf-v2-contract.ts +34 -59
  99. package/scripts/check-entwurf-v2-decider.ts +17 -177
  100. package/scripts/check-entwurf-v2-lock.ts +5 -2
  101. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  102. package/scripts/check-entwurf-v2-production.ts +2 -91
  103. package/scripts/check-entwurf-v2-release.ts +10 -105
  104. package/scripts/check-entwurf-v2-runner.ts +4 -85
  105. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  106. package/scripts/check-entwurf-v2-send.ts +0 -28
  107. package/scripts/check-entwurf-v2-surface.ts +198 -128
  108. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  109. package/scripts/check-fresh-cut-gate.sh +1 -1
  110. package/scripts/check-gate-qualification.ts +101 -7
  111. package/scripts/check-install-container.sh +10 -2
  112. package/scripts/check-install-surface.ts +1 -1
  113. package/scripts/check-keyset-overlap.py +1 -1
  114. package/scripts/check-meta-facts.ts +249 -0
  115. package/scripts/check-meta-identity-consumers.ts +1 -1
  116. package/scripts/check-meta-session.ts +169 -0
  117. package/scripts/check-mux-launch-tmux.ts +316 -0
  118. package/scripts/check-mux-launch.ts +288 -0
  119. package/scripts/check-mux-launcher-fence.ts +264 -0
  120. package/scripts/check-mux-parent-artifact.ts +195 -0
  121. package/scripts/check-mux-placement-tmux.ts +322 -0
  122. package/scripts/check-mux-placement.ts +323 -0
  123. package/scripts/check-mux-resume-call.ts +282 -0
  124. package/scripts/check-probe-cli-shim.ts +25 -22
  125. package/scripts/check-probe-ordering.ts +121 -81
  126. package/scripts/check-release-gate-outcomes.ts +452 -0
  127. package/scripts/check-resume-launch-identity.ts +244 -0
  128. package/scripts/check-socket-discovery.ts +1 -1
  129. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  130. package/scripts/inventory-verification-surface.ts +349 -0
  131. package/scripts/lib/claude-launcher-fence.ts +322 -0
  132. package/scripts/lib/live-skip.ts +33 -0
  133. package/scripts/lib/mutation-qualify.ts +109 -3
  134. package/scripts/lib/probe-acp-turn.ts +33 -12
  135. package/scripts/lib/step-outcome.sh +88 -0
  136. package/scripts/meta-bridge-doctor.sh +6 -8
  137. package/scripts/meta-facts.ts +60 -0
  138. package/scripts/mutants/acp-augment.json +106 -0
  139. package/scripts/mutants/acp-cortex.json +2 -2
  140. package/scripts/mutants/acp-overlay.json +17 -0
  141. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  142. package/scripts/mutants/acp-stop-reason.json +80 -0
  143. package/scripts/mutants/acp-stream-hooks.json +158 -0
  144. package/scripts/mutants/bridge-boot-resume.json +45 -0
  145. package/scripts/mutants/meta-facts.json +50 -0
  146. package/scripts/mutants/meta-identity.json +36 -0
  147. package/scripts/mutants/mux-boundary.json +196 -0
  148. package/scripts/mutants/mux-fresh-call.json +185 -0
  149. package/scripts/mutants/mux-launcher-fence.json +123 -0
  150. package/scripts/mutants/mux-parent-artifact.json +39 -0
  151. package/scripts/mutants/mux-resume-call.json +148 -0
  152. package/scripts/mutants/probe-ordering.json +6 -1021
  153. package/scripts/mutants/release-gate.json +140 -0
  154. package/scripts/mutants/resume-args.json +76 -0
  155. package/scripts/mutants/resume-launch-identity.json +96 -0
  156. package/scripts/mutants/v2-surface.json +74 -23
  157. package/scripts/mutants/v2-visible-resume.json +215 -0
  158. package/scripts/new-session-id.ts +9 -4
  159. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  160. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  161. package/scripts/smoke-acp-cortex-live.ts +8 -14
  162. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  163. package/scripts/smoke-acp-mcp-live.ts +2 -2
  164. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  165. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  166. package/scripts/smoke-acp-overlay-live.ts +2 -2
  167. package/scripts/smoke-acp-provider-live.ts +2 -2
  168. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  169. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  170. package/scripts/smoke-acp-skill-live.ts +2 -2
  171. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  172. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  173. package/scripts/smoke-agy-native-push-live.ts +10 -20
  174. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  175. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  176. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
  177. package/scripts/smoke-meta-honesty.sh +1 -1
  178. package/scripts/smoke-meta-install-state.sh +4 -0
  179. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  180. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  181. package/scripts/smoke-pi-attach.ts +1 -1
  182. package/scripts/smoke-user-scope-citizen.sh +1 -1
  183. package/scripts/tsconfig.json +1 -0
  184. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  185. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  186. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  187. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  188. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  189. package/scripts/check-acp-sdk-surface.ts +0 -236
  190. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  191. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  192. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  193. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
@@ -11,7 +11,7 @@
11
11
  * Currently exposed tools (scope is deliberately narrow — anything that can live
12
12
  * as a local skill should live as a skill, not here):
13
13
  * - entwurf_v2 — canonical delivery surface for existing garden citizens; the decider
14
- * chooses live control-socket send / dormant spawn-bg resume / meta-mailbox.
14
+ * chooses live control-socket send / meta-mailbox enqueue / native-push.
15
15
  * - entwurf_peers — entwurf fact surface: garden citizens (meta-records) with liveness +
16
16
  * diagnostics (#50 C4: record-less sockets surface THERE, never as identity).
17
17
  * Brain = pi-extensions/lib/entwurf-fact-provider (listEntwurfFacts) +
@@ -24,6 +24,18 @@
24
24
  * garden record may call this — so the surface is "drain the inbox you were
25
25
  * pointed at", not "drain your own". A rung doorbell is a wake attempt;
26
26
  * this read is the receipt.
27
+ * - entwurf_register_native — explicit/manual fallback binding an ALREADY-RUNNING native
28
+ * conversation (antigravity) to a garden id. Never a spawn.
29
+ * - entwurf_fresh_call — open ONE fresh visible sibling in the operator's own tmux session;
30
+ * returns a LAUNCH receipt only, and the new address arrives later as the
31
+ * sender envelope of the sibling's nonce callback.
32
+ * - entwurf_resume_call — reopen ONE DORMANT pi citizen under its OWN garden id in a visible
33
+ * window; target-only, runs no turn, LAUNCH and OBSERVATION receipts stay
34
+ * apart.
35
+ *
36
+ * That list is the WHOLE public surface — seven verbs — and `check-entwurf-bridge-boot`
37
+ * (G1f) holds it as an exact set on the runtime tools/list, so a verb added or dropped
38
+ * here without a decision is red rather than merely undocumented.
27
39
  *
28
40
  * Removed from this v2-only surface: legacy MCP `entwurf`, `entwurf_resume`, and
29
41
  * `entwurf_send`. Use `entwurf_v2` for delivery to existing garden citizens.
@@ -57,8 +69,11 @@ import { renderEntwurfPeers } from "../../../pi-extensions/lib/entwurf-peers-ren
57
69
  import { computeSelfAddressability } from "../../../pi-extensions/lib/entwurf-self-address.js";
58
70
  import { nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contract.js";
59
71
  import { runAndRenderEntwurfV2FromSurface } from "../../../pi-extensions/lib/entwurf-v2-surface.js";
72
+ import { makeVisibleResumeDeps, renderVisibleResume, visibleResume, } from "../../../pi-extensions/lib/entwurf-v2-visible-resume.js";
60
73
  import { probeNativeSenderAlive, resolveTrustedMetaSenderIdentity, } from "../../../pi-extensions/lib/meta-sender-identity.js";
61
74
  import { defaultMetaMailboxDir, defaultMetaSessionsDir, makeStoreRecordReader, readActiveStoreEntries, readMetaInbox, readMetaReceiverMarker, } from "../../../pi-extensions/lib/meta-session.js";
75
+ import { freshCall, renderFreshCall } from "../../../pi-extensions/lib/mux-fresh-call.js";
76
+ import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.js";
62
77
  import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.js";
63
78
  const HOME = os.homedir();
64
79
  // Directory SOURCE is this adapter's own policy — the bridge honours an explicit
@@ -286,77 +301,49 @@ function abbreviateHomeMcp(cwd) {
286
301
  }
287
302
  // entwurf_v2 — the unified v2 dispatch verb (0.11 step 5d-3b). It hands the
288
303
  // target + intent to the 5b decider, which chooses the transport (live
289
- // control-socket send / spawn-bg resume / meta-mailbox enqueue / native-push
290
- // direct injection) and reports one outcome. The per-target lock is NOT taken by
291
- // every rail: the decider locks only a control-socket-domain dispatch, which
292
- // covers the live send AND the dormant cell's spawn-bg resume; the mailbox and
293
- // native-push branches carry `lock: null` (entwurf-v2-decider.ts). It runs
304
+ // control-socket send / meta-mailbox enqueue / native-push direct injection) and
305
+ // reports one outcome. The per-target lock is NOT taken by every rail: the decider
306
+ // locks only a control-socket-domain dispatch; the mailbox and native-push branches
307
+ // carry `lock: null` (entwurf-v2-decider.ts). It runs
294
308
  // IN-PROCESS here (the same production runner pi-native uses) — NOT a delegating
295
- // RPC — so control, mailbox, native-push, AND spawn-bg all flow through
309
+ // RPC — so control, mailbox and native-push all flow through
296
310
  // `runEntwurfV2`. The sender envelope is
297
311
  // `buildSendSenderEnvelope()` verbatim (origin/replyable as resolved) — v2 does
298
312
  // NOT gate on replyability (a `wants_reply` from an external/non-replyable caller
299
313
  // is surfaced honestly, not rejected; the decider routes on target + intent, not
300
314
  // sender replyability).
301
- server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids. When you have a garden id and want to reach " +
302
- "whoever it names message / reply / hand-off use THIS verb. A garden id alone does " +
303
- "not tell you which rail that citizen answers on a live socket citizen, a dormant one, a " +
304
- "mailbox-backed self-fetch session, or a native-push session and entwurf_v2 is the one " +
305
- 'surface that reads that for you and routes correctly (so "when unsure which transport, use ' +
306
- 'entwurf_v2"). You give the target ' +
307
- "garden id + your intent; the decider picks the transport from the target's liveness " +
308
- "(live socket citizen control-socket send; dormant socket citizen spawn-bg resume; active " +
309
- "deliverable self-fetch citizen → meta-bridge mailbox; probe-alive native-push citizen direct " +
310
- "injection into its live conversation) under the v2 lock policy, and reports ONE outcome " +
311
- "(delivered / rejected / lock-retained / delivered-but-lock-dirty). LOCK POLICY (do not " +
312
- "over-generalize it): the per-target lock is taken for a control-socket-DOMAIN dispatch, which is " +
313
- "both the live send AND the dormant cell's spawn-bg resume spawn-bg is a separate relaunch " +
314
- "transport yet it still runs under that domain's lock. The mailbox and native-push rails are " +
315
- "lock-free: the mailbox is guarded instead by active-receiver deliverability, and native-push by " +
316
- "its adapter probe. The decider not the " +
317
- "caller chooses the transport. Note: entwurf_v2 dispatches to EXISTING targets; " +
318
- "brand-new sibling creation is deferred to a later v2 lane. " +
319
- "CHOOSING INTENT (read thispicking wrong is rejected, never auto-fixed): to message / " +
320
- "reply / hand off a peer that entwurf_peers shows as liveness=alive (a live socket citizen, " +
321
- "currently backend pi) use intent: fire-and-forget it routes to the live control-socket; set " +
322
- "wants_reply:true if you need an answer (wants_reply is NOT owned-outcome). Replies to a citizen " +
323
- "with NO socket liveness (liveness=unsupported) are ALSO fire-and-forget, and the decider picks " +
324
- "that citizen's own rail: a self-fetch backend (e.g. Claude Code) gets the meta-bridge mailbox, " +
325
- "while a native-push backend (e.g. Antigravity) gets direct injection into its live conversation " +
326
- "and has NO mailbox at all — do not assume mailbox semantics for every unsupported citizen. A " +
327
- "native-push target IS measured by its own adapter probe, and that probe is THREE-valued, so " +
328
- "the send is never silently queued: alive → injected; dead → rejected as " +
329
- "native-push-target-dead; indeterminate → rejected as native-push-probe-indeterminate. " +
330
- "Those two rejects stay distinct because 'we could not establish it' is not 'it is gone' — " +
331
- "collapsing them reports a guess as a fact. THERE IS A " +
332
- "THIRD RESULT, so do not read liveness=unsupported as 'reachable by some rail': the mailbox path " +
333
- "delivers only to a DELIVERABLE citizen (a self-fetch backend whose receiver is live and armed), " +
334
- "so a terminated Claude Code session — and any record whose backend has no adapter on this lane, " +
335
- "e.g. codex — is rejected as mailbox-undeliverable rather than queued into an inbox nobody drains. " +
336
- "unsupported means only 'this backend has no control-socket probe'. " +
337
- "owned-outcome is ONLY for waking a DORMANT socket-domain citizen (spawn-bg resume, currently " +
338
- "backend pi); on a live target it is rejected as owned-live-no-autosend. Neither self-fetch nor " +
339
- "native-push has resume authority, but they reject under DIFFERENT reasons — self-fetch as " +
340
- "backend-liveness-unsupported, native-push as native-push-no-resume-authority. " +
341
- "It is NEVER auto-converted — so pick the right intent up front. " +
342
- "mode applies to a CONTROL-SOCKET send only — it is the injection style for a live pi turn, and " +
343
- "the mailbox, native-push, and spawn-bg plans carry no mode at all, so setting it for those " +
344
- "targets changes nothing (a native-push send IS live and still ignores it). wants_reply rides " +
345
- "every rail. Use entwurf_peers to discover targets. " +
346
- "Payload guidance: message hard cap 16000 chars. For larger reviews/logs, write an " +
347
- "artifact and dispatch its path plus a short digest; avoid multi-part sends because " +
348
- "mailbox doorbells may coalesce.", {
315
+ server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id " +
316
+ "alone does not say which rail that citizen answers on. Give target + intent; the decider picks transport " +
317
+ "from liveness (live socket citizen control-socket send; deliverable self-fetch citizen meta-bridge " +
318
+ "mailbox; probe-alive native-push citizen direct injection into its conversation) and reports ONE " +
319
+ "outcome (delivered / rejected / delivered-but-lock-dirty). EXISTING targets only; discover with " +
320
+ "entwurf_peers. A peer entwurf_peers shows as liveness=alive → fire-and-forget. A " +
321
+ "citizen with NO socket liveness (liveness=unsupported) is ALSO fire-and-forget unsupported means only " +
322
+ '"no control-socket probe" and the decider picks its own rail: a self-fetch backend (e.g. Claude Code) ' +
323
+ "gets the mailbox, a native-push backend (e.g. Antigravity) gets direct injection and has NO mailbox at " +
324
+ "all. THERE IS A THIRD RESULT: the mailbox delivers only to a DELIVERABLE citizen, so a terminated " +
325
+ "session, or a backend with no adapter here (e.g. codex), is mailbox-undeliverable, not queued for an " +
326
+ "inbox nobody drains. The native-push probe is 3-valued: alive injected; dead " +
327
+ "native-push-target-dead; indeterminate native-push-probe-indeterminate (unestablished gone). " +
328
+ "DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets dormant-fire-forget-unsupported " +
329
+ " same receiver rule as the mailbox, no active drainer means no delivery. " +
330
+ "The intent that used to answer there, owned-outcome, resumed it by launching a hidden background child " +
331
+ "and was withdrawn under the visible-first rule re-open the session yourself, then dispatch again. " +
332
+ "LOCK: taken for a control-socket-DOMAIN dispatch. The " +
333
+ "mailbox and native-push rails are lock-free deliverability and the adapter probe guard them. mode " +
334
+ "applies to a CONTROL-SOCKET send only; other plans carry no mode. wants_reply rides every rail. message " +
335
+ "caps at 16000 chars; send an artifact path + digest for more.", {
349
336
  target: z.string().min(1).describe("Target garden id (use entwurf_peers to discover)"),
350
337
  intent: z
351
- .enum(["fire-and-forget", "owned-outcome"])
338
+ .enum(["fire-and-forget"])
352
339
  .describe("fire-and-forget = send/reply/hand-off to a LIVE socket target or to any citizen with no " +
353
340
  "socket liveness — the decider picks its rail, and a rail can also REJECT (self-fetch → " +
354
341
  "mailbox when deliverable, else mailbox-undeliverable; native-push → alive: direct injection, " +
355
342
  "dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate); set " +
356
- "wants_reply for an answer. owned-outcome = " +
357
- "wake a DORMANT socket-domain citizen via spawn-bg resume ONLY on a live target rejected " +
358
- "as owned-live-no-autosend, on self-fetch as backend-liveness-unsupported, on native-push as " +
359
- "native-push-no-resume-authority, and never auto-converted"),
343
+ "wants_reply for an answer. This is the ONLY intent: owned-outcome, which resumed a dormant " +
344
+ "citizen by launching a hidden background child, was withdrawn under the visible-first rule. " +
345
+ "It is not selectable, not deprecated-but-tolerated; a dormant citizen is currently " +
346
+ "unreachable by this verb and rejects as dormant-fire-forget-unsupported."),
360
347
  message: z
361
348
  .string()
362
349
  .min(1)
@@ -366,8 +353,8 @@ server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids. When you h
366
353
  .enum(["steer", "follow_up"])
367
354
  .optional()
368
355
  .describe("Injection style for a CONTROL-SOCKET send only: steer (interrupt the current turn) or " +
369
- "follow_up (queue after it). The mailbox, native-push, and spawn-bg plans carry no mode, so it " +
370
- "has no effect on those rails."),
356
+ "follow_up (queue after it). The mailbox and native-push plans carry no mode, so it has no " +
357
+ "effect on those rails."),
371
358
  wants_reply: z.boolean().optional().describe("Human-conversation reply hint (default false)"),
372
359
  }, async ({ target, intent, message, mode, wants_reply }) => {
373
360
  try {
@@ -375,8 +362,9 @@ server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids. When you h
375
362
  // + the mailbox body sender share one envelope. No replyability gate (see above).
376
363
  const sender = await buildSendSenderEnvelope();
377
364
  const rendered = await runAndRenderEntwurfV2FromSurface({ target, intent, message, mode, wants_reply },
378
- // agentDir / prefixRoots intentionally omitted: runAndRenderEntwurfV2FromSurface falls
379
- // back to the ENTWURF_PREFIX_ROOTS env SSOT for prefixRoots (5d-4); agentDir stays undefined.
365
+ // No trust-preflight inputs are passed, and none exist to pass: the preflight on this
366
+ // path guarded the resume verdict and left with `owned-outcome`. `senderProvider` is
367
+ // the whole options surface now (see the pi-native surface for the same note).
380
368
  { senderProvider: () => sender });
381
369
  return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
382
370
  }
@@ -459,7 +447,9 @@ server.tool("entwurf_peers", "List the entwurf fact surface: garden citizens (fr
459
447
  "`unsupported` for a backend with no control-socket probe such as claude-code); the dispatch " +
460
448
  "decision (send vs resume) is computed LATER by the entwurf_v2 contract from that liveness, " +
461
449
  "not here — this surface carries no per-row routing field, so do not read a transport off a " +
462
- "row. In particular `unsupported` does NOT mean mailbox: it means this backend has no " +
450
+ "row. A `dead` row is a REPORTED FACT and nothing more: that citizen is dormant and is " +
451
+ "currently unreachable by any verb, so listing it grants no action — appearing here is not " +
452
+ "an invitation to dispatch. In particular `unsupported` does NOT mean mailbox: it means this backend has no " +
463
453
  "control-socket probe. Which rail it answers on is a capability the decider resolves at dispatch " +
464
454
  "time, and there are THREE possible answers, not two — a self-fetch mailbox (only while that " +
465
455
  "mailbox is deliverable), native-push direct injection (only while its adapter probe is alive), " +
@@ -469,8 +459,8 @@ server.tool("entwurf_peers", "List the entwurf fact surface: garden citizens (fr
469
459
  "is not a departed host. So " +
470
460
  "`unsupported` does not promise reachability either: a record whose backend has no adapter on " +
471
461
  "this lane resolves to that reject. " +
472
- "Note: this is the *active* world. It is NOT a fresh-sibling creation surface; pass an " +
473
- "existing garden id to entwurf_v2.", {}, async () => {
462
+ "Note: this is the *active* world and it is facts-only it creates nothing. Pass an " +
463
+ "existing garden id to entwurf_v2; to open a NEW sibling use entwurf_fresh_call.", {}, async () => {
474
464
  try {
475
465
  // Meta-store axis: entries WITH their kind (ENOENT = fresh install = empty; any
476
466
  // other readdir failure is a real error, not a silent empty). The name-only
@@ -562,6 +552,109 @@ server.tool("entwurf_register_native", "Register an ALREADY-RUNNING native conve
562
552
  return textErr(`entwurf_register_native error: ${err instanceof Error ? err.message : String(err)}`);
563
553
  }
564
554
  });
555
+ // The caller identity for THIS surface comes from the same authoritative resolution
556
+ // `entwurf_self` uses — BOTH of its certified paths: the pi carrier this child inherited
557
+ // (PI_SESSION_ID + PI_AGENT_ID, which a record minted at birth), or a trusted meta-sender marker
558
+ // the native host's own hook wrote. Neither is a guess and neither is deprecated here.
559
+ //
560
+ // What is refused is a caller-supplied id. The measured failure mode is exactly that: asked for
561
+ // its own garden id, a fresh cell answered with its uuidv7 `PI_SESSION_ID` value read out of the
562
+ // environment by an MCP server it had spawned itself — confidently, and wrong. A sibling launched
563
+ // against that answer would call home to a garden id nobody holds.
564
+ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operator's own tmux session and hand it a first task. Two fixed " +
565
+ "backends only: pi, claude-code. The sibling's FIRST action is a callback to you carrying a nonce, and the " +
566
+ "sender envelope of that callback is its garden id — that is how you learn the address of something that " +
567
+ "did not exist a moment ago. This returns a LAUNCH receipt (tmux window/pane plus that nonce) and nothing " +
568
+ "else: it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls " +
569
+ "for the callback; if it never arrives the window is visible and can be read directly. For EXISTING " +
570
+ "citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and " +
571
+ "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code); there are no " +
572
+ "arbitrary command/cwd/env knobs. Do not put secrets in the task — model and task argv are visible to " +
573
+ "same-user processes on this host. Requires that this agent itself runs " +
574
+ "inside tmux: without a pane anchor there is no session to open a sibling beside.", {
575
+ backend: z
576
+ .enum(["pi", "claude-code"])
577
+ .describe("Which fixed runtime to open. Only these two; there is no arbitrary command."),
578
+ model: z
579
+ .string()
580
+ .min(1)
581
+ .max(200)
582
+ // The `\[` below is USELESS TO JS AND LOAD-BEARING TO THE HOST. zod emits this source
583
+ // text verbatim as the JSON Schema `pattern`, and the host's tool-schema validator is a
584
+ // Rust regex engine that rejects an unescaped `[` inside a character class as "unclosed
585
+ // character class" — a 400 on tools/list that stops EVERY Claude session from opening
586
+ // (7-M-fix). Biome's safe fix removes it; do not accept that fix. The suppression must
587
+ // stay on the line DIRECTLY above the expression, or it attaches to nothing and reads
588
+ // as an unused suppression while the escape goes back to being fixable.
589
+ // biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above
590
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._/:\[\]-]*$/)
591
+ .describe("Required runtime model: canonical provider/model for pi, or a Claude Code model id/alias."),
592
+ task: z
593
+ .string()
594
+ .min(1)
595
+ .max(16000)
596
+ .describe("What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description)."),
597
+ }, async ({ backend, model, task }) => {
598
+ let callerGardenId = null;
599
+ try {
600
+ const self = await buildAuthoritativeSelfEnvelope();
601
+ callerGardenId = self.envelope.sessionId;
602
+ }
603
+ catch (err) {
604
+ // ONE error is a legitimate answer here: this host has no authoritative identity at all
605
+ // (no pi carrier inherited, no trusted marker written), so the sibling would have nowhere
606
+ // to call home. That normalises to the named refusal.
607
+ //
608
+ // Everything else — a corrupt record, an unreadable store, a broken marker — keeps its
609
+ // own diagnosis and fails loud. Folding those into "you are anonymous" would relabel a
610
+ // store defect as a wiring choice, and the operator would go looking in the wrong place.
611
+ if (!(err instanceof EntwurfEnvelopeWiringError))
612
+ throw err;
613
+ callerGardenId = null;
614
+ }
615
+ try {
616
+ const rendered = renderFreshCall(freshCall({ backend, model, task, callerGardenId }));
617
+ return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
618
+ }
619
+ catch (err) {
620
+ return textErr(`entwurf_fresh_call error: ${err instanceof Error ? err.message : String(err)}`);
621
+ }
622
+ });
623
+ // The composition root for a visible resume: this surface is the only layer that may know BOTH
624
+ // halves, so it is where the mux launcher is handed to the v2 composition. `entwurf-v2-visible-
625
+ // resume` cannot open a window on its own and `mux-resume-call` cannot read a record — that is
626
+ // the import fence in docs/mux-launch-rail.md §11, expressed as a function argument.
627
+ const visibleResumeLaunch = (input) => {
628
+ const launched = resumeCall({ cwd: input.cwd, runtimeArgs: input.runtimeArgs });
629
+ return launched.ok
630
+ ? { ok: true, handle: launched.receipt }
631
+ : { ok: false, reason: launched.reason, hint: RESUME_CALL_REJECT_HINT[launched.reason] };
632
+ };
633
+ server.tool("entwurf_resume_call", "Reopen ONE DORMANT pi citizen under its OWN garden id, in a visible window in the operator's own tmux " +
634
+ "session. The record supplies everything — which transcript, which model, which provider, which cwd — so " +
635
+ "the only input is the target id: there is no model override, no task, and no prompt. This runs NO turn: " +
636
+ "the window comes back with the conversation and waits, and talking to it is still entwurf_v2 " +
637
+ "fire-and-forget on the socket this call stands up. You get TWO receipts and they mean different things: a " +
638
+ "LAUNCH receipt (tmux made a window and was asked to start pi) and an OBSERVATION receipt (the control " +
639
+ "socket answered under the same id, or resume-unobserved). Unobserved is a real outcome, not an error to " +
640
+ "retry — the window is visible, so read it. A citizen that is already LIVE is refused: address it with " +
641
+ "entwurf_v2 instead. Only pi citizens have a same-id resume, because only they stand a control socket up. " +
642
+ "Requires that this agent itself runs inside tmux.", {
643
+ target: z
644
+ .string()
645
+ .min(1)
646
+ .regex(/^\d{8}T\d{6}-[0-9a-f]{6}$/)
647
+ .describe("Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers)."),
648
+ }, async ({ target }) => {
649
+ try {
650
+ const result = await visibleResume(target, makeVisibleResumeDeps(visibleResumeLaunch));
651
+ const rendered = renderVisibleResume(result);
652
+ return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
653
+ }
654
+ catch (err) {
655
+ return textErr(`entwurf_resume_call error: ${err instanceof Error ? err.message : String(err)}`);
656
+ }
657
+ });
565
658
  // ============================================================================
566
659
  // Main
567
660
  // ============================================================================
@@ -39,6 +39,18 @@ export function connectAcpClient(stream, handlers) {
39
39
  newSession: (params) => agent.request(AGENT_METHODS.session_new, params),
40
40
  prompt: (params) => agent.request(AGENT_METHODS.session_prompt, params),
41
41
  setSessionConfigOption: (params) => agent.request(AGENT_METHODS.session_set_config_option, params),
42
+ cancel: (params) => {
43
+ // Notification, not a request: nothing resolves it, and the connection
44
+ // may already be closing when the operator aborts. Swallow both the sync
45
+ // throw and the rejected send — the caller's abort path continues either
46
+ // way (it escalates to teardown after a bounded grace).
47
+ try {
48
+ void Promise.resolve(agent.notify(AGENT_METHODS.session_cancel, params)).catch(() => { });
49
+ }
50
+ catch {
51
+ // connection already closed — the abort path escalates on its own.
52
+ }
53
+ },
42
54
  close: (error) => {
43
55
  // Best-effort by contract (see AcpConnectionLike.close): a teardown-path
44
56
  // close that threw would mask the turn's real error and skip the child
@@ -1,9 +1,10 @@
1
1
  // ACP backend adapter rail — the PRODUCT seam by which a curated model id selects
2
- // which ACP backend (claude / future backend / …) drives a turn. See docs/acp-backend-rail.md §9.
2
+ // which ACP backend (claude / future backend / …) drives a turn. See the adapter contract in
3
+ // docs/acp-backend-rail.md.
3
4
  //
4
5
  // This seam is DISTINCT from `AcpTurnDeps` (backend.ts), which is the test/runtime
5
- // seam (fake spawn/connection/clock for the gates). The two are kept apart on
6
- // purpose (GPT-agreed §9-2): merging them would make a fake-deps fixture look like
6
+ // seam (fake spawn/connection/clock for the gates). Merging them would make a
7
+ // fake-deps fixture look like
7
8
  // a fake backend and force the adapter to carry clock/sessionDir/createConnection.
8
9
  // The wiring is `defaultDeps(adapter)` — the turn loop in backend.ts stays
9
10
  // backend-invariant; only these per-backend functions change with `adapter`.
@@ -54,7 +55,7 @@ function resolveClaudeLaunch() {
54
55
  }
55
56
  export const claudeAdapter = {
56
57
  backend: "claude",
57
- // Claude owns its UNPREFIXED curated ids only (GPT-agreed §9-1). The native id
58
+ // Claude owns its UNPREFIXED curated ids only (rail “Adapter contract”). The native id
58
59
  // equals the curated id — claude carries no curation prefix to strip.
59
60
  routeModel(modelId) {
60
61
  return SUPPORTED_CLAUDE_IDS.has(modelId) ? { nativeModelId: modelId } : undefined;
@@ -121,7 +122,7 @@ export const claudeAdapter = {
121
122
  * adapters claim one id. */
122
123
  // ---------------------------------------------------------------------------
123
124
  // cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
124
- // rail (docs/acp-backend-rail.md §4/§6). It adds ZERO to the common layer:
125
+ // rail (docs/acp-backend-rail.md, “Shipped adapters”). It adds ZERO to the common layer:
125
126
  // everything cortex-specific lives here + models.ts + overlay.ts + the gates.
126
127
  // ---------------------------------------------------------------------------
127
128
  const SUPPORTED_CORTEX_IDS = new Set(SUPPORTED_CORTEX_MODEL_IDS);
@@ -131,7 +132,7 @@ const SUPPORTED_CORTEX_IDS = new Set(SUPPORTED_CORTEX_MODEL_IDS);
131
132
  export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
132
133
  export const cortexAdapter = {
133
134
  backend: "cortex",
134
- // Cortex owns the reserved `cortex-` prefix (§9-1). routeModel strips it to the
135
+ // Cortex owns the reserved `cortex-` prefix (rail “Adapter contract”). routeModel strips it to the
135
136
  // native id: `cortex-auto` → "auto", `cortex-claude-sonnet-5` → "claude-sonnet-5".
136
137
  routeModel(modelId) {
137
138
  if (!SUPPORTED_CORTEX_IDS.has(modelId))
@@ -213,7 +214,7 @@ export const cortexAdapter = {
213
214
  });
214
215
  return { envOverrides: { HOME: overlay.home, SNOWFLAKE_HOME: overlay.snowflakeHome } };
215
216
  },
216
- // System-prompt-carrier-less (§9-4/§11-8): Cortex ACP exposes no
217
+ // System-prompt-carrier-less (ACP rail “Cortex Code audit”): Cortex ACP exposes no
217
218
  // `_meta.systemPrompt` and has no developer_instructions / GEMINI_SYSTEM_MD
218
219
  // equivalent. (It does READ `_meta` — a caller-session-id seam, measured but
219
220
  // unexplored and deliberately not part of this contract.) loadCarrier returns
@@ -242,7 +243,7 @@ export const cortexAdapter = {
242
243
  }
243
244
  await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
244
245
  },
245
- // A connection change must invalidate a reused session (§4/§7). Flat,
246
+ // A connection change must invalidate a reused session (rail: Adapter contract). Flat,
246
247
  // sorted-stable primitive map; reads ONLY the opaque adapterSettings. `backend`
247
248
  // + `nativeModelId` are added by backend.ts.
248
249
  configSignatureFields(adapterSettings) {
@@ -254,7 +255,7 @@ const ADAPTERS = [claudeAdapter, cortexAdapter];
254
255
  /**
255
256
  * Resolve the backend adapter that owns `modelId`.
256
257
  *
257
- * GPT-agreed §9-1 fail-fast contract:
258
+ * Routing fail-fast contract (rail “Adapter contract”):
258
259
  * - 0 matches → throw (unknown model — no silent default).
259
260
  * - 2+ matches → throw (prefix collision — a startup-visible registry bug).
260
261
  *
@@ -278,7 +279,7 @@ export function resolveAcpBackendAdapter(modelId) {
278
279
  return matches[0];
279
280
  }
280
281
  /** Every curated model row across all registered adapters — for provider registration.
281
- * GPT-agreed §9-6 registration-time fail-fast: every curated id must route to EXACTLY
282
+ * Registration-time fail-fast (rail “Adapter contract”): every curated id must route to EXACTLY
282
283
  * one adapter and no id may be duplicated across adapters. Catching it here means a
283
284
  * prefix-collision / duplicate surfaces at provider registration, not mid-turn. */
284
285
  export function allCuratedModels() {
@@ -23,7 +23,7 @@
23
23
  // the backend-invariant exclude-tools preflight and folds every one of them into
24
24
  // `bridgeConfigSignature` (so changing one still invalidates a reused cortex
25
25
  // session). Backend-OWNED settings never land here: they ride the opaque
26
- // `adapterSettings` seam (§10 B), which is what keeps a backend-named key like
26
+ // `adapterSettings` seam (rail “Adapter contract”), which is what keeps a backend-named key like
27
27
  // `cortexConnection` off this common shape.
28
28
  //
29
29
  // PURITY / SIGNATURE contract (NEXT oracle C / 핀1 / GPT `…2f9325` boost):
@@ -35,11 +35,30 @@
35
35
  // incompatible and the next turn opens a fresh ACP session with the new carrier.
36
36
  // That per-turn rebuild is the accepted cost of the A/B opt-in surface, never the
37
37
  // shipped default (which stays cached precisely so a resident never rebuilds).
38
+ //
39
+ // A-JOIN (measured LIVE 2026-07-31, 0.64.0 adapter, fresh Claude ACP): the model's
40
+ // system prompt arrived as
41
+ // `You are a Claude agent, built on Anthropic's Claude Agent SDK.# Engraving Here`
42
+ // A string-form `_meta.systemPrompt` replaces the `claude_code` preset, but the
43
+ // SDK still PREFIXES its own fixed identity sentence and joins the two with
44
+ // NOTHING — so the operator's heading was swallowed into the tail of the SDK's
45
+ // sentence. The boundary therefore belongs to the CARRIER, and it cannot be
46
+ // delegated to engraving.md: the render is trimmed (below), so a leading blank
47
+ // line in the markdown is eaten before it ever reaches the wire.
38
48
  import { readFileSync } from "node:fs";
39
49
  import { dirname, join, resolve } from "node:path";
40
50
  import { fileURLToPath } from "node:url";
41
51
  const HERE = dirname(fileURLToPath(import.meta.url));
42
52
  const DEFAULT_ENGRAVING_PATH = join(HERE, "prompts", "engraving.md");
53
+ /**
54
+ * The carrier's LEADING boundary — the one the Claude Agent SDK does not supply
55
+ * (see A-JOIN above). A constant, never derived from the template, so the render
56
+ * stays a pure function of (template, backend, mcpServerNames) and the operator's
57
+ * file whitespace can never drift `bridgeConfigSignature`. One blank line is the
58
+ * whole lever: it puts the carrier's first line at the start of its own block
59
+ * instead of at the end of the SDK's fixed sentence.
60
+ */
61
+ export const CARRIER_LEAD_SEPARATOR = "\n\n";
43
62
  let cached = null;
44
63
  /** Point the loader at an alternate engraving file (A/B); bypasses the cache. */
45
64
  function resolveEngravingPath() {
@@ -65,6 +84,22 @@ function interpolate(template, params) {
65
84
  const mcpList = names.length > 0 ? names.join(", ") : "(none registered)";
66
85
  return template.replace(/\{\{backend\}\}/g, params.backend).replace(/\{\{mcp_servers\}\}/g, mcpList);
67
86
  }
87
+ /**
88
+ * Render one template into a wire-ready carrier, or `""` when the template has no
89
+ * body (the caller turns that into the opt-out / fail-loud branches).
90
+ *
91
+ * The ORDER here is the A-join fix. Trim first: the operator template's own
92
+ * leading/trailing whitespace must not reach the wire (it would drift
93
+ * bridgeConfigSignature) and the emptiness test must see the BODY — a
94
+ * separator-only string is an opt-out, not a carrier. Then attach OUR boundary,
95
+ * which is what the SDK's fixed sentence has nothing of.
96
+ */
97
+ function renderCarrier(source, params) {
98
+ const body = interpolate(source, params).trim();
99
+ if (body.length === 0)
100
+ return "";
101
+ return `${CARRIER_LEAD_SEPARATOR}${body}`;
102
+ }
68
103
  /**
69
104
  * The rendered engraving carrier, or null when an ENV-OVERRIDE engraving file
70
105
  * (`ENTWURF_ACP_ENGRAVING_PATH`) is empty, whitespace-only, missing, or
@@ -77,6 +112,12 @@ function interpolate(template, params) {
77
112
  * Callers MUST treat null as "no carrier configured" and omit `_meta.systemPrompt`
78
113
  * entirely (passing "" as the `appendSystemPrompt` signature input) so
79
114
  * subscription billing is never reclassified.
115
+ *
116
+ * A non-null carrier always LEADS with `CARRIER_LEAD_SEPARATOR`, and callers must
117
+ * pass it on BYTE-FOR-BYTE: the same string feeds `bridgeConfigSignature`
118
+ * (`appendSystemPrompt`) and the wire (`_meta.systemPrompt`), so normalizing it at
119
+ * either hop both re-opens the A-join and makes reuse key on a string that was
120
+ * never sent.
80
121
  */
81
122
  export function loadEngraving(params) {
82
123
  const filePath = resolveEngravingPath();
@@ -92,7 +133,7 @@ export function loadEngraving(params) {
92
133
  }
93
134
  return null;
94
135
  }
95
- const rendered = interpolate(source, params).trim();
136
+ const rendered = renderCarrier(source, params);
96
137
  if (rendered.length === 0) {
97
138
  if (isShippedDefault) {
98
139
  throw new Error(`entwurf: shipped engraving carrier at ${filePath} is empty — it is the auto-memory ` +
@@ -9,8 +9,8 @@
9
9
  // Two curated sets live here, one per adapter on the rail
10
10
  // (backend-adapter.ts): the unprefixed Claude ids (the reference backend) and
11
11
  // the `cortex-` prefixed Snowflake Cortex Code ids (the second backend, landed
12
- // 0.13.0 — docs/acp-backend-rail.md §6/§11-8). Codex is already a native garden
13
- // citizen and Gemini/major tools use native, so neither is an ACP backend here.
12
+ // 0.13.0 — docs/acp-backend-rail.md “Cortex Code audit”). Codex has native
13
+ // delivery-probe evidence but no managed citizen lane; neither it nor Gemini is an ACP backend here.
14
14
  // A third governed backend EXTENDS this file the same way — it does not change
15
15
  // the pattern.
16
16
  // pi 0.80 migration: the standalone root `getModels()` moved to the deprecated
@@ -77,9 +77,9 @@ function claudeContextWindow(model) {
77
77
  // ── Cortex (Snowflake Cortex Code) curated surface ──────────────────────────
78
78
  //
79
79
  // entwurf's ACP rail is backend-extensible: Cortex is the first NON-claude
80
- // backend to land through it (docs/acp-backend-rail.md §6). pi-ai carries no
80
+ // backend to land through it (docs/acp-backend-rail.md, “Shipped adapters”). pi-ai carries no
81
81
  // snowflake/cortex model source, so this surface is HAND-CURATED. Every id
82
- // carries the reserved `cortex-` prefix (§9-1): the prefix is the SINGLE routing
82
+ // carries the reserved `cortex-` prefix: the prefix is the SINGLE routing
83
83
  // authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
84
84
  // native ids Cortex routes to (`cortex-claude-sonnet-5` vs the unprefixed
85
85
  // `claude-sonnet-5` the claude adapter owns). `routeModel` strips the prefix to
@@ -2,8 +2,8 @@
2
2
  //
3
3
  // claude-agent-acp's SettingsManager loads the operator's `~/.claude/settings.json`
4
4
  // DIRECTLY (CLAUDE_CONFIG_DIR is the only knob that redirects that read). So the
5
- // operator's native `permissions.defaultMode` ("auto"), hooks, plugins, and
6
- // per-cwd memory/projects state would otherwise leak into entwurf ACP
5
+ // operator's native `permissions.defaultMode` (whatever its current value), hooks,
6
+ // plugins, and per-cwd memory/projects state would otherwise leak into entwurf ACP
7
7
  // sessions. The overlay redirects SettingsManager at a pi-owned directory whose
8
8
  // `settings.json` WE author (minimal, `hooks:{}`), while keeping exactly the
9
9
  // operator entries a backend needs (credentials, caches, built-in skills)
@@ -15,7 +15,7 @@
15
15
  // materializer, and cortex did: `ensureCortexDualHomeOverlay` (+ projectCortexMcpJson /
16
16
  // sweepDeadCortexOverlays / cortexOverlayScopeId) lives further down this file with its
17
17
  // own D-number rationale — an isolated HOME rather than a redirect knob, because cortex
18
- // has no CLAUDE_CONFIG_DIR equivalent (§11-8 D2). The two share the module, never the
18
+ // has no CLAUDE_CONFIG_DIR equivalent (ACP rail Cortex audit D2). The two share the module, never the
19
19
  // strategy. Codex/Gemini overlays (CODEX_HOME / admin.toml) remain behavior-oracle
20
20
  // territory and are out of scope here.
21
21
  //
@@ -86,9 +86,11 @@ export const OVERLAY_EMPTY_DIRS = new Set(["projects", "sessions"]);
86
86
  export const OVERLAY_BINARY_OWNED = new Set([".claude.json", "backups", "settings.json"]);
87
87
  /**
88
88
  * Minimal overlay settings.json. Only fields with a reason to pin:
89
- * - `permissions.defaultMode: "default"` neutralizes the operator's native
90
- * "auto"; combined with the explicit `tools`/`permissionAllow` surface,
91
- * "default" auto-passes every tool we expose without prompts.
89
+ * - `permissions.defaultMode: "bypassPermissions"` is deliberate unattended
90
+ * ACP operation: a tool call must never suspend a model turn on an interactive
91
+ * permission prompt. This does not widen the callable surface — explicit
92
+ * `tools`/`disallowedTools` still shape it, and `permissionAllow` still rides
93
+ * the inline Claude settings — or bypass backend authentication.
92
94
  * - `autoMemoryEnabled: false` — SDK opt-out for auto-memory (defense in
93
95
  * depth; the tiny non-empty engraving/preset replacement is the primary
94
96
  * write-containment lever for Claude ACP).
@@ -97,7 +99,7 @@ export const OVERLAY_BINARY_OWNED = new Set([".claude.json", "backups", "setting
97
99
  */
98
100
  export function overlaySettingsJson() {
99
101
  return `${JSON.stringify({
100
- permissions: { defaultMode: "default" },
102
+ permissions: { defaultMode: "bypassPermissions" },
101
103
  autoMemoryEnabled: false,
102
104
  hooks: {},
103
105
  }, null, 2)}\n`;
@@ -201,11 +203,11 @@ export function ensureClaudeConfigOverlay(realDir = CLAUDE_REAL_CONFIG_DIR, over
201
203
  }
202
204
  // ============================================================================
203
205
  // Cortex dual-HOME overlay — the as-measured containment for `cortex acp serve`
204
- // (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md §4/§11-8).
206
+ // (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md “Cortex Code audit”).
205
207
  // ============================================================================
206
208
  //
207
209
  // Why this is NOT the claude-shaped `SNOWFLAKE_HOME`-only overlay PR #40 shipped
208
- // (each point is a measured defect, not a preference — §11-8 D-numbers):
210
+ // (each point is a measured defect, not a preference — Cortex audit D-numbers):
209
211
  //
210
212
  // - D2: cortex reads `CONFIG_DIRS = [".claude", ".cortex"]` at `homedir()` and
211
213
  // `~/.claude/skills` — a SNOWFLAKE_HOME redirect cannot move that axis. An
@@ -16,12 +16,14 @@
16
16
  import * as net from "node:net";
17
17
  /**
18
18
  * THE `<sender_info>` synthesis — the one place the sender envelope becomes
19
- * message text. Two consumers, one shape (#50 C3):
19
+ * message text. ONE consumer since the visible-first cut (#50 C3):
20
20
  * - the live socket rail's RECEIVER (entwurf-control handleCommand("send")
21
- * appends it to the delivered customMessage);
22
- * - the dormant spawn-resume rail's SENDER (entwurf-v2-production appends it
23
- * to the resume prompt, so a resumed citizen wakes knowing who called in
24
- * exactly the shape a live delivery would have rendered).
21
+ * appends it to the delivered customMessage).
22
+ *
23
+ * The second consumer was the dormant spawn-resume rail's SENDER, which appended
24
+ * the same shape to a resume prompt so a resumed citizen woke knowing who called.
25
+ * That rail is gone. The shape stays single-sourced here so a future VISIBLE
26
+ * resume renders an identical envelope rather than inventing a second one.
25
27
  * `wants_reply` is emitted only when explicitly true — an unset/false marker
26
28
  * renders nothing (etiquette marker, not transport contract).
27
29
  */