@junghanacs/entwurf 0.45.0 → 0.50.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 (105) hide show
  1. package/AGENTS.md +3 -2
  2. package/BASELINE.md +110 -1
  3. package/CHANGELOG.md +169 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/ENTWURF-CLI.json +1229 -0
  6. package/ENTWURF.json +63 -15
  7. package/FAQ.md +4 -4
  8. package/README.md +124 -188
  9. package/VERIFY.md +2 -2
  10. package/docs/external-mcp-host.md +3 -3
  11. package/docs/setup-clean-host.md +37 -12
  12. package/mcp/entwurf-bridge/dist/ENTWURF.json +63 -15
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +19 -8
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +483 -18
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +50 -5
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
  25. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +30 -15
  26. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
  27. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
  28. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
  29. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  30. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
  31. package/mcp/entwurf-bridge/src/index.ts +28 -8
  32. package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
  33. package/native/win32-x64/manifest.json +1 -1
  34. package/package.json +6 -3
  35. package/pi/pi-durable/bootstrap.mjs +179 -6
  36. package/pi/pi-durable/carrier/runtime.js +108 -9
  37. package/pi/pi-durable/carrier/session-name.js +11 -0
  38. package/pi/pi-durable/carrier/sessions.js +96 -20
  39. package/pi/pi-durable/overlay/upstream-pin.json +11 -5
  40. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +710 -38
  41. package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
  42. package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
  43. package/pi-extensions/lib/herdr-resume-call.ts +810 -0
  44. package/pi-extensions/lib/meta-session.ts +13 -8
  45. package/pi-extensions/lib/mux-resume-call.ts +77 -4
  46. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
  47. package/pi-extensions/lib/pi-durable-location.ts +446 -0
  48. package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
  49. package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
  50. package/pi-extensions/lib/resume-launch-identity.ts +31 -12
  51. package/pi-extensions/meta-bridge-pi-durable.ts +29 -15
  52. package/run.sh +411 -657
  53. package/scripts/check-claude-launch.ts +341 -0
  54. package/scripts/check-entwurf-v2-surface.ts +2 -2
  55. package/scripts/check-fresh-call-dispatch.ts +70 -9
  56. package/scripts/check-fresh-cut-gate.sh +23 -9
  57. package/scripts/check-gate-qualification.ts +18 -13
  58. package/scripts/check-herdr-activation.ts +301 -23
  59. package/scripts/check-herdr-fresh-call.ts +161 -25
  60. package/scripts/check-herdr-plugin-build.ts +173 -1
  61. package/scripts/check-herdr-plugin.ts +28 -2
  62. package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
  63. package/scripts/check-install-surface.ts +1236 -6
  64. package/scripts/check-meta-doctor-oracle.sh +1 -1
  65. package/scripts/check-meta-facts.ts +20 -1
  66. package/scripts/check-mux-resume-call.ts +28 -0
  67. package/scripts/check-peer-facts.ts +39 -2
  68. package/scripts/cli-help.mjs +222 -0
  69. package/scripts/dev-bin.sh +39 -2
  70. package/scripts/herdr-activation.mjs +28 -6
  71. package/scripts/herdr-operator-link.mjs +389 -0
  72. package/scripts/herdr-plugin-activate.mjs +5 -0
  73. package/scripts/herdr-plugin-deactivate.mjs +18 -5
  74. package/scripts/herdr-rails.mjs +43 -7
  75. package/scripts/herdr-runtime.mjs +27 -0
  76. package/scripts/meta-bridge-fresh-cut.ts +1 -27
  77. package/scripts/meta-bridge-prune.ts +1 -1
  78. package/scripts/meta-facts.ts +1 -1
  79. package/scripts/mutants/agent-guidance.json +4 -4
  80. package/scripts/mutants/claude-launch.json +142 -0
  81. package/scripts/mutants/cli-help-source.json +420 -0
  82. package/scripts/mutants/dev-gate-dispatch.json +109 -0
  83. package/scripts/mutants/fresh-call-dispatch.json +38 -2
  84. package/scripts/mutants/fresh-cut.json +15 -0
  85. package/scripts/mutants/herdr-activation.json +128 -0
  86. package/scripts/mutants/herdr-fresh-call.json +278 -5
  87. package/scripts/mutants/herdr-operator-link.json +318 -0
  88. package/scripts/mutants/herdr-plugin-build.json +55 -0
  89. package/scripts/mutants/herdr-plugin.json +17 -0
  90. package/scripts/mutants/herdr-resume-call.json +288 -0
  91. package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
  92. package/scripts/mutants/meta-facts.json +15 -0
  93. package/scripts/mutants/mux-resume-call.json +63 -2
  94. package/scripts/mutants/peer-facts.json +15 -0
  95. package/scripts/mutants/pi-durable.json +979 -1
  96. package/scripts/mutants/source-install.json +35 -0
  97. package/scripts/mutants/v2-surface.json +2 -2
  98. package/scripts/mutants/v2-visible-resume.json +255 -6
  99. package/scripts/peer-facts.ts +1 -1
  100. package/scripts/smoke-agy-install-state.sh +66 -0
  101. package/scripts/smoke-meta-prune.sh +23 -0
  102. package/scripts/smoke-omp-fresh-live.ts +12 -3
  103. package/scripts/smoke-omp-receive-live.ts +7 -1
  104. package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
  105. package/team.json.example +39 -0
