@junghanacs/entwurf 0.41.2 → 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 (122) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +110 -1
  3. package/CHANGELOG.md +328 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/ENTWURF-CLI.json +1229 -0
  6. package/ENTWURF.json +284 -0
  7. package/FAQ.md +4 -4
  8. package/README.md +125 -184
  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 +284 -0
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +46 -196
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +498 -29
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +23 -20
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +68 -21
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +7 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
  29. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -60
  30. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
  31. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
  33. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  34. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
  35. package/mcp/entwurf-bridge/src/index.ts +149 -327
  36. package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
  37. package/native/win32-x64/manifest.json +1 -1
  38. package/package.json +8 -4
  39. package/pi/pi-durable/bootstrap.mjs +179 -6
  40. package/pi/pi-durable/carrier/runtime.js +108 -9
  41. package/pi/pi-durable/carrier/session-name.js +11 -0
  42. package/pi/pi-durable/carrier/sessions.js +96 -20
  43. package/pi/pi-durable/overlay/upstream-pin.json +11 -5
  44. package/pi-extensions/lib/agent-guidance.ts +341 -0
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
  46. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +725 -54
  47. package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
  48. package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
  49. package/pi-extensions/lib/herdr-resume-call.ts +810 -0
  50. package/pi-extensions/lib/meta-session.ts +13 -8
  51. package/pi-extensions/lib/mux-fresh-call.ts +23 -26
  52. package/pi-extensions/lib/mux-resume-call.ts +95 -23
  53. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
  54. package/pi-extensions/lib/pi-durable-location.ts +446 -0
  55. package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
  56. package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
  57. package/pi-extensions/lib/remote-pair-send.ts +7 -4
  58. package/pi-extensions/lib/resume-launch-identity.ts +31 -12
  59. package/pi-extensions/meta-bridge-pi-durable.ts +52 -79
  60. package/run.sh +417 -654
  61. package/scripts/build-bridge.sh +10 -0
  62. package/scripts/check-capability-bundle-reach.ts +86 -0
  63. package/scripts/check-claude-launch.ts +341 -0
  64. package/scripts/check-entwurf-self-address.ts +3 -3
  65. package/scripts/check-entwurf-v2-surface.ts +94 -98
  66. package/scripts/check-fresh-call-dispatch.ts +70 -9
  67. package/scripts/check-fresh-cut-gate.sh +23 -9
  68. package/scripts/check-gate-qualification.ts +24 -17
  69. package/scripts/check-herdr-activation.ts +301 -23
  70. package/scripts/check-herdr-fresh-call.ts +161 -25
  71. package/scripts/check-herdr-plugin-build.ts +173 -1
  72. package/scripts/check-herdr-plugin.ts +28 -2
  73. package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
  74. package/scripts/check-install-surface.ts +1236 -6
  75. package/scripts/check-meta-doctor-oracle.sh +1 -1
  76. package/scripts/check-meta-facts.ts +20 -1
  77. package/scripts/check-mux-resume-call.ts +48 -3
  78. package/scripts/check-peer-facts.ts +39 -2
  79. package/scripts/cli-help.mjs +222 -0
  80. package/scripts/dev-bin.sh +39 -2
  81. package/scripts/herdr-activation.mjs +28 -6
  82. package/scripts/herdr-operator-link.mjs +389 -0
  83. package/scripts/herdr-plugin-activate.mjs +5 -0
  84. package/scripts/herdr-plugin-deactivate.mjs +18 -5
  85. package/scripts/herdr-rails.mjs +43 -7
  86. package/scripts/herdr-runtime.mjs +27 -0
  87. package/scripts/meta-bridge-fresh-cut.ts +1 -27
  88. package/scripts/meta-bridge-prune.ts +1 -1
  89. package/scripts/meta-facts.ts +1 -1
  90. package/scripts/mutants/agent-guidance.json +160 -0
  91. package/scripts/mutants/bridge-boot-resume.json +6 -8
  92. package/scripts/mutants/capability-bundle-reach.json +124 -0
  93. package/scripts/mutants/claude-launch.json +142 -0
  94. package/scripts/mutants/cli-help-source.json +420 -0
  95. package/scripts/mutants/control-send-receipt.json +21 -4
  96. package/scripts/mutants/dev-gate-dispatch.json +109 -0
  97. package/scripts/mutants/fresh-call-dispatch.json +38 -2
  98. package/scripts/mutants/fresh-cut.json +15 -0
  99. package/scripts/mutants/herdr-activation.json +128 -0
  100. package/scripts/mutants/herdr-fresh-call.json +278 -5
  101. package/scripts/mutants/herdr-operator-link.json +318 -0
  102. package/scripts/mutants/herdr-plugin-build.json +55 -0
  103. package/scripts/mutants/herdr-plugin.json +17 -0
  104. package/scripts/mutants/herdr-resume-call.json +288 -0
  105. package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
  106. package/scripts/mutants/meta-facts.json +15 -0
  107. package/scripts/mutants/mux-fresh-call.json +14 -3
  108. package/scripts/mutants/mux-resume-call.json +89 -3
  109. package/scripts/mutants/peer-facts.json +15 -0
  110. package/scripts/mutants/pi-durable.json +996 -6
  111. package/scripts/mutants/remote-pair-send.json +21 -2
  112. package/scripts/mutants/self-address.json +10 -10
  113. package/scripts/mutants/source-install.json +35 -0
  114. package/scripts/mutants/v2-surface.json +50 -64
  115. package/scripts/mutants/v2-visible-resume.json +266 -6
  116. package/scripts/peer-facts.ts +1 -1
  117. package/scripts/smoke-agy-install-state.sh +66 -0
  118. package/scripts/smoke-meta-prune.sh +23 -0
  119. package/scripts/smoke-omp-fresh-live.ts +12 -3
  120. package/scripts/smoke-omp-receive-live.ts +7 -1
  121. package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
  122. package/team.json.example +39 -0
