@junghanacs/entwurf 0.12.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 (183) hide show
  1. package/AGENTS.md +240 -0
  2. package/BASELINE.md +227 -0
  3. package/CHANGELOG.md +1210 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/DELIVERY.md +209 -0
  6. package/LICENSE +21 -0
  7. package/README.md +504 -0
  8. package/VERIFY.md +260 -0
  9. package/demo/README.md +188 -0
  10. package/demo/demo-baseline.sh +156 -0
  11. package/demo/demo.sh +183 -0
  12. package/docs/assets/entwurf-demo.gif +0 -0
  13. package/docs/assets/entwurf-doomemacs.gif +0 -0
  14. package/docs/assets/entwurf-entwurf.gif +0 -0
  15. package/docs/assets/entwurf-hero.jpg +0 -0
  16. package/docs/setup-clean-host.md +305 -0
  17. package/mcp/entwurf-bridge/src/index.ts +513 -0
  18. package/mcp/entwurf-bridge/start.sh +25 -0
  19. package/mcp/entwurf-bridge/test.sh +54 -0
  20. package/mcp/tsconfig.json +29 -0
  21. package/package.json +130 -0
  22. package/pi/entwurf-capabilities.json +9 -0
  23. package/pi/entwurf-targets.json +20 -0
  24. package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
  25. package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
  26. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
  27. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
  28. package/pi/settings.reference.json +43 -0
  29. package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
  30. package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
  31. package/pi-extensions/acp-provider.ts +63 -0
  32. package/pi-extensions/entwurf-control.ts +1692 -0
  33. package/pi-extensions/lib/acp/acp-client.ts +90 -0
  34. package/pi-extensions/lib/acp/augment.ts +238 -0
  35. package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
  36. package/pi-extensions/lib/acp/backend.ts +873 -0
  37. package/pi-extensions/lib/acp/config.ts +552 -0
  38. package/pi-extensions/lib/acp/context.ts +177 -0
  39. package/pi-extensions/lib/acp/engraving.ts +123 -0
  40. package/pi-extensions/lib/acp/event-mapper.ts +339 -0
  41. package/pi-extensions/lib/acp/models.ts +102 -0
  42. package/pi-extensions/lib/acp/overlay.ts +220 -0
  43. package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
  44. package/pi-extensions/lib/acp/session-store.ts +418 -0
  45. package/pi-extensions/lib/acp/tool-surface.ts +184 -0
  46. package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
  47. package/pi-extensions/lib/entwurf-core.ts +2033 -0
  48. package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
  49. package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
  50. package/pi-extensions/lib/entwurf-facts.ts +251 -0
  51. package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
  52. package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
  53. package/pi-extensions/lib/entwurf-preflight.ts +247 -0
  54. package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
  55. package/pi-extensions/lib/entwurf-self-address.ts +117 -0
  56. package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
  57. package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
  58. package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
  59. package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
  60. package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
  61. package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
  62. package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
  63. package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
  64. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
  65. package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
  66. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
  67. package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
  68. package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
  69. package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
  70. package/pi-extensions/lib/meta-session.ts +1951 -0
  71. package/pi-extensions/lib/project-trust-handler.ts +154 -0
  72. package/pi-extensions/lib/session-id.js +57 -0
  73. package/pi-extensions/lib/socket-discovery.ts +346 -0
  74. package/pi-extensions/lib/socket-probe.ts +92 -0
  75. package/pi-extensions/meta-bridge-hook.ts +250 -0
  76. package/pi-extensions/model-lock.ts +235 -0
  77. package/prompts/engraving.md +27 -0
  78. package/protocol.js +31 -0
  79. package/run.sh +2832 -0
  80. package/scripts/check-acp-backend-preflight.ts +130 -0
  81. package/scripts/check-acp-carrier-augment.ts +297 -0
  82. package/scripts/check-acp-config.ts +322 -0
  83. package/scripts/check-acp-event-mapper.ts +253 -0
  84. package/scripts/check-acp-overlay.ts +154 -0
  85. package/scripts/check-acp-prompt-builder.ts +191 -0
  86. package/scripts/check-acp-provider-surface.ts +159 -0
  87. package/scripts/check-acp-sdk-surface.ts +211 -0
  88. package/scripts/check-acp-session-reuse.ts +756 -0
  89. package/scripts/check-acp-session-store.ts +387 -0
  90. package/scripts/check-acp-tool-surface.ts +159 -0
  91. package/scripts/check-entwurf-bridge-boot.ts +199 -0
  92. package/scripts/check-entwurf-capabilities.ts +123 -0
  93. package/scripts/check-entwurf-control-rpc.ts +187 -0
  94. package/scripts/check-entwurf-deliverability.ts +158 -0
  95. package/scripts/check-entwurf-fact-provider.ts +316 -0
  96. package/scripts/check-entwurf-facts.ts +358 -0
  97. package/scripts/check-entwurf-mailbox-guard.ts +264 -0
  98. package/scripts/check-entwurf-peers-surface.ts +235 -0
  99. package/scripts/check-entwurf-resume-args.ts +149 -0
  100. package/scripts/check-entwurf-self-address.ts +209 -0
  101. package/scripts/check-entwurf-session-identity.ts +703 -0
  102. package/scripts/check-entwurf-v2-contract.ts +536 -0
  103. package/scripts/check-entwurf-v2-decider.ts +795 -0
  104. package/scripts/check-entwurf-v2-lock.ts +368 -0
  105. package/scripts/check-entwurf-v2-mailbox.ts +228 -0
  106. package/scripts/check-entwurf-v2-matrix.ts +437 -0
  107. package/scripts/check-entwurf-v2-production.ts +406 -0
  108. package/scripts/check-entwurf-v2-release.ts +245 -0
  109. package/scripts/check-entwurf-v2-runner.ts +452 -0
  110. package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
  111. package/scripts/check-entwurf-v2-send.ts +454 -0
  112. package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
  113. package/scripts/check-entwurf-v2-spawn.ts +398 -0
  114. package/scripts/check-entwurf-v2-surface.ts +352 -0
  115. package/scripts/check-keyset-overlap.py +120 -0
  116. package/scripts/check-mailbox-receipt-state.ts +170 -0
  117. package/scripts/check-meta-capability-source.ts +112 -0
  118. package/scripts/check-meta-dual-consumers.ts +154 -0
  119. package/scripts/check-meta-dual-read.ts +158 -0
  120. package/scripts/check-meta-listing.ts +138 -0
  121. package/scripts/check-meta-mailbox-state-write.ts +135 -0
  122. package/scripts/check-meta-migration.ts +212 -0
  123. package/scripts/check-meta-receiver-marker.ts +185 -0
  124. package/scripts/check-meta-record-v2.ts +191 -0
  125. package/scripts/check-meta-session.ts +673 -0
  126. package/scripts/check-model-lock.ts +408 -0
  127. package/scripts/check-package-source-routing.ts +253 -0
  128. package/scripts/check-pi-preflight.ts +304 -0
  129. package/scripts/check-project-trust-handler.ts +265 -0
  130. package/scripts/check-shell-quote.ts +121 -0
  131. package/scripts/check-socket-discovery.ts +428 -0
  132. package/scripts/check-socket-probe.ts +106 -0
  133. package/scripts/fixtures/probe-mcp-server.ts +33 -0
  134. package/scripts/gnew-rpc-drive.ts +211 -0
  135. package/scripts/lib/acp-child-cleanup.ts +116 -0
  136. package/scripts/meta-bridge-doctor.sh +315 -0
  137. package/scripts/meta-bridge-hook-log.sh +26 -0
  138. package/scripts/meta-bridge-install.sh +135 -0
  139. package/scripts/meta-bridge-prune.ts +199 -0
  140. package/scripts/meta-bridge-state.py +549 -0
  141. package/scripts/meta-bridge-statusline.sh +192 -0
  142. package/scripts/meta-bridge-store-doctor.ts +64 -0
  143. package/scripts/meta-bridge-uninstall.sh +39 -0
  144. package/scripts/new-session-id.ts +25 -0
  145. package/scripts/postinstall-chmod.cjs +58 -0
  146. package/scripts/raw-async-delivery/README.md +258 -0
  147. package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
  148. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
  149. package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
  150. package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
  151. package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
  152. package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
  153. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
  154. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
  155. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
  156. package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
  157. package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
  158. package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
  159. package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
  160. package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
  161. package/scripts/resolve-acp-bridge.ts +25 -0
  162. package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
  163. package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
  164. package/scripts/smoke-acp-mcp-live.ts +129 -0
  165. package/scripts/smoke-acp-memory-containment-live.ts +389 -0
  166. package/scripts/smoke-acp-overlay-live.ts +314 -0
  167. package/scripts/smoke-acp-provider-live.ts +162 -0
  168. package/scripts/smoke-acp-raw-turn-live.ts +261 -0
  169. package/scripts/smoke-acp-session-reuse-live.ts +172 -0
  170. package/scripts/smoke-acp-skill-live.ts +144 -0
  171. package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
  172. package/scripts/smoke-claude-native-resume-live.sh +198 -0
  173. package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
  174. package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
  175. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
  176. package/scripts/smoke-meta-async-drift.sh +171 -0
  177. package/scripts/smoke-meta-honesty.sh +147 -0
  178. package/scripts/smoke-meta-install-state.sh +403 -0
  179. package/scripts/smoke-meta-keyset-guard.sh +111 -0
  180. package/scripts/smoke-meta-prune.sh +174 -0
  181. package/scripts/smoke-resident-garden-guard.sh +433 -0
  182. package/scripts/smoke-session-id-name.ts +187 -0
  183. package/scripts/tsconfig.json +34 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * entwurf-v2-surface — 5d-3a: the ctx-free SURFACE adapter that joins a surface's raw params