@@ -0,0 +1,497 @@
1
+ /**
2
+ * herdr-resume-call — reopen ONE dormant pi citizen's own transcript in a new herdr tab, from inside
3
+ * herdr (#157 G1a).
4
+ *
5
+ * WHAT THIS IS. The herdr half of `entwurf_resume_call`, and only its LAUNCH. The v2 composition
6
+ * (`entwurf-v2-visible-resume.ts`) owns everything that makes a resume a resume: the record, the
7
+ * per-gid lock, the dormancy proof and the same-gid control-socket observation. It reaches this
8
+ * module through the injected launch seam the bridge composes (`mcp/entwurf-bridge/src/index.ts`),
9
+ * exactly as it reaches the tmux leaf `mux-resume-call.ts`; which of the two runs is chosen by the
10
+ * shared rail root (`dispatchResumeLaunch` in `fresh-call-dispatch.ts`), not by the bridge and not
11
+ * here — so this file never sees an address, never reads a record and never decides whether the
12
+ * citizen came back.
13
+ *
14
+ * PI ONLY ON THIS PATH. A Claude Code resume proves its return by joining the receiver marker's owner
15
+ * pid to the pid of the process its launch started, and `agent start` types the command into the
16
+ * pane's interactive shell, so the pane's direct child is that shell, not Claude Code. A Claude Code
17
+ * target therefore launches through ONE layout.apply instead (`herdrClaudeCodeResumeCall` below), and
18
+ * like every herdr refusal nothing here falls back to tmux: a window somewhere the operator is not
19
+ * looking is worse than a refusal.
20
+ *
21
+ * WHAT IS REUSED AND WHAT IS NOT. Every herdr protocol primitive comes from `herdr-fresh-call.ts`:
22
+ * the context, the cwd classification, the caller-pane read, the tab-create reply parser, the start
23
+ * argv and its reply parser, the argv echo check, herdr's error code and the conditional reclaim.
24
+ * The fresh tab-create BUILDER is not reused, because it carries the fresh callback env by contract
25
+ * and a resume has no callback, no nonce, no task, no prompt and no model of its own — the runtime
26
+ * argv is the record's, built on the v2 side. Borrowing that builder would mean faking a callback for
27
+ * a sibling nobody is waiting on. No other fresh-specific stage is reused either: no birth framing
28
+ * and no settle witness.
29
+ *
30
+ * THE ORDER IS THE FRESH RAIL'S, FOR THE SAME REASON. `tab create` and `agent start` are two
31
+ * mutations, so everything decidable is decided before the first: context, backend, cwd, control
32
+ * characters in the runtime argv, and the read-only question of which workspace the caller is in.
33
+ * Every failure after the tab create carries a recovery that says what is out there; none of them
34
+ * says that nothing was opened.
35
+ *
36
+ * WHAT THE RECEIPT IS NOT. A successful `agent start` is a LAUNCH: herdr made a tab and was asked to
37
+ * start pi on that transcript there. herdr's readiness and its `agent_session` are herdr's view of
38
+ * the pane, not garden addressability, so this module does not wait for either. The same-gid control
39
+ * socket the v2 side waits for afterwards is the only observation that says the citizen is back.
40
+ *
41
+ * PI-DURABLE IS A SECOND, SEPARATE LAUNCH (#157 B3). herdr has no agent kind for the durable app, so
42
+ * its same-id resume reuses the fresh rail's ONE `layout.apply` on the caller's own herdr socket
43
+ * (`herdrPiDurableResumeCall` below) rather than `tab create` + `agent start`. The pi path above is
44
+ * untouched and still refuses every non-pi backend; `launchHerdrResume` is the one place that tells
45
+ * the three apart, by the backend the v2 side resolved from the record.
46
+ *
47
+ * CLAUDE CODE IS A THIRD, SEPARATE LAUNCH (#157 stage 4): the same ONE layout.apply, then ONE
48
+ * `pane.process_info` read on the pane it created, so the v2 side can join the receiver marker's owner
49
+ * to that pane's direct child exactly as it joins the tmux pane pid.
50
+ */
51
+ import { spawn as spawnChildProcess } from "node:child_process";
52
+ import { randomBytes } from "node:crypto";
53
+ import { argvMatchesRequest, boundHerdrDiagnostic, buildHerdrAgentStartArgs, buildHerdrLayoutApplyPacket, buildHerdrPaneGetArgs, buildHerdrPaneGetPacket, classifyHerdrCwd, classifyHerdrLayoutApplyReply, classifyHerdrLayoutPaneRead, containsControlChar, createHerdrRunner, HERDR_AGENT_KIND, HERDR_LAYOUT_PRODUCTION_DEPS, HERDR_REJECT_HINT, parseHerdrAgentStartResponse, parseHerdrPaneGetResponse, parseHerdrTabCreateResponse, readHerdrEnvelope, readHerdrErrorCode, reclaim, resolveHerdrContext, resolveHerdrSocketPath, resolvePiDurableLauncher, } from "./herdr-fresh-call.js";
54
+ /**
55
+ * The agent name herdr will know this launch by: `entwurf-resume-` plus 16 random hex, 31 characters.
56
+ * `[file:line @ c77af189]` `src/app/agents.rs:15-20` requires `[a-z][a-z0-9_-]{0,31}`. It is random
57
+ * per call, not derived from the session or the citizen: this module never learns either, and a name
58
+ * an operator could read an address into is the promotion this rail refuses.
59
+ */
60
+ export function mintHerdrResumeAgentName() {
61
+ return `entwurf-resume-${randomBytes(8).toString("hex")}`;
62
+ }
63
+ /**
64
+ * `tab create` for a resume: the fresh rail's placement policy — a NEW tab in the caller's own
65
+ * workspace, named explicitly, without taking focus — and its fixed identity scrub, with NO callback
66
+ * env. The scrub is the reason the tmux resume leaf carries one too: a pi that inherits a stale
67
+ * `PI_SESSION_ID` would attach as the citizen that env names instead of the one its transcript
68
+ * belongs to. `--cwd` is always present because a resume always has the record's directory.
69
+ */
70
+ export function buildHerdrResumeTabCreateArgs(params) {
71
+ return [
72
+ "tab",
73
+ "create",
74
+ "--workspace",
75
+ params.workspaceId,
76
+ "--no-focus",
77
+ "--cwd",
78
+ params.cwd,
79
+ "--env",
80
+ "PI_SESSION_ID=",
81
+ "--env",
82
+ "PI_AGENT_ID=",
83
+ ];
84
+ }
85
+ /**
86
+ * Open pi on the record's transcript in a new herdr tab.
87
+ *
88
+ * `makeRun` receives the herdr binary the context names and returns the one way this call talks to
89
+ * herdr; production passes `createHerdrRunner`, a test passes a table. It is called only after the
90
+ * context resolved, so a refusal for a missing context never builds a runner.
91
+ */
92
+ export async function herdrResumeCall(params, makeRun, env, agentName = mintHerdrResumeAgentName()) {
93
+ const context = resolveHerdrContext(env);
94
+ if (!context.ok) {
95
+ if (context.reason === "herdr-context-missing" || context.reason === "herdr-caller-pane-missing") {
96
+ return { ok: false, reason: context.reason };
97
+ }
98
+ // Crash, don't warn: a context refusal this rail has no word for is drift in the shared
99
+ // primitive, not something to rename into the nearest reason.
100
+ throw new Error(`herdr-resume-call: the herdr context refused as ${context.reason}, which this rail does not name`);
101
+ }
102
+ // Pi only, before anything is read or created — see the module header.
103
+ if (params.backend !== "pi")
104
+ return { ok: false, reason: "herdr-resume-backend-unsupported" };
105
+ const badCwd = classifyHerdrCwd(params.cwd);
106
+ if (badCwd !== null)
107
+ return { ok: false, reason: badCwd };
108
+ // herdr refuses ANY control character in an agent argument; it is checked while it is still free.
109
+ if (params.runtimeArgs.some(containsControlChar))
110
+ return { ok: false, reason: "herdr-argv-control-character" };
111
+ const run = makeRun(context.context.bin);
112
+ // Which workspace is the caller in? herdr's own answer about the caller's own pane, read and bound
113
+ // exactly as the fresh rail binds it. A read creates nothing, so every failure here is a refusal.
114
+ const callerPaneRun = await run(buildHerdrPaneGetArgs(context.context.callerPaneId));
115
+ if (callerPaneRun.status !== 0)
116
+ return { ok: false, reason: "herdr-caller-pane-get-failed" };
117
+ const callerPane = parseHerdrPaneGetResponse(callerPaneRun.stdout);
118
+ if (callerPane === null)
119
+ return { ok: false, reason: "herdr-caller-pane-unparsable" };
120
+ if (callerPane.paneId !== context.context.callerPaneId)
121
+ return { ok: false, reason: "herdr-caller-pane-drift" };
122
+ if (callerPane.workspaceId === undefined)
123
+ return { ok: false, reason: "herdr-caller-workspace-missing" };
124
+ // ── everything above this line leaves nothing behind ──────────────────────────────────
125
+ const tabRun = await run(buildHerdrResumeTabCreateArgs({ workspaceId: callerPane.workspaceId, cwd: params.cwd }));
126
+ if (tabRun.status !== 0) {
127
+ // herdr's own envelope means herdr answered and declined before making a tab; no envelope
128
+ // means our runner's own timeout/kill/spawn failure, after which a tab may well exist.
129
+ const code = readHerdrErrorCode(tabRun.stderr);
130
+ return {
131
+ ok: false,
132
+ reason: "herdr-tab-create-failed",
133
+ ...(code === undefined ? {} : { herdrErrorCode: code }),
134
+ recovery: code === undefined ? { outcome: "unknown" } : { outcome: "none" },
135
+ };
136
+ }
137
+ const tab = parseHerdrTabCreateResponse(tabRun.stdout);
138
+ if (tab === null)
139
+ return { ok: false, reason: "herdr-tab-create-unparsable", recovery: { outcome: "unknown" } };
140
+ const pane = tab.rootPane;
141
+ // Every coordinate below comes from the reply that CREATED the tab: it is the one the reclaim is
142
+ // bound to, so the coordinates an operator reads and the pane this rail may close are the same.
143
+ const created = {
144
+ herdrWorkspaceId: tab.workspaceId,
145
+ herdrTabId: tab.tabId,
146
+ herdrPaneId: pane.paneId,
147
+ herdrTerminalId: pane.terminalId,
148
+ };
149
+ // The tab must be where this call ASKED for it: the caller's own workspace. The shared parser
150
+ // proves the reply agrees with itself (tab and root pane name one workspace), not that it agrees
151
+ // with the request — a coherent reply about another workspace would start pi where the operator
152
+ // is not looking, under a receipt that reads as a launch. So it is a named failure, reclaimed.
153
+ if (tab.workspaceId !== callerPane.workspaceId) {
154
+ return { ok: false, reason: "herdr-tab-create-workspace-drift", recovery: await reclaim(pane, run), created };
155
+ }
156
+ // Occupancy is TWO facts, as the shared reclaim reads them (`decideConditionalClose`): herdr naming
157
+ // an agent in the pane, and herdr holding that agent's session. An agent whose session has not been
158
+ // reported yet is still somebody in the pane, so either fact stops the start.
159
+ if (pane.agent !== undefined || pane.hasAgentSession) {
160
+ return { ok: false, reason: "herdr-tab-root-pane-occupied", recovery: await reclaim(pane, run), created };
161
+ }
162
+ const startRun = await run(buildHerdrAgentStartArgs({
163
+ agentName,
164
+ kind: HERDR_AGENT_KIND.pi,
165
+ paneId: pane.paneId,
166
+ backendArgs: params.runtimeArgs,
167
+ }));
168
+ if (startRun.status !== 0) {
169
+ const code = readHerdrErrorCode(startRun.stderr);
170
+ return {
171
+ ok: false,
172
+ reason: "herdr-agent-start-failed",
173
+ ...(code === undefined ? {} : { herdrErrorCode: code }),
174
+ recovery: await reclaim(pane, run),
175
+ created,
176
+ };
177
+ }
178
+ const started = parseHerdrAgentStartResponse(startRun.stdout);
179
+ if (started === null) {
180
+ return { ok: false, reason: "herdr-agent-start-unparsable", recovery: await reclaim(pane, run), created };
181
+ }
182
+ if (started.pane.paneId !== pane.paneId ||
183
+ started.pane.terminalId !== pane.terminalId ||
184
+ (started.pane.tabId !== undefined && started.pane.tabId !== tab.tabId)) {
185
+ return { ok: false, reason: "herdr-agent-start-pane-drift", recovery: await reclaim(pane, run), created };
186
+ }
187
+ // The argv IS the record's transcript, provider and model. A pi started with anything else is a
188
+ // citizen this call did not ask for, and nothing downstream would reveal it.
189
+ if (!argvMatchesRequest(started.argv, "pi", params.runtimeArgs)) {
190
+ return { ok: false, reason: "herdr-agent-start-argv-drift", recovery: await reclaim(pane, run), created };
191
+ }
192
+ return {
193
+ ok: true,
194
+ receipt: { ...created, requestedKind: HERDR_AGENT_KIND.pi, cwd: params.cwd, herdrAgentName: agentName },
195
+ };
196
+ }
197
+ /** Why each outcome happened, in the caller's terms. Code-owned like every herdr rail hint; root
198
+ * ENTWURF.json does not carry them. */
199
+ export const HERDR_RESUME_HINT = {
200
+ "herdr-context-missing": "HERDR_ENV/HERDR_BIN_PATH are absent, so this process cannot reach herdr and this rail does not exist here; nothing was sent to tmux instead",
201
+ "herdr-caller-pane-missing": "herdr did not give this process a HERDR_PANE_ID, so there is no way to ask herdr which workspace this caller is in",
202
+ "herdr-caller-pane-get-failed": "herdr would not report this process's own pane — run `herdr pane get $HERDR_PANE_ID` to see what it says",
203
+ "herdr-caller-pane-unparsable": "herdr's reply about this process's own pane could not be read",
204
+ "herdr-caller-pane-drift": "herdr answered about a different pane than HERDR_PANE_ID names, so its workspace says nothing about where this caller is",
205
+ "herdr-caller-workspace-missing": "herdr reported this process's pane with no workspace, and guessing one would reopen the citizen wherever the operator is looking",
206
+ "herdr-resume-backend-unsupported": "inside herdr, same-id resume reopens pi, pi-durable and Claude Code; this agent-start path carries pi alone, and nothing was sent to tmux instead",
207
+ "herdr-argv-control-character": "the recorded runtime argv holds a control character, which herdr refuses in an agent argument",
208
+ "cwd-not-absolute": "the recorded start directory is not an absolute path",
209
+ "cwd-missing": "the recorded start directory no longer exists",
210
+ "cwd-not-directory": "the recorded start path is not a directory",
211
+ "herdr-tab-create-failed": "the tab create did not succeed — read the recovery line for whether anything exists",
212
+ "herdr-tab-create-unparsable": "herdr's tab reply could not be read, so a tab may exist that this call cannot name",
213
+ "herdr-tab-create-workspace-drift": "herdr created the tab in a different workspace than the caller's own one this call asked for, so pi was not started there",
214
+ "herdr-tab-root-pane-occupied": "the new tab's initial pane already holds an agent, so pi was not started into it",
215
+ "herdr-agent-start-failed": "herdr did not report pi as started in the tab that was just created",
216
+ "herdr-agent-start-unparsable": "herdr's start reply could not be read",
217
+ "herdr-agent-start-pane-drift": "herdr reported a different pane, terminal or tab than the one it just created",
218
+ "herdr-agent-start-argv-drift": "herdr echoed an argv that is not the recorded one this call asked it to start",
219
+ };
220
+ export function herdrResumeLaunchOutcome(result) {
221
+ if (result.ok)
222
+ return { ok: true, handle: { rail: "herdr", ...result.receipt } };
223
+ const hint = HERDR_RESUME_HINT[result.reason];
224
+ if (!("recovery" in result))
225
+ return { ok: false, reason: result.reason, hint };
226
+ return {
227
+ ok: false,
228
+ reason: result.reason,
229
+ hint,
230
+ failure: {
231
+ rail: "herdr",
232
+ ...(result.herdrErrorCode === undefined ? {} : { vendorCode: result.herdrErrorCode }),
233
+ recovery: result.recovery,
234
+ ...(result.created === undefined ? {} : { created: result.created }),
235
+ },
236
+ };
237
+ }
238
+ /** The production herdr launch the shared rail root (`dispatchResumeLaunch`) returns to the v2 seam
239
+ * when this process is inside herdr. A pi-durable resume (#157 B3) and a Claude Code resume (#157
240
+ * stage 4) are the layout launches below; every other backend goes to the pi path, which refuses all
241
+ * but pi. */
242
+ export async function launchHerdrResume(input, env, spawn = spawnChildProcess) {
243
+ if (input.backend === "pi-durable") {
244
+ if (input.piDurableAdoptEnv === undefined) {
245
+ throw new Error("herdr-resume-call: a pi-durable resume reached the herdr rail without its adopt projection");
246
+ }
247
+ return herdrPiDurableResumeLaunchOutcome(await herdrPiDurableResumeCall({ cwd: input.cwd, runtimeArgs: input.runtimeArgs, adoptEnv: input.piDurableAdoptEnv }, env));
248
+ }
249
+ if (input.backend === "claude-code") {
250
+ return herdrClaudeCodeResumeLaunchOutcome(await herdrClaudeCodeResumeCall({ cwd: input.cwd, runtimeArgs: input.runtimeArgs }, env));
251
+ }
252
+ return herdrResumeLaunchOutcome(await herdrResumeCall(input, (bin) => createHerdrRunner(bin, env, spawn), env));
253
+ }
254
+ /**
255
+ * Open the reserved pi-durable session in a new herdr tab: the verified launcher, the record-derived
256
+ * argv (the managed verb and its closed adopt context, built on the v2 side and passed through
257
+ * untouched), the reservation's cwd, and an env map of exactly the identity scrub plus the five adopt
258
+ * names. `deps` is the fresh layout branch's own seam; `requestBase` names this call's two request ids.
259
+ */
260
+ export async function herdrPiDurableResumeCall(params, env, deps = HERDR_LAYOUT_PRODUCTION_DEPS, requestBase = mintHerdrResumeAgentName()) {
261
+ const herdr = resolveHerdrContext(env);
262
+ if (!herdr.ok) {
263
+ if (herdr.reason === "herdr-context-missing" || herdr.reason === "herdr-caller-pane-missing") {
264
+ return { ok: false, reason: herdr.reason };
265
+ }
266
+ throw new Error(`herdr-resume-call: the herdr context refused as ${herdr.reason}, which this rail does not name`);
267
+ }
268
+ const badCwd = classifyHerdrCwd(params.cwd);
269
+ if (badCwd !== null)
270
+ return { ok: false, reason: badCwd };
271
+ const socketPath = resolveHerdrSocketPath(env);
272
+ if (socketPath === null)
273
+ return { ok: false, reason: "herdr-socket-path-missing" };
274
+ const layout = deps.packageLayout();
275
+ if (layout === null)
276
+ return { ok: false, reason: "pi-durable-package-incomplete" };
277
+ const runtimeRefusal = deps.runtimeVerdict(layout);
278
+ if (runtimeRefusal !== null)
279
+ return { ok: false, reason: runtimeRefusal };
280
+ const runtime = resolvePiDurableLauncher(layout);
281
+ if (runtime === null)
282
+ return { ok: false, reason: "pi-durable-launcher-missing" };
283
+ const command = [runtime, ...params.runtimeArgs];
284
+ const launchEnv = {
285
+ PI_SESSION_ID: "",
286
+ PI_AGENT_ID: "",
287
+ PI_CODING_AGENT_DIR: params.adoptEnv.PI_CODING_AGENT_DIR,
288
+ ENTWURF_META_SESSIONS_DIR: params.adoptEnv.ENTWURF_META_SESSIONS_DIR,
289
+ ENTWURF_META_MAILBOX_DIR: params.adoptEnv.ENTWURF_META_MAILBOX_DIR,
290
+ ENTWURF_META_SENDERS_DIR: params.adoptEnv.ENTWURF_META_SENDERS_DIR,
291
+ ENTWURF_META_RECEIVERS_DIR: params.adoptEnv.ENTWURF_META_RECEIVERS_DIR,
292
+ };
293
+ // A control character anywhere in the vector or the env map is refused while it is still free:
294
+ // this exec path has no Cc rule of its own, and herdr's env check would answer only after the write.
295
+ if (command.some(containsControlChar) || Object.values(launchEnv).some(containsControlChar)) {
296
+ return { ok: false, reason: "herdr-argv-control-character" };
297
+ }
298
+ const call = deps.socket(socketPath);
299
+ const readId = `${requestBase}:pane.get`;
300
+ const read = classifyHerdrLayoutPaneRead(await call(buildHerdrPaneGetPacket(readId, herdr.context.callerPaneId)), readId, herdr.context.callerPaneId);
301
+ if (!read.ok)
302
+ return { ok: false, reason: read.reason, reported: read.reported };
303
+ // ── everything above this line leaves nothing behind; the ONE layout mutation write follows ──
304
+ const writeId = `${requestBase}:layout.apply`;
305
+ const applied = classifyHerdrLayoutApplyReply(await call(buildHerdrLayoutApplyPacket(writeId, {
306
+ workspaceId: read.workspaceId,
307
+ cwd: params.cwd,
308
+ command,
309
+ env: launchEnv,
310
+ })), { id: writeId, workspaceId: read.workspaceId, cwd: params.cwd, command });
311
+ if (!applied.ok)
312
+ return applied;
313
+ return {
314
+ ok: true,
315
+ receipt: {
316
+ runtime,
317
+ cwd: params.cwd,
318
+ herdrWorkspaceId: applied.workspaceId,
319
+ herdrTabId: applied.tabId,
320
+ herdrPaneId: applied.paneId,
321
+ },
322
+ };
323
+ }
324
+ /** Why each pi-durable outcome happened. The context and caller-pane words are this verb's own (the
325
+ * pi resume table); the package, launcher, endpoint and layout words are the fresh layout branch's. */
326
+ export const HERDR_PI_DURABLE_RESUME_HINT = {
327
+ "herdr-context-missing": HERDR_RESUME_HINT["herdr-context-missing"],
328
+ "herdr-caller-pane-missing": HERDR_RESUME_HINT["herdr-caller-pane-missing"],
329
+ "cwd-not-absolute": HERDR_RESUME_HINT["cwd-not-absolute"],
330
+ "cwd-missing": HERDR_RESUME_HINT["cwd-missing"],
331
+ "cwd-not-directory": HERDR_RESUME_HINT["cwd-not-directory"],
332
+ "herdr-socket-path-missing": HERDR_REJECT_HINT["herdr-socket-path-missing"],
333
+ "pi-durable-package-incomplete": HERDR_REJECT_HINT["pi-durable-package-incomplete"],
334
+ "pi-durable-carrier-drift": HERDR_REJECT_HINT["pi-durable-carrier-drift"],
335
+ "pi-durable-sdk-absent": HERDR_REJECT_HINT["pi-durable-sdk-absent"],
336
+ "pi-durable-sdk-mismatch": HERDR_REJECT_HINT["pi-durable-sdk-mismatch"],
337
+ "pi-durable-launcher-missing": HERDR_REJECT_HINT["pi-durable-launcher-missing"],
338
+ "herdr-argv-control-character": "the reservation's launch argv or adopt env holds a control character, which this rail refuses before asking herdr anything",
339
+ "herdr-caller-pane-get-failed": "herdr would not report this process's own pane through one `pane.get` on HERDR_SOCKET_PATH; any cause herdr reported is quoted",
340
+ "herdr-caller-pane-unparsable": HERDR_RESUME_HINT["herdr-caller-pane-unparsable"],
341
+ "herdr-caller-pane-drift": HERDR_RESUME_HINT["herdr-caller-pane-drift"],
342
+ "herdr-caller-workspace-missing": HERDR_RESUME_HINT["herdr-caller-workspace-missing"],
343
+ "herdr-layout-transport-failed": HERDR_REJECT_HINT["herdr-layout-transport-failed"],
344
+ "herdr-layout-apply-refused": HERDR_REJECT_HINT["herdr-layout-apply-refused"],
345
+ "herdr-layout-apply-failed": HERDR_REJECT_HINT["herdr-layout-apply-failed"],
346
+ "herdr-layout-reply-unparsable": HERDR_REJECT_HINT["herdr-layout-reply-unparsable"],
347
+ "herdr-layout-reply-drift": HERDR_REJECT_HINT["herdr-layout-reply-drift"],
348
+ };
349
+ /** The reported diagnostics, already cut, quoted onto the hint so a value cannot pose as prose. */
350
+ function reportedSuffix(reported) {
351
+ if (reported === undefined)
352
+ return "";
353
+ const all = [
354
+ ["transport", reported.transport],
355
+ ["code", reported.errorCode],
356
+ ["message", reported.errorMessage],
357
+ ["workspace", reported.workspaceId],
358
+ ["tab", reported.tabId],
359
+ ["pane", reported.paneId],
360
+ ];
361
+ const fields = all.filter((field) => field[1] !== undefined);
362
+ if (fields.length === 0)
363
+ return "";
364
+ return ` (reported, display only and unverified: ${fields.map(([name, value]) => `${name} ${JSON.stringify(value)}`).join(", ")})`;
365
+ }
366
+ /** The pi-durable layout result as the v2 seam reads it: a `herdr-layout` handle, a refusal, or a
367
+ * failure carrying the layout's own `none | unknown` certainty — no created coordinates, none known. */
368
+ export function herdrPiDurableResumeLaunchOutcome(result) {
369
+ if (result.ok)
370
+ return { ok: true, handle: { rail: "herdr-layout", ...result.receipt } };
371
+ const hint = `${HERDR_PI_DURABLE_RESUME_HINT[result.reason]}${reportedSuffix(result.reported)}`;
372
+ if ("recovery" in result) {
373
+ return {
374
+ ok: false,
375
+ reason: result.reason,
376
+ hint,
377
+ failure: {
378
+ rail: "herdr-layout",
379
+ ...(result.herdrErrorCode === undefined ? {} : { vendorCode: result.herdrErrorCode }),
380
+ recovery: result.recovery,
381
+ },
382
+ };
383
+ }
384
+ return { ok: false, reason: result.reason, hint };
385
+ }
386
+ // ── #157 stage 4: the Claude Code same-id resume through ONE layout.apply ───────────────────────────
387
+ //
388
+ // A Claude Code resume is observed by joining the receiver marker its Entwurf hook re-arms to the
389
+ // process this launch started: the hook names its parent, the Claude Code process, as the marker's
390
+ // owner. `agent start` types the command into the pane's interactive shell, so that process is the
391
+ // shell's child and no herdr coordinate names it. A layout argv pane spawns the vector's program as
392
+ // the pane's direct child (`[측정 S4-CC-A H1, 2026-11-02, herdr 0.9.5 at the pinned aarch64 digest]`:
393
+ // a program's self-reported pid, written before one exec, equalled `pane.process_info` `shell_pid`
394
+ // and its foreground pid, at the same kernel start time, with the herdr server as its parent). So
395
+ // this branch is the pi-durable layout launch with the record's argv and no adopt env, followed by ONE
396
+ // read of that report for the pane it created. The read mutates nothing; whatever it says, the tab
397
+ // stays as it is — this branch closes nothing, as the pi-durable one closes nothing.
398
+ //
399
+ // The runtime is the bare name `claude`, resolved by herdr exactly as its `agent start` resolves the
400
+ // fresh Claude Code launch: this module imports no tmux leaf, so it carries no PATH resolver, and the
401
+ // receipt says herdr resolved it. Which binary ran is not what the observation proves; the join does.
402
+ /** What herdr is asked to run first in a Claude Code resume's layout vector. */
403
+ export const HERDR_CLAUDE_CODE_RESUME_RUNTIME = "claude";
404
+ /** The fixed process read. Product function, not an RPC surface: method and shape are literal. */
405
+ export function buildHerdrPaneProcessInfoPacket(id, paneId) {
406
+ return `${JSON.stringify({ id, method: "pane.process_info", params: { pane_id: paneId } })}\n`;
407
+ }
408
+ function isRecord(value) {
409
+ return typeof value === "object" && value !== null && !Array.isArray(value);
410
+ }
411
+ /**
412
+ * The process read, reduced to the pane's direct-child pid. A success must name THIS pane and carry
413
+ * `shell_pid` (herdr's name for the pane runtime's direct child, `integer | null` in its bundled
414
+ * schema) as an integer above 1; anything else is `unread` with its cause — a pane whose process
415
+ * already exited reports null, and that is not a pid to join.
416
+ */
417
+ export function classifyHerdrPaneProcessInfo(outcome, id, paneId) {
418
+ if (outcome.kind !== "reply") {
419
+ return { unread: boundHerdrDiagnostic("code" in outcome ? `${outcome.kind}: ${outcome.code}` : outcome.kind) };
420
+ }
421
+ const envelope = readHerdrEnvelope(outcome.line, id);
422
+ if (envelope === null)
423
+ return { unread: "herdr's process reply could not be read" };
424
+ if (envelope.arm === "error")
425
+ return { unread: boundHerdrDiagnostic(`herdr error ${envelope.code}: ${envelope.message}`) };
426
+ const info = envelope.result.process_info;
427
+ if (envelope.result.type !== "pane_process_info" || !isRecord(info) || info.pane_id !== paneId) {
428
+ return { unread: "herdr's process reply does not describe the pane this call created" };
429
+ }
430
+ const pid = info.shell_pid;
431
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 1) {
432
+ return { unread: `herdr reported no live direct child for the pane (shell_pid ${JSON.stringify(pid ?? null)})` };
433
+ }
434
+ return { pid: String(pid) };
435
+ }
436
+ /**
437
+ * Open the dormant Claude Code session in a new herdr tab: `claude` plus the record-derived argv
438
+ * (`--resume <session id>`, built on the v2 side and passed through untouched), the record's cwd, and
439
+ * the identity scrub as the env map — then read which process the new pane runs.
440
+ */
441
+ export async function herdrClaudeCodeResumeCall(params, env, deps = HERDR_LAYOUT_PRODUCTION_DEPS, requestBase = mintHerdrResumeAgentName()) {
442
+ const herdr = resolveHerdrContext(env);
443
+ if (!herdr.ok) {
444
+ if (herdr.reason === "herdr-context-missing" || herdr.reason === "herdr-caller-pane-missing") {
445
+ return { ok: false, reason: herdr.reason };
446
+ }
447
+ throw new Error(`herdr-resume-call: the herdr context refused as ${herdr.reason}, which this rail does not name`);
448
+ }
449
+ const badCwd = classifyHerdrCwd(params.cwd);
450
+ if (badCwd !== null)
451
+ return { ok: false, reason: badCwd };
452
+ const socketPath = resolveHerdrSocketPath(env);
453
+ if (socketPath === null)
454
+ return { ok: false, reason: "herdr-socket-path-missing" };
455
+ const command = [HERDR_CLAUDE_CODE_RESUME_RUNTIME, ...params.runtimeArgs];
456
+ const launchEnv = { PI_SESSION_ID: "", PI_AGENT_ID: "" };
457
+ if (command.some(containsControlChar))
458
+ return { ok: false, reason: "herdr-argv-control-character" };
459
+ const call = deps.socket(socketPath);
460
+ const readId = `${requestBase}:pane.get`;
461
+ const read = classifyHerdrLayoutPaneRead(await call(buildHerdrPaneGetPacket(readId, herdr.context.callerPaneId)), readId, herdr.context.callerPaneId);
462
+ if (!read.ok)
463
+ return { ok: false, reason: read.reason, reported: read.reported };
464
+ // ── everything above this line leaves nothing behind; the ONE layout mutation write follows ──
465
+ const writeId = `${requestBase}:layout.apply`;
466
+ const applied = classifyHerdrLayoutApplyReply(await call(buildHerdrLayoutApplyPacket(writeId, { workspaceId: read.workspaceId, cwd: params.cwd, command, env: launchEnv })), { id: writeId, workspaceId: read.workspaceId, cwd: params.cwd, command });
467
+ if (!applied.ok)
468
+ return applied;
469
+ // The tab exists from here on. ONE read of what the pane this call created is running; whatever it
470
+ // says is reported, and nothing is closed or retried.
471
+ const { workspaceId, tabId, paneId } = applied;
472
+ const processId = `${requestBase}:pane.process_info`;
473
+ const paneChild = classifyHerdrPaneProcessInfo(await call(buildHerdrPaneProcessInfoPacket(processId, paneId)), processId, paneId);
474
+ const receipt = {
475
+ runtime: HERDR_CLAUDE_CODE_RESUME_RUNTIME,
476
+ cwd: params.cwd,
477
+ herdrWorkspaceId: workspaceId,
478
+ herdrTabId: tabId,
479
+ herdrPaneId: paneId,
480
+ paneChild,
481
+ };
482
+ return { ok: true, receipt };
483
+ }
484
+ /** The one shared word whose fresh-rail hint names pi-durable: for this launch it names Claude Code. */
485
+ export const HERDR_CLAUDE_CODE_SOCKET_PATH_HINT = "HERDR_SOCKET_PATH is absent, empty or not absolute, so there is no herdr endpoint for Claude Code's one layout request — this rail does not guess or scan for one, and nothing was sent to tmux instead";
486
+ /** The Claude Code layout result as the v2 seam reads it: a `herdr-layout-claude` handle, or — every
487
+ * refusal and layout failure being one the pi-durable layout branch also names — that branch's own
488
+ * mapping, so the two layout launches cannot classify one herdr fact two ways. The one word whose hint
489
+ * names pi-durable is reworded for Claude Code; every other hint names no backend. */
490
+ export function herdrClaudeCodeResumeLaunchOutcome(result) {
491
+ if (result.ok)
492
+ return { ok: true, handle: { rail: "herdr-layout-claude", ...result.receipt } };
493
+ const shared = herdrPiDurableResumeLaunchOutcome(result);
494
+ if (shared.ok || result.reason !== "herdr-socket-path-missing")
495
+ return shared;
496
+ return { ...shared, hint: HERDR_CLAUDE_CODE_SOCKET_PATH_HINT };
497
+ }
@@ -1228,7 +1228,9 @@ export function defaultMetaReceiversDir() {
1228
1228
  * sandboxed receivers root sandboxes it too and no new environment carrier exists. It is
1229
1229
  * deliberately NOT the v2 dispatch lock dir — visible resume holds that per-gid lock across
1230
1230
  * its whole launch + observation window, and the relaunched session's own arm must not wait
1231
- * on it. Only the Claude Code hook takes this lock; nothing else reads or writes here.
1231
+ * on it. The Claude Code hook and the pi-durable birth (`pi-durable-location.ts`, #157 B3 C4)
1232
+ * take this lock, each around the upsert that mints `attachIncarnation`; nothing else reads or
1233
+ * writes here.
1232
1234
  */
1233
1235
  export function defaultMetaRecordArmLocksDir(receiversDir = defaultMetaReceiversDir()) {
1234
1236
  return path.join(path.dirname(receiversDir), "meta-record-arm-locks");
@@ -23,7 +23,11 @@
23
23
  * This is the tmux rail. A caller inside herdr (`HERDR_ENV=1`, herdr's own marker, read for its
24
24
  * exact value as `fresh-call-dispatch` does) is refused by name before anything else: herdr owns
25
25
  * placement there, and an ambient `TMUX` that happens to be set would otherwise put the window in
26
- * a session the operator is not looking at. There is no herdr resume rail yet and no fallback.
26
+ * a session the operator is not looking at. Since #157 G1a the shared rail root (`fresh-call-dispatch`)
27
+ * hands a resume inside herdr to the separate herdr rail (`herdr-resume-call.ts`) and never reaches
28
+ * this leaf there;
29
+ * this guard stays so that a direct caller still cannot open a tmux window from inside herdr. No
30
+ * fallback in either direction.
27
31
  *
28
32
  * ── The cwd is the whole reason this module owns its own tmux argv ──
29
33
  *
@@ -56,24 +60,53 @@ import { APPEND_FORMAT, assertSelector, assertTmuxOk, inspectPlacement, parseWin
56
60
  * backend boundary is enforced upstream by `resolveResumeLaunchIdentity`, and this is the mux half
57
61
  * of the same fact. */
58
62
  export const RESUME_CALL_RUNTIME = { pi: "pi", "claude-code": "claude" };
63
+ /**
64
+ * #157 B3: a pi-durable resume reopens through the MANAGED verb, exactly as its fresh call does —
65
+ * `entwurf pi-durable` verifies this package's carrier and its SDK set before anything runs, and the
66
+ * durable app is never started bare. It is its own entry (`resumePiDurableCall`) rather than a row of
67
+ * the table above, because it is the one resume whose launch also carries an environment.
68
+ */
69
+ export const RESUME_CALL_PI_DURABLE_RUNTIME = "entwurf";
70
+ /** The five `NAME=value` assignments, in the fixed order, or none. Every value must be an absolute
71
+ * path: a relative one would resolve against whatever directory the child starts in. */
72
+ function adoptEnvAssignments(adoptEnv) {
73
+ if (adoptEnv === undefined)
74
+ return [];
75
+ const pairs = [
76
+ ["PI_CODING_AGENT_DIR", adoptEnv.PI_CODING_AGENT_DIR],
77
+ ["ENTWURF_META_SESSIONS_DIR", adoptEnv.ENTWURF_META_SESSIONS_DIR],
78
+ ["ENTWURF_META_MAILBOX_DIR", adoptEnv.ENTWURF_META_MAILBOX_DIR],
79
+ ["ENTWURF_META_SENDERS_DIR", adoptEnv.ENTWURF_META_SENDERS_DIR],
80
+ ["ENTWURF_META_RECEIVERS_DIR", adoptEnv.ENTWURF_META_RECEIVERS_DIR],
81
+ ];
82
+ for (const [name, value] of pairs) {
83
+ if (typeof value !== "string" || !value.startsWith("/")) {
84
+ throw new Error(`mux-resume-call: refusing to build argv with a non-absolute ${name}: ${JSON.stringify(value)}`);
85
+ }
86
+ }
87
+ return pairs.map(([name, value]) => `${name}=${value}`);
88
+ }
59
89
  /**
60
90
  * Launch argv: the leaf's detached-append shape, the fixed identity scrub (`SCRUBBED_INHERITED_ENV`,
61
- * the same two assignments every fresh window carries), `-c`, the runtime, then the caller's flags.
91
+ * the same two assignments every fresh window carries), for a pi-durable adoption the five fixed
92
+ * projection assignments after it (#157 B3), `-c`, the runtime, then the caller's flags.
62
93
  * `--` is what keeps tmux from reading the runtime or its flags as tmux options. The scrub is not a
63
94
  * carrier: it only empties identity a tmux server may have inherited, and a resumed Claude Code
64
95
  * (which, unlike pi, never re-exports its own) would otherwise present a stale one to its bridge.
65
96
  */
66
- export function buildResumeCallArgs(placement, cwd, runtimePath, runtimeArgs) {
97
+ export function buildResumeCallArgs(placement, cwd, runtimePath, runtimeArgs, adoptEnv) {
67
98
  assertSelector("session", placement.sessionId);
68
99
  assertLaunchTarget(runtimePath);
69
100
  const bad = classifyTmuxCwd(cwd);
70
101
  if (bad)
71
102
  throw new Error(`mux-resume-call: refusing to build argv with an unusable cwd (${bad}): ${cwd}`);
103
+ const adoptAssignments = adoptEnvAssignments(adoptEnv);
72
104
  return [
73
105
  "new-window",
74
106
  "-d",
75
107
  "-a",
76
108
  ...SCRUBBED_INHERITED_ENV.flatMap((assignment) => ["-e", assignment]),
109
+ ...adoptAssignments.flatMap((assignment) => ["-e", assignment]),
77
110
  "-t",
78
111
  `${placement.sessionId}:{end}`,
79
112
  "-c",
@@ -94,6 +127,18 @@ export function buildResumeCallArgs(placement, cwd, runtimePath, runtimeArgs) {
94
127
  * `runtimeArgs` is passed through untouched. This module does not know what `--session` means.
95
128
  */
96
129
  export function resumeCall(params, env = process.env) {
130
+ return openResumeWindow(RESUME_CALL_RUNTIME[params.backend], params, undefined, env);
131
+ }
132
+ /**
133
+ * #157 B3: the pi-durable resume window. The same order, refusals and receipt as `resumeCall` — the
134
+ * runtime is the managed `entwurf`, and the window additionally carries the fixed adopt projection.
135
+ * `runtimeArgs` (the managed verb and its closed adopt context) is passed through untouched; this
136
+ * module never decodes it.
137
+ */
138
+ export function resumePiDurableCall(params, env = process.env) {
139
+ return openResumeWindow(RESUME_CALL_PI_DURABLE_RUNTIME, params, params.adoptEnv, env);
140
+ }
141
+ function openResumeWindow(runtime, params, adoptEnv, env) {
97
142
  if (env.HERDR_ENV === "1")
98
143
  return { ok: false, reason: "herdr-resume-unsupported" };
99
144
  const badCwd = classifyTmuxCwd(params.cwd);
@@ -101,7 +146,7 @@ export function resumeCall(params, env = process.env) {
101
146
  return { ok: false, reason: badCwd };
102
147
  let runtimePath;
103
148
  try {
104
- runtimePath = resolveRuntimeOnPath(RESUME_CALL_RUNTIME[params.backend], env);
149
+ runtimePath = resolveRuntimeOnPath(runtime, env);
105
150
  }
106
151
  catch (err) {
107
152
  if (err instanceof LaunchPreconditionError)
@@ -113,7 +158,7 @@ export function resumeCall(params, env = process.env) {
113
158
  return { ok: false, reason: inspected.reason };
114
159
  const placement = inspected.placement;
115
160
  requireSameContext("resumeCall", placement, env);
116
- const run = runTmux(buildResumeCallArgs(placement, params.cwd, runtimePath, params.runtimeArgs), env);
161
+ const run = runTmux(buildResumeCallArgs(placement, params.cwd, runtimePath, params.runtimeArgs, adoptEnv), env);
117
162
  assertTmuxOk("new-window", run);
118
163
  let fields;
119
164
  try {
@@ -19,7 +19,7 @@
19
19
  * `{v:2,task}` payload, whose decoder caps `task` at 16000 characters. A framed
20
20
  * task over that cap would open a window whose bootstrap refuses its own argv.
21
21
  *
22
- * ── Then the carrier, then the four units ──
22
+ * ── Then the carrier, then the five units ──
23
23
  *
24
24
  * carrier the package's own pi-durable carrier and its declared SDK set, verified
25
25
  * (`pi-durable-runtime.ts`); passed in as that check's verdict.
@@ -29,6 +29,9 @@
29
29
  * it every dispatch to the citizen is the honest `mailbox-undeliverable`.
30
30
  * visible identity the packaged bootstrap — the one that hands the TUI the view whose conversation
31
31
  * labels carry the garden id.
32
+ * owner fence the compiled owner fence in the same closure — the packaged bootstrap imports it,
33
+ * so without it the bootstrap cannot load. Asked here as a file this package must
34
+ * ship, nothing more; the carrier verdict above does not cover it.
32
35
  *
33
36
  * ── What this deliberately does NOT claim ──
34
37
  *
@@ -78,6 +81,8 @@ export function piDurableFreshPreflight(params) {
78
81
  return "pi-durable-receive-unit-missing";
79
82
  if (!existsSync(layout.bootstrap))
80
83
  return "pi-durable-visible-identity-missing";
84
+ if (!existsSync(layout.ownerFence))
85
+ return "pi-durable-owner-fence-missing";
81
86
  return null;
82
87
  }
83
88
  /** Repair text beside the predicate that decides it. The runtime reasons keep theirs on the runtime
@@ -89,4 +94,5 @@ export const PI_DURABLE_PREFLIGHT_HINT = {
89
94
  "pi-durable-mcp-hand-missing": "this Entwurf package has no compiled entwurf-bridge entry, so the sibling's callback tool would not exist — reinstall or rebuild Entwurf",
90
95
  "pi-durable-receive-unit-missing": "this Entwurf package's compiled owner join does not admit pi-durable-host, so nothing could ever be delivered to the sibling — reinstall or rebuild Entwurf",
91
96
  "pi-durable-visible-identity-missing": "this Entwurf package has no packaged pi-durable bootstrap (pi/pi-durable/bootstrap.mjs), so no TUI would carry the garden id — reinstall Entwurf",
97
+ "pi-durable-owner-fence-missing": "this Entwurf package has no compiled pi-durable owner fence (mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js), which its packaged bootstrap imports — reinstall or rebuild Entwurf",
92
98
  };