@junghanacs/entwurf 0.30.1 → 0.31.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 (58) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +112 -0
  3. package/DELIVERY.md +1 -0
  4. package/README.md +47 -7
  5. package/VERIFY.md +17 -1
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/setup-clean-host.md +104 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
  19. package/mcp/entwurf-bridge/src/index.ts +7 -7
  20. package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
  21. package/package.json +13 -10
  22. package/pi/entwurf-capabilities.json +1 -0
  23. package/pi/pi-durable/bootstrap.mjs +281 -0
  24. package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
  25. package/pi/pi-durable/overlay/upstream-pin.json +16 -0
  26. package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
  27. package/pi-extensions/lib/fresh-call-composition.ts +42 -7
  28. package/pi-extensions/lib/meta-sender-identity.ts +7 -1
  29. package/pi-extensions/lib/meta-session.ts +28 -2
  30. package/pi-extensions/lib/mux-fresh-call.ts +34 -2
  31. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
  32. package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
  33. package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
  34. package/run.sh +200 -33
  35. package/scripts/check-fresh-call-dispatch.ts +18 -0
  36. package/scripts/check-gate-qualification.ts +2 -1
  37. package/scripts/check-pi-durable-contact.ts +834 -0
  38. package/scripts/check-pi-durable-receive.ts +1116 -0
  39. package/scripts/check-pi-durable-send.ts +1661 -0
  40. package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
  41. package/scripts/lib/pi-durable-r1.ts +100 -0
  42. package/scripts/lib/pi-durable-reap.ts +27 -0
  43. package/scripts/lib/pi-durable-receive.ts +137 -0
  44. package/scripts/mutants/control-send-receipt.json +1 -1
  45. package/scripts/mutants/copilot-birth.json +2 -2
  46. package/scripts/mutants/meta-hook-session-switch.json +4 -2
  47. package/scripts/mutants/mux-fresh-call.json +69 -3
  48. package/scripts/mutants/omp-birth.json +2 -2
  49. package/scripts/mutants/omp-fresh.json +6 -4
  50. package/scripts/mutants/pack-install.json +3 -3
  51. package/scripts/mutants/pi-durable.json +390 -0
  52. package/scripts/mutants/pi-floor.json +2 -2
  53. package/scripts/pi-durable-contact-host.mjs +105 -0
  54. package/scripts/pi-durable-receive-host.mjs +127 -0
  55. package/scripts/pi-durable-send-host.mjs +213 -0
  56. package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
  57. package/scripts/smoke-setup-verdict.sh +21 -2
  58. package/scripts/tsconfig.json +1 -0
@@ -25,11 +25,12 @@
25
25
  import { randomBytes } from "node:crypto";
26
26
 
27
27
  /** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
28
- * not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, and `codex`
29
- * by #95 after its system birth and app-server rails were measured. The set is joined to the
28
+ * not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, `codex`
29
+ * by #95 after its system birth and app-server rails were measured, and `pi-durable` by #129
30
+ * (the operator-provided durable app through the managed `entwurf pi-durable`). The set is joined to the
30
31
  * citizen backends by `check-harness-admission-parity`: a harness that mints records but is
31
32
  * missing HERE is not an unwired convenience, it is a release blocker. */
32
- export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex"] as const;
33
+ export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex", "pi-durable"] as const;
33
34
  export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
34
35
 
35
36
  /**
@@ -68,6 +69,11 @@ export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
68
69
  // Codex's tool process is the operator app-server, not the pane: the no-arg
69
70
  // verb refuses there by name. First action stays the delivery verb with args.
70
71
  codex: "mcp__entwurf_bridge__entwurf_v2",
72
+ // The durable contact registers the bridge verbs as its OWN tools under their bare names, and the
73
+ // app offers a tool to the model by its registered name (`durable/harness/registry.ts:99`
74
+ // @cd32f77). Measured for `entwurf_v2` on the S+native-H wire (receipt 4012c066); the callback
75
+ // spelling follows the same rule and is owed its first LIVE observation.
76
+ "pi-durable": "entwurf_callback",
71
77
  };
72
78
 
73
79
  /** Where a sibling SENDS the task result. Always the delivery verb, never the birth callback. */