3
+ * to `runEntwurfV2` and renders the outcome-rich result back to a human/tool string. It is the
4
+ * ONE place a surface (pi-native `entwurf_v2` tool now; the MCP `entwurf_v2` verb in 5d-3b)
5
+ * crosses into the v2 fence — so `entwurf-control.ts` (a root-tsc, emit-capable surface that
6
+ * CANNOT statically import the `.ts`-extension fence without TS5097) reaches this via a
7
+ * NON-LITERAL dynamic import, and the MCP bridge (already a `.ts`-import consumer) imports it
8
+ * directly. Either way the ctx binding stays OUT of the fence: the caller builds
9
+ * `senderProvider` from its own envelope source and passes it in `opts`.
10
+ *
11
+ * Three exports:
12
+ * - `toDispatchInput` — surface params → `DispatchInput` (the `wants_reply`→`wantsReply`
13
+ * snake→camel bridge; `intent` is REQUIRED and passed through verbatim, never inferred
14
+ * from mode/action — that would blur the F1 ownership contract).
15
+ * - `renderEntwurfV2Result` — the result union → `{ text, isError }`, surfacing the carry-overs
16
+ * the surface must NOT drop: a reject's reason+diagnostic, a control `rejectReason` (N3), a
17
+ * spawn `lock-retained` diagnostic, and the N1 delivered+lock-dirty `execution-failed`.
18
+ * - `runAndRenderEntwurfV2FromSurface` — assemble production deps + run + render, so the root
19
+ * surface never needs to name the `EntwurfV2RunResult` union (it only sees `{text,isError}`).
20
+ */
21
+
22
+ import * as path from "node:path";
23
+ import type { SenderEnvelope } from "./entwurf-control-rpc.ts";
24
+ import type { DispatchInput, EntwurfV2Mode } from "./entwurf-v2-decider.ts";
25
+ import { makeProductionEntwurfV2Deps, type ProductionEntwurfV2Opts } from "./entwurf-v2-production.ts";
26
+ import { type EntwurfV2RunResult, runEntwurfV2 } from "./entwurf-v2-runner.ts";
27
+
28
+ /** The operator-policy SSOT for v2 dispatch's preflight prefix-auto-approve roots (5d-4b).
29
+ * ONE shared env var feeds BOTH surfaces (pi-native + MCP) — a pi session and an MCP child
30
+ * both inherit it, so there is no per-surface config fork. `prefixRoots` is operator policy,
31
+ * not session-local UX, so it is an env var, not a pi flag. */
32
+ export const ENTWURF_PREFIX_ROOTS_ENV = "ENTWURF_PREFIX_ROOTS";
33
+
34
+ /** Parse `ENTWURF_PREFIX_ROOTS` into the preflight's `prefixRoots`. `path.delimiter`-
35
+ * separated (`:` on Linux/macOS); entries are trimmed, empty segments dropped. Unset / empty
36
+ * / delimiters-only ⇒ `[]` (no prefix promotion — frozen decision 7, no package default).
37
+ * It does NOT throw on a nonexistent/typo path: `preflight`'s normalize keeps an absolute
38
+ * fallback, so a bad root simply never matches (a typo must not broaden approve, and must not
39
+ * turn every owned-outcome dispatch into a loud failure). `~` is left for preflight to expand. */
40
+ export function parseEntwurfPrefixRootsEnv(raw: string | undefined = process.env[ENTWURF_PREFIX_ROOTS_ENV]): string[] {
41
+ if (!raw) return [];
42
+ return raw
43
+ .split(path.delimiter)
44
+ .map((s) => s.trim())
45
+ .filter((s) => s.length > 0);
46
+ }
47
+
48
+ /** The raw shape a surface (pi tool / MCP verb) collects. `wants_reply` is snake_case to
49
+ * match the external `entwurf_v2` convention; the runner sees `wantsReply`. */
50
+ export interface SurfaceEntwurfV2Params {
51
+ target: string;
52
+ intent: "fire-and-forget" | "owned-outcome";
53
+ mode?: EntwurfV2Mode;
54
+ wants_reply?: boolean;
55
+ message: string;
56
+ }
57
+
58
+ /** ctx-free run options. The caller (entwurf-control.ts / MCP bridge) builds `senderProvider`
59
+ * from its own envelope source — this module never touches `ExtensionContext`. Both surfaces
60
+ * leave `agentDir`/`prefixRoots` undefined by design: `runAndRenderEntwurfV2FromSurface` falls
61
+ * back to the `ENTWURF_PREFIX_ROOTS` env SSOT for `prefixRoots` (5d-4), and `agentDir` stays
62
+ * undefined (no surface sets it). Explicit opts still win — kept for tests / a future surface. */
63
+ export interface EntwurfV2SurfaceRunOptions {
64
+ senderProvider: () => SenderEnvelope | undefined;
65
+ agentDir?: string;
66
+ prefixRoots?: readonly string[];
67
+ }
68
+
69
+ /** What the surface renders: the human/tool text + whether it is an error (a non-delivery). */
70
+ export interface EntwurfV2SurfaceRendered {
71
+ text: string;
72
+ isError: boolean;
73
+ }
74
+
75
+ /** Surface params → the runner's `DispatchInput`. `wants_reply`→`wantsReply`; `intent` and
76
+ * `message` pass through; `mode`/`wantsReply` are left undefined when absent so the decider's
77
+ * own defaults (follow_up / false) apply — no double-default. */
78
+ export function toDispatchInput(params: SurfaceEntwurfV2Params): DispatchInput {
79
+ return {
80
+ target: params.target,
81
+ intent: params.intent,
82
+ message: params.message,
83
+ mode: params.mode,
84
+ wantsReply: params.wants_reply,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Detour B (B-a) — actionable rendering of an honest reject. The decider is UNCHANGED:
90
+ * a reject stays a reject (Hard Rule 3), and intent is NEVER auto-converted (Hard Rule 2 —
91
+ * owned→fire-and-forget mailbox fallback would break the F1 ownership contract). This only
92
+ * appends a one-line "what to do instead" to the reject TEXT, so an honest reject stops
93
+ * reading as "delivery impossible". Returns undefined for rejects with no useful next step.
94
+ */
95
+ export function actionableRejectHint(reason: string): string | undefined {
96
+ switch (reason) {
97
+ case "backend-liveness-unsupported":
98
+ // A meta-session backend (e.g. claude-code self-fetch) has no liveness predicate, so
99
+ // owned-outcome has nothing to own. Replies go to the mailbox via fire-and-forget.
100
+ return (
101
+ "meta-session backend has no liveness predicate → owned-outcome is unsupported. " +
102
+ "To reply, dispatch with intent: fire-and-forget — it routes to the meta-mailbox when a " +
103
+ "deliverable/active receiver is armed (else it fail-closes as mailbox-undeliverable). " +
104
+ "(Intent is not auto-converted; you choose it.)"
105
+ );
106
+ case "owned-live-no-autosend":
107
+ // A live target is reachable, but owned-outcome is not an auto-send (Q2/F1).
108
+ return "target is live — owned-outcome never auto-sends. Use intent: fire-and-forget (with wants_reply if you need a reply).";
109
+ default:
110
+ return undefined;
111
+ }
112
+ }
113
+
114
+ /** Render the outcome-rich result to `{ text, isError }`. A reject or a thrown/failed/dirty
115
+ * delivery is `isError:true`; a sent/fallback-sent/enqueued/observed delivery is `isError:false`.
116
+ * A control in-band `rejected` is a non-delivery (isError:true) and carries N3 `rejectReason`
117
+ * when present; a spawn `lock-retained` is fail-closed (isError:true) with its diagnostic. */
118
+ export function renderEntwurfV2Result(result: EntwurfV2RunResult): EntwurfV2SurfaceRendered {
119
+ switch (result.kind) {
120
+ case "rejected": {
121
+ const r = result.receipt;
122
+ let text = `entwurf_v2 rejected: ${r.reason} (observed liveness: ${r.observedLiveness ?? "n/a"})`;
123
+ const hint = actionableRejectHint(r.reason);
124
+ if (hint) text += `\n → ${hint}`;
125
+ if (result.diagnostic?.kind === "target-locked") {
126
+ const c = result.diagnostic.conflict;
127
+ text +=
128
+ `\n target-locked: ${c.lockPath}` +
129
+ `\n ${c.detail}` +
130
+ (c.holder ? `\n holder: pid ${c.holder.pid} on ${c.holder.hostname} since ${c.holder.createdAt}` : "");
131
+ }
132
+ return { text, isError: true };
133
+ }
134
+ case "executed": {
135
+ const o = result.outcome;
136
+ if (o.transport === "control-socket") {
137
+ const delivered = o.outcome === "sent" || o.outcome === "fallback-sent";
138
+ const reason = o.rejectReason ? ` (reason: ${o.rejectReason})` : "";
139
+ return {
140
+ text: `entwurf_v2 control-socket → ${o.outcome}${reason}`,
141
+ isError: !delivered,
142
+ };
143
+ }
144
+ if (o.transport === "spawn-bg") {
145
+ const res = o.result;
146
+ if (res.kind === "lock-retained") {
147
+ const d = res.diagnostic;
148
+ return {
149
+ text:
150
+ `entwurf_v2 spawn-bg LOCK RETAINED (${res.reason}) — lock NOT released, operator must clear:` +
151
+ `\n target: ${d.targetGardenId}` +
152
+ `\n lockPath: ${d.lockPath}` +
153
+ `\n expectedSocketPath: ${d.expectedSocketPath}` +
154
+ `\n observeTimeoutMs: ${d.observeTimeoutMs}, killGraceMs: ${d.killGraceMs}` +
155
+ (res.error ? `\n error: ${res.error}` : ""),
156
+ isError: true,
157
+ };
158
+ }
159
+ if (res.kind === "spawn-start-failed") {
160
+ return { text: `entwurf_v2 spawn-bg failed to start: ${res.error}`, isError: true };
161
+ }
162
+ const pid = "pid" in res && res.pid !== undefined ? ` (pid ${res.pid})` : "";
163
+ const exit = res.kind === "child-exited" ? ` exitCode=${res.exitCode}` : "";
164
+ return { text: `entwurf_v2 spawn-bg → ${res.kind}${pid}${exit}, lock released`, isError: false };
165
+ }
166
+ // meta-mailbox
167
+ return { text: "entwurf_v2 meta-mailbox → enqueued", isError: false };
168
+ }
169
+ case "execution-failed": {
170
+ if (result.releaseFailed && result.finalizedOutcome) {
171
+ // N1: the delivery/refusal reached a terminal outcome but releaseLock then threw.
172
+ return {
173
+ text:
174
+ `entwurf_v2 ${result.transport} DELIVERED (${result.finalizedOutcome}) but the lock is DIRTY ` +
175
+ `(release failed) — do NOT retry, a re-send would double-deliver. Clear the lock by hand.` +
176
+ `\n error: ${result.error}`,
177
+ isError: true,
178
+ };
179
+ }
180
+ return {
181
+ text: `entwurf_v2 ${result.transport} execution failed: ${result.error} (retry-safe: ${result.retrySafe})`,
182
+ isError: true,
183
+ };
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Assemble production deps, run the v2 dispatch, and render the result. The root surface only
190
+ * ever sees `{ text, isError }` — it never names the `EntwurfV2RunResult` union — so the v2
191
+ * fence types stay behind this one entry point.
192
+ */
193
+ export async function runAndRenderEntwurfV2FromSurface(
194
+ params: SurfaceEntwurfV2Params,
195
+ opts: EntwurfV2SurfaceRunOptions,
196
+ ): Promise<EntwurfV2SurfaceRendered> {
197
+ const prodOpts: ProductionEntwurfV2Opts = {
198
+ senderProvider: opts.senderProvider,
199
+ agentDir: opts.agentDir,
200
+ // Explicit opts win (test / future surface override); otherwise the shared env SSOT.
201
+ prefixRoots: opts.prefixRoots ?? parseEntwurfPrefixRootsEnv(),
202
+ };
203
+ const result = await runEntwurfV2(toDispatchInput(params), makeProductionEntwurfV2Deps(prodOpts));
204
+ return renderEntwurfV2Result(result);
205
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * meta-mailbox-body — the SINGLE source for rendering an entwurf message as a
3
+ * meta-bridge mailbox body. Both transports that can deliver to a garden citizen
4
+ * with no live control socket use this:
5
+ * - the MCP bridge entwurf_v2 (mcp/entwurf-bridge) — external/Claude-host sends
6
+ * - the pi-native entwurf_v2 (pi-extensions/entwurf-control.ts) — pi-session sends
7
+ *
8
+ * The control-socket path carries the sender envelope inside its RPC framing; the
9
+ * mailbox path is just a file, so the envelope must be SERIALIZED INTO the body —
10
+ * else a receiver reading entwurf_inbox_read would not know who sent it, whether
11
+ * the sender is replyable (and at which sessionId), or whether a reply was wanted.
12
+ * The render mirrors the live "[entwurf received ⟵]" header so a transcript reads
13
+ * the same whether the message arrived over a socket or a mailbox.
14
+ *
15
+ * No filesystem/network IO and no mutation — the only ambient read is
16
+ * process.env.HOME for display abbreviation (so not strictly referentially pure,
17
+ * but deterministic per environment). Extracted so the two senders cannot drift
18
+ * in how a mailbox message presents who-sent-it — the field that round-trips
19
+ * garden-id replies.
20
+ */
21
+
22
+ /** The fields a mailbox body needs from a sender. Structurally compatible with
23
+ * the SenderEnvelope of both entwurf_v2 surfaces. */
24
+ export interface MailboxSenderEnvelope {
25
+ sessionId: string;
26
+ agentId: string;
27
+ cwd: string;
28
+ timestamp: string; // ISO 8601 UTC
29
+ origin?: "pi-session" | "external-mcp" | "meta-session";
30
+ replyable?: boolean;
31
+ }
32
+
33
+ /** `~`-abbreviate a home-relative cwd for display. Reads process.env.HOME. */
34
+ function abbreviateHome(cwd: string): string {
35
+ const home = process.env.HOME;
36
+ if (!home) return cwd;
37
+ if (cwd === home) return "~";
38
+ if (cwd.startsWith(`${home}/`)) return `~${cwd.slice(home.length)}`;
39
+ return cwd;
40
+ }
41
+
42
+ /** Format a UTC ISO timestamp as `YYYY-MM-DD HH:MM:SS KST` (UTC+9, no DST).
43
+ * Returns the raw input when it does not parse. Pure. */
44
+ function formatKstTimestamp(iso: string): string {
45
+ const ms = Date.parse(iso);
46
+ if (Number.isNaN(ms)) return iso;
47
+ const kst = new Date(ms + 9 * 60 * 60 * 1000);
48
+ const pad = (n: number) => n.toString().padStart(2, "0");
49
+ return (
50
+ `${kst.getUTCFullYear()}-${pad(kst.getUTCMonth() + 1)}-${pad(kst.getUTCDate())} ` +
51
+ `${pad(kst.getUTCHours())}:${pad(kst.getUTCMinutes())}:${pad(kst.getUTCSeconds())} KST`
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Render the full mailbox body: header envelope + separator + message. A
57
+ * replyable sender (pi-session, or a trusted meta-session) advertises its
58
+ * sessionId as the reply address; a non-replyable sender says so WITHOUT
59
+ * losing its origin — a record-backed meta-session that is currently inactive
60
+ * renders as `(meta-session, non-replyable)`, not degraded to `external`.
61
+ */
62
+ export function formatMetaMailboxBody(sender: MailboxSenderEnvelope, message: string, wantsReply: boolean): string {
63
+ const replyable = sender.replyable === true;
64
+ const isMeta = sender.origin === "meta-session";
65
+ const kind = isMeta ? "meta-session, " : "";
66
+ const sessionLine = replyable
67
+ ? `${sender.sessionId} (${kind}replyable — reply via entwurf_v2 to this sessionId, intent=fire-and-forget)`
68
+ : isMeta
69
+ ? `${sender.sessionId} (meta-session, non-replyable)`
70
+ : `${sender.sessionId} (external, non-replyable)`;
71
+ return (
72
+ `[entwurf received ⟵]\n` +
73
+ ` from: ${sender.agentId} @ ${abbreviateHome(sender.cwd)}\n` +
74
+ ` session: ${sessionLine}\n` +
75
+ ` at: ${formatKstTimestamp(sender.timestamp)}\n` +
76
+ ` wants reply: ${wantsReply ? "yes" : "no"}\n` +
77
+ `────────────────────────────────────────\n` +
78
+ `${message}\n`
79
+ );
80
+ }