@@ -360,9 +360,12 @@ export interface MetaIdentity {
360
360
  * arm-capable event and writes in the SAME atomic upsert as that attach, then copies into
361
361
  * the receiver marker it writes under the record-arm lock. A dead marker carrying the
362
362
  * value the record still carries is evidence that no later arm-capable attach happened;
363
- * one that does not carry it is not. `null` means no binding was ever observed — every
364
- * other backend, a record no arm has touched since this field existed, a create — and
365
- * is never filled by inference. Every writer that does not mint PRESERVES it.
363
+ * one that does not carry it is not. The pi-durable birth (#157 B3 C4) mints it the same way
364
+ * under the same lock and copies it into its location evidence (`pi-durable-location.ts`)
365
+ * instead of a marker; there it binds a location, never a dormancy verdict. `null` means no
366
+ * binding was ever observed — every other backend, a pi-durable record no birth has bound,
367
+ * a record no arm has touched since this field existed, a create — and is never filled by
368
+ * inference. Every writer that does not mint PRESERVES it.
366
369
  */
367
370
  attachIncarnation: string | null;
368
371
  }
@@ -614,10 +617,10 @@ export interface MetaIdentityMintInput {
614
617
  model?: string | null;
615
618
  transcriptPath?: string | null;
616
619
  /**
617
- * The arm binding to commit with this write. ONLY the Claude Code hook passes it, and
618
- * only on an arm-capable event under the record-arm lock; `undefined` (every other
619
- * writer, every UserPromptSubmit) keeps the existing value, and a create without it is
620
- * `null`.
620
+ * The arm binding to commit with this write. ONLY the Claude Code hook (on an arm-capable
621
+ * event) and the pi-durable birth (#157 B3 C4) pass it, each under the record-arm lock;
622
+ * `undefined` (every other writer, every UserPromptSubmit) keeps the existing value, and a
623
+ * create without it is `null`.
621
624
  */
622
625
  attachIncarnation?: string;
623
626
  }
@@ -1471,7 +1474,9 @@ export function defaultMetaReceiversDir(): string {
1471
1474
  * sandboxed receivers root sandboxes it too and no new environment carrier exists. It is
1472
1475
  * deliberately NOT the v2 dispatch lock dir — visible resume holds that per-gid lock across
1473
1476
  * its whole launch + observation window, and the relaunched session's own arm must not wait
1474
- * on it. Only the Claude Code hook takes this lock; nothing else reads or writes here.
1477
+ * on it. The Claude Code hook and the pi-durable birth (`pi-durable-location.ts`, #157 B3 C4)
1478
+ * take this lock, each around the upsert that mints `attachIncarnation`; nothing else reads or
1479
+ * writes here.
1475
1480
  */
1476
1481
  export function defaultMetaRecordArmLocksDir(receiversDir: string = defaultMetaReceiversDir()): string {
1477
1482
  return path.join(path.dirname(receiversDir), "meta-record-arm-locks");
@@ -87,6 +87,7 @@
87
87
  */
88
88
 
89
89
  import { ACP_MODEL_NOT_CURATED_HINT } from "./acp/model-ids.js";
90
+ import { ENTWURF_FRESH_LAUNCH_REJECT_HINTS } from "./agent-guidance.ts";
90
91
  import { callbackEnvAssignments } from "./callback-env.ts";
91
92
  import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
92
93
  import {
@@ -629,7 +630,9 @@ export function freshCall(
629
630
  }
630
631
 
631
632
  /** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
632
- * they will guess about. */
633
+ * they will guess about. The static tmux launch-stage text is authored in root
634
+ * ENTWURF.json (#154 C1-D) and bound key by key, never spread, so a duplicate or missing reason
635
+ * still fails typecheck; slot-bearing, ACP and preflight-leaf hints stay code-owned. */
633
636
  const REJECT_HINT: Record<FreshCallRejectReason, string> = {
634
637
  // The Copilot capability reasons keep their repair text on the leaf that decides them, so
635
638
  // the sentence an operator reads cannot drift away from the predicate that produced it.
@@ -641,34 +644,28 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
641
644
  ...CODEX_CALLER_PREFLIGHT_HINT,
642
645
  ...CODEX_CALLER_SEAT_HINT,
643
646
  ...FRESH_CALL_PROVIDER_HINT,
644
- "no-tmux-context": "this agent is not running inside tmux, so there is no session to open a sibling beside",
645
- "anchor-malformed": "TMUX_PANE is not a native pane id",
646
- "anchor-unresolved": "tmux resolved no pane for this agent's anchor",
647
- "anchor-mismatch": "tmux answered about a different pane than the one asked about",
648
- "caller-identity-unavailable":
649
- "this surface has no record-backed garden id for the caller, so the sibling would have no address to call back to",
650
- "cwd-not-absolute":
651
- "the requested cwd is not an absolute path (the value is taken literally — nothing trims or resolves it)",
652
- "cwd-format-token":
653
- "the requested cwd contains '#', which tmux expands as a format inside -c — it would silently rewrite the path or run a command",
654
- "cwd-missing":
655
- "the requested cwd does not exist; tmux would not report this, it would open the window in $HOME and look successful",
656
- "cwd-not-directory": "the requested cwd exists but is not a directory",
657
- "tmux-session-name-invalid":
658
- "the requested tmux session name is outside the shape this rail addresses (start with a letter or digit, then letters, digits, '_' or '-') — some other shapes tmux cannot resolve at all ('#' is expanded when the name is stored; '.' and ':' are its own pane/window separators inside a target; a name like '$0' loses to the session id '$0'), and the rest are declined to keep one narrow grammar, so rename the session or open one whose name fits",
659
- "tmux-session-missing":
660
- "no session with that exact name answers on this agent's tmux server (or that server stopped answering) — nothing was created, so open the session yourself and call again",
661
- "model-empty": "model is empty after trimming; fresh calls require an explicit model",
647
+ "no-tmux-context": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["no-tmux-context"],
648
+ "anchor-malformed": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-malformed"],
649
+ "anchor-unresolved": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-unresolved"],
650
+ "anchor-mismatch": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-mismatch"],
651
+ "caller-identity-unavailable": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["caller-identity-unavailable"],
652
+ "cwd-not-absolute": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-not-absolute"],
653
+ "cwd-format-token": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-format-token"],
654
+ "cwd-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-missing"],
655
+ "cwd-not-directory": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-not-directory"],
656
+ "tmux-session-name-invalid": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["tmux-session-name-invalid"],
657
+ "tmux-session-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["tmux-session-missing"],
658
+ "model-empty": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["model-empty"],
662
659
  "model-invalid": `model must be one ${MODEL_MAX_CHARS}-character argv-safe id/alias without whitespace or tmux syntax`,
663
660
  "acp-model-not-curated": ACP_MODEL_NOT_CURATED_HINT,
664
- "task-empty": "task is empty after trimming",
661
+ "task-empty": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["task-empty"],
665
662
  "task-too-long": `task exceeds ${TASK_MAX_CHARS} characters`,
666
- "runtime-unresolved": "the backend's runtime is not installed on PATH",
667
- "runtime-not-absolute": "the resolved runtime path is not absolute",
668
- "runtime-path-whitespace": "the resolved runtime path contains whitespace, which tmux would re-split",
669
- "runtime-missing": "nothing exists at the resolved runtime path",
670
- "runtime-not-regular-file": "the resolved runtime path is not a regular file",
671
- "runtime-not-executable": "the resolved runtime path is not executable",
663
+ "runtime-unresolved": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-unresolved"],
664
+ "runtime-not-absolute": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-absolute"],
665
+ "runtime-path-whitespace": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-path-whitespace"],
666
+ "runtime-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-missing"],
667
+ "runtime-not-regular-file": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-regular-file"],
668
+ "runtime-not-executable": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-executable"],
672
669
  };
673
670
 
674
671
  /**
@@ -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
  *
@@ -49,6 +53,7 @@
49
53
  * to acceptance, not to the product's launch receipt.
50
54
  */
51
55
 
56
+ import { ENTWURF_RESUME_LAUNCH_REJECT_HINTS } from "./agent-guidance.ts";
52
57
  import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
53
58
  import {
54
59
  assertLaunchTarget,
@@ -76,6 +81,49 @@ import {
76
81
  export const RESUME_CALL_RUNTIME = { pi: "pi", "claude-code": "claude" } as const;
77
82
  export type ResumeCallBackend = keyof typeof RESUME_CALL_RUNTIME;
78
83
 
84
+ /**
85
+ * #157 B3: a pi-durable resume reopens through the MANAGED verb, exactly as its fresh call does —
86
+ * `entwurf pi-durable` verifies this package's carrier and its SDK set before anything runs, and the
87
+ * durable app is never started bare. It is its own entry (`resumePiDurableCall`) rather than a row of
88
+ * the table above, because it is the one resume whose launch also carries an environment.
89
+ */
90
+ export const RESUME_CALL_PI_DURABLE_RUNTIME = "entwurf";
91
+
92
+ /**
93
+ * The pi-durable adopt launch projection (#157 B3, L3-A1 §1): EXACTLY these five names, each read by
94
+ * name and emitted in this order — the SDK's own profile input, and the standard children of ONE meta
95
+ * root. It is a fixed projection of two coordinates the caller derived, not an environment dictionary:
96
+ * no other name can be expressed through it. The launched child checks the four roots it actually
97
+ * received against its closed adopt context, and its raw profile against the reservation, before
98
+ * anything opens, so a value this module mangled would refuse there rather than open something else.
99
+ */
100
+ export interface ResumeCallPiDurableAdoptEnv {
101
+ readonly PI_CODING_AGENT_DIR: string;
102
+ readonly ENTWURF_META_SESSIONS_DIR: string;
103
+ readonly ENTWURF_META_MAILBOX_DIR: string;
104
+ readonly ENTWURF_META_SENDERS_DIR: string;
105
+ readonly ENTWURF_META_RECEIVERS_DIR: string;
106
+ }
107
+
108
+ /** The five `NAME=value` assignments, in the fixed order, or none. Every value must be an absolute
109
+ * path: a relative one would resolve against whatever directory the child starts in. */
110
+ function adoptEnvAssignments(adoptEnv: ResumeCallPiDurableAdoptEnv | undefined): string[] {
111
+ if (adoptEnv === undefined) return [];
112
+ const pairs: readonly (readonly [string, string])[] = [
113
+ ["PI_CODING_AGENT_DIR", adoptEnv.PI_CODING_AGENT_DIR],
114
+ ["ENTWURF_META_SESSIONS_DIR", adoptEnv.ENTWURF_META_SESSIONS_DIR],
115
+ ["ENTWURF_META_MAILBOX_DIR", adoptEnv.ENTWURF_META_MAILBOX_DIR],
116
+ ["ENTWURF_META_SENDERS_DIR", adoptEnv.ENTWURF_META_SENDERS_DIR],
117
+ ["ENTWURF_META_RECEIVERS_DIR", adoptEnv.ENTWURF_META_RECEIVERS_DIR],
118
+ ];
119
+ for (const [name, value] of pairs) {
120
+ if (typeof value !== "string" || !value.startsWith("/")) {
121
+ throw new Error(`mux-resume-call: refusing to build argv with a non-absolute ${name}: ${JSON.stringify(value)}`);
122
+ }
123
+ }
124
+ return pairs.map(([name, value]) => `${name}=${value}`);
125
+ }
126
+
79
127
  /** Why a resume window could not be opened. Every value is a NAMED refusal; this module has no
80
128
  * fallback launch and no fallback directory. The cwd members come from the shared classification
81
129
  * leaf and their string values are stable contract. */
@@ -96,7 +144,8 @@ export type ResumeCallResult = { ok: true; receipt: ResumeCallReceipt } | { ok:
96
144
 
97
145
  /**
98
146
  * Launch argv: the leaf's detached-append shape, the fixed identity scrub (`SCRUBBED_INHERITED_ENV`,
99
- * the same two assignments every fresh window carries), `-c`, the runtime, then the caller's flags.
147
+ * the same two assignments every fresh window carries), for a pi-durable adoption the five fixed
148
+ * projection assignments after it (#157 B3), `-c`, the runtime, then the caller's flags.
100
149
  * `--` is what keeps tmux from reading the runtime or its flags as tmux options. The scrub is not a
101
150
  * carrier: it only empties identity a tmux server may have inherited, and a resumed Claude Code
102
151
  * (which, unlike pi, never re-exports its own) would otherwise present a stale one to its bridge.
@@ -106,16 +155,19 @@ export function buildResumeCallArgs(
106
155
  cwd: string,
107
156
  runtimePath: string,
108
157
  runtimeArgs: readonly string[],
158
+ adoptEnv?: ResumeCallPiDurableAdoptEnv,
109
159
  ): string[] {
110
160
  assertSelector("session", placement.sessionId);
111
161
  assertLaunchTarget(runtimePath);
112
162
  const bad = classifyTmuxCwd(cwd);
113
163
  if (bad) throw new Error(`mux-resume-call: refusing to build argv with an unusable cwd (${bad}): ${cwd}`);
164
+ const adoptAssignments = adoptEnvAssignments(adoptEnv);
114
165
  return [
115
166
  "new-window",
116
167
  "-d",
117
168
  "-a",
118
169
  ...SCRUBBED_INHERITED_ENV.flatMap((assignment) => ["-e", assignment]),
170
+ ...adoptAssignments.flatMap((assignment) => ["-e", assignment]),
119
171
  "-t",
120
172
  `${placement.sessionId}:{end}`,
121
173
  "-c",
@@ -139,6 +191,28 @@ export function buildResumeCallArgs(
139
191
  export function resumeCall(
140
192
  params: { backend: ResumeCallBackend; cwd: string; runtimeArgs: readonly string[] },
141
193
  env: NodeJS.ProcessEnv = process.env,
194
+ ): ResumeCallResult {
195
+ return openResumeWindow(RESUME_CALL_RUNTIME[params.backend], params, undefined, env);
196
+ }
197
+
198
+ /**
199
+ * #157 B3: the pi-durable resume window. The same order, refusals and receipt as `resumeCall` — the
200
+ * runtime is the managed `entwurf`, and the window additionally carries the fixed adopt projection.
201
+ * `runtimeArgs` (the managed verb and its closed adopt context) is passed through untouched; this
202
+ * module never decodes it.
203
+ */
204
+ export function resumePiDurableCall(
205
+ params: { cwd: string; runtimeArgs: readonly string[]; adoptEnv: ResumeCallPiDurableAdoptEnv },
206
+ env: NodeJS.ProcessEnv = process.env,
207
+ ): ResumeCallResult {
208
+ return openResumeWindow(RESUME_CALL_PI_DURABLE_RUNTIME, params, params.adoptEnv, env);
209
+ }
210
+
211
+ function openResumeWindow(
212
+ runtime: string,
213
+ params: { cwd: string; runtimeArgs: readonly string[] },
214
+ adoptEnv: ResumeCallPiDurableAdoptEnv | undefined,
215
+ env: NodeJS.ProcessEnv,
142
216
  ): ResumeCallResult {
143
217
  if (env.HERDR_ENV === "1") return { ok: false, reason: "herdr-resume-unsupported" };
144
218
  const badCwd = classifyTmuxCwd(params.cwd);
@@ -146,7 +220,7 @@ export function resumeCall(
146
220
 
147
221
  let runtimePath: string;
148
222
  try {
149
- runtimePath = resolveRuntimeOnPath(RESUME_CALL_RUNTIME[params.backend], env);
223
+ runtimePath = resolveRuntimeOnPath(runtime, env);
150
224
  } catch (err) {
151
225
  if (err instanceof LaunchPreconditionError) return { ok: false, reason: err.reason };
152
226
  throw err;
@@ -157,7 +231,7 @@ export function resumeCall(
157
231
  const placement = inspected.placement;
158
232
  requireSameContext("resumeCall", placement, env);
159
233
 
160
- const run = runTmux(buildResumeCallArgs(placement, params.cwd, runtimePath, params.runtimeArgs), env);
234
+ const run = runTmux(buildResumeCallArgs(placement, params.cwd, runtimePath, params.runtimeArgs, adoptEnv), env);
161
235
  assertTmuxOk("new-window", run);
162
236
 
163
237
  let fields: ReturnType<typeof parseWindowFields>;
@@ -187,24 +261,22 @@ export function resumeCall(
187
261
  }
188
262
 
189
263
  /** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
190
- * they will guess about. */
264
+ * they will guess about. The text is authored in root ENTWURF.json (#154 C1-D); each
265
+ * key is bound explicitly so the reason union stays exhaustive here. */
191
266
  export const RESUME_CALL_REJECT_HINT: Record<ResumeCallRejectReason, string> = {
192
- "no-tmux-context": "this agent is not running inside tmux, so there is no session to reopen the citizen in",
193
- "anchor-malformed": "TMUX_PANE is not a native pane id",
194
- "anchor-unresolved": "tmux resolved no pane for this agent's anchor",
195
- "anchor-mismatch": "tmux answered about a different pane than the one asked about",
196
- "cwd-not-absolute": "the recorded cwd is not an absolute path",
197
- "cwd-format-token":
198
- "the recorded cwd contains '#', which tmux expands as a format inside -c — it would silently rewrite the path or run a command",
199
- "cwd-missing":
200
- "the recorded cwd no longer exists; tmux would not report this, it would open the window in $HOME and look successful",
201
- "cwd-not-directory": "the recorded cwd exists but is not a directory",
202
- "herdr-resume-unsupported":
203
- "this agent runs inside herdr, and same-id resume has only a tmux rail — nothing was opened in any ambient tmux session",
204
- "runtime-unresolved": "the citizen's runtime (pi or claude) is not installed on PATH",
205
- "runtime-not-absolute": "the resolved runtime path is not absolute",
206
- "runtime-path-whitespace": "the resolved runtime path contains whitespace, which tmux would re-split",
207
- "runtime-missing": "nothing exists at the resolved runtime path",
208
- "runtime-not-regular-file": "the resolved runtime path is not a regular file",
209
- "runtime-not-executable": "the resolved runtime path is not executable",
267
+ "no-tmux-context": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["no-tmux-context"],
268
+ "anchor-malformed": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-malformed"],
269
+ "anchor-unresolved": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-unresolved"],
270
+ "anchor-mismatch": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-mismatch"],
271
+ "cwd-not-absolute": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-not-absolute"],
272
+ "cwd-format-token": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-format-token"],
273
+ "cwd-missing": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-missing"],
274
+ "cwd-not-directory": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-not-directory"],
275
+ "herdr-resume-unsupported": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["herdr-resume-unsupported"],
276
+ "runtime-unresolved": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-unresolved"],
277
+ "runtime-not-absolute": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-absolute"],
278
+ "runtime-path-whitespace": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-path-whitespace"],
279
+ "runtime-missing": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-missing"],
280
+ "runtime-not-regular-file": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-regular-file"],
281
+ "runtime-not-executable": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-executable"],
210
282
  };
@@ -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
  *
@@ -52,7 +55,8 @@ export type PiDurablePreflightRejectReason =
52
55
  | "pi-durable-birth-unit-missing"
53
56
  | "pi-durable-mcp-hand-missing"
54
57
  | "pi-durable-receive-unit-missing"
55
- | "pi-durable-visible-identity-missing";
58
+ | "pi-durable-visible-identity-missing"
59
+ | "pi-durable-owner-fence-missing";
56
60
 
57
61
  /** `<provider>/<model id>` at the FIRST `/`, or `null` when either side is empty. */
58
62
  export function splitPiDurableModel(model: string): { provider: string; model: string } | null {
@@ -89,6 +93,7 @@ export function piDurableFreshPreflight(params: {
89
93
  if (!existsSync(layout.bridgeEntry)) return "pi-durable-mcp-hand-missing";
90
94
  if (!ownerJoinAdmitsDurableHost(layout.deliverability)) return "pi-durable-receive-unit-missing";
91
95
  if (!existsSync(layout.bootstrap)) return "pi-durable-visible-identity-missing";
96
+ if (!existsSync(layout.ownerFence)) return "pi-durable-owner-fence-missing";
92
97
  return null;
93
98
  }
94
99
 
@@ -110,4 +115,6 @@ export const PI_DURABLE_PREFLIGHT_HINT: Record<
110
115
  "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",
111
116
  "pi-durable-visible-identity-missing":
112
117
  "this Entwurf package has no packaged pi-durable bootstrap (pi/pi-durable/bootstrap.mjs), so no TUI would carry the garden id — reinstall Entwurf",
118
+ "pi-durable-owner-fence-missing":
119
+ "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",
113
120
  };