@@ -77,6 +83,7 @@ export const FRESH_CALL_DELIVERY_TOOL: Record<FreshCallBackend, string> = {
77
83
  copilot: "entwurf-bridge-entwurf_v2",
78
84
  omp: "mcp__entwurf_bridge_entwurf_v",
79
85
  codex: "mcp__entwurf_bridge__entwurf_v2",
86
+ "pi-durable": "entwurf_v2",
80
87
  };
81
88
 
82
89
  /**
@@ -99,6 +106,7 @@ export const FRESH_CALL_PEERS_TOOL: Record<FreshCallBackend, string> = {
99
106
  copilot: "entwurf-bridge-entwurf_peers",
100
107
  omp: "mcp__entwurf_bridge_entwurf_peers",
101
108
  codex: "mcp__entwurf_bridge__entwurf_peers",
109
+ "pi-durable": "entwurf_peers",
102
110
  };
103
111
 
104
112
  /**
@@ -139,13 +147,16 @@ export const FRESH_CALL_TOOL_LOAD_HINT: Record<FreshCallBackend, readonly string
139
147
  copilot: [],
140
148
  omp: [],
141
149
  codex: [],
150
+ "pi-durable": [],
142
151
  };
143
152
 
144
153
  /**
145
- * What a launch has to say, in the two shapes the five backends need. Four of them are
146
- * handed a first-turn PROMPT; omp is handed a bootstrap PAYLOAD its own installed extension
147
- * unpacks. Both are always built, because building one is cheap and a backend switch must
148
- * never be able to reach a field that was not composed.
154
+ * What a launch has to say, in the two shapes the six backends need. pi, claude-code, copilot
155
+ * and codex are handed the first-turn PROMPT as an argument; pi-durable is handed that same
156
+ * PROMPT inside the closed `{v,task}` payload on the bootstrap flag, which its packaged bootstrap
157
+ * decodes; omp is handed its own bootstrap PAYLOAD, which its installed extension unpacks. Both
158
+ * are always built, because building one is cheap and a backend switch must never be able to
159
+ * reach a field that was not composed.
149
160
  */
150
161
  export interface FreshCallComposition {
151
162
  prompt: string;
@@ -339,6 +350,30 @@ export function composeBackendArgs(
339
350
  "--dangerously-bypass-approvals-and-sandbox",
340
351
  composition.prompt,
341
352
  ];
353
+ case "pi-durable": {
354
+ // The managed verb (`entwurf pi-durable`) carries the explicit model as pi's own
355
+ // `--provider`/`--model` pair, split at the FIRST `/` (the preflight already refused a model
356
+ // with no provider), the explicit width, and the WHOLE framing as the closed `{v,task}`
357
+ // payload — the durable bootstrap submits it to the root once, after the birth. The
358
+ // caller's address rides the env pair, never this payload.
359
+ const at = model.indexOf("/");
360
+ if (at <= 0 || at === model.length - 1) {
361
+ throw new Error(
362
+ `fresh-call composition: pi-durable model ${JSON.stringify(model)} is not <provider>/<model id>`,
363
+ );
364
+ }
365
+ return [
366
+ "pi-durable",
367
+ "--provider",
368
+ model.slice(0, at),
369
+ "--model",
370
+ model.slice(at + 1),
371
+ "--width",
372
+ "task-wide",
373
+ `--${OMP_BOOTSTRAP_FLAG}`,
374
+ JSON.stringify({ v: OMP_BOOTSTRAP_VERSION, task: composition.prompt }),
375
+ ];
376
+ }
342
377
  }
343
378
  }
344
379
 
@@ -73,7 +73,13 @@ import { type NativePushAdapter, resolveNativePushAdapter } from "./native-push/
73
73
  * exactly the #46 defect on agy, and copilot joined the list only once its own hook wrote one
74
74
  * (#82 RAIL 5b). Membership says a marker may EXIST, never that a reply can land: the reply
75
75
  * rail is chosen from `nativePushSupported` at the bridge, not from this list. */
76
- export const META_SENDER_BACKENDS: readonly MetaBackend[] = ["claude-code", "antigravity", "copilot", "omp"];
76
+ export const META_SENDER_BACKENDS: readonly MetaBackend[] = [
77
+ "claude-code",
78
+ "antigravity",
79
+ "copilot",
80
+ "omp",
81
+ "pi-durable",
82
+ ];
77
83
 
78
84
  /** A marker that passed BOTH guards, together with the record that vouches for it. */
79
85
  export interface TrustedMetaSender {
@@ -81,7 +81,7 @@ export class MetaRecordError extends Error {
81
81
  * every write, including its own. `./run.sh doctor-meta-bridge` already flags a
82
82
  * stale deployed writer — run it after the change.
83
83
  */
84
- export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp"] as const;
84
+ export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp", "pi-durable"] as const;
85
85
  export type MetaBackend = (typeof META_BACKENDS)[number];
86
86
 
87
87
  /**
@@ -222,6 +222,24 @@ export const META_BACKEND_DESCRIPTORS: Record<MetaBackend, MetaBackendDescriptor
222
222
  deliveryLevel: "D6",
223
223
  nativeIdLabel: "sessionId",
224
224
  },
225
+ // The long-running durable app (#129). Graded only as far as its RECORDED receipts reach.
226
+ // sessionId the durable session directory name `<13-digit ms>-<uuid>` that
227
+ // `selectSession` creates and `--continue` reopens
228
+ // (`experimental/durable/sessions.ts:18-55` @cd32f77), surfaced as
229
+ // `view.current().session.id`. Not `pi.provider.sessionId`, which a
230
+ // fork changes.
231
+ // D2 receiver armed: the contact mints the record, names the host as sender, and arms
232
+ // a root doorbell (the receiver marker is owned by the host pid as `pi-durable-host`,
233
+ // which the owner join admits). The addressed-enqueue → idle-wake → root-notice → drain
234
+ // chain has ONE recorded receipt, on the scripted-model + native-harness lab cell
235
+ // (`check-pi-durable-receive`, S+H, V not run) — lab evidence, not a vendor-turn D4–D6
236
+ // acceptance, and no sibling-isolation (D3) or LIVE fresh receipt exists yet.
237
+ "pi-durable": {
238
+ backend: "pi-durable",
239
+ wakeMode: "self-fetch",
240
+ deliveryLevel: "D2",
241
+ nativeIdLabel: "sessionId",
242
+ },
225
243
  };
226
244
 
227
245
  // ---------------------------------------------------------------------------
@@ -305,7 +323,15 @@ function isoNow(now: Date): string {
305
323
  export const META_SCHEMA_VERSION_V3 = 3 as const;
306
324
 
307
325
  /** Every backend admitted by the one V3 record-citizen schema. */
308
- export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp", "pi"] as const;
326
+ export const META_CITIZEN_BACKENDS = [
327
+ "claude-code",
328
+ "antigravity",
329
+ "codex",
330
+ "copilot",
331
+ "omp",
332
+ "pi-durable",
333
+ "pi",
334
+ ] as const;
309
335
  export type MetaCitizenBackend = (typeof META_CITIZEN_BACKENDS)[number];
310
336
 
311
337
  /**
@@ -143,6 +143,12 @@ import {
143
143
  } from "./mux-placement.ts";
144
144
  import { resolveCodexDefaultSocketPath } from "./native-push/codex-ws-client.ts";
145
145
  import { OMP_PREFLIGHT_HINT, type OmpPreflightRejectReason, ompFreshPreflight } from "./omp-fresh-preflight.ts";
146
+ import {
147
+ PI_DURABLE_PREFLIGHT_HINT,
148
+ type PiDurablePreflightRejectReason,
149
+ piDurableFreshPreflight,
150
+ } from "./pi-durable-fresh-preflight.ts";
151
+ import { checkPiDurableRuntime, PI_DURABLE_RUNTIME_HINT, piDurablePackageLayout } from "./pi-durable-runtime.ts";
146
152
  import { classifyTmuxSessionName, resolveTmuxSessionId, type TmuxSessionRejectReason } from "./resolve-tmux-session.ts";
147
153
 
148
154
  /**
@@ -238,6 +244,10 @@ export const FRESH_CALL_RUNTIME: Record<FreshCallBackend, string> = {
238
244
  copilot: "entwurf",
239
245
  omp: "omp",
240
246
  codex: "codex",
247
+ // #129: like copilot, a MANAGED verb and never the bare app. The durable app is source-only
248
+ // and runs under the operator-provided runtime's own resolver; `entwurf pi-durable` is the one
249
+ // fixed invocation that verifies that runtime against the pin and execs the packaged bootstrap.
250
+ "pi-durable": "entwurf",
241
251
  };
242
252
 
243
253
  /** A launch that was refused, or a placement that could not be established. Every value is a
@@ -251,6 +261,7 @@ export type FreshCallRejectReason =
251
261
  | TmuxSessionRejectReason
252
262
  | CopilotPreflightRejectReason
253
263
  | OmpPreflightRejectReason
264
+ | PiDurablePreflightRejectReason
254
265
  | CodexPreflightRejectReason
255
266
  | CodexCallerPreflightRejectReason
256
267
  | CodexCallerSeatRejectReason
@@ -350,9 +361,9 @@ export type FreshCallResult = { ok: true; receipt: FreshCallReceipt } | { ok: fa
350
361
  * for. That is sufficient and not a compromise: every reader of the carrier trims and tests
351
362
  * truthiness (`index.ts:212-217`), so empty and absent are the same answer by construction.
352
363
  *
353
- * It is applied to all five backends because the leak is a property of tmux, not of a vendor. A
364
+ * It is applied to all six backends because the leak is a property of tmux, not of a vendor. A
354
365
  * scrub only on the backend whose measurement surfaced it would encode the claim that the other
355
- * four are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
366
+ * five are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
356
367
  * authoritative when the process that owns it exported it ITSELF, and a fresh `pi` sibling does
357
368
  * exactly that after this argv has run. This is a fixed seam and deliberately NOT a general env
358
369
  * carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping power this
@@ -506,6 +517,25 @@ export function freshCall(
506
517
  const missing = ompFreshPreflight(env);
507
518
  if (missing) return { ok: false, reason: missing };
508
519
  }
520
+ if (params.backend === "pi-durable") {
521
+ // The framed first input is computed here only to MEASURE it against the bootstrap payload
522
+ // cap; the same pure function composes it again below. The runtime is located and verified
523
+ // with THIS process's env, before anything is placed. That is the bridge's env, not
524
+ // necessarily the window's: a new-window pane inherits the tmux SERVER's environment
525
+ // (measured, see SCRUBBED_INHERITED_ENV), and the window runs whatever `entwurf` PATH names.
526
+ // The managed verb re-verifies with its own env before it execs anything, so the window never
527
+ // runs an unverified runtime — but when HOME/XDG_DATA_HOME or the package closure differ, this
528
+ // verdict and the window's can disagree, and the window is left showing its own refusal.
529
+ const layout = piDurablePackageLayout();
530
+ const runtime = layout === null ? null : checkPiDurableRuntime(env, layout);
531
+ const missing = piDurableFreshPreflight({
532
+ model,
533
+ firstInput: buildFreshCallPrompt({ backend: params.backend, task, callerGardenId, nonce }),
534
+ layout,
535
+ runtime: runtime === null || runtime.ok ? null : runtime.reason,
536
+ });
537
+ if (missing) return { ok: false, reason: missing };
538
+ }
509
539
  const inspected = inspectPlacement(env);
510
540
  if (!inspected.ok) return { ok: false, reason: inspected.reason };
511
541
  const placement = inspected.placement;
@@ -626,6 +656,8 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
626
656
  // the sentence an operator reads cannot drift away from the predicate that produced it.
627
657
  ...COPILOT_PREFLIGHT_HINT,
628
658
  ...OMP_PREFLIGHT_HINT,
659
+ ...PI_DURABLE_RUNTIME_HINT,
660
+ ...PI_DURABLE_PREFLIGHT_HINT,
629
661
  ...CODEX_PREFLIGHT_HINT,
630
662
  ...CODEX_CALLER_PREFLIGHT_HINT,
631
663
  ...CODEX_CALLER_SEAT_HINT,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * pi-durable-fresh-preflight — the pre-mutation check a pi-durable fresh call needs (#129 W, L6;
3
+ * `docs/adding-a-harness.md` step 9 clauses 2, 3 and 5).
4
+ *
5
+ * Same question as `omp-fresh-preflight.ts`, at the same moment: before `mux-fresh-call` mutates the
6
+ * operator's tmux session, is everything the fresh contract needs actually in place? A refusal HERE,
7
+ * before placement, leaves no window behind. A pass here does not promise the window's own pass: the
8
+ * managed verb re-verifies inside the window, whose env is the tmux SERVER's and whose `entwurf` is
9
+ * whatever PATH names, so under env or package-closure skew it can still refuse after the window
10
+ * exists — and that refusal is left showing in it.
11
+ *
12
+ * ── The inputs, first ──
13
+ *
14
+ * model the fresh-call model grammar already admits `/`, and a durable launch needs the
15
+ * provider spelled out (the bootstrap refuses a model resolved by inference), so
16
+ * the model here is `<provider>/<model id>` split at the FIRST `/` — a catalog id
17
+ * that itself contains `/` keeps it. A model with no provider part is refused.
18
+ * first input the whole framing (callback first, then the task) rides the bootstrap's closed
19
+ * `{v:2,task}` payload, whose decoder caps `task` at 16000 characters. A framed
20
+ * task over that cap would open a window whose bootstrap refuses its own argv.
21
+ *
22
+ * ── Then the runtime, then the four units ──
23
+ *
24
+ * runtime operator-provided at the fixed locator and verified against the pin
25
+ * (`pi-durable-runtime.ts`); passed in as that check's verdict.
26
+ * birth the compiled contact in this package's closure — it mints the record.
27
+ * MCP hand the compiled entwurf-bridge entry in the same closure — the contact spawns it.
28
+ * receive the compiled owner join in the same closure admits `pi-durable-host`; without
29
+ * it every dispatch to the citizen is the honest `mailbox-undeliverable`.
30
+ * visible identity the packaged bootstrap — the one that hands the TUI the view whose conversation
31
+ * labels carry the garden id.
32
+ *
33
+ * ── What this deliberately does NOT claim ──
34
+ *
35
+ * Filesystem truth about ONE package closure and ONE runtime directory. It does not prove the durable
36
+ * app opens, the bridge connects, the doorbell arms or a garden id renders — that is runtime truth for
37
+ * the clause 7 LIVE receipt. No spawn, no network, no mutation; every path arrives as an argument, so
38
+ * this leaf imports nothing but types.
39
+ */
40
+
41
+ import { existsSync, readFileSync } from "node:fs";
42
+ import type { PiDurablePackageLayout, PiDurableRuntimeRejectReason } from "./pi-durable-runtime.ts";
43
+
44
+ /** The bootstrap decoder's task cap (`meta-bridge-omp.ts` OMP_BOOTSTRAP_TASK_MAX_CHARS); the beside
45
+ * test holds the two equal. */
46
+ export const PI_DURABLE_FIRST_INPUT_MAX_CHARS = 16000;
47
+
48
+ export type PiDurablePreflightRejectReason =
49
+ | "pi-durable-model-not-provider-qualified"
50
+ | "pi-durable-first-input-too-long"
51
+ | PiDurableRuntimeRejectReason
52
+ | "pi-durable-birth-unit-missing"
53
+ | "pi-durable-mcp-hand-missing"
54
+ | "pi-durable-receive-unit-missing"
55
+ | "pi-durable-visible-identity-missing";
56
+
57
+ /** `<provider>/<model id>` at the FIRST `/`, or `null` when either side is empty. */
58
+ export function splitPiDurableModel(model: string): { provider: string; model: string } | null {
59
+ const at = model.indexOf("/");
60
+ if (at <= 0 || at === model.length - 1) return null;
61
+ return { provider: model.slice(0, at), model: model.slice(at + 1) };
62
+ }
63
+
64
+ /** The compiled owner join, read from the emitted bytes the bridge actually loads. */
65
+ function ownerJoinAdmitsDurableHost(file: string): boolean {
66
+ try {
67
+ const join = /SENDER_JOINED_RECEIVER_OWNER_KINDS\s*=\s*(\[[^\]]*\])/.exec(readFileSync(file, "utf8"))?.[1];
68
+ return join?.includes('"pi-durable-host"') ?? false;
69
+ } catch {
70
+ // Bounded environment probe: an unreadable file is a missing unit.
71
+ return false;
72
+ }
73
+ }
74
+
75
+ /** The FIRST missing thing, in repair order, or `null` when a fresh launch may proceed. */
76
+ export function piDurableFreshPreflight(params: {
77
+ readonly model: string;
78
+ readonly firstInput: string;
79
+ readonly layout: PiDurablePackageLayout | null;
80
+ /** The verdict of `checkPiDurableRuntime` for the caller's env: `null` = verified. */
81
+ readonly runtime: PiDurableRuntimeRejectReason | null;
82
+ }): PiDurablePreflightRejectReason | null {
83
+ if (splitPiDurableModel(params.model) === null) return "pi-durable-model-not-provider-qualified";
84
+ if (params.firstInput.length > PI_DURABLE_FIRST_INPUT_MAX_CHARS) return "pi-durable-first-input-too-long";
85
+ if (params.layout === null) return "pi-durable-package-incomplete";
86
+ if (params.runtime !== null) return params.runtime;
87
+ const layout = params.layout;
88
+ if (!existsSync(layout.adapter)) return "pi-durable-birth-unit-missing";
89
+ if (!existsSync(layout.bridgeEntry)) return "pi-durable-mcp-hand-missing";
90
+ if (!ownerJoinAdmitsDurableHost(layout.deliverability)) return "pi-durable-receive-unit-missing";
91
+ if (!existsSync(layout.bootstrap)) return "pi-durable-visible-identity-missing";
92
+ return null;
93
+ }
94
+
95
+ /** Repair text beside the predicate that decides it. The runtime reasons keep theirs on the runtime
96
+ * leaf; `mux-fresh-call` spreads both tables. */
97
+ export const PI_DURABLE_PREFLIGHT_HINT: Record<
98
+ Exclude<PiDurablePreflightRejectReason, PiDurableRuntimeRejectReason>,
99
+ string
100
+ > = {
101
+ "pi-durable-model-not-provider-qualified":
102
+ "a pi-durable model is `<provider>/<model id>` — the durable bootstrap resolves no provider by inference",
103
+ "pi-durable-first-input-too-long":
104
+ "the framed first input exceeds the 16000-character bootstrap payload cap — shorten the task",
105
+ "pi-durable-birth-unit-missing":
106
+ "this Entwurf package has no compiled pi-durable contact (mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js), so the sibling would mint no record — reinstall or rebuild Entwurf",
107
+ "pi-durable-mcp-hand-missing":
108
+ "this Entwurf package has no compiled entwurf-bridge entry, so the sibling's callback tool would not exist — reinstall or rebuild Entwurf",
109
+ "pi-durable-receive-unit-missing":
110
+ "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
+ "pi-durable-visible-identity-missing":
112
+ "this Entwurf package has no packaged pi-durable bootstrap (pi/pi-durable/bootstrap.mjs), so no TUI would carry the garden id — reinstall Entwurf",
113
+